Skip to content

Commit b8f927b

Browse files
committed
Implement llms.txt generation endpoint with navigation sections processing
1 parent 82253eb commit b8f927b

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

app/llms.txt/route.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { getNavSections } from "@/services/docs/getNavSections";
2+
import { Config } from "@/util/config";
3+
import { type NavItem, type NavSection } from "@/util/navTransform";
4+
5+
function escapeMarkdownTitle(title: string): string {
6+
return title.replace(/\[/g, '\\[').replace(/\]/g, '\\]');
7+
}
8+
9+
function buildFullUrl(href: string, baseUrl: string): string {
10+
if (href.startsWith('http://') || href.startsWith('https://')) {
11+
return href;
12+
}
13+
return new URL(href, baseUrl).href;
14+
}
15+
16+
function generateLlmTxt(sections: NavSection[], baseUrl: string): string {
17+
const lines: string[] = [];
18+
19+
lines.push('# dotCMS Documentation');
20+
lines.push('');
21+
lines.push(`@base-url: ${baseUrl}`);
22+
lines.push('');
23+
24+
function processItems(items: NavItem[], depth: number = 0): void {
25+
for (const item of items) {
26+
const indent = ' '.repeat(depth);
27+
const hasChildren = item.items && item.items.length > 0;
28+
29+
if (item.href === '#' || !item.href) {
30+
if (hasChildren && item.items) {
31+
if (depth === 0) {
32+
if (lines.length > 0 && lines[lines.length - 1] !== '') {
33+
lines.push('');
34+
}
35+
lines.push(`- ${escapeMarkdownTitle(item.title)}`);
36+
} else {
37+
lines.push(`${indent}- ${escapeMarkdownTitle(item.title)}`);
38+
}
39+
processItems(item.items, depth + 1);
40+
}
41+
} else {
42+
const fullUrl = buildFullUrl(item.href, baseUrl);
43+
const safeTitle = escapeMarkdownTitle(item.title);
44+
45+
lines.push(`${indent}- [${safeTitle}](${fullUrl})`);
46+
47+
if (hasChildren && item.items) {
48+
processItems(item.items, depth + 1);
49+
}
50+
}
51+
}
52+
}
53+
54+
// Process each section (skip empty sections)
55+
for (const section of sections) {
56+
if (!section.items || section.items.length === 0) {
57+
continue;
58+
}
59+
60+
if (lines.length > 0 && lines[lines.length - 1] !== '') {
61+
lines.push('');
62+
}
63+
lines.push(`- ${escapeMarkdownTitle(section.title)}`);
64+
processItems(section.items, 1);
65+
}
66+
67+
return lines.join('\n');
68+
}
69+
70+
export async function GET() {
71+
try {
72+
const sections = await getNavSections({
73+
path: '/docs/nav',
74+
depth: 4,
75+
languageId: 1
76+
});
77+
78+
if (!sections || !Array.isArray(sections) || sections.length === 0) {
79+
throw new Error("Invalid or empty navigation sections returned from getNavSections()");
80+
}
81+
82+
const markdown = generateLlmTxt(sections, Config.CDNHost);
83+
84+
return new Response(markdown, {
85+
headers: {
86+
"Content-Type": "text/markdown; charset=utf-8",
87+
"Cache-Control": "public, max-age=3600",
88+
},
89+
});
90+
} catch (error) {
91+
console.error("Error generating llms.txt:", error);
92+
return new Response(
93+
`# Error\n\nFailed to generate llms.txt: ${error instanceof Error ? error.message : 'Unknown error'}`,
94+
{
95+
status: 500,
96+
headers: {
97+
"Content-Type": "text/markdown; charset=utf-8",
98+
},
99+
}
100+
);
101+
}
102+
}
103+

0 commit comments

Comments
 (0)