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 ---');