Lossless Changelog
component

memopop-site Ships — GitHub Pages Live with a Submodule-Powered, Cross-Project Changelog Hub

From an empty Astro starter to a deployed splash page in a single session. Dark Lossless redesign, a click-to-expand GUI callout, and — the centerpiece — a permissive content-collection renderer that turns four scattered project-level changelogs into one unified timeline.

Michael StatonMusic Editoraugmented with Claude Code (Opus 4.7, 1M context)
feat(memopop-site): ship the splash page on GitHub Pages with a
                    cross-project changelog hub

Empty Astro starter → live, branded, deployed site with a /changelog
that pulls from four separate markdown sources (one of them a git
submodule). Single session, single deploy pipeline.

* Splash page. Dark Lossless theme (cyan #04e5e5 → purple #BF23F7),
  hero with 33-agent stat + supporters list, full 9-step pipeline,
  CLI preview, dual CTA. Ported in spirit from the consulting-site
  project page; styles re-implemented as scoped CSS so memopop-site
  doesn't depend on the consulting site's design system.

* Submodule-as-content. apps/memopop-orchestrator/ pulls in the real
  investment-memo-orchestrator repo as a git submodule. Site reads
  its 60-entry changelog directly. `git submodule update --remote`
  is the only sync command required.

* Changelog hub. Four Astro content collections, one permissive
  schema. Older orchestrator entries with no frontmatter render
  cleanly: title falls back to the slug, date is parsed from the
  filename. Newer entries with rich frontmatter (lede, version,
  tags, authors, augmented_with) render fully styled.

* Lightbox callout. Reusable GuiCallout.astro on the home page and
  every changelog page. Native <dialog>, 90vw expand, body-scroll
  lock, escape-to-close. Click-to-expand is a button, not an anchor —
  the text CTA still routes to the GUI marketing page.

* Deploy pipeline. .github/workflows/deploy-site.yml, Bun-based,
  submodules: true on checkout, actions/deploy-pages@v4 for upload.
  Triggers only on apps/memopop-site/** + packages/shared-styles/**
  + the workflow file itself.

What's live

https://lossless-group.github.io/memopop-ai/ — the canonical URL going forward. Default org-pattern URL (<org>.github.io/<repo>/), no custom domain. The Lossless Group's project-page habit going forward is exactly this pattern: every project gets a GitHub-hosted page, every project's URL is predictable from the repo name.

The splash page

The previous index.astro was a generic marketing-template Astro page (white background, navy/teal corporate palette, placeholder pricing tiers, fake "50+ VC firms"). It's been replaced wholesale.

The replacement is a structural port of site/src/pages/projects/gallery/memopop-ai.astro from the consulting site, redrafted to stand alone:

  • Dark Lossless brand. BaseLayout.astro now ships CSS variables for --clr-primary-bg: #0a0a0f, --clr-lossless-accent--brightest: #04e5e5, --clr-lossless-accent--aquamarine: #6FFFD6, --clr-lossless-accent--purple: #BF23F7. Inter for sans, JetBrains Mono for code/CLI blocks.

  • Real numbers. 33 specialized agents (was 12 in the placeholder). 9 VC firms. Time comparison (1–2 days → 10–15 minutes → analyst+MemoPop = 1–2 hours). Three "Why This Exists" cards. The full 9-step pipeline with cast-of-characters preamble (Extractor → Researcher → Writer → Validator → Editor → Versioned Output) and the Orchestrator wrapping all of them.

  • CLI preview block. A faux terminal window showing the interactive prompt — same content as the consulting page but rebuilt from scratch with scoped CSS so this site has no dependency on the consulting site's component library.

  • Standalone footer. No Header/Footer component imports. A small page-level footer that links back to the consulting site, the source repo, and /changelog.

The AlternatingSideImage feature-deep-dive section from the consulting page was deliberately not ported — it depends on messages/projectMemopopAI.json which lives in the consulting site. Easy to reintroduce by copying the JSON across, deferred for now.

The /changelog hub — the actual interesting bit

Four content collections, one schema, one timeline.

TS
// src/content.config.ts
const changelogSchema = z.object({
  title: lenientString,
  lede: lenientString,
  summary: lenientString,
  date: lenientDate,
  date_authored_initial_draft: lenientDate,
  // ... ten more optional date fields ...
  category: lenientString,
  status: lenientString,
  at_semantic_version: lenientString,
  augmented_with: lenientString,
  tags: lenientStringArray,
  authors: lenientStringArray,
  // ...
}).passthrough();

const monorepo     = defineCollection({ loader: glob({ pattern: '*.md', base: '../../changelog' }),                schema: changelogSchema });
const orchestrator = defineCollection({ loader: glob({ pattern: '*.md', base: '../memopop-orchestrator/changelog' }), schema: changelogSchema });
const site         = defineCollection({ loader: glob({ pattern: '*.md', base: './changelog' }),                    schema: changelogSchema });
const native       = defineCollection({ loader: glob({ pattern: '*.md', base: '../memopop-native/changelog' }),    schema: changelogSchema });

Why "permissive" was the load-bearing word

A survey before writing the schema turned up four very different frontmatter shapes:

SourceDate fieldTags styleFrontmatter at all?
Orchestrator (newer)date_authored_initial_draftYAML flow [Foo, Bar]Yes, rich
Orchestrator (older)(none — the file just starts with # Changelog)n/aNo
Nativedate (single field)YAML listYes
Sitedate_authored_initial_draft + usage_index + publish + app + files_addedYAML flowYes
Monorepodate_authored_initial_draftYAML flowYes

The schema accepts all of it — every field is lenientString / lenientDate / lenientStringArray (a z.preprocess wrapper that coerces empty strings and null to undefined before type-checking), and the schema is .passthrough() so unknown fields don't trigger validation errors. The 8+ early orchestrator entries that have zero frontmatter render with title falling back to the slug (2025-11-16_01) and date parsed from the slug's YYYY-MM-DD prefix.

Routes

  • /changelog — unified timeline, newest-first, source-filter pills with counts (Orchestrator 60 / Native 5 / Site 2 / Monorepo 1).

  • /changelog/[source]/[...slug] — one per entry. [...slug] (rest segment) is used so future nested paths in any source's changelog folder won't break the route.

Sorting

TS
function pickDate(data, id) {
  for (const key of [
    'date_authored_initial_draft', 'date', 'date_first_published',
    'date_authored_current_draft', 'date_created', 'date_modified',
    'date_last_updated',
  ]) {
    const v = data[key];
    if (v instanceof Date && !Number.isNaN(v.getTime())) return v;
  }
  return deriveDateFromId(id);  // YYYY-MM-DD prefix from filename
}

Date formatting on the rendered pages explicitly sets timeZone: 'UTC' — a 2025-11-16 ISO date parses as UTC midnight, then toLocaleDateString() shifts to local TZ and silently displays the day before. Caught on the first render-and-refresh pass.

The orchestrator submodule

apps/memopop-orchestrator/ is a git submodule pointing at lossless-group/investment-memo-orchestrator. The orchestrator is its own repo, lived on its own release cadence, and was sitting at ai-labs/investment-memo-orchestrator/ as a peer to memopop-ai/. Now it also lives inside the monorepo, where:

  • apps/memopop-site/src/content.config.ts reads the orchestrator's changelog/*.md directly

  • git submodule update --remote apps/memopop-orchestrator pulls upstream changes

  • The deploy workflow's actions/checkout@v4 uses submodules: true so CI sees the same content

Submodule was chosen over git subtree add (which would fold history into this repo and complicate pushing back upstream) and over a plain cp -R (which would lose the upstream link entirely). The trade-off is that contributors cloning fresh need git clone --recurse-submodules or a follow-up git submodule update --init — documented for now via the workflow file as a reference.

The orchestrator's changelog/releases/ subfolder (release-notes files like v0.4.1-release-notes.md) is intentionally excluded — the glob pattern is *.md, not **/*.md. Different shape, can be added as its own collection later if useful.

The GuiCallout component

Cross-page CTA aside that asks readers "Don't want to run LangChain through the command line?" and routes them to the consulting-site project page. Lives on the home page (after the CLI section) and on every changelog page (between header and timeline on the index, after the body on detail pages).

Three iterations on the layout in this session:

  1. First pass: single-column, text-only.

  2. Second pass: two-column 1fr/320px (text left, screenshot right).

  3. Final: two-column 1fr/2fr — the bordered "box" wraps only the text + CTA, the screenshot floats free at 2/3 of the container with its own subtle border + soft shadow + cyan-tinted hover lift.

The screenshot is now click-to-expand using a native <dialog> element:

ASTRO
<dialog class="callout-lightbox" id={dialogId}>
  <button data-lightbox-close>✕</button>
  <img src={screenshotUrl} class="lightbox-image" loading="lazy" />
</dialog>

<dialog> chosen over a custom modal because it ships with Escape-to-close, focus trap, top-layer rendering, and ::backdrop pseudo-element styling for free. The wiring script binds via data-lightbox-trigger / data-lightbox-target (a per-instance random ID), so multiple GuiCallouts on the same page won't collide. Body scroll locks while the lightbox is open. Clicking the backdrop, the ✕ button, or the image itself all close it.

A loading="lazy" on the lightbox image means the full-size webp isn't fetched until the dialog opens — important since the callout appears on 67+ detail pages.

The deploy pipeline

.github/workflows/deploy-site.yml:

YAML
on:
  push:
    branches: [main]
    paths:
      - 'apps/memopop-site/**'
      - 'packages/shared-styles/**'
      - '.github/workflows/deploy-site.yml'
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true            # pulls memopop-orchestrator
      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile
      - run: bun run --filter memopop-site build
      - uses: actions/configure-pages@v5
        with:
          enablement: true            # see "gotcha" below
      - uses: actions/upload-pages-artifact@v3
        with:
          path: apps/memopop-site/dist
  deploy:
    needs: build
    environment: github-pages
    steps:
      - uses: actions/deploy-pages@v4

Path filters mean the workflow only fires when the site or its declared dependencies change — pushes that touch the orchestrator (via submodule bump) or the desktop client don't trigger redeploys until the site itself is updated.

The two gotchas worth recording

1. The base: '/memopop-ai/' in astro.config.mjs. Default org-pattern URL is https://<org>.github.io/<repo>/. Astro needs to know about that subpath at build time so generated asset URLs (<img src="/_astro/...">) resolve. The BaseLayout.astro favicon link uses import.meta.env.BASE_URL so it picks up the prefix automatically; every internal link in index.astro and the changelog pages does the same. If a custom domain ever gets added, both site and base change in one place.

2. The .nojekyll file in public/. GitHub Pages will Jekyll-process any directory by default, which silently drops files starting with _. Astro's bundled assets land in _astro/. An empty .nojekyll opts out and is mandatory for any non-Jekyll Pages site.

And the gotcha that shipped this commit four times

actions/configure-pages@v5 failed three times with Resource not accessible by integration before the deploy succeeded. The error meant Pages wasn't enabled on the repo yet, and enablement: true (which I added in commit 098feb5) couldn't enable it itself because the org's GITHUB_TOKEN permission policy doesn't grant Pages-enable to integrations. The fix was the manual UI toggle: Settings → Pages → Source = "GitHub Actions" — a one-time setup per repo. After flipping that, a gh run rerun on the same commit succeeded. enablement: true was left in the workflow on the theory that future repos with more permissive token policies will benefit and current behavior is unchanged.

Versioning

Bumping at_semantic_version from 0.0.1.0 (the original Dec 27 monorepo scaffold) to 0.1.0. First public-facing release of the site. Subsequent splash-page or changelog-hub work bumps the patch; meaningful new sections or content collections bump the minor.

What's next, in rough order

  1. Fonts. The Header and Footer components from the consulting site weren't ported. Fine for now, but the splash page will eventually want a top nav. Decide whether to keep the page footer-only or add a minimal top nav with MemoPop AI (logo) + Changelog + GitHub.

  2. Releases collection. The orchestrator's releases/*.md files are excluded from the changelog hub. They have a different shape (no dated filename, content is release-note narrative). Worth surfacing eventually as a separate /releases route.

  3. Image rendering for changelog entries. Many entries have image_prompt and image frontmatter fields. Right now neither is used by the renderer. A header banner per entry would lift the visual weight of long technical entries.

  4. Pattern documentation. The directions baked into this work — apps/<name>-site/, the .nojekyll + base-path setup, the deploy workflow shape — should become a short README at the monorepo root for the next project that adds a Pages site.

  5. Deprecation cleanup. The Actions deprecation warning (Node.js 20 actions are deprecated) applies to actions/checkout@v4, actions/configure-pages@v5, actions/upload-artifact@v4. June 2nd, 2026 is the forced-default date. Bump versions when releases are out, or set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true to opt in early.