docs: migrate site from astro to fumadocs

Merged
SE
2mo ago
+6198-3678

Replaces Astro 6 + Starlight with Fumadocs on Next 16 static export, ported from the working setup in the sibling playwright-page-object repo rather than authored fresh.

URL parity is exact

This is what made the migration low risk. Starlight (base: "", default trailing slash) and Fumadocs (baseUrl: "/" + Next trailingSlash: true) emit identical paths, so all 15 doc routes plus / survive with no redirects.

scripts/validate-routes.mjs pins this as a build gate — it asserts every route emitted an out/**/index.html and that each canonical is https://path.shmakov.tools<route>. The route list in that script is the old Starlight route set, unchanged.

Content barely moved

Every page already carried just title and description, which satisfies the Fumadocs pageSchema, and no code fence used expressiveCode meta — so Shiki is a drop-in on the same github-light/github-dark themes. Three content changes total:

  • Two Starlight :::note asides → <Callout type="info"> (guides/templates.mdx, integrations/zod.mdx)
  • index.mdx (template: splash) → app/(home)/page.tsx
  • Starlight's explicit sidebar array → five meta.json files preserving the existing order

All 15 content files land as git renames (100% similarity on 13, 97%/99% on the two Callout conversions), so per-file history survives — which matters because Fumadocs reads lastModified from git log per file.

Theme

Yellow accent over the neutral.css token preset. --color-fd-primary-foreground is forced near-black in both themes: the preset's light value is near-white, which on yellow is ~2.6:1 and fails WCAG. The shipped pair gives 6.6:1 light / 11.7:1 dark, verified against the compiled CSS rather than the source, since the override has to win on cascade order.

Brand assets are generated in-repo — favicon, logo, hero, and a 1200×630 OG card sharing one indent-cascade motif. scripts/render-png.mjs rasterises the card so the PNG cannot drift from its SVG source.

Blog ships without posts

Infrastructure is included, but blog/[slug] and blog/tags/[tag] sit under a Next private folder (app/(home)/blog/_pending/). output: "export" fails the build outright on an empty generateStaticParams() — it does not silently emit nothing. /blog/, the author page and the RSS feed build fine, because the author list is a static map rather than post-derived.

Activation is two git mvs, documented in app/(home)/blog/_pending/README.md. I chose this over inventing a placeholder post so no fabricated content ships.

CI

Build output moves docs/dist → docs/out, so the Pages artifact path changes with it. The workflow now also gates PRs and runs lint → typecheck → build → validate:links → validate:routes, and drops the root-sitemap dispatch that no longer has a consumer (per the 2026-08-06 decision that each custom-domain host owns its sitemap). fetch-depth: 0 is retained — Fumadocs needs git history for lastModified.

Verification

All green locally: build, typecheck, lint, validate:links (0 errors), validate:routes (18 routes). Exported route list diffed against the old Astro route set — exact match. Pre-commit hook ran the library suite too: 205 runtime + 60 type tests passing.

Reviewer notes

  • /sitemap-index.xml → /sitemap.xml. Astro used the former, Next emits the latter. If the old path is submitted in Search Console it will 404 after deploy and needs resubmitting.
  • Four biome warnings are intentional and match the reference repo: two noImgElement (next/image buys nothing with images.unoptimized + static export of SVGs) and two noImportantStyles (scroll-lock overrides that must defeat react-remove-scroll).

🤖 Generated with Claude Code


Summary by cubic

Migrates the docs site from astro/@astrojs/starlight to fumadocs on next 16 static export. Old: builds to docs/dist with /sitemap-index.xml; new: builds to docs/out with /sitemap.xml. All documentation URLs are unchanged; new public routes include /blog/, author pages, and an RSS feed.

  • URL parity is enforced: scripts/validate-routes.mjs asserts every exported route, canonical URL, and selected titles/H1s match the legacy set.
  • Content moved with minimal edits: homepage is a React page; two Starlight notes → <Callout type="info">; sidebar arrays → meta.json. Files were git-renamed to preserve lastModified.
  • Theme: yellow accent over fumadocs-ui neutral; forces --color-fd-primary-foreground to near-black in both themes for WCAG contrast.
  • Blog infrastructure ships without posts: /blog/, author pages, and /blog/rss.xml are live; post and tag routes sit under a private _pending/ folder until there is content.
  • Search is static: app/api/search serves the index; a client dialog is wired via fumadocs-core’s static client.
  • Robots and sitemap are generated by Next routes: app/robots.ts points to /sitemap.xml.
  • CI: Pages workflow now runs on PRs, executes lint → typecheck → build → link and route validators, and uploads docs/out with actions/deploy-pages@v5. Root sitemap dispatch is removed.

Rollout and follow-ups

  • Update Search Console to use /sitemap.xml (the old /sitemap-index.xml will 404 after deploy).
  • No redirects are required; documentation route set is identical.
  • To enable blog posts later: move app/(home)/blog/_pending/[slug] → app/(home)/blog/[slug] and _pending/tags → tags, re-add the “Blog” nav link, and include the new routes in scripts/validate-links.mjs and scripts/validate-routes.mjs.