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
Context
章节标题:“Context”Current state
章节标题:“Current state”| 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.tswalks the file system once and produces the explorer sidebar, the breadcrumb labels and the generateddocs/sitemap.md. Page order follows the link order in each directory'sindex.mdtables, then alphabetical.theme/suppliesBreadcrumb.vue,DocMeta.vue(renders the standard frontmatter), a diagram zoom behaviour, andcustom.cssincluding the.vp-tablescroll wrapper.config.mtsadds a CJK-bigram MiniSearch tokenizer,markdown.html: falseso<placeholder>text never reaches the Vue compiler, atable_open/table_closeoverride, the emoji rule disable (so<3 mAis not turned into an emoji),copyAssetsinbuildEndwith a 25 MiB per-asset guard, andignoreDeadLinksfor links into source-tree files that are not pages..vitepress/public/_redirectsholds 66 permanent redirects from the documentation-standard migration; Cloudflare static assets honour the file.lint-docs.mjsenforcesdocs/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.
Why the framework has to change
章节标题:“Why the framework has to change”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.
Verified platform facts
章节标题:“Verified platform facts”| 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.
Target versions
章节标题:“Target versions”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.
Proposal
章节标题:“Proposal”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
docscollection with a plain Astroglob()loader (base: '.', patterns overbizplan/**anddocs/**) combined withdocsSchema(), and agenerateIdthat reproduces today's URLs, instead ofdocsLoader(). Verify Starlight renders those entries. If Starlight rejects a foreign loader, the fallback is to move content undersrc/content/docs/{bizplan,docs}/withgit mv— which preserves published URLs and relative links, but breaks thedocs/task/,docs/plan/anddocs/decisions/paths thatCLAUDE.md, PMA andlint-docs.mjsdepend 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 = falsereading a Cloudflare binding,nodejs_compatenabled.
Phase 1 - Port the infrastructure
章节标题:“Phase 1 - Port the infrastructure”| 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 |
Phase 2 - Dynamic layer
章节标题:“Phase 2 - Dynamic layer”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.
Phase 3 - Verify and cut over
章节标题:“Phase 3 - Verify and cut over”- URL parity: enumerate the URL set of the current
distand of the Astro build, diff them, and require an empty diff. - Update
package.jsonscripts and.github/workflows/deploy.yml; the quality gate commandbun run docs:lint && bun run docs:build && git diff --checkkeeps working. - Remove
.vitepress/and the VitePress dependencies only after parity passes. - Record the migration in
docs/changelog.mdand, if the G1 fallback was taken, add a decision record underdocs/decisions/.
Risks
章节标题:“Risks”| 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 |
Scope
章节标题:“Scope”- 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 thevitepress,vitepress-plugin-mermaiddependencies.
Alternatives
章节标题:“Alternatives”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.
Annotations
章节标题:“Annotations”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.