Skip to content

Commit 818a8f5

Browse files
rashadismMirage20
authored andcommitted
feat: generate versioned llms.txt on build
Adds a local Docusaurus plugin that emits one `llms-<version>.txt` per docs version (and `llms-next.txt` for the unreleased branch), plus an unversioned `llms.txt` that mirrors whichever version is flagged `lastVersion` so it auto-tracks releases. Each file is built from the version's own sidebar — top-level sidebar categories render as `## H2`, nested categories as `### H3` and deeper, and bullet links point at the page's Markdown source (reusing the URL convention already produced by the existing `docusaurus-plugin-markdown-export` plugin). A footer at the bottom links to all available versions. Refs openchoreo/openchoreo#3399 Signed-off-by: Rashad Sirajudeen <rashad@wso2.com>
1 parent 81ca615 commit 818a8f5

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

docusaurus.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const config: Config = {
5252
'@docsearch/docusaurus-adapter',
5353
'./plugins/docusaurus-plugin-swagger-dark-mode',
5454
'./plugins/docusaurus-plugin-markdown-export',
55+
'./plugins/docusaurus-plugin-llms-txt',
5556
'./plugins/docusaurus-plugin-docs-scripts',
5657
function webpackPolyfillsPlugin() {
5758
const webpack = require('webpack');
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
module.exports = function pluginLlmsTxt(context, options = {}) {
5+
const { siteConfig } = context;
6+
const sidebarName = options.sidebarName || 'docsSidebar';
7+
8+
let loadedVersions = null;
9+
10+
function fileNameForVersion(version) {
11+
if (version.versionName === 'current') return 'llms-next.txt';
12+
return `llms-${version.versionName}.txt`;
13+
}
14+
15+
function absoluteUrl(permalink) {
16+
if (!permalink) return '';
17+
if (/^https?:\/\//.test(permalink)) return permalink;
18+
const host = (siteConfig.url || '').replace(/\/$/, '');
19+
return host + permalink;
20+
}
21+
22+
// Mirror the URL convention from docusaurus-plugin-markdown-export:
23+
// /docs/<prefix>/<slug> → /docs/<prefix>/<slug>.md
24+
// /docs/<prefix>/ (root slug) → /docs/<prefix>.md
25+
// /docs/ → /docs.md
26+
function markdownUrl(permalink) {
27+
if (!permalink) return '';
28+
const trimmed = permalink.replace(/\/+$/, '');
29+
return absoluteUrl(trimmed + '.md');
30+
}
31+
32+
function escapeInline(s) {
33+
if (!s) return '';
34+
return String(s).replace(/\s+/g, ' ').trim();
35+
}
36+
37+
function formatBullet(doc, overrideLabel) {
38+
const title = escapeInline(overrideLabel || doc.title || doc.id);
39+
const url = markdownUrl(doc.permalink);
40+
return `- [${title}](${url})`;
41+
}
42+
43+
function getDocByRef(ref, docsById) {
44+
return docsById.get(ref) || null;
45+
}
46+
47+
// Render a category's body: direct doc/link bullets first, then nested
48+
// categories as deeper headings. `depth` is the heading level for nested
49+
// categories (e.g. 3 → "###").
50+
function renderCategoryBody(items, depth, lines, docsById) {
51+
const directBullets = [];
52+
const subcategories = [];
53+
54+
for (const item of items) {
55+
if (typeof item === 'string') {
56+
const doc = getDocByRef(item, docsById);
57+
if (doc) directBullets.push(formatBullet(doc));
58+
} else if (!item || typeof item !== 'object') {
59+
continue;
60+
} else if (item.type === 'doc' || item.type === 'ref') {
61+
const doc = getDocByRef(item.id, docsById);
62+
if (doc) directBullets.push(formatBullet(doc, item.label));
63+
} else if (item.type === 'category') {
64+
subcategories.push(item);
65+
} else if (item.type === 'link' && item.href) {
66+
directBullets.push(
67+
`- [${escapeInline(item.label || item.href)}](${item.href})`
68+
);
69+
}
70+
}
71+
72+
for (const b of directBullets) lines.push(b);
73+
if (directBullets.length) lines.push('');
74+
75+
const heading = '#'.repeat(Math.min(depth, 6));
76+
for (const sub of subcategories) {
77+
lines.push(`${heading} ${escapeInline(sub.label)}`);
78+
lines.push('');
79+
if (sub.link?.type === 'doc') {
80+
const doc = getDocByRef(sub.link.id, docsById);
81+
if (doc) {
82+
lines.push(formatBullet(doc, sub.label));
83+
lines.push('');
84+
}
85+
}
86+
renderCategoryBody(sub.items || [], depth + 1, lines, docsById);
87+
}
88+
}
89+
90+
function siteRootUrl() {
91+
const host = (siteConfig.url || '').replace(/\/$/, '');
92+
const base = siteConfig.baseUrl || '/';
93+
return host + (base.startsWith('/') ? base : '/' + base);
94+
}
95+
96+
function appendVersionsFooter(lines, allVersions) {
97+
if (!allVersions || allVersions.length === 0) return;
98+
const root = siteRootUrl();
99+
lines.push('## Other versions');
100+
lines.push('');
101+
const lastVersion = allVersions.find((v) => v.isLast);
102+
if (lastVersion) {
103+
lines.push(
104+
`- [Latest stable (${lastVersion.label || lastVersion.versionName})](${root}llms.txt)`
105+
);
106+
}
107+
for (const v of allVersions) {
108+
if (v.versionName === 'current') {
109+
lines.push(`- [Bleeding edge (next)](${root}llms-next.txt)`);
110+
} else {
111+
lines.push(`- [${v.label || v.versionName}](${root}llms-${v.versionName}.txt)`);
112+
}
113+
}
114+
lines.push('');
115+
}
116+
117+
function buildContent(version, allVersions) {
118+
const sidebar = version.sidebars?.[sidebarName];
119+
const docsById = new Map();
120+
for (const d of version.docs || []) {
121+
docsById.set(d.id, d);
122+
if (d.unversionedId && !docsById.has(d.unversionedId)) {
123+
docsById.set(d.unversionedId, d);
124+
}
125+
}
126+
127+
const versionLabel = version.label || version.versionName;
128+
const lines = [];
129+
lines.push(`# ${siteConfig.title} Documentation (${versionLabel})`);
130+
lines.push('');
131+
if (siteConfig.tagline) {
132+
lines.push(`> ${siteConfig.tagline}`);
133+
lines.push('');
134+
}
135+
136+
if (!sidebar) {
137+
lines.push(`_No sidebar named "${sidebarName}" found for this version._`);
138+
return lines.join('\n') + '\n';
139+
}
140+
141+
for (const item of sidebar) {
142+
if (typeof item === 'string') {
143+
const doc = getDocByRef(item, docsById);
144+
if (!doc) continue;
145+
lines.push(`## ${escapeInline(doc.title || doc.id)}`);
146+
lines.push('');
147+
lines.push(formatBullet(doc));
148+
lines.push('');
149+
} else if (item.type === 'doc' || item.type === 'ref') {
150+
const doc = getDocByRef(item.id, docsById);
151+
if (!doc) continue;
152+
lines.push(`## ${escapeInline(item.label || doc.title || doc.id)}`);
153+
lines.push('');
154+
lines.push(formatBullet(doc, item.label));
155+
lines.push('');
156+
} else if (item.type === 'category') {
157+
lines.push(`## ${escapeInline(item.label)}`);
158+
lines.push('');
159+
if (item.link?.type === 'doc') {
160+
const doc = getDocByRef(item.link.id, docsById);
161+
if (doc) {
162+
lines.push(formatBullet(doc, item.label));
163+
lines.push('');
164+
}
165+
}
166+
renderCategoryBody(item.items || [], 3, lines, docsById);
167+
} else if (item.type === 'link' && item.href) {
168+
lines.push(`## ${escapeInline(item.label || item.href)}`);
169+
lines.push('');
170+
lines.push(`- [${escapeInline(item.label || item.href)}](${item.href})`);
171+
lines.push('');
172+
}
173+
}
174+
175+
appendVersionsFooter(lines, allVersions);
176+
177+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
178+
}
179+
180+
return {
181+
name: 'docusaurus-plugin-llms-txt',
182+
183+
async allContentLoaded({ allContent }) {
184+
const docsPlugin = allContent?.['docusaurus-plugin-content-docs'];
185+
const docsContent = docsPlugin?.default;
186+
if (!docsContent?.loadedVersions) {
187+
console.warn('[llms-txt] docs plugin content not found; skipping');
188+
return;
189+
}
190+
loadedVersions = docsContent.loadedVersions;
191+
},
192+
193+
async postBuild({ outDir }) {
194+
if (!loadedVersions) {
195+
console.warn('[llms-txt] no loaded versions; skipping');
196+
return;
197+
}
198+
199+
let written = 0;
200+
let lastVersionContent = null;
201+
202+
for (const version of loadedVersions) {
203+
const content = buildContent(version, loadedVersions);
204+
const fileName = fileNameForVersion(version);
205+
const outPath = path.join(outDir, fileName);
206+
fs.writeFileSync(outPath, content);
207+
written++;
208+
if (version.isLast) lastVersionContent = content;
209+
}
210+
211+
if (lastVersionContent) {
212+
fs.writeFileSync(path.join(outDir, 'llms.txt'), lastVersionContent);
213+
written++;
214+
} else {
215+
console.warn('[llms-txt] no version flagged isLast; llms.txt not written');
216+
}
217+
218+
console.log(`[llms-txt] Wrote ${written} llms*.txt files to build/`);
219+
},
220+
};
221+
};

0 commit comments

Comments
 (0)