ADR-005: Link Validation Strategy
Status
Section titled “Status”Accepted
Context
Section titled “Context”Documentation contains many internal links using relative paths (](./file.md)). These links will break when:
- File reorganization - Moving files between directories
- Content Collections migration - Moving from
docs/tosrc/content/ - Documentation restructuring - Flattening or reorganizing guide structure
Problem Statement
Section titled “Problem Statement”Hard-coded relative links create fragile documentation that breaks silently:
- Links work in development but 404 in production
- No build-time validation of internal references
- Manual link maintenance is error-prone and scales poorly
- Future Content Collections migration will require link updates
Requirements
Section titled “Requirements”- Validate all internal links at build time
- Support both current relative links and future absolute references
- Fail fast with clear error messages
- Prepare for Content Collections migration
- Zero runtime performance impact
Decision Drivers
Section titled “Decision Drivers”- Documentation Reliability: Broken links erode trust and waste developer time
- Build-time Safety: Catching errors before deployment is cheaper than fixing production 404s
- Migration Readiness: Future Content Collections migration will require systematic link updates
- Zero Runtime Cost: Validation must not impact end-user performance
- Developer Experience: Clear error messages with file paths and line numbers accelerate fixes
Decision
Section titled “Decision”Implement build-time link validation using a custom remark plugin.
Architecture
Section titled “Architecture”graph TD A[Markdown Files] --> B[Remark Plugin] B --> C{Link Type?} C -->|Relative ./| D[Resolve from Current Dir] C -->|Absolute /docs/| E[Resolve from Root] C -->|External http| F[Skip Validation] D --> G{File Exists?} E --> G G -->|Yes| H[Continue Build] G -->|No| I[Fail Build with Error]Implementation Details
Section titled “Implementation Details”-
Remark Plugin (
scripts/src/remark-validate-links.mjs)- Processes all
.mdand.mdxfiles during build - Validates file existence for internal links
- Supports multiple base paths for future migration
- Processes all
-
Link Resolution Strategy
// Priority order:1. Absolute paths: /docs/file.md → ${rootDir}/docs/file.md2. Relative paths: ./file.md → ${currentDir}/file.md3. Extension inference: ./file → ./file.md (with warning) -
Error Handling
- Strict mode: Fail build on broken links
- Lenient mode: Log warnings but continue
- Helpful errors: Show file path, line number, target
Considered Alternatives
Section titled “Considered Alternatives”Option 1: Manual Link Maintenance
Section titled “Option 1: Manual Link Maintenance”Rejected - Doesn’t scale, error-prone, reactive not proactive
Option 2: External Link Checker Tools
Section titled “Option 2: External Link Checker Tools”- markdown-link-check: External tool, requires separate CI step
- broken-link-checker: Runtime validation, not build-time
- remark-validate-links: Existing plugin but limited customization
Rejected - Prefer integrated build-time validation with custom requirements
Option 3: Astro Content Collections Only
Section titled “Option 3: Astro Content Collections Only”Rejected - Requires immediate migration, doesn’t solve current link validation
Option 4: URL-based References
Section titled “Option 4: URL-based References”[Link](https://example.com/docs/file)Rejected - Requires server setup, breaks offline development
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Build-time validation - Catch broken links before deployment
- Clear error messages - Know exactly which links are broken and where
- Future-proof architecture - Ready for Content Collections migration
- Zero runtime overhead - Validation only during build
- Gradual migration path - Existing relative links continue working
Negative
Section titled “Negative”- Build time increase - Additional processing during build (minimal impact)
- New dependency - Requires
unist-util-visitpackage - Learning curve - Team needs to understand new link formats
Neutral
Section titled “Neutral”- Link format flexibility - Support both relative and absolute during transition
- Configuration required - Need to specify base paths for validation
Implementation Plan
Section titled “Implementation Plan”Amendment (2026-08-02): Phase 1’s “validation script in package.json” was never shipped — validation runs inside
astro buildvia the strict remark plugin, and CI’s build job executes it, so Phase 2’s “add validation to CI pipeline” is effectively complete. The dedicatedlink-checkCI job is advisory only (continue-on-error: true); the enforced gate is the plugin failing the build.Amendment (2026-08-03): the advisory job moved from
ci.ymlto its ownlink-check.ymlrunning weekly and on markdown-touching PRs, since its unique value is external-URL rot detection — a calendar-time failure mode the per-PR run could not surface (internal links were already gated by the build).continue-on-errorwas dropped so a failed run is visible; the job is not a required status check, so it informs without blocking.
Phase 1: Foundation (Complete)
Section titled “Phase 1: Foundation (Complete)”- Create remark plugin for link validation
- Integrate with Astro config (MDX + markdown)
- Add validation script to package.json
- Document migration strategy
Phase 2: Validation (Next)
Section titled “Phase 2: Validation (Next)”- Test validation with existing documentation
- Fix any broken links discovered
- Add validation to CI pipeline
- Create monitoring for link health
Phase 3: Future Migration (Optional)
Section titled “Phase 3: Future Migration (Optional)”- Convert relative links to absolute references
- Implement Content Collections schema
- Migrate documentation to
src/content/ - Update link validation config for new structure
Configuration
Section titled “Configuration”Amendment (2026-08-02): the shipped configuration has since evolved.
astro.config.mjsnow passes{ rootDir, basePaths: ["/docs", "/adr"], routeMap: { "/adr/": "docs/adr/" }, excludePaths: ["docs"] }—strict: trueandvalidateAnchors: falseare no longer passed explicitly; they are the plugin defaults inscripts/src/remark-validate-links.mjs.
[remarkValidateLinks, { rootDir: process.cwd(), basePaths: ['/docs', '/src/content'], // Search these directories strict: true, // Fail build on broken links validateAnchors: false // TODO: Future enhancement}]Rollback Strategy
Section titled “Rollback Strategy”If validation causes issues:
- Disable plugin - Comment out from astro.config.mjs
- Lenient mode - Set
strict: falseto log warnings only - Remove dependency - Uninstall
unist-util-visitif needed
All existing relative links continue working without the plugin.
Monitoring
Section titled “Monitoring”- Build logs - Show validation results and suggestions
- CI pipeline - Fail builds on broken links
- Documentation health - Track link validation over time
Related Decisions
Section titled “Related Decisions”- ADR-000: Starter Template Architecture - Foundation
- ADR-001: Preact Island Usage Policy - Component architecture
- ADR-002: Future CSS Tooling Considerations - Tooling decisions
References
Section titled “References”Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1: every internal markdown link resolves to an existing file — the build fails otherwise.
- TC-2: the repo-wide markdown link-check job passes in CI.
- Checks:
- TC-1 →
remark-validate-linksin the build (status: block, pre-existing gate) - TC-2 →
link-check.ymlworkflow, weekly + markdown-touching PRs (status: warn, visible failure but not a required check)
- TC-1 →
- Not machine-checkable: external-URL liveness policy nuances (rate limits, transient failures) are handled by the job’s configuration, not asserted absolutely.
- Graduation log:
- 2026-08-03: TC-2 recorded as block at creation but the job had
continue-on-error: truesince inception — it could never fail the build. Corrected to warn and moved to a scheduled workflow (see amendment above); TC-1 remains the blocking gate for internal links.
- 2026-08-03: TC-2 recorded as block at creation but the job had
Date: 2025-06-10
Participants: Template maintainers
Outcome: Accepted