Skip to content

Commit 4c4a0ca

Browse files
committed
fixed issues raised by CCR
1 parent cae340d commit 4c4a0ca

3 files changed

Lines changed: 185 additions & 8 deletions

File tree

.changeset/shiny-pans-help.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ export async function GET() {
2020
}
2121
```
2222

23-
Important: Ensure that `title`, `description` and `keyword` fields are populated in your contents frontmatter.
23+
Important: Ensure that `title`, `description` and `keywords` fields are populated in your content's frontmatter.

packages/theme/llms.test.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import {describe, it, expect, vi, beforeEach} from 'vitest'
2+
import type {MdxFile, Folder, MetaJsonFile, PageMapItem} from 'nextra'
3+
4+
vi.mock('nextra/page-map', () => ({
5+
getPageMap: vi.fn(),
6+
}))
7+
8+
import {getPageMap} from 'nextra/page-map'
9+
import {generateLLMsTxt} from './llms'
10+
11+
const mockGetPageMap = vi.mocked(getPageMap)
12+
13+
function createMockMdxFile(overrides: Partial<MdxFile> & {name: string; route: string}): MdxFile {
14+
return {frontMatter: {}, ...overrides}
15+
}
16+
17+
function createMockFolder(name: string, route: string, children: PageMapItem[]): Folder {
18+
return {name, route, children}
19+
}
20+
21+
const metaFile: MetaJsonFile = {data: {'index.mdx': 'Home'}}
22+
23+
beforeEach(() => {
24+
vi.restoreAllMocks()
25+
delete process.env.NEXT_PUBLIC_SITE_TITLE
26+
})
27+
28+
describe('generateLLMsTxt', () => {
29+
it('infers title and description from homepage frontmatter', async () => {
30+
mockGetPageMap.mockResolvedValue([
31+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'My Docs', description: 'A docs site'}}),
32+
])
33+
34+
const result = await generateLLMsTxt()
35+
36+
expect(result).toContain('# My Docs')
37+
expect(result).toContain('> A docs site')
38+
})
39+
40+
it('falls back to NEXT_PUBLIC_SITE_TITLE env var', async () => {
41+
process.env.NEXT_PUBLIC_SITE_TITLE = 'Env Title'
42+
mockGetPageMap.mockResolvedValue([createMockMdxFile({name: 'index', route: '/'})])
43+
44+
const result = await generateLLMsTxt()
45+
46+
expect(result).toContain('# Env Title')
47+
})
48+
49+
it('falls back to "Documentation" when no title source exists', async () => {
50+
mockGetPageMap.mockResolvedValue([createMockMdxFile({name: 'index', route: '/'})])
51+
52+
const result = await generateLLMsTxt()
53+
54+
expect(result).toContain('# Documentation')
55+
expect(result).not.toContain('>')
56+
})
57+
58+
it('lists pages with title and description', async () => {
59+
mockGetPageMap.mockResolvedValue([
60+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
61+
createMockMdxFile({name: 'guide', route: '/guide', frontMatter: {title: 'Guide', description: 'A guide'}}),
62+
])
63+
64+
const result = await generateLLMsTxt()
65+
66+
expect(result).toContain('- [Home](/)')
67+
expect(result).toContain('- [Guide](/guide): A guide')
68+
})
69+
70+
it('appends tab-label to title', async () => {
71+
mockGetPageMap.mockResolvedValue([
72+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
73+
createMockMdxFile({
74+
name: 'button',
75+
route: '/button',
76+
frontMatter: {title: 'Button', 'tab-label': 'React'},
77+
}),
78+
])
79+
80+
const result = await generateLLMsTxt()
81+
82+
expect(result).toContain('- [Button - React](/button)')
83+
})
84+
85+
it('includes keywords when present', async () => {
86+
mockGetPageMap.mockResolvedValue([
87+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
88+
createMockMdxFile({
89+
name: 'testimonials',
90+
route: '/testimonials',
91+
frontMatter: {title: 'Testimonials', keywords: ['quotes', 'reviews']},
92+
}),
93+
])
94+
95+
const result = await generateLLMsTxt()
96+
97+
expect(result).toContain('- [Testimonials](/testimonials) (quotes, reviews)')
98+
})
99+
100+
it('omits keywords when array is empty', async () => {
101+
mockGetPageMap.mockResolvedValue([
102+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
103+
createMockMdxFile({name: 'page', route: '/page', frontMatter: {title: 'Page', keywords: []}}),
104+
])
105+
106+
const result = await generateLLMsTxt()
107+
108+
const line = result.split('\n').find(l => l.includes('[Page]'))
109+
expect(line).toBe('- [Page](/page)')
110+
})
111+
112+
it('recursively collects pages from nested folders', async () => {
113+
mockGetPageMap.mockResolvedValue([
114+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
115+
createMockFolder('components', '/components', [
116+
createMockMdxFile({name: 'button', route: '/components/button', frontMatter: {title: 'Button'}}),
117+
createMockFolder('patterns', '/components/patterns', [
118+
createMockMdxFile({name: 'forms', route: '/components/patterns/forms', frontMatter: {title: 'Forms'}}),
119+
]),
120+
]),
121+
])
122+
123+
const result = await generateLLMsTxt()
124+
125+
expect(result).toContain('- [Button](/components/button)')
126+
expect(result).toContain('- [Forms](/components/patterns/forms)')
127+
})
128+
129+
it('skips MetaJsonFile entries', async () => {
130+
mockGetPageMap.mockResolvedValue([
131+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
132+
metaFile,
133+
])
134+
135+
const result = await generateLLMsTxt()
136+
137+
expect(result).not.toContain('index.mdx')
138+
expect(result).toContain('- [Home](/)')
139+
})
140+
141+
it('uses file name when title is missing from frontmatter', async () => {
142+
mockGetPageMap.mockResolvedValue([
143+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
144+
createMockMdxFile({name: 'untitled-page', route: '/untitled-page'}),
145+
])
146+
147+
const result = await generateLLMsTxt()
148+
149+
expect(result).toContain('- [untitled-page](/untitled-page)')
150+
})
151+
152+
it('combines description and keywords in one entry', async () => {
153+
mockGetPageMap.mockResolvedValue([
154+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
155+
createMockMdxFile({
156+
name: 'hero',
157+
route: '/hero',
158+
frontMatter: {title: 'Hero', description: 'Hero section', keywords: ['banner', 'header']},
159+
}),
160+
])
161+
162+
const result = await generateLLMsTxt()
163+
164+
expect(result).toContain('- [Hero](/hero): Hero section (banner, header)')
165+
})
166+
167+
it('prepends basePath to routes when NEXT_PUBLIC_DOCTOCAT_BASE_PATH is set', async () => {
168+
process.env.NEXT_PUBLIC_DOCTOCAT_BASE_PATH = '/doctocat-nextjs'
169+
mockGetPageMap.mockResolvedValue([
170+
createMockMdxFile({name: 'index', route: '/', frontMatter: {title: 'Home'}}),
171+
createMockMdxFile({name: 'guide', route: '/guide', frontMatter: {title: 'Guide'}}),
172+
])
173+
174+
const result = await generateLLMsTxt()
175+
176+
expect(result).toContain('- [Home](/doctocat-nextjs/)')
177+
expect(result).toContain('- [Guide](/doctocat-nextjs/guide)')
178+
})
179+
})

packages/theme/llms.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import {getPageMap} from 'nextra/page-map'
2-
import type {PageMapItem, MdxFile, Folder} from 'nextra'
2+
import type {PageMapItem, MdxFile} from 'nextra'
3+
import {hasChildren} from './helpers/hasChildren'
34

45
function isMdxFile(item: PageMapItem): item is MdxFile {
56
return 'route' in item && 'name' in item && !('children' in item) && !('data' in item)
67
}
78

8-
function isFolder(item: PageMapItem): item is Folder {
9-
return 'children' in item
10-
}
11-
129
function getTitle(item: MdxFile): string {
1310
const title = (item.frontMatter?.title as string) || item.name
1411
const tabLabel = item.frontMatter?.['tab-label'] as string | undefined
@@ -29,7 +26,7 @@ function getPages(items: PageMapItem[]): MdxFile[] {
2926
for (const item of items) {
3027
if (isMdxFile(item)) {
3128
pages.push(item)
32-
} else if (isFolder(item)) {
29+
} else if (hasChildren(item)) {
3330
pages.push(...getPages(item.children))
3431
}
3532
}
@@ -39,6 +36,7 @@ function getPages(items: PageMapItem[]): MdxFile[] {
3936
export async function generateLLMsTxt(): Promise<string> {
4037
const pageMap = await getPageMap()
4138
const pages = getPages(pageMap)
39+
const basePath = process.env.NEXT_PUBLIC_DOCTOCAT_BASE_PATH || ''
4240

4341
// use homepage to auto-infer site title and descriptions. most users should have filled these out.
4442
const homepage = pages.find(page => page.route === '/')
@@ -58,7 +56,7 @@ export async function generateLLMsTxt(): Promise<string> {
5856
const pageTitle = getTitle(page)
5957
const pageDesc = getDescription(page)
6058
const keywords = getKeywords(page)
61-
const route = page.route || '/'
59+
const route = `${basePath}${page.route || '/'}`
6260

6361
let entry = `- [${pageTitle}](${route})`
6462
if (pageDesc) entry += `: ${pageDesc}`

0 commit comments

Comments
 (0)