import fs from 'node:fs'; import path from 'node:path'; // Adjust path if needed to point to your actual sidebar file import { guidesSidebar, apisSidebar, legalSidebar } from '../lib/sidebar-data'; const CONTENT_DIR = path.join(process.cwd(), 'content'); // --- HELPERS --- function ensureDirectoryExistence(filePath: string) { const dirname = path.dirname(filePath); if (fs.existsSync(dirname)) { return true; } ensureDirectoryExistence(dirname); fs.mkdirSync(dirname); } function toTitleCase(str: string) { return str .replace(/[-_]/g, ' ') .replace(/\b\w/g, (char) => char.toUpperCase()); } /** * Clean a path segment. * e.g., "docs/guides/overview" -> "guides/overview" * e.g., "/guides/overview/" -> "guides/overview" */ function cleanPath(p: string) { if (!p) return ''; return p .replace(/^\/+/, '') // Remove leading slash .replace(/^docs\//, '') // Remove leading 'docs/' if present (prevents duplication) .replace(/\/+$/, ''); // Remove trailing slash } /** * Resolve HREF */ function getItemHref(item: any, urlPrefix: string): string { // Normalize prefix: // If urlPrefix is '', prefix becomes just '/' // If urlPrefix is '/v2', prefix becomes '/v2/' const prefix = urlPrefix ? (urlPrefix.startsWith('/') ? urlPrefix : `/${urlPrefix}`) : ''; let segment = ''; // CASE A: String (Doc ID) if (typeof item === 'string') { segment = cleanPath(item); } // CASE B: Link (External or Internal) else if (item.type === 'link' && item.href) { // If external, return immediately if (item.href.startsWith('http')) return item.href; // If absolute path provided manually, return it (e.g. /legal/...) if (item.href.startsWith('/')) return item.href; segment = cleanPath(item.href); } // CASE C: Doc Object else if (item.type === 'doc' && item.id) { segment = cleanPath(item.id); } // CASE D: Category (Link to index) else if (item.type === 'category') { if (item.link?.slug) { segment = cleanPath(item.link.slug); } else if (item.link?.id) { segment = cleanPath(item.link.id); } } // CASE E: Autogenerated else if (item.type === 'autogenerated' && item.dirName) { segment = cleanPath(item.dirName); } if (!segment) return ''; // Combine prefix and segment // Ensure we usually start with / unless prefix was empty and logic demands it let finalUrl = prefix ? `${prefix}/${segment}` : `/${segment}`; // FINAL SAFETY CHECK: Deduplicate /docs/docs/ // This catches cases where 'urlPrefix' was empty but 'segment' started with docs/ finalUrl = finalUrl.replace(/\/docs\/docs\//g, '/docs/'); return finalUrl; } /** * Resolve TITLE */ function getItemLabel(item: any): string { if (typeof item === 'string') { const parts = item.split('/'); return toTitleCase(parts[parts.length - 1]); } if (item.label) return item.label; if (item.id) { const parts = item.id.split('/'); return toTitleCase(parts[parts.length - 1]); } if (item.type === 'autogenerated' && item.dirName) { const parts = item.dirName.split('/'); return toTitleCase(parts[parts.length - 1]); } return 'Section'; } /** * Resolve ICON */ function getItemIcon(item: any): string { if (typeof item === 'string') return ''; if (item.type === 'category') return ''; if (item.type === 'autogenerated') return ''; if (item.type === 'link') return ''; return ''; } // --- GENERATOR --- function generateContent(title: string, description: string, items: any[], urlPrefix: string) { const safeDescription = description .replace(/\\/g, '\\\\') .replace(/"/g, '\\"'); const cardsHtml = items .map((item) => { const href = getItemHref(item, urlPrefix); const label = getItemLabel(item); const icon = getItemIcon(item); if (!href) return null; const isExternal = href.startsWith('http'); const externalProp = isExternal ? ' external' : ''; return ` `; }) .filter(Boolean) .join('\n'); return `--- title: "${title}" description: "${safeDescription}" --- {/* THIS FILE IS AUTO-GENERATED FROM SIDEBAR-DATA. ANY MANUAL CHANGES WILL BE OVERWRITTEN. */} import { Card, Cards } from 'fumadocs-ui/components/card'; import { FileText, Folder, Link as LinkIcon } from 'lucide-react'; ${cardsHtml} `; } function traverse(items: readonly any[], baseDir: string, urlPrefix: string) { items.forEach((item) => { // Skip strings (leaves) if (typeof item === 'string') return; // Generate page only for categories with 'generated-index' if ( item.type === 'category' && item.link && item.link.type === 'generated-index' && item.link.slug ) { const slug = item.link.slug; const cleanSlug = cleanPath(slug); const filePath = path.join(baseDir, cleanSlug, 'index.mdx'); console.log(`[+] Generating Virtual Page: ${path.relative(CONTENT_DIR, filePath)}`); ensureDirectoryExistence(filePath); const content = generateContent( item.link.title || item.label || 'Overview', item.link.description || '', item.items || [], urlPrefix ); fs.writeFileSync(filePath, content); } // Recursively traverse if (item.items) { traverse(item.items, baseDir, urlPrefix); } }); } // --- EXECUTION --- const VERSIONS_FILE = path.join(CONTENT_DIR, 'versions.json'); console.log('--- Scanning Sidebar for Virtual Pages ---'); // 1. Generate for Latest // CRITICAL CHANGE: Pass empty string '' instead of '/docs' // This generates href="/guides/..." instead of "/docs/guides/..." // preventing the double prefix if the Component/Link adds it automatically. console.log(`\nProcessing: Latest`); traverse(guidesSidebar, CONTENT_DIR, ''); traverse(apisSidebar, CONTENT_DIR, ''); traverse(legalSidebar, CONTENT_DIR, ''); // 2. Generate for Versions if (fs.existsSync(VERSIONS_FILE)) { const versions = JSON.parse(fs.readFileSync(VERSIONS_FILE, 'utf8')); versions.forEach((v: any) => { if (!v.param || v.param === 'latest' || !v.param.startsWith('v')) return; const versionDir = path.join(CONTENT_DIR, v.param); if (!fs.existsSync(versionDir)) return; console.log(`\nProcessing: ${v.param}`); // For versions, we prefix with just the version param (e.g. /v2) // If double docs persist here, change this to just `/${v.param}` or verify version routing. traverse(guidesSidebar, versionDir, `/${v.param}`); traverse(apisSidebar, versionDir, `/${v.param}`); traverse(legalSidebar, versionDir, `/${v.param}`); }); } console.log('--- Done ---');