Skip to content

Commit 5ce8ccb

Browse files
jcsteinjcsteingemini-code-assist[bot]
authored
feat: add LLMs workflow (#2332)
* feat: add LLMs workflow * feat: new workflow to discover pages on each build for llms.txt * fix: rm llms.txt and add to gitignore * docs: rm llms.txt * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Apply suggestions from code review --------- Co-authored-by: jcstein <[email protected]> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 5690e4e commit 5ce8ccb

File tree

4 files changed

+99
-1
lines changed

4 files changed

+99
-1
lines changed

.github/workflows/preview.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ jobs:
2727
- name: Install dependencies
2828
run: yarn install --frozen-lockfile
2929

30+
- name: Generate llms.txt
31+
run: yarn generate:llms
32+
3033
- name: Build static site
3134
env:
3235
BASE: /docs-preview/new_docs/

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ _pagefind/
4545

4646
# agents llms
4747
AGENTS.md
48+
/public/llms.txt

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"postbuild:static": "npm run postbuild",
1212
"start": "next start",
1313
"lint": "eslint",
14-
"prepare": "simple-git-hooks"
14+
"prepare": "simple-git-hooks",
15+
"generate:llms": "node scripts/generate-llms.js"
1516
},
1617
"simple-git-hooks": {
1718
"pre-push": "npm run lint"

scripts/generate-llms.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env node
2+
3+
import { promises as fs } from 'fs';
4+
import path from 'path';
5+
6+
const RAW_BASE = 'https://raw.githubusercontent.com/celestiaorg/docs/main/';
7+
8+
9+
const header = [
10+
'# Celestia documentation',
11+
'',
12+
'> Official documentation for Celestia, the modular blockchain powering unstoppable apps with full-stack control.',
13+
'',
14+
'These docs are built with Next.js + Nextra and exported statically. Links below point to the raw MDX sources in `main` so tools and LLMs can ingest clean text.',
15+
];
16+
17+
const titleize = (segment) =>
18+
segment
19+
.split('-')
20+
.filter(Boolean)
21+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
22+
.join(' ');
23+
24+
const formatItem = (path, label) => `- [${label}](${RAW_BASE}${path})`;
25+
26+
const walkPages = async (dir) => {
27+
const pages = [];
28+
const entries = await fs.readdir(dir, { withFileTypes: true });
29+
for (const entry of entries) {
30+
const full = path.join(dir, entry.name);
31+
if (entry.isDirectory()) {
32+
pages.push(...(await walkPages(full)));
33+
} else if (entry.isFile() && entry.name === 'page.mdx') {
34+
pages.push(full);
35+
}
36+
}
37+
return pages;
38+
};
39+
40+
const buildSections = async () => {
41+
const files = (await walkPages('app')).sort();
42+
43+
const grouped = new Map();
44+
45+
for (const file of files) {
46+
const rel = file.replace(/^app\//, '');
47+
const parts = rel.split('/');
48+
if (parts.length < 2) continue; // skip unexpected roots
49+
50+
const [top, second, ...rest] = parts;
51+
// Section title: "Top" or "Top: Second"
52+
const sectionTitle = second ? `${titleize(top)}: ${titleize(second)}` : titleize(top);
53+
54+
// Item label: remaining path segments (excluding page.mdx)
55+
const leafParts = rest.slice(0, -1); // drop page.mdx
56+
const labelSegments = leafParts.length ? leafParts : [second ?? top];
57+
const label = labelSegments.map(titleize).join(' / ');
58+
59+
if (!grouped.has(sectionTitle)) grouped.set(sectionTitle, []);
60+
grouped.get(sectionTitle).push({ path: file, label });
61+
}
62+
63+
// Sort sections and items for stability
64+
const sortedSections = [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b));
65+
return sortedSections.map(([title, items]) => ({
66+
title,
67+
items: items.sort((a, b) => a.path.localeCompare(b.path)),
68+
}));
69+
};
70+
71+
const main = async () => {
72+
const sections = await buildSections();
73+
const lines = [...header, ''];
74+
sections.forEach((section) => {
75+
if (!section.items.length) return;
76+
lines.push(`## ${section.title}`, '');
77+
section.items.forEach((item) => lines.push(formatItem(item.path, item.label)));
78+
lines.push('');
79+
});
80+
81+
const output = lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
82+
83+
// Write to public/ so it gets published with static export (serves at /llms.txt).
84+
await fs.mkdir('public', { recursive: true });
85+
await fs.writeFile('public/llms.txt', output, 'utf8');
86+
87+
console.log('llms.txt generated (public/)');
88+
};
89+
90+
main().catch((err) => {
91+
console.error(err);
92+
process.exit(1);
93+
});

0 commit comments

Comments
 (0)