mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-01-30 04:32:02 +00:00
* 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>
283 lines
9.7 KiB
JavaScript
283 lines
9.7 KiB
JavaScript
/**
|
||
* 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 1–6), 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);
|
||
});
|