#!/usr/bin/env node /** * Generates public/llms-full.txt by walking pages/, reading _meta.json * for ordering, and concatenating all MDX/MD content as clean Markdown * with per-page frontmatter (Next.js llms-full.txt format). * * Run from repo root or documentation/docs/: * node documentation/scripts/next-scripts/generate-llms-txt.mjs */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PAGES_DIR = path.resolve(__dirname, '../../docs/pages'); const OUTPUT_FILE = path.resolve(__dirname, '../../docs/public/llms-full.txt'); const SITE_URL = 'https://nym.com/docs'; // Directories to skip entirely (auto-generated API reference, archives, etc.) const SKIP_DIRS = new Set(['api', 'archive', 'playground']); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Read _meta.json for ordered keys; fall back to alphabetical. */ function getPageOrder(dir) { const metaPath = path.join(dir, '_meta.json'); if (fs.existsSync(metaPath)) { try { const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); return Object.keys(meta); } catch { /* fall through */ } } return fs.readdirSync(dir) .filter(f => !f.startsWith('_') && !f.startsWith('.')) .map(f => f.replace(/\.mdx?$/, '')) .filter((v, i, a) => a.indexOf(v) === i) .sort(); } /** Extract title from frontmatter or first H1. */ function extractTitle(content, fallback) { const fm = content.match(/^---[\s\S]*?title:\s*["']?(.+?)["']?\s*$/m); if (fm) return fm[1]; const h1 = content.match(/^#\s+(.+)$/m); if (h1) return h1[1]; return fallback.replace(/[-_]/g, ' '); } /** Extract description from frontmatter. */ function extractDescription(content) { const fm = content.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m); return fm ? fm[1] : ''; } /** Strip frontmatter, imports, and JSX from MDX, leaving clean Markdown. */ function stripMdx(content) { let s = content; // Frontmatter s = s.replace(/^---[\s\S]*?---\n*/m, ''); // Import statements s = s.replace(/^import\s+.*$/gm, ''); // Self-closing JSX tags: s = s.replace(/^\s*<\w[\w.-]*(?:\s[^>]*)?\s*\/>\s*$/gm, ''); // JSX block tags on their own line: , , etc. // Keep the children — only remove the tag lines themselves. s = s.replace(/^\s*<\/?\w[\w.-]*(?:\s[^>]*)?\s*>\s*$/gm, ''); // Collapse 3+ blank lines → 2 s = s.replace(/\n{3,}/g, '\n\n'); return s.trim(); } /** Convert file path to URL. */ function fileToUrl(filePath) { let rel = path.relative(PAGES_DIR, filePath) .replace(/\.mdx?$/, '') .replace(/\/index$/, ''); return `${SITE_URL}/${rel}`; } // --------------------------------------------------------------------------- // Recursive page collector // --------------------------------------------------------------------------- function collectPages(dir) { const pages = []; const order = getPageOrder(dir); for (const key of order) { const subDir = path.join(dir, key); // Skip excluded directories if (SKIP_DIRS.has(key) && fs.existsSync(subDir) && fs.statSync(subDir).isDirectory()) { continue; } // Find the page file (.mdx preferred over .md) let filePath = null; for (const ext of ['.mdx', '.md']) { const p = path.join(dir, `${key}${ext}`); if (fs.existsSync(p)) { filePath = p; break; } } // Or an index file inside a subdirectory if (!filePath && fs.existsSync(subDir) && fs.statSync(subDir).isDirectory()) { for (const ext of ['.mdx', '.md']) { const p = path.join(subDir, `index${ext}`); if (fs.existsSync(p)) { filePath = p; break; } } } if (filePath) { const raw = fs.readFileSync(filePath, 'utf-8'); const title = extractTitle(raw, key); const description = extractDescription(raw); const body = stripMdx(raw); if (body.length > 0) { pages.push({ title, description, url: fileToUrl(filePath), body }); } } // Recurse into subdirectory if (fs.existsSync(subDir) && fs.statSync(subDir).isDirectory()) { pages.push(...collectPages(subDir)); } } return pages; } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- console.log(`Scanning ${PAGES_DIR} ...`); const pages = collectPages(PAGES_DIR); const lines = []; // Global header lines.push(`# Nym Documentation\n`); lines.push(`@version: 1.20.4`); lines.push(`@generated: ${new Date().toISOString().split('T')[0]}`); lines.push(`@pages: ${pages.length}`); lines.push(`@source: https://github.com/nymtech/nym/tree/develop/documentation/docs`); lines.push(''); // Per-page blocks for (const page of pages) { lines.push('---'); lines.push(`title: ${page.title}`); if (page.description) { lines.push(`description: ${page.description}`); } lines.push(`url: ${page.url}`); lines.push('---'); lines.push(''); lines.push(page.body); lines.push(''); } const output = lines.join('\n'); fs.writeFileSync(OUTPUT_FILE, output); const sizeKb = (Buffer.byteLength(output, 'utf-8') / 1024).toFixed(0); console.log(`Wrote ${pages.length} pages to ${OUTPUT_FILE} (${sizeKb} KB)`);