Skip to content

Runbook: Content editing

How to author and edit case studies and blog posts. Content is git-based (ADR 0004); there is no CMS server and no external content API. Authoring today is done by editing MDX directly (Path A); a git-CMS UI for non-developers is planned but not yet built (Path B). Field-level reference: ../guides/content-authoring.md.

apps/web/src/content/
cases/ # case studies -> /work/<slug>
posts/ # blog posts -> /blog/<slug>
services/ # capabilities
team/ # people / authors

Each file is MDX with a frontmatter block validated by a Zod schema. Invalid or missing required fields fail the build — broken content cannot ship.

Path A — Edit MDX directly (current path)

Section titled “Path A — Edit MDX directly (current path)”
  1. Create a branch.
  2. Add or edit an .mdx file in the correct collection folder. Use an existing entry as a template.
  3. Fill in frontmatter per the authoring guide. Set draft: true while a piece is in progress.
  4. Add images alongside the entry — they’re committed to the repo under an enforced size budget (see the guide); the build generates the optimized derivatives.
  5. Run locally: pnpm dev to preview, pnpm typecheck to validate frontmatter against the schema.
  6. Open a PR. CI validates and publishes a preview. Merge to publish.

Path B — Git-CMS UI (planned; not yet available)

Section titled “Path B — Git-CMS UI (planned; not yet available)”

Not implemented yet. ADR 0004 adopts a git-based CMS (Decap CMS, with Sveltia CMS as the alternative) mounted at an admin route, to give non-developers a form-and-preview UI. It has not been built — there is no admin route or CMS dependency in the repo today. Until it ships, use Path A. The intended flow, once implemented, is:

  1. Sign in to the admin route.
  2. Pick a collection (Cases, Posts, …) and New or edit an existing entry.
  3. Fill the form fields (these map 1:1 to frontmatter), write the body, and add images.
  4. Save. The CMS commits to a branch and opens/updates a pull request.
  5. A reviewer approves; merge triggers the deploy pipeline. A preview deploy is available on the PR before publish.

The design goal: non-developers never touch git directly, but every change still flows through review and history.

  • Images are committed to the repo alongside content (apps/web/src/{content,assets}) under an enforced size budget; Astro’s build generates the optimized, display-sized derivatives, served as immutable hashed static assets by the Worker (ADR 0004, ADR 0006).
  • Provide meaningful alt text for every image (accessibility is a gate — see the NFR doc).
  • Image budgets are enforced. Committed content rasters (apps/web/src/{content,assets}) must be ≤2560px long edge and ≤900 KB each — sources feed Astro’s pipeline, which generates the display-sized derivatives, so oversized originals only bloat the repo. The pre-commit hook auto-normalizes staged images; if CI’s Image budgets check ever flags one, run pnpm images:fix and re-commit. (Genuine exceptions go in the ALLOWLIST in scripts/images.mjs, with a reason.)
  • No animated GIF. The build can’t emit an animated derivative, so the Image budgets check rejects any .gif — ship motion as animated WebP instead (same ≤900 KB budget). See content-authoring for how.
  • Required frontmatter complete and valid (pnpm typecheck passes).
  • draft: false when ready to publish.
  • Slug is correct and stable. For blog posts migrated from the legacy site, the slug must match the legacy /blog/<slug> to preserve SEO.
  • Images have alt text and pass the size budget (run pnpm images:fix if CI’s Image budgets check flags one).
  • Preview deploy reviewed.
  • Build fails after editing frontmatter → a required field is missing or mistyped; read the schema error and the authoring guide.
  • Post not appearing → still draft: true, or a future publishDate.
  • Broken blog URL after migration → slug changed; restore the legacy slug or add a redirect (ADR 0011).