Skip to content

Use symlinked content collection + stock docsLoader for Starlight docs

Date: 2026-06-08 Status: Accepted Decision: hl-bit.30 Deciders: Sean Brandt

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).

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.

  • 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.
  • Keep custom externalDocsLoader + root-relative links — rejected because it loses docsLoader id semantics, causes 82 relative-link 404s, and breaks starlight-links-validator entirely.
  • 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 every docs/... path across all CLAUDE.md files and tooling; highest blast radius. Rejected.
  • Positive: docsLoader id/slug semantics fully intact; all 82 relative .md links resolve; starlight-links-validator operates 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 in website/src/route-data.ts).
  • Neutral: Internal-link validation owned by starlight-links-validator using root-relative URLs; lychee rescoped to external-link + drift checks (hl-bit.28).
  • Supersedes: hl-6iz