跳转到内容

20260915-1901-astro-starlight-migration Rebuild the docs site on Astro + Starlight with on-demand rendering

  • status: completed
  • createdAt: 2026-09-15 19:01
  • approvedAt: 2026-09-15 19:10
  • relatedTask: 20260915-1901-astro-starlight-migration
Item Fact
Content 294 Markdown pages under bizplan/ and docs/, sourced in place (srcDir: '.')
Framework VitePress 1.6.4 (latest; 2.0 is still 2.0.0-alpha.20)
Deployment wrangler.jsonc, assets-only, no Worker script; manual workflow_dispatch deploy
Custom infrastructure ~690 lines: .vitepress/site-tree.ts (171), scripts/lint-docs.mjs (263), .vitepress/theme/ (307), plus ~150 lines of config.mts

The custom infrastructure is everything VitePress does not provide and the project had to build:

  • site-tree.ts walks the file system once and produces the explorer sidebar, the breadcrumb labels and the generated docs/sitemap.md. Page order follows the link order in each directory's index.md tables, then alphabetical.
  • theme/ supplies Breadcrumb.vue, DocMeta.vue (renders the standard frontmatter), a diagram zoom behaviour, and custom.css including the .vp-table scroll wrapper.
  • config.mts adds a CJK-bigram MiniSearch tokenizer, markdown.html: false so <placeholder> text never reaches the Vue compiler, a table_open/table_close override, the emoji rule disable (so <3 mA is not turned into an emoji), copyAssets in buildEnd with a 25 MiB per-asset guard, and ignoreDeadLinks for links into source-tree files that are not pages.
  • .vitepress/public/_redirects holds 66 permanent redirects from the documentation-standard migration; Cloudflare static assets honour the file.
  • lint-docs.mjs enforces docs/documentation-standard.md: frontmatter schema and vocabularies, filename rules, directory index coverage with 类型/状态 column agreement, and product package slots. It reads the file tree directly and is framework-independent.

VitePress is build-time SSG plus a client-side SPA. It has no runtime rendering, no dynamic routes, no request-time data loading and no way to reach Cloudflare bindings. Dynamic content on VitePress would have to be client-side fetch into a separately routed Worker, giving up SSR, SEO and shared routing. That was evaluated and rejected in favour of Astro.

Fact Source
Starlight's prerender option defaults to true even when Astro is configured output: 'server' starlight/src/utils/user-config.ts
Astro supports per-route export const prerender = false, so only opted-out routes run in a Worker Astro routing reference
@astrojs/cloudflare 14.3.1 (peers astro ^7.2.0, wrangler ^4.125.0) exposes bindings via Astro.locals.runtime.env npm + Astro Cloudflare integration guide
Starlight under an SSR adapter requires the nodejs_compat compatibility flag in Wrangler Starlight manual setup
<StarlightPage> lets hand-written dynamic pages reuse the Starlight shell (nav, sidebar, search) Starlight pages guide
docsLoader() accepts only generateId; its glob base is fixed to src/content/docs starlight/loaders.ts, starlight/src/loaders.ts

The last row is the migration's central risk: Starlight's own loader cannot source content from bizplan/ and docs/ where they sit today. Resolving it is gate G1 below.

astro@7.3.2, @astrojs/starlight@0.42.0, @astrojs/cloudflare@14.3.1, wrangler@4.131.2, all verified at the npm registry on 2026-09-15.

Four phases. Phase 0 is a gate: if any of its four checks fails, stop and return to proposal rather than continuing into Phase 1.

Phase 0 - Proof of concept on a branch (gate)

章节标题:“Phase 0 - Proof of concept on a branch (gate)”

