Use symlinked content collection + stock docsLoader for Starlight docs
Date: 2026-06-08 Status: Accepted Decision: hl-bit.30 Deciders: Sean Brandt
Context
Section titled “Context”The custom externalDocsLoader (raw Astro glob() with ../docs base) implemented in hl-6iz lacks docsLoader’s id/slug semantics. With it, 82 relative .md links 404’d because the loader did not compute page IDs from content-collection paths. Additionally, starlight-links-validator computes page IDs as path.relative(srcDir+content/docs, filePath) — it could not resolve externally-based entries at all, making the validator effectively inoperable for the docs collection.
The original hl-6iz rationale rejected symlinks as “explicitly unsupported by Astro and OS-inconsistent.” In practice this was over-cautious for our specific constraints: macOS dev + Linux CI, git-tracked symlinks, and Cloudflare Pages deploying the BUILT output (not traversing the symlink).
Decision
Section titled “Decision”Symlink website/src/content/docs → ../../../docs (relative, git-tracked) and use the stock @astrojs/starlight docsLoader(). A route-data middleware (website/src/route-data.ts) strips the src/content/docs/ prefix from editLink URLs because the symlink makes filePaths project-relative instead of repo-relative.
Rationale
Section titled “Rationale”docsLoader’s id/slug semantics are required for working internal links and correct slugs — the custom externalDocsLoader cannot emulate them without re-implementing significant framework internals.- Symlinks are reliable in THIS environment: macOS dev, Linux CI, git-tracked (git follows symlinks on add/commit), Cloudflare Pages deploys the built output and never traverses the symlink.
- hl-6iz’s blanket “symlinked content collections unsupported” rejection was based on general Astro community reports, not empirical testing of our specific platform constraints.
docs/stays the canonical source; all CLAUDE.md routing pointers, edit links, and cross-check script paths remain valid.
Alternatives Considered
Section titled “Alternatives Considered”- Keep custom externalDocsLoader + root-relative links — rejected because it loses
docsLoaderid semantics, causes 82 relative-link 404s, and breaksstarlight-links-validatorentirely. - Astro at the repo root (Approach A) —
docsLoader()works natively with no shim, but lands Node tooling at the root of an Ansible/Terraform/k8s repo. Retained only as last-resort fallback. - Idiomatic
src/content/docs/move — cleanest for the framework but rewrites everydocs/...path across all CLAUDE.md files and tooling; highest blast radius. Rejected.
Consequences
Section titled “Consequences”- Positive:
docsLoaderid/slug semantics fully intact; all 82 relative.mdlinks resolve;starlight-links-validatoroperates correctly;docs/stays canonical; CLAUDE.md path-pointer routing preserved. - Negative: editLink URLs require route-data middleware to strip the
src/content/docs/prefix (one-time shim, already implemented inwebsite/src/route-data.ts). - Neutral: Internal-link validation owned by
starlight-links-validatorusing root-relative URLs; lychee rescoped to external-link + drift checks (hl-bit.28).
References
Section titled “References”- Supersedes: hl-6iz