Files
BMAD-METHOD/tools/check-doc-links.js
Alex Verkhovsky 2e16650067 feat(docs): Diataxis restructure + Astro/Starlight migration (#1263)
* feat(docs): add Diataxis folder structure and update sidebar styling

- Create tutorials, how-to, explanation, reference directories with subdirectories
- Add index.md files for each main Diataxis section
- Update homepage with Diataxis card navigation layout
- Implement clean React Native-inspired sidebar styling
- Convert sidebar to autogenerated for both Diataxis and legacy sections
- Update docusaurus config with dark mode default and navbar changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(docs): migrate Phase 1 files to Diataxis structure

Move 21 files to new locations:
- Tutorials: quick-start guides, agent creation guide
- How-To: installation, customization, workflows
- Explanation: core concepts, features, game-dev, builder
- Reference: merged glossary from BMM and BMGD

Also:
- Copy images to new locations
- Update internal links via migration script (73 links updated)
- Build verified successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(docs): add category labels for sidebar folders

Add _category_.json files to control display labels and position
for autogenerated sidebar categories.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style(docs): improve welcome page and visual styling

- Rewrite index.md with React Native-inspired welcoming layout
- Add Diataxis section cards with descriptions
- Remove sidebar separator, add spacing instead
- Increase navbar padding with responsive breakpoints
- Add rounded admonitions without left border bar
- Use system font stack for better readability
- Add lighter chevron styling in sidebar
- Constrain max-width to 1600px for wide viewports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use baseUrl in meta tag paths for correct deployment URLs

* feat(docs): complete Phase 2 - split files and fix broken links

Phase 2 of Diataxis migration:
- Split 16 large legacy files into 42+ focused documents
- Created FAQ section with 7 topic-specific files
- Created brownfield how-to guides (3 files)
- Created workflow how-to guides (15+ files)
- Created architecture explanation files (3 files)
- Created TEA/testing explanation files
- Moved remaining legacy module files to proper Diataxis locations

Link fixes:
- Fixed ~50 broken internal links across documentation
- Updated relative paths for new file locations
- Created missing index files for installation, advanced tutorials
- Simplified TOC anchors to fix Docusaurus warnings

Cleanup:
- Removed legacy sidebar entries for deleted folders
- Deleted duplicate and empty placeholder files
- Moved workflow diagram assets to tutorials/images

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(build): use file glob instead of sidebar parsing for llms-full.txt

Replace brittle sidebar.js regex parsing with recursive file glob.
The old approach captured non-file strings like 'autogenerated' and
category labels, resulting in only 5 files being processed.

Now correctly processes all 86+ markdown files (~95k tokens).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(seo): use absolute URLs in AI meta tags for agent discoverability

AI web-browsing agents couldn't follow relative paths in meta tags due to
URL security restrictions. Changed llms-full.txt and llms.txt meta tag
URLs from relative (baseUrl) to absolute (urlParts.origin + baseUrl).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(docs): recategorize misplaced files per Diataxis analysis

Phase 2.5 categorization fixes based on post-migration analysis:

Moved to correct Diataxis categories:
- tutorials/installation.md → deleted (duplicate of how-to/install-bmad.md)
- tutorials/brownfield-onboarding.md → how-to/brownfield/index.md
- reference/faq/* (8 files) → explanation/faq/
- reference/agents/barry-quick-flow.md → explanation/agents/
- reference/agents/bmgd-agents.md → explanation/game-dev/agents.md

Created:
- explanation/agents/index.md

Fixed all broken internal links (14 total)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(docs): add Getting Started tutorial and simplify build script

- Add comprehensive Getting Started tutorial with installation as Step 1
- Simplify build-docs.js to read directly from docs/ (no consolidation)
- Remove backup/restore dance that could corrupt docs folder on build failure
- Remove ~150 lines of unused consolidation code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(css): use fixed width layout to prevent content shifting

Apply React Native docs approach: set both width and max-width at
largest breakpoint (1400px) so content area maintains consistent
size regardless of content length. Switches to fluid 100% below
1416px breakpoint.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(docs): restructure tutorials with renamed entry point

- Rename index.md to bmad-tutorial.md for clearer navigation
- Remove redundant tutorials/index.md
- Update sidebar and config references

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(docs): add tutorial style guide and AI agent announcement bar

- Add docs/_contributing/ with tutorial style guide
- Reformat quick-start-bmm.md and bmad-tutorial.md per style guide
- Remove horizontal separators, add strategic admonitions
- Add persistent announcement bar for AI agents directing to llms-full.txt
- Fix footer broken link to tutorials

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(docs): add markdown demo page and UI refinements

- Add comprehensive markdown-demo.md for style testing
- Remove doc category links from navbar (use sidebar instead)
- Remove card buttons from welcome page
- Add dark mode styling for announcement bar
- Clean up index.md card layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(docs): apply unified tutorial style and update references

- Reformat create-custom-agent.md to follow tutorial style guide
- Update tutorial-style.md with complete unified structure
- Update all internal references to renamed tutorial files
- Remove obsolete advanced/index.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(docs): migrate from Docusaurus to Astro+Starlight

Replace Docusaurus with Astro and the Starlight documentation theme
for improved performance, better customization, and modern tooling.

Build pipeline changes:
- New build-docs.js orchestrates link checking, artifact generation,
  and Astro build in sequence
- Add check-doc-links.js for validating internal links and anchors
- Generate llms.txt and llms-full.txt for LLM-friendly documentation
- Create downloadable source bundles (bmad-sources.zip, bmad-prompts.zip)
- Suppress MODULE_TYPELESS_PACKAGE_JSON warning in Astro build
- Output directly to build/site for cleaner deployment

Website architecture:
- Add rehype-markdown-links.js plugin to transform .md links to routes
- Add site-url.js helper for GitHub Pages URL resolution with strict
  validation (throws on invalid GITHUB_REPOSITORY format)
- Custom Astro components: Banner, Header, MobileMenuFooter
- Symlink docs/ into website/src/content/docs for Starlight

Documentation cleanup:
- Remove Docusaurus _category_.json files (Starlight uses frontmatter)
- Convert all docs to use YAML frontmatter with title field
- Move downloads.md from website/src/pages to docs/
- Consolidate style guide and workflow diagram docs
- Add 404.md and tutorials/index.md

---------

Co-authored-by: forcetrainer <bryan@inagaki.us>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 14:42:15 +08:00

283 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Internal documentation link checker
* Scans markdown files in docs/ and verifies:
* - All relative links point to existing files
* - All anchor links (#section) point to valid headings
* - No duplicate/conflicting paths
*
* Exits with code 1 if broken links are found (fails the build).
*/
const { readFileSync, existsSync } = require('node:fs');
const { resolve, dirname, join, normalize, relative } = require('node:path');
const { glob } = require('glob');
const DOCS_DIR = resolve(process.cwd(), 'docs');
// Regex to match markdown links: [text](path) and reference-style [text]: path
const LINK_PATTERNS = [
/\[([^\]]*)\]\(([^)]+)\)/g, // [text](path)
/\[([^\]]+)\]:\s*(\S+)/g, // [text]: path
];
// Regex to extract headings for anchor validation
const HEADING_PATTERN = /^#{1,6}\s+(.+)$/gm;
/**
* Determines whether a link should be ignored during validation.
* @param {string} link - The link URL or path to test.
* @returns {boolean} `true` if the link is external, uses a special protocol (`http://`, `https://`, `mailto:`, `tel:`), or is an absolute path starting with `/`, `false` otherwise.
*/
function shouldIgnore(link) {
return (
link.startsWith('http://') ||
link.startsWith('https://') ||
link.startsWith('mailto:') ||
link.startsWith('tel:') ||
link.startsWith('/') // Absolute paths handled by Astro routing
);
}
/**
* Convert a markdown heading into the anchor slug used by common Markdown processors.
*
* Produces a lowercase slug with emojis and most punctuation removed, whitespace collapsed to single
* hyphens, consecutive hyphens collapsed, and leading/trailing hyphens trimmed.
* @param {string} heading - The heading text to convert.
* @returns {string} The resulting anchor slug.
*/
function headingToAnchor(heading) {
return heading
.toLowerCase()
.replaceAll(/[\u{1F300}-\u{1F9FF}]/gu, '') // Remove emojis
.replaceAll(/[^\w\s-]/g, '') // Remove special chars except hyphens
.replaceAll(/\s+/g, '-') // Spaces to hyphens
.replaceAll(/-+/g, '-') // Collapse multiple hyphens
.replaceAll(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
}
/**
* Extracts anchor slugs from Markdown content by converting headings to their anchor form.
*
* Strips inline formatting (code spans, emphasis, bold, and inline links), processes
* Markdown headings (levels 16), and returns the resulting anchor slugs.
*
* @param {string} content - The Markdown text to scan for headings.
* @returns {Set<string>} A set of anchor slugs derived from the headings in `content`.
*/
function extractAnchors(content) {
const anchors = new Set();
let match;
HEADING_PATTERN.lastIndex = 0;
while ((match = HEADING_PATTERN.exec(content)) !== null) {
const headingText = match[1].trim();
// Remove inline code, bold, italic, links from heading
const cleanHeading = headingText
.replaceAll(/`[^`]+`/g, '')
.replaceAll(/\*\*([^*]+)\*\*/g, '$1')
.replaceAll(/\*([^*]+)\*/g, '$1')
.replaceAll(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.trim();
anchors.add(headingToAnchor(cleanHeading));
}
return anchors;
}
/**
* Remove fenced and inline code segments from Markdown content.
*
* @param {string} content - Markdown text to sanitize.
* @returns {string} The input content with fenced code blocks (```...``` and ~~~...~~~) and inline code (backtick-enclosed) removed.
*/
function stripCodeBlocks(content) {
// Remove fenced code blocks (``` or ~~~)
return content
.replaceAll(/```[\s\S]*?```/g, '')
.replaceAll(/~~~[\s\S]*?~~~/g, '')
.replaceAll(/`[^`\n]+`/g, ''); // Also remove inline code
}
/**
* Extracts all non-external link targets from markdown content, ignoring links inside code blocks.
* @param {string} content - Markdown source to scan for link targets.
* @returns {string[]} Array of raw link strings (paths and optional anchors) found in the content; external or protocol-based links are excluded.
*/
function extractLinks(content) {
const strippedContent = stripCodeBlocks(content);
const links = [];
for (const pattern of LINK_PATTERNS) {
let match;
pattern.lastIndex = 0;
while ((match = pattern.exec(strippedContent)) !== null) {
const rawLink = match[2];
if (!shouldIgnore(rawLink)) {
links.push(rawLink);
}
}
}
return links;
}
/**
* Split a link into its path and anchor components.
* @param {string} link - The link string to parse; may include a `#` followed by an anchor.
* @returns {{path: string|null, anchor: string|null}} An object where `path` is the portion before `#` (or `null` when empty, indicating a same-file anchor), and `anchor` is the portion after `#` (or `null` when no `#` is present). Note: `anchor` may be an empty string if the link ends with `#`.
*/
function parseLink(link) {
const hashIndex = link.indexOf('#');
if (hashIndex === -1) {
return { path: link, anchor: null };
}
return {
path: link.slice(0, hashIndex) || null, // Empty path means same file
anchor: link.slice(hashIndex + 1),
};
}
/**
* Resolve a relative markdown link path from a source file to a concrete absolute file path.
* @param {string} fromFile - Absolute path of the file containing the link.
* @param {string|null} linkPath - Link target as written in markdown; may be `null` or empty for same-file anchors.
* @returns {string} The resolved absolute path. If `linkPath` is null/empty returns `fromFile`. If the resolved path has no extension, an existing `.md` file or an `index.md` inside a matching directory is preferred; otherwise the normalized resolved path is returned.
*/
function resolveLink(fromFile, linkPath) {
if (!linkPath) return fromFile; // Same file anchor
const fromDir = dirname(fromFile);
let resolved = normalize(resolve(fromDir, linkPath));
// If link doesn't have extension, try .md
if (!resolved.endsWith('.md') && !existsSync(resolved)) {
const withMd = resolved + '.md';
if (existsSync(withMd)) {
return withMd;
}
// Try as directory with index.md
const asIndex = join(resolved, 'index.md');
if (existsSync(asIndex)) {
return asIndex;
}
}
return resolved;
}
// Cache for file anchors to avoid re-reading files
const anchorCache = new Map();
/**
* Retrieve and cache the set of markdown anchor slugs for a given file.
*
* Reads the file at the provided path, extracts heading-based anchor slugs, stores them in an internal cache, and returns them.
* @param {string} filePath - Absolute or relative path to the markdown file.
* @returns {Set<string>} The set of anchor slugs present in the file.
*/
function getAnchorsForFile(filePath) {
if (anchorCache.has(filePath)) {
return anchorCache.get(filePath);
}
const content = readFileSync(filePath, 'utf-8');
const anchors = extractAnchors(content);
anchorCache.set(filePath, anchors);
return anchors;
}
/**
* Validate Markdown files in docs/ for broken relative links and anchor targets.
*
* Scans all `.md` and `.mdx` files under DOCS_DIR, checks that relative links resolve to existing
* files and that any `#anchor` references point to existing headings. Prints a grouped,
* colored report of issues to stdout and terminates the process with exit code `0` if no issues
* were found or `1` if any broken links or anchors are detected.
*/
async function main() {
console.log(' → Scanning for broken links and anchors...');
const files = await glob('**/*.{md,mdx}', { cwd: DOCS_DIR, absolute: true });
const errors = [];
// Track all resolved paths for duplicate detection
const pathRegistry = new Map(); // normalized path -> [source files]
for (const file of files) {
const content = readFileSync(file, 'utf-8');
const links = extractLinks(content);
const relativePath = relative(DOCS_DIR, file);
for (const rawLink of links) {
const { path: linkPath, anchor } = parseLink(rawLink);
// Resolve target file
const targetFile = resolveLink(file, linkPath);
const normalizedTarget = normalize(targetFile);
// Check if file exists (skip for same-file anchors)
if (linkPath && !existsSync(targetFile)) {
errors.push({
type: 'broken-link',
file: relativePath,
link: rawLink,
message: `File not found: ${linkPath}`,
});
continue;
}
// Check anchor if present
if (anchor) {
const anchors = getAnchorsForFile(targetFile);
if (!anchors.has(anchor)) {
errors.push({
type: 'broken-anchor',
file: relativePath,
link: rawLink,
message: `Anchor "#${anchor}" not found in ${linkPath || 'same file'}`,
});
}
}
// Track paths for duplicate detection
if (linkPath) {
if (!pathRegistry.has(normalizedTarget)) {
pathRegistry.set(normalizedTarget, []);
}
pathRegistry.get(normalizedTarget).push({ from: relativePath, link: rawLink });
}
}
}
// Report results
if (errors.length === 0) {
console.log(` \u001B[32m✓\u001B[0m Checked ${files.length} files - no broken links found.`);
process.exit(0);
}
console.log(`\n \u001B[31m✗\u001B[0m Found ${errors.length} issue(s):\n`);
// Group by file
const byFile = {};
for (const error of errors) {
if (!byFile[error.file]) byFile[error.file] = [];
byFile[error.file].push(error);
}
for (const [file, fileErrors] of Object.entries(byFile)) {
console.log(` \u001B[36m${file}\u001B[0m`);
for (const error of fileErrors) {
const icon = error.type === 'broken-link' ? '🔗' : '⚓';
console.log(` ${icon} ${error.link}`);
console.log(` └─ ${error.message}`);
}
console.log();
}
process.exit(1);
}
main().catch((error) => {
console.error('Error:', error.message);
process.exit(1);
});