Scaffold Astro + Starlight + the Cloudflare adapter on a branch, covering a first-level index page, one bizplan/product/ package directory, one of the three Mermaid pages, and one on-demand route. Nothing under bizplan/ or docs/ is modified.

  • G1 - content stays in place. Define the docs collection with a plain Astro glob() loader (base: '.', patterns over bizplan/** and docs/**) combined with docsSchema(), and a generateId that reproduces today's URLs, instead of docsLoader(). Verify Starlight renders those entries. If Starlight rejects a foreign loader, the fallback is to move content under src/content/docs/{bizplan,docs}/ with git mv — which preserves published URLs and relative links, but breaks the docs/task/, docs/plan/ and docs/decisions/ paths that CLAUDE.md, PMA and lint-docs.mjs depend on. That fallback is a separate decision, not an implementation detail; it returns to the proposal gate.
  • G2 - Chinese search. Compare Pagefind against the current MiniSearch bigram tokenizer on a fixed query set drawn from the corpus (product names, Chinese phrases, mixed CJK/Latin part numbers). Pagefind must be no worse. If it is worse, keep a MiniSearch-based index instead.
  • G3 - Mermaid. Render the ELK flowchart on one of the three Mermaid pages. Preferred approach is a client-side island mirroring today's behaviour, avoiding rehype-mermaid's build-time Playwright dependency. Only three pages use Mermaid, so this is contained.
  • G4 - hybrid deploy. Deploy the PoC to a preview: Starlight pages as static assets, one route with export const prerender = false reading a Cloudflare binding, nodejs_compat enabled.
Today Target
site-tree.ts -> VitePress sidebar/breadcrumbs/sitemap Same walk, emitting Starlight's sidebar shape at config time; sitemap generation unchanged
Breadcrumb.vue, DocMeta.vue Starlight component overrides, rewritten as .astro
custom.css, .vp-table wrapper Starlight custom CSS plus a rehype plugin for the table wrapper
diagram-zoom.ts Client script on the Starlight layout
markdown.html: false, emoji rule disabled Astro has no Vue compiler, so <placeholder> behaves differently; confirm <3 mA and angle-bracket placeholders survive and disable remark-gfm autolinking only if needed
copyAssets + 25 MiB guard astro:build:done integration hook, same logic
ignoreDeadLinks predicate Astro link checking is opt-in; carry the same allowance if a checker is enabled
.vitepress/public/_redirects public/_redirects, byte-identical
scripts/lint-docs.mjs Unchanged — it reads the file tree, not the framework. Folding its frontmatter checks into docsSchema({ extend }) is explicitly out of scope

Switch wrangler.jsonc from assets-only to a Worker with an assets binding, add nodejs_compat, and land one on-demand route as the working example. The concrete dynamic feature is not yet specified, so this phase delivers the mechanism and one representative route only.

  • URL parity: enumerate the URL set of the current dist and of the Astro build, diff them, and require an empty diff.
  • Update package.json scripts and .github/workflows/deploy.yml; the quality gate command bun run docs:lint && bun run docs:build && git diff --check keeps working.
  • Remove .vitepress/ and the VitePress dependencies only after parity passes.
  • Record the migration in docs/changelog.md and, if the G1 fallback was taken, add a decision record under docs/decisions/.
Risk Impact Mitigation
docsLoader()'s fixed base forces content relocation High - breaks PMA's docs/task/ convention and lint-docs.mjs scopes G1 tests the custom-loader path first; the fallback returns to the proposal gate
Pagefind Chinese segmentation is worse than the current bigram tokenizer Medium - search regression on a Chinese-first corpus G2 measures against a fixed query set before committing
Mermaid + ELK regression Low - only three pages G3; client-side island avoids a Playwright build dependency
Starlight is 0.x Medium - breaking changes between minors Pin exact versions; the theme overrides are the exposed surface
Markdown rendering differences (tables, angle brackets, emoji) across 294 pages Medium - silent visual drift Phase 3 parity check must include rendered-HTML spot checks, not only the URL set
Switching from assets-only to Worker + assets Low-medium - changes the cost and failure model Starlight pages stay prerendered, so static traffic keeps bypassing the Worker
lastUpdated depends on full git history Low fetch-depth: 0 already set in the deploy workflow; keep it
  • Infrastructure: roughly 15-20 files added or rewritten (Astro config, content config, Starlight overrides, sidebar generator, integration hooks, CSS, Wrangler config, deploy workflow).
  • Content: 0 files changed, provided G1 passes.
  • Removed at the end: .vitepress/ and the vitepress, vitepress-plugin-mermaid dependencies.

Evaluated and rejected before this plan:

  • Stay on VitePress and add a Worker API consumed by client-side fetch. Lowest cost, but no SSR, no dynamic routes, no SEO for dynamic pages.
  • Keep VitePress and build the dynamic part as a separate application routed on the same domain. Zero migration cost, but the dynamic content cannot share the docs routing, shell or search. Correct only if the dynamic part is a neighbouring product rather than part of the docs.
  • React Router v8 / Remix. A dynamic-first SSR framework with no content pipeline, sidebar, search or syntax highlighting; all of it would be hand-built, and prerendered docs would lose their zero-Worker cost profile.
  • Rspress, Nextra, Docusaurus, Vocs. Either no better hybrid story on Cloudflare than Astro, or a heavier runtime for no gain.

2026-09-15: Direction B (Astro + Starlight hybrid) chosen by the owner. 2026-09-15: Approved. Execution runs as BKD three-tier campaign l1-5k89db54-20260915190438 in project 7li8qh57, with L1 = session issue 5k89db54 and three L2 workstreams (platform, site shell, build pipeline). G4 uses a runtime probe route only; no Cloudflare resource (D1/KV/R2) is provisioned in this campaign. 2026-09-16: Owner granted L1 standing authority to review and merge L2 branches into main through to campaign completion, without a per-merge confirmation round trip. Not covered and still owner-only: triggering a Cloudflare deploy, git push, and moving BKD issues to done. A direction change such as relocating content still returns to the owner. 2026-09-16: Homepage decided - the root index.md is rewritten into Starlight splash form with the five feature cards as CardGrid/LinkCard in the body, keeping the landing copy in Markdown. It is the only content file this campaign may edit. Owned by L2-B; requires a glob pattern change in src/content.config.ts sequenced by L1 with L2-A.