diff --git a/data/2025-astropy-coordinator.json b/data/2025-astropy-coordinator.json
index 249111a0..68d59703 100644
--- a/data/2025-astropy-coordinator.json
+++ b/data/2025-astropy-coordinator.json
@@ -13,7 +13,7 @@
},
"title": "Coordinator",
"details": [
- "Member, Coordination Committee (executive committee)."
+ "Coordination Committee (executive committee), 2025–2028."
],
"institution": "Astropy",
"location": "Remote",
diff --git a/data/2025-astropy-strategic-planner.json b/data/2025-astropy-strategic-planner.json
index 31688517..e9d676fe 100644
--- a/data/2025-astropy-strategic-planner.json
+++ b/data/2025-astropy-strategic-planner.json
@@ -13,7 +13,7 @@
},
"title": "Strategic Planner",
"details": [
- "Member, Strategic Planning and Organizing Committee."
+ "Strategic Planning and Organizing Committee, 2025–2026."
],
"institution": "Astropy",
"location": "Remote",
diff --git a/src/components/CvView.astro b/src/components/CvView.astro
index 1115e722..2ed8e502 100644
--- a/src/components/CvView.astro
+++ b/src/components/CvView.astro
@@ -5,7 +5,7 @@ import { PRESET } from '../lib/cards.js';
import '../styles/cv/page.css';
import '../styles/cv/minimap.css';
import CvHeader from './CvHeader.astro';
-import { softwarePapers, positionStops, institutionGroups, groupedByDefault, groupDate, links, REL_ICON, relKey, dateLabel } from '../lib/data.js';
+import { softwarePapers, positionStops, institutionGroups, groupedByDefault, groupDate, groupTitle, links, REL_ICON, relKey, dateLabel } from '../lib/data.js';
import { detailLines } from '../lib/inline.js';
import CollabMini from './CollabMini.astro';
import ConfMini from './ConfMini.astro';
@@ -35,8 +35,7 @@ const { cv, pick = false, picked = null, level = 'full', completeLines = cv.incl
// Roles at one institution as the one row the CV reads: the roles joined, the
// dates spanning them.
const merged = (list) => {
- const names = list.map((i) => i.title);
- return { ...list[0], title: names.length > 1 ? `${names.slice(0, -1).join(', ')} & ${names.at(-1)}` : names[0],
+ return { ...list[0], title: groupTitle(list),
date: groupDate(list), details: undefined };
};
const isPicked = (id) => !picked || picked.has(id);
diff --git a/src/lib/cvmodel.js b/src/lib/cvmodel.js
index bd4a061a..68255e5d 100644
--- a/src/lib/cvmodel.js
+++ b/src/lib/cvmodel.js
@@ -7,7 +7,7 @@
import person from '/config/person.json';
import { resolve } from './presets.js';
-import { authors, venueLine, dateLabel, links, money, softwarePapers, REL_ICON, relKey, institutionGroups, groupedByDefault, groupDate } from './data.js';
+import { authors, venueLine, dateLabel, links, money, softwarePapers, REL_ICON, relKey, institutionGroups, groupedByDefault, groupDate, groupTitle } from './data.js';
import { spans, detailLines } from './inline.js';
/**
@@ -150,12 +150,11 @@ function itemsOf(s, cv, keepLine, group) {
const rows = g.items.map(row);
const [first] = g.items;
const home = links(first).find((l) => l.rel === 'homepage');
- const names = rows.map((r) => r.title);
return [{
...rows[0],
id: `group:${first.institution}`,
when: dateLabel({ date: groupDate(g.items) }, { month: true }),
- title: names.length > 1 ? `${names.slice(0, -1).join(', ')} & ${names.at(-1)}` : names[0],
+ title: groupTitle(g.items),
subject: subject(first) ? [{ t: first.institution, b: false }] : [],
lines: rows.flatMap((r) => r.lines),
trailing: first.location ?? '',
diff --git a/src/lib/data.js b/src/lib/data.js
index 28f1b910..662a8065 100644
--- a/src/lib/data.js
+++ b/src/lib/data.js
@@ -437,6 +437,14 @@ export function institutionGroups(list, grouped = groupedByDefault) {
return out;
}
+/** A group's roles joined as one title — "Coordinator (2025 – 2028) & Core
+ * Developer" — a role's own dates given where they differ from the group's. */
+export function groupTitle(items) {
+ const span = dateLabel({ date: groupDate(items) });
+ const names = items.map((i) => (dateLabel(i) === span ? i.title : `${i.title} (${dateLabel(i)})`));
+ return names.length > 1 ? `${names.slice(0, -1).join(', ')} & ${names.at(-1)}` : names[0];
+}
+
/** The span a group of dated items covers, as one item's `date`: earliest start,
* and open-ended if any member is, else the latest end. */
export function groupDate(items) {
diff --git a/src/pages/index.astro b/src/pages/index.astro
index cecedae3..2ed7c635 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -3,7 +3,7 @@ import { PRESET } from '../lib/cards.js';
import Base from '../layouts/Base.astro';
import Card from '../components/Card.astro';
import SoftwareSection from '../components/SoftwareSection.astro';
-import { byType, featured, dateLabel } from '../lib/data.js';
+import { byType, featured, dateLabel, institutionGroups, groupDate } from '../lib/data.js';
import { lines as longLines } from '../lib/inline.js';
import Inline from '../components/Inline.astro';
import portrait from '../../cv/assets/portrait.jpg';
@@ -16,6 +16,9 @@ const pubs = featured('publication').slice(0, 5);
// so undergraduate summer positions stay on the CV where they belong.
const background = [...byType('position'), ...byType('education')]
.filter((i) => (i.cvs ?? []).includes('2page'));
+// Roles at one institution that opt in with `groupRoles` read as one place,
+// as on the CV — so the Astropy committees sit under Astropy.
+const rows = institutionGroups(background);
---