← All changelog entries
April 27, 2026 · Feature

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.

Authors
Michael Staton
Augmented with
Claude Code on Opus 4.7
Tags
#Working-Groups#Projects#Content-Collection#Cross-Collection-Joins#Many-to-Many#Schema-Clone#Rename-Refactor#JumboPopdown#Section-Gallery#MarkdownReader#Header-Navigation#Design-System#Pattern-Library-Philosophy#Plan-Mode

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:

PathTimeCouplingWhat you can do later
Refactor to polymorphic~5–6 hoursHeavy — one component must serve bothHard to diverge them later; abstraction tax compounds
YOLO copy-and-adapt~3 hoursIndependent — each can evolve freelyEasy 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:

  1. 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.

  2. A new workingGroups collection with the schema cloned verbatim from projects — same fields, same shapes, same enums (active | proposed | archived). The working_group_leads and working_group_members fields 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:

ComponentDecisionWhy
ProjectReader.astroMarkdownReader.astroRenamed + moved + generalizedAlready 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.astroUntouchedAlready domain-neutral.
Section__ProjectGallery.astroSection__WorkingGroupGallery.astroCloned + adaptedSame 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.astroJumboPopdown__WorkingGroups.astroCloned + adaptedVerbatim 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.astroWorkingGroupMetadataDisplay.astroCloned + adaptedRenames 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:

    1. HeroBannerWithMessageHierarchy (or plain hero if no og_image or hero_image)
    2. WorkingGroupMetadataDisplay
    3. Active projects railSection__ProjectGallery variant="lite" filtered to active projects whose working_group_slugs includes this WG. Reuses the existing project gallery component verbatim — zero new gallery code for this surface.
    4. Proposed projects rail (same component, filtered to proposed)
    5. MarkdownReader — body of the WG charter, with the back-link customized to “All working groups”
    6. Past projects shelfSection__ProjectGallery variant="shelf" for archived projects in this WG
    7. Adjacent WG nav (prev/next)
  • /projects/[slug]/ (additive change) — when working_group_slugs is non-empty, a small Part 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 GroupslugInitial active projects
Data-Driven Venturedata-driven-venturememopop-ai (active) · augment-it (proposed)
Professional Grade Content Developmentprofessional-grade-content-developmentcontent-farm (active) · lossless-flavored-markdown (archived)
Hack & Shiphack-and-shipmemopop-ai (active)
Tech Stack Deep Divestech-stack-deep-divesastro-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:

  1. Many-to-many, not one-to-many. The original plan proposed working_group: string (singular) as the back-reference. The user redirected to working_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.

  2. 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.

  3. Don’t touch existing project content. The original plan proposed deprecating the inline working_group_name, working_group_leads, working_group_members fields on projects. The user redirected: those are fine, leave them, the existing project pages render correctly. The new working_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: false on a WG hides it from the header; popdown_order: 1 pins 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 (charter vs summary, conveners vs leads, 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 WG image_prompt fields are seeded so the existing pipeline can run; per-WG title-baked-in OG images remain the proper fix.
  • /working-groups/propose page. 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].astro and utils/api-connectors/ideogram.ts).
  • pnpm dev boots 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 the working_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.astroMarkdownReader.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.ts
  • sites/fullstack-vc/src/content/working-groups/active/data-driven-venture.md
  • sites/fullstack-vc/src/content/working-groups/active/professional-grade-content-development.md
  • sites/fullstack-vc/src/content/working-groups/active/hack-and-ship.md
  • sites/fullstack-vc/src/content/working-groups/active/tech-stack-deep-dives.md
  • sites/fullstack-vc/src/content/working-groups/README.md
  • sites/fullstack-vc/src/content/projects/active/content-farm.md
  • sites/fullstack-vc/src/content/projects/active/memopop-ai.md
  • sites/fullstack-vc/src/content/projects/proposed/augment-it.md
  • sites/fullstack-vc/src/content/projects/proposed/astro-knots.md
  • sites/fullstack-vc/src/content/projects/proposed/context-vigilance.md
  • sites/fullstack-vc/src/content/projects/archived/lossless-flavored-markdown.md
  • sites/fullstack-vc/src/lib/load-working-groups.ts
  • sites/fullstack-vc/src/components/sections/Section__WorkingGroupGallery.astro
  • sites/fullstack-vc/src/components/sections/WorkingGroupMetadataDisplay.astro
  • sites/fullstack-vc/src/components/ui/menus/JumboPopdown__WorkingGroups.astro
  • sites/fullstack-vc/src/components/markdown/MarkdownReader.astro
  • sites/fullstack-vc/src/components/sections/ProjectReader.astro
  • sites/fullstack-vc/src/components/basics/Header.astro
  • sites/fullstack-vc/src/pages/working-groups/index.astro
  • sites/fullstack-vc/src/pages/working-groups/[slug].astro
  • sites/fullstack-vc/src/pages/projects/[slug].astro
  • sites/fullstack-vc/src/pages/design-system/sections/working-group-gallery.astro
  • sites/fullstack-vc/src/pages/design-system/components/jumbo-popdown-working-groups.astro
  • sites/fullstack-vc/src/pages/design-system/markdown/markdown-reader.astro
  • sites/fullstack-vc/src/pages/design-system/index.astro