Skip to content

Commit c239354

Browse files
committed
.md files for llms
1 parent 27858d6 commit c239354

3 files changed

Lines changed: 252 additions & 3 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ tags
4141
/testing/extract-tutorial-code/target
4242
Cargo.lock
4343

44-
.idea
44+
.idea
45+
46+
# Generated per-page Markdown files for LLM consumption (generate-md-urls.js)
47+
static/**/*.md

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@
1212
"scripts": {
1313
"docusaurus": "docusaurus",
1414
"start": "docusaurus start",
15-
"prebuild": "node scripts/generate-llms-txt.js",
15+
"prebuild": "node scripts/generate-llms-txt.js && node scripts/generate-md-urls.js",
1616
"build": "docusaurus build",
1717
"swizzle": "docusaurus swizzle",
1818
"deploy": "docusaurus deploy",
1919
"clear": "docusaurus clear",
2020
"serve": "docusaurus serve",
2121
"write-translations": "docusaurus write-translations",
2222
"write-heading-ids": "docusaurus write-heading-ids",
23-
"generate:llms": "node scripts/generate-llms-txt.js"
23+
"generate:llms": "node scripts/generate-llms-txt.js",
24+
"generate:md-urls": "node scripts/generate-md-urls.js"
2425
},
2526
"dependencies": {
2627
"@docusaurus/core": "3.8.1",

scripts/generate-md-urls.js

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#!/usr/bin/env node
2+
/*
3+
generate-md-urls.js
4+
5+
For each documentation page, generates a clean Markdown file at the
6+
URL-matching path under static/. This lets users (and LLMs) append
7+
.md to any docs URL to get raw Markdown.
8+
9+
Example:
10+
docs.multiversx.com/developers/overview
11+
→ docs.multiversx.com/developers/overview.md (raw Markdown)
12+
13+
The files are placed in static/ so Docusaurus copies them as-is to
14+
the build output. MDX-specific syntax (imports, JSX components,
15+
Docusaurus comments) is stripped to produce clean, LLM-friendly
16+
Markdown.
17+
18+
Usage: node scripts/generate-md-urls.js
19+
*/
20+
21+
const fs = require('fs');
22+
const fsp = require('fs/promises');
23+
const path = require('path');
24+
25+
const ROOT = path.join(__dirname, '..');
26+
const DOCS_DIR = path.join(ROOT, 'docs');
27+
const STATIC_DIR = path.join(ROOT, 'static');
28+
29+
// ---------------------------------------------------------------------------
30+
// Sidebar parsing
31+
// ---------------------------------------------------------------------------
32+
33+
function safeRequire(p) {
34+
try {
35+
return require(p);
36+
} catch {
37+
return null;
38+
}
39+
}
40+
41+
function collectDocIdsFromItems(items, acc) {
42+
if (!items) return;
43+
for (const it of items) {
44+
if (typeof it === 'string' || it instanceof String) {
45+
acc.add(String(it));
46+
continue;
47+
}
48+
if (it && typeof it === 'object') {
49+
if (it.type === 'category') {
50+
if (it.link && it.link.type === 'doc' && it.link.id) {
51+
acc.add(String(it.link.id));
52+
}
53+
collectDocIdsFromItems(it.items, acc);
54+
} else if (it.type === 'doc' && it.id) {
55+
acc.add(String(it.id));
56+
} else if (it.id) {
57+
acc.add(String(it.id));
58+
}
59+
}
60+
}
61+
}
62+
63+
// ---------------------------------------------------------------------------
64+
// File resolution (mirrors generate-llms-txt.js logic)
65+
// ---------------------------------------------------------------------------
66+
67+
async function resolveDocPath(docId) {
68+
const directMd = path.join(DOCS_DIR, `${docId}.md`);
69+
const directMdx = path.join(DOCS_DIR, `${docId}.mdx`);
70+
if (fs.existsSync(directMd)) return directMd;
71+
if (fs.existsSync(directMdx)) return directMdx;
72+
73+
const dir = path.join(DOCS_DIR, path.dirname(docId));
74+
const base = path.basename(docId);
75+
const kebab = base.replace(/\s+/g, '-');
76+
const kebabMd = path.join(dir, `${kebab}.md`);
77+
const kebabMdx = path.join(dir, `${kebab}.mdx`);
78+
if (fs.existsSync(kebabMd)) return kebabMd;
79+
if (fs.existsSync(kebabMdx)) return kebabMdx;
80+
81+
try {
82+
const entries = await fsp.readdir(dir, { withFileTypes: true });
83+
for (const e of entries) {
84+
if (!e.isFile() || !/\.(md|mdx)$/i.test(e.name)) continue;
85+
const full = path.join(dir, e.name);
86+
try {
87+
const content = await fsp.readFile(full, 'utf8');
88+
if (content.startsWith('---')) {
89+
const end = content.indexOf('\n---', 3);
90+
if (end !== -1) {
91+
const block = content.slice(3, end);
92+
const idm = block.match(/^\s*id:\s*(["']?)(.+?)\1\s*$/m);
93+
if (idm && idm[2].trim() === base) return full;
94+
}
95+
}
96+
} catch {}
97+
}
98+
} catch {}
99+
100+
return null;
101+
}
102+
103+
// ---------------------------------------------------------------------------
104+
// Frontmatter helpers
105+
// ---------------------------------------------------------------------------
106+
107+
function parseFrontmatter(mdContent) {
108+
const meta = {};
109+
if (!mdContent.startsWith('---')) return meta;
110+
const end = mdContent.indexOf('\n---', 3);
111+
if (end === -1) return meta;
112+
const block = mdContent.slice(3, end);
113+
const pairs = {
114+
title: block.match(/^\s*title:\s*(["']?)(.+?)\1\s*$/m),
115+
slug: block.match(/^\s*slug:\s*(["']?)(.+?)\1\s*$/m),
116+
description: block.match(/^\s*description:\s*(["']?)([\s\S]*?)\1\s*$/m),
117+
};
118+
if (pairs.title) meta.title = pairs.title[2].trim();
119+
if (pairs.slug) meta.slug = pairs.slug[2].trim();
120+
if (pairs.description) meta.description = pairs.description[2].trim();
121+
meta._fmEnd = end + '\n---'.length;
122+
return meta;
123+
}
124+
125+
// ---------------------------------------------------------------------------
126+
// URL path computation (without site URL prefix)
127+
// ---------------------------------------------------------------------------
128+
129+
async function computeUrlPath(docId) {
130+
const filePath = await resolveDocPath(docId);
131+
let defaultPath = `/${docId}`;
132+
133+
if (filePath) {
134+
const rel = path.relative(DOCS_DIR, filePath).replace(/\\/g, '/');
135+
defaultPath = `/${rel.replace(/\.(md|mdx)$/i, '')}`;
136+
137+
try {
138+
const content = await fsp.readFile(filePath, 'utf8');
139+
const fm = parseFrontmatter(content);
140+
if (fm.slug && fm.slug.startsWith('/')) return fm.slug;
141+
} catch {}
142+
}
143+
144+
return defaultPath;
145+
}
146+
147+
// ---------------------------------------------------------------------------
148+
// MDX → clean Markdown
149+
// ---------------------------------------------------------------------------
150+
151+
function cleanMdxContent(content) {
152+
// Strip frontmatter — we'll prepend our own header
153+
if (content.startsWith('---')) {
154+
const end = content.indexOf('\n---', 3);
155+
if (end !== -1) content = content.slice(end + 4);
156+
}
157+
158+
// Remove ```mdx-code-block ... ``` fenced blocks (usually wrapping imports)
159+
content = content.replace(/```mdx-code-block\n[\s\S]*?```\n?/g, '');
160+
161+
// Remove standalone import statements
162+
content = content.replace(/^import\s+.+$/gm, '');
163+
164+
// Remove [comment]: # (...) lines
165+
content = content.replace(/^\[comment\]:\s*#\s*\(.*\)\s*$/gm, '');
166+
167+
// Remove JSX wrapper components (Tabs, TabItem) but keep inner content.
168+
// Opening tags can span multiple lines: <Tabs\n defaultValue=...\n ...>
169+
content = content.replace(/<Tabs[\s\S]*?>/g, '');
170+
content = content.replace(/<\/Tabs>/g, '');
171+
content = content.replace(/<TabItem[\s\S]*?>/g, '');
172+
content = content.replace(/<\/TabItem>/g, '');
173+
174+
// Remove other common Docusaurus JSX wrappers
175+
content = content.replace(/<details[\s\S]*?>/gi, '');
176+
content = content.replace(/<\/details>/gi, '');
177+
content = content.replace(/<summary[\s\S]*?>/gi, '');
178+
content = content.replace(/<\/summary>/gi, '');
179+
180+
// Collapse 3+ consecutive blank lines into 2
181+
content = content.replace(/\n{3,}/g, '\n\n');
182+
183+
return content.trim();
184+
}
185+
186+
// ---------------------------------------------------------------------------
187+
// Main
188+
// ---------------------------------------------------------------------------
189+
190+
async function main() {
191+
const sidebars = safeRequire(path.join(ROOT, 'sidebars.js'));
192+
if (!sidebars || !sidebars.docs) {
193+
console.error('Could not load sidebars.js or missing "docs" sidebar.');
194+
process.exit(1);
195+
}
196+
197+
// Collect all doc IDs from every sidebar category
198+
const allIds = new Set();
199+
for (const items of Object.values(sidebars.docs)) {
200+
collectDocIdsFromItems(items, allIds);
201+
}
202+
203+
let written = 0;
204+
let skipped = 0;
205+
const generatedPaths = [];
206+
207+
for (const docId of allIds) {
208+
const filePath = await resolveDocPath(docId);
209+
if (!filePath) {
210+
skipped++;
211+
continue;
212+
}
213+
214+
const urlPath = await computeUrlPath(docId);
215+
const rawContent = await fsp.readFile(filePath, 'utf8');
216+
const fm = parseFrontmatter(rawContent);
217+
const cleaned = cleanMdxContent(rawContent);
218+
219+
// Build a clean markdown file with a descriptive header
220+
const lines = [];
221+
if (fm.title) lines.push(`# ${fm.title}`);
222+
if (fm.description) lines.push('', `> ${fm.description}`);
223+
if (lines.length > 0) lines.push('');
224+
lines.push(cleaned);
225+
226+
// Write to static/ at the URL-matching path
227+
const outPath = path.join(STATIC_DIR, `${urlPath}.md`);
228+
await fsp.mkdir(path.dirname(outPath), { recursive: true });
229+
await fsp.writeFile(outPath, lines.join('\n') + '\n', 'utf8');
230+
231+
generatedPaths.push(`${urlPath}.md`);
232+
written++;
233+
}
234+
235+
console.log(
236+
`generate-md-urls: wrote ${written} files, skipped ${skipped} (unresolved)`
237+
);
238+
239+
return generatedPaths;
240+
}
241+
242+
main().catch((err) => {
243+
console.error(err);
244+
process.exit(1);
245+
});

0 commit comments

Comments
 (0)