Working Groups, alongside Projects — disambiguating the surface, codifying the relationship
Stood up a `working-groups` content collection sibling to `projects`, with the relationship modeled as **many-to-many** via a new `working_group_slugs: string[]` field on the project frontmatter (project files are authoritative; many-to-many because a project can be of interest to multiple WGs). The WG schema is a verbatim clone of the projects schema — fix-as-we-go discipline, no upfront vocabulary redesign. Four starter Working Groups seeded (Data-Driven Venture, Professional Grade Content Development, Hack & Ship, Tech Stack Deep Dives), each with a strong `image_prompt` so the existing banner-generation pipeline can run later. Hybrid component approach: one already-generic component (`ProjectReader.astro`) renamed to `MarkdownReader.astro` and moved to `src/components/markdown/` (single import-path change in one consumer); three project-bound components (`Section__ProjectGallery`, `JumboPopdown__Projects`, `ProjectMetadataDisplay`) copied-and-adapted into working-group siblings. The gallery's WG variant adds a small **active-projects chip row** to the card body — pre-joined by the loader (`loadWorkingGroupsWithProjects(status)`) so the component never queries mid-render. The header now exposes two sibling popdowns — `Working Groups ▾` and `Projects ▾` — keyboard-isolated and visually identical because the popdown's behavior is copied verbatim. The project detail page gains a small `Part of: [WG-A · WG-B]` breadcrumb when `working_group_slugs` is set; the WG detail page renders an active-projects rail (reusing `Section__ProjectGallery variant="lite"` filtered to the WG — zero new gallery code), then the LFM-rendered charter, then a past-projects shelf. Three new design-system catalog entries (working-group-gallery, jumbo-popdown-working-groups, markdown-reader) plus four new entries in the catalog index. The deeper story is the architectural lesson: when the user identified that I'd conflated 'working group' and 'project' from the start, we used plan mode to interrogate the choice — refactor for polymorphism vs. YOLO copy-and-adapt — and chose the second path explicitly because the pattern-library philosophy in `astro-knots/CLAUDE.md` endorses it for UI surfaces. Total cost of the YOLO path: the four cloned components diff cleanly from their parents and can evolve independently. The cost of refactoring would have been a polymorphic abstraction we'd have to maintain across two domains.
Changelog — 2026-04-27 (03)
Working Groups, alongside Projects
Why Care?
For the community (the people doing the work). Yesterday’s Projects surface was right about the artifact (concrete deliverables, lifecycle from active → archived) but wrong about the people surface. In FullStack VC, the people surface is the Working Group — the recurring meeting, the WhatsApp thread, the roster you show up to learn alongside. Projects come and go within a Working Group. The surface that introduces a new member to “the community” should be Working Groups first, Projects second. As of today, that’s what the site says.
For Mike (the firm). This commit codifies a clean pattern for cross-collection relationships in the Astro content layer — something we explicitly looked for prior art on across the workspace and found none of. The pattern is:
# In src/content/projects/active/some-project.md
working_group_slugs:
- data-driven-venture
- tech-stack-deep-dives
The project file is the authoritative source of truth for membership. The WG page filters projects whose working_group_slugs includes its slug. Many-to-many — a project can be claimed by multiple WGs, because in a peer-learning community, the same artifact often serves as evidence for more than one practice (e.g., Lossless Flavored Markdown is both a Professional Grade Content thing AND a Tech Stack Deep Dives thing). The WG file does not need to redundantly list its projects. One source of truth, no drift.
This is the pattern the next collection-to-collection relationship on this site should copy verbatim — events ↔ working groups, case studies ↔ projects, partner profiles ↔ WG conveners, etc.
For lay readers wondering why we didn’t just refactor. Because we used plan mode to interrogate the choice. The question was: abstract the existing project components so they’re polymorphic and serve both domains, or copy-and-adapt them into working-group siblings? Both paths build the same surface. The trade-off:
| Path | Time | Coupling | What you can do later |
|---|---|---|---|
| Refactor to polymorphic | ~5–6 hours | Heavy — one component must serve both | Hard to diverge them later; abstraction tax compounds |
| YOLO copy-and-adapt | ~3 hours | Independent — each can evolve freely | Easy to diverge or eventually consolidate when patterns stabilize |
The pattern-library philosophy in astro-knots/CLAUDE.md explicitly endorses copy-and-adapt for UI surfaces: “copy when each site customizes; publish a package when multiple sites need identical processing logic.” Working Groups and Projects are the same kind of customization concern within one site. So we copied. The total drift after the dust settles is two cloned-but-evolvable components instead of one straitjacketed polymorphic one.
The Idea in One Diagram
┌────────────────┐
│ Header.astro │
└────────┬───────┘
│
┌──────────────────────────┴──────────────────────────┐
▼ ▼
JumboPopdown__WorkingGroups JumboPopdown__Projects
(sibling, hover/click, (existing, untouched)
keyboard isolated)
│ │
▼ ▼
/working-groups/ /projects/
(gallery, 4 variants) (gallery, 4 variants)
│ │
▼ ▼
/working-groups/[slug]/ /projects/[slug]/
│ │
│ (cross-collection joins; many-to-many) │
│ │
│ ┌──────────────────────────────────┐ │
│ │ project frontmatter: │ │
└──────▶│ working_group_slugs: │◀────────┘
│ - tech-stack-deep-dives │
│ - hack-and-ship │
└──────────────────────────────────┘
Loader joins (pure, build-time):
loadProjectsForWorkingGroup(wgSlug) → projects.filter(p => p.data.working_group_slugs?.includes(wgSlug))
loadWorkingGroupsForProject(p) → workingGroups.filter(wg => p.data.working_group_slugs?.includes(wgSlug(wg)))
loadWorkingGroupsWithProjects(status) → enriched [{ wg, currentProjects }] — gallery cards consume this directly
What Got Built
Schema — one new field, one cloned collection
src/content.config.ts gains:
-
A new optional field on the projects schema:
working_group_slugs: z.array(z.string()).optional(),That’s it. Existing project files don’t need to be touched unless you want them in a WG.
-
A new
workingGroupscollection with the schema cloned verbatim fromprojects— same fields, same shapes, same enums (active | proposed | archived). Theworking_group_leadsandworking_group_membersfields that were already on the projects schema (for inline denormalized roster) carry naturally onto WG entries — on a WG they’re the WG’s own leads and members. Zero new vocabulary.
The choice to clone-not-redesign is deliberate. We’re early in the WG surface and we don’t know which of the projects-schema fields will feel wrong at a WG entry level until we render real ones. Fix-as-we-go beats redesign-then-fix.
Component decisions: hybrid (rename one, copy three)
Five components ended up touching this change:
| Component | Decision | Why |
|---|---|---|
ProjectReader.astro → MarkdownReader.astro | Renamed + moved + generalized | Already accepted only tree/citations — nothing project-specific. Moved out of sections/ (it’s a markdown wrapper, not a section pattern) and out of project-namespace. Added optional backHref and backLabelTxt props so the WG detail page can override “All projects” → “All working groups”. One-file rename, one consumer to update. |
HeroBannerWithMessageHierarchy.astro | Untouched | Already domain-neutral. |
Section__ProjectGallery.astro → Section__WorkingGroupGallery.astro | Cloned + adapted | Same four variants, same CSS, but the prop shape changes to entries: WorkingGroupWithProjects[] (each entry is { wg, currentProjects }). The portfolio + mission-brief variants gain a small active-projects chip row showing 1–3 of the WG’s currently-running projects. The chip data is pre-joined by the loader so the component never queries mid-render. |
JumboPopdown__Projects.astro → JumboPopdown__WorkingGroups.astro | Cloned + adapted | Verbatim hover/click/Esc/arrow-key behavior. Different aria-controls panel id so both popdowns coexist in the header without colliding. Different footer links (/working-groups/ and /working-groups/propose). |
ProjectMetadataDisplay.astro → WorkingGroupMetadataDisplay.astro | Cloned + adapted | Renames the prop from project to workingGroup. Otherwise the schema is the same so all field accessors carry. |
The cost of choosing copy-and-adapt over polymorphism is exactly four cloned components carrying nearly-identical CSS. The payoff is each can evolve independently when its domain demands a different treatment (e.g., a future “communication channels” icon row that only makes sense on a WG). Reversal cost is also low — if we ever want to consolidate, the diff is small.
Loaders — new file src/lib/load-working-groups.ts
Mirrors load-projects.ts (filter-by-status, popdown loader, slug helpers) and adds the cross-collection joins:
// Many-to-many forward: WG → its projects
export async function loadProjectsForWorkingGroup(wgSlug: string)
// Many-to-many reverse: project → its WGs (multiple)
export async function loadWorkingGroupsForProject(p: ProjectEntry)
// Enriched gallery shape: WG + its currently-active projects
export interface WorkingGroupWithProjects { wg, currentProjects }
export async function loadWorkingGroupsWithProjects(status)
All pure, all build-time. The popdown does not phone home; the gallery does not query mid-render. Same discipline as the existing project loaders.
Pages
-
/working-groups/— gallery page, three sections (active / proposed / archived). Active section uses the portfolio variant with chip rows. Hero copy localized: “Working groups, where the work happens.” -
/working-groups/[slug]/— detail page composition:HeroBannerWithMessageHierarchy(or plain hero if noog_imageorhero_image)WorkingGroupMetadataDisplay- Active projects rail —
Section__ProjectGallery variant="lite"filtered to active projects whoseworking_group_slugsincludes this WG. Reuses the existing project gallery component verbatim — zero new gallery code for this surface. - Proposed projects rail (same component, filtered to proposed)
MarkdownReader— body of the WG charter, with the back-link customized to “All working groups”- Past projects shelf —
Section__ProjectGallery variant="shelf"for archived projects in this WG - Adjacent WG nav (prev/next)
-
/projects/[slug]/(additive change) — whenworking_group_slugsis non-empty, a smallPart of: [WG-A · WG-B]breadcrumb appears just above the metadata strip. Multiple WGs supported because of many-to-many.
Header — two sibling popdowns
Dojo · Working Groups ▾ · Projects ▾ · Webinars · Changelog · Log in
Loaded in parallel:
const [popdownProjects, popdownWorkingGroups] = await Promise.all([
loadProjectsForPopdown({ showProposed: true, maxItems: 6 }),
loadWorkingGroupsForPopdown({ showProposed: true, maxItems: 6 }),
]);
Each popdown has its own panel id (jpop-projects-panel / jpop-workinggroups-panel) so they don’t collide. The shared script (deduplicated by Astro because the source content is identical between the two popdown components) initializes both wraps independently — handlers attach per [data-jpop] element.
Seed content — four starter Working Groups
| Working Group | slug | Initial active projects |
|---|---|---|
| Data-Driven Venture | data-driven-venture | memopop-ai (active) · augment-it (proposed) |
| Professional Grade Content Development | professional-grade-content-development | content-farm (active) · lossless-flavored-markdown (archived) |
| Hack & Ship | hack-and-ship | memopop-ai (active) |
| Tech Stack Deep Dives | tech-stack-deep-dives | astro-knots (proposed) · context-vigilance (proposed) · lossless-flavored-markdown (archived) |
Note: memopop-ai and lossless-flavored-markdown each appear in two working groups — the many-to-many relationship is real and exercised. Each WG has a strong image_prompt so the existing pnpm gen:content-banners pipeline can run when ready.
The Plan-Mode Conversation (worth recording)
This work was scoped through plan mode rather than YOLO-built directly. The valuable corrections during planning:
-
Many-to-many, not one-to-many. The original plan proposed
working_group: string(singular) as the back-reference. The user redirected toworking_group_slugs: string[](plural array). This matters: a project that’s interesting to two WGs at once is a normal case in this community, not an exception. Encoding singular would have forced the curator to pick a primary WG arbitrarily. -
Don’t redesign the schema upfront. The original plan proposed new vocabulary (
charter,conveners,themes,meeting_format,communication_channels). The user rejected: “just cp the schema from projects to working groups, we can fix or customize as we go.” This is the right call for an early surface — premature schema design is friction we don’t need yet. -
Don’t touch existing project content. The original plan proposed deprecating the inline
working_group_name,working_group_leads,working_group_membersfields on projects. The user redirected: those are fine, leave them, the existing project pages render correctly. The newworking_group_slugs:field is purely additive.
The plan file is preserved at ~/.claude/plans/okay-cool-so-now-ancient-porcupine.md for reference.
Authoring Affordances
- Add a project to a WG: add
working_group_slugs: [<wg-slug>]to the project file. Done. The WG page picks it up; the project page gains the breadcrumb. - Add a project to multiple WGs: list multiple slugs. Both WG pages show it.
- Promote a working group from proposed → active: one-line frontmatter edit.
- Curate the popdown:
feature_in_popdown: falseon a WG hides it from the header;popdown_order: 1pins it. - Schema fields the WG doesn’t need yet (e.g., communication_channels, themes): just add them later. Schema is
passthrough(), so unknown frontmatter fields don’t break validation in the meantime — they just don’t render anywhere.
Yak-Shaving Deferred
- Communication channels icon row on the WG detail. The WG metadata strip doesn’t yet render WhatsApp / Discord / Slack / GitHub icons because those fields don’t yet exist on the WG schema. Will add when the first WG actually wires its channels and we know the icon-set.
- WG vocabulary divergence (
chartervssummary,convenersvsleads, etc.). Schema clone today; rename when usage tells us what feels wrong. - OG image localization for working groups. Same parking-lot issue as the projects equivalent — see
context-v/issue-resolutions/Optimize-for-Local-OpenGraph-Metadata-and-Image-w-Overlay.md. The WGimage_promptfields are seeded so the existing pipeline can run; per-WG title-baked-in OG images remain the proper fix. /working-groups/proposepage. Linked from the gallery hero CTA and the popdown footer; not yet implemented.- Polymorphic
Section__EntryGallery<T>abstraction. Explicitly rejected (see plan-mode discussion above). If a third “list of cards with header” surface appears that genuinely shares the data shape, that’s the moment to extract — not this one. - Two popdowns simultaneously open is currently possible (hover popdown A, then hover popdown B → A doesn’t auto-close). Minor polish; will fix if it’s annoying in practice.
Verification
pnpm astro check— 0 new errors. 5 pre-existing errors unchanged (changelog/[id].astroandutils/api-connectors/ideogram.ts).pnpm devboots cleanly in 1.8s. All 14 routes return HTTP 200:/ HTTP 200 /projects/ HTTP 200 /projects/content-farm/ HTTP 200 /projects/lossless-flavored-markdown/ HTTP 200 /projects/augment-it/ HTTP 200 /working-groups/ HTTP 200 /working-groups/data-driven-venture/ HTTP 200 /working-groups/professional-grade-content-development/ HTTP 200 /working-groups/hack-and-ship/ HTTP 200 /working-groups/tech-stack-deep-dives/ HTTP 200 /design-system/ HTTP 200 /design-system/sections/working-group-gallery HTTP 200 /design-system/components/jumbo-popdown-working-groups HTTP 200 /design-system/markdown/markdown-reader HTTP 200- Cross-collection forward join verified —
/working-groups/tech-stack-deep-dives/lists Astro Knots and Context Vigilance via theworking_group_slugs.includes(slug)filter. - Cross-collection reverse join verified —
/projects/augment-it/shows “Part of: Data-Driven Venture” linking back to that WG. - Active-projects chip rows render on three of the four WG cards (the fourth, Tech Stack Deep Dives, has no active projects yet — its members are all proposed/archived, which is the correct behavior).
- Both header popdowns render on every page. Status dots use the same
--color-status-*semantic tokens (no new tokens added; reused from the projects ship).
Files Touched
See frontmatter files_modified. The single rename (ProjectReader.astro → MarkdownReader.astro) preserves git history via git mv. Three project files in proposed/ and archived/ and two in active/ gained the new working_group_slugs: field; nothing else in projects was touched.
Files modified (26)
sites/fullstack-vc/src/content.config.tssites/fullstack-vc/src/content/working-groups/active/data-driven-venture.mdsites/fullstack-vc/src/content/working-groups/active/professional-grade-content-development.mdsites/fullstack-vc/src/content/working-groups/active/hack-and-ship.mdsites/fullstack-vc/src/content/working-groups/active/tech-stack-deep-dives.mdsites/fullstack-vc/src/content/working-groups/README.mdsites/fullstack-vc/src/content/projects/active/content-farm.mdsites/fullstack-vc/src/content/projects/active/memopop-ai.mdsites/fullstack-vc/src/content/projects/proposed/augment-it.mdsites/fullstack-vc/src/content/projects/proposed/astro-knots.mdsites/fullstack-vc/src/content/projects/proposed/context-vigilance.mdsites/fullstack-vc/src/content/projects/archived/lossless-flavored-markdown.mdsites/fullstack-vc/src/lib/load-working-groups.tssites/fullstack-vc/src/components/sections/Section__WorkingGroupGallery.astrosites/fullstack-vc/src/components/sections/WorkingGroupMetadataDisplay.astrosites/fullstack-vc/src/components/ui/menus/JumboPopdown__WorkingGroups.astrosites/fullstack-vc/src/components/markdown/MarkdownReader.astrosites/fullstack-vc/src/components/sections/ProjectReader.astrosites/fullstack-vc/src/components/basics/Header.astrosites/fullstack-vc/src/pages/working-groups/index.astrosites/fullstack-vc/src/pages/working-groups/[slug].astrosites/fullstack-vc/src/pages/projects/[slug].astrosites/fullstack-vc/src/pages/design-system/sections/working-group-gallery.astrosites/fullstack-vc/src/pages/design-system/components/jumbo-popdown-working-groups.astrosites/fullstack-vc/src/pages/design-system/markdown/markdown-reader.astrosites/fullstack-vc/src/pages/design-system/index.astro