Skip to content

Commit c9e7ff3

Browse files
committed
docs(en): merging all conflicts
2 parents 45f817d + a2a19ba commit c9e7ff3

File tree

8 files changed

+354
-25
lines changed

8 files changed

+354
-25
lines changed

next.config.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@ const nextConfig = {
1919
scrollRestoration: true,
2020
reactCompiler: true,
2121
},
22+
async rewrites() {
23+
return [
24+
// Serve markdown when Accept header prefers text/markdown
25+
// Useful for LLM agents - https://www.skeptrune.com/posts/use-the-accept-header-to-serve-markdown-instead-of-html-to-llms/
26+
{
27+
source: '/:path*',
28+
has: [
29+
{
30+
type: 'header',
31+
key: 'accept',
32+
value: '(.*text/markdown.*)',
33+
},
34+
],
35+
destination: '/api/md/:path*',
36+
},
37+
// Explicit .md extension also serves markdown
38+
{
39+
source: '/:path*.md',
40+
destination: '/api/md/:path*',
41+
},
42+
];
43+
},
2244
env: {},
2345
webpack: (config, {dev, isServer, ...options}) => {
2446
if (process.env.ANALYZE) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"classnames": "^2.2.6",
3636
"debounce": "^1.2.1",
3737
"github-slugger": "^1.3.0",
38-
"next": "15.1.11",
38+
"next": "15.1.12",
3939
"next-remote-watch": "^1.0.0",
4040
"parse-numeric-range": "^1.2.0",
4141
"react": "^19.0.0",

src/components/Layout/SidebarNav/SidebarNav.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import {Suspense} from 'react';
1313
import * as React from 'react';
1414
import cn from 'classnames';
15-
import {Feedback} from '../Feedback';
1615
import {SidebarRouteTree} from '../Sidebar/SidebarRouteTree';
1716
import type {RouteItem} from '../getRouteMeta';
1817

@@ -63,9 +62,6 @@ export default function SidebarNav({
6362
</Suspense>
6463
<div className="h-20" />
6564
</nav>
66-
<div className="fixed bottom-0 hidden lg:block">
67-
<Feedback />
68-
</div>
6965
</aside>
7066
</div>
7167
</div>

src/components/Layout/TopNav/TopNav.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {IconHamburger} from 'components/Icon/IconHamburger';
2929
import {IconSearch} from 'components/Icon/IconSearch';
3030
import {Search} from 'components/Search';
3131
import {Logo} from '../../Logo';
32-
import {Feedback} from '../Feedback';
3332
import {SidebarRouteTree} from '../Sidebar';
3433
import type {RouteItem} from '../getRouteMeta';
3534
import {siteConfig} from 'siteConfig';
@@ -448,9 +447,6 @@ export default function TopNav({
448447
</Suspense>
449448
<div className="h-16" />
450449
</nav>
451-
<div className="fixed bottom-0 hidden lg:block">
452-
<Feedback />
453-
</div>
454450
</aside>
455451
</div>
456452
)}

src/pages/api/md/[...path].ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import type {NextApiRequest, NextApiResponse} from 'next';
9+
import fs from 'fs';
10+
import path from 'path';
11+
12+
const FOOTER = `
13+
---
14+
15+
## Sitemap
16+
17+
[Overview of all docs pages](/llms.txt)
18+
`;
19+
20+
export default function handler(req: NextApiRequest, res: NextApiResponse) {
21+
const pathSegments = req.query.path;
22+
if (!pathSegments) {
23+
return res.status(404).send('Not found');
24+
}
25+
26+
const filePath = Array.isArray(pathSegments)
27+
? pathSegments.join('/')
28+
: pathSegments;
29+
30+
// Block /index.md URLs - use /foo.md instead of /foo/index.md
31+
if (filePath.endsWith('/index') || filePath === 'index') {
32+
return res.status(404).send('Not found');
33+
}
34+
35+
// Try exact path first, then with /index
36+
const candidates = [
37+
path.join(process.cwd(), 'src/content', filePath + '.md'),
38+
path.join(process.cwd(), 'src/content', filePath, 'index.md'),
39+
];
40+
41+
for (const fullPath of candidates) {
42+
try {
43+
const content = fs.readFileSync(fullPath, 'utf8');
44+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
45+
res.setHeader('Cache-Control', 'public, max-age=3600');
46+
return res.status(200).send(content + FOOTER);
47+
} catch {
48+
// Try next candidate
49+
}
50+
}
51+
52+
res.status(404).send('Not found');
53+
}

src/pages/llms.txt.tsx

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import type {GetServerSideProps} from 'next';
9+
import {siteConfig} from '../siteConfig';
10+
import sidebarLearn from '../sidebarLearn.json';
11+
import sidebarReference from '../sidebarReference.json';
12+
13+
interface RouteItem {
14+
title?: string;
15+
path?: string;
16+
routes?: RouteItem[];
17+
hasSectionHeader?: boolean;
18+
sectionHeader?: string;
19+
}
20+
21+
interface Sidebar {
22+
title: string;
23+
routes: RouteItem[];
24+
}
25+
26+
interface Page {
27+
title: string;
28+
url: string;
29+
}
30+
31+
interface SubGroup {
32+
heading: string;
33+
pages: Page[];
34+
}
35+
36+
interface Section {
37+
heading: string | null;
38+
pages: Page[];
39+
subGroups: SubGroup[];
40+
}
41+
42+
// Clean up section header names (remove version placeholders)
43+
function cleanSectionHeader(header: string): string {
44+
return header
45+
.replace(/@\{\{version\}\}/g, '')
46+
.replace(/-/g, ' ')
47+
.split(' ')
48+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
49+
.join(' ')
50+
.trim();
51+
}
52+
53+
// Extract routes for sidebars that use hasSectionHeader to define major sections
54+
// (like the API Reference sidebar)
55+
function extractSectionedRoutes(
56+
routes: RouteItem[],
57+
baseUrl: string
58+
): Section[] {
59+
const sections: Section[] = [];
60+
let currentSection: Section | null = null;
61+
62+
for (const route of routes) {
63+
// Skip external links
64+
if (route.path?.startsWith('http')) {
65+
continue;
66+
}
67+
68+
// Start a new section when we hit a section header
69+
if (route.hasSectionHeader && route.sectionHeader) {
70+
if (currentSection) {
71+
sections.push(currentSection);
72+
}
73+
currentSection = {
74+
heading: cleanSectionHeader(route.sectionHeader),
75+
pages: [],
76+
subGroups: [],
77+
};
78+
continue;
79+
}
80+
81+
// If no section started yet, skip
82+
if (!currentSection) {
83+
continue;
84+
}
85+
86+
// Route with children - create a sub-group
87+
if (route.title && route.routes && route.routes.length > 0) {
88+
const subGroup: SubGroup = {
89+
heading: route.title,
90+
pages: [],
91+
};
92+
93+
// Include parent page if it has a path
94+
if (route.path) {
95+
subGroup.pages.push({
96+
title: route.title,
97+
url: `${baseUrl}${route.path}.md`,
98+
});
99+
}
100+
101+
// Add child pages
102+
for (const child of route.routes) {
103+
if (child.title && child.path && !child.path.startsWith('http')) {
104+
subGroup.pages.push({
105+
title: child.title,
106+
url: `${baseUrl}${child.path}.md`,
107+
});
108+
}
109+
}
110+
111+
if (subGroup.pages.length > 0) {
112+
currentSection.subGroups.push(subGroup);
113+
}
114+
}
115+
// Single page without children
116+
else if (route.title && route.path) {
117+
currentSection.pages.push({
118+
title: route.title,
119+
url: `${baseUrl}${route.path}.md`,
120+
});
121+
}
122+
}
123+
124+
// Don't forget the last section
125+
if (currentSection) {
126+
sections.push(currentSection);
127+
}
128+
129+
return sections;
130+
}
131+
132+
// Extract routes for sidebars that use routes with children as the primary grouping
133+
// (like the Learn sidebar)
134+
function extractGroupedRoutes(
135+
routes: RouteItem[],
136+
baseUrl: string
137+
): SubGroup[] {
138+
const groups: SubGroup[] = [];
139+
140+
for (const route of routes) {
141+
// Skip section headers
142+
if (route.hasSectionHeader) {
143+
continue;
144+
}
145+
146+
// Skip external links
147+
if (route.path?.startsWith('http')) {
148+
continue;
149+
}
150+
151+
// Route with children - create a group
152+
if (route.title && route.routes && route.routes.length > 0) {
153+
const pages: Page[] = [];
154+
155+
// Include parent page if it has a path
156+
if (route.path) {
157+
pages.push({
158+
title: route.title,
159+
url: `${baseUrl}${route.path}.md`,
160+
});
161+
}
162+
163+
// Add child pages
164+
for (const child of route.routes) {
165+
if (child.title && child.path && !child.path.startsWith('http')) {
166+
pages.push({
167+
title: child.title,
168+
url: `${baseUrl}${child.path}.md`,
169+
});
170+
}
171+
}
172+
173+
if (pages.length > 0) {
174+
groups.push({
175+
heading: route.title,
176+
pages,
177+
});
178+
}
179+
}
180+
// Single page without children - group under its own heading
181+
else if (route.title && route.path) {
182+
groups.push({
183+
heading: route.title,
184+
pages: [
185+
{
186+
title: route.title,
187+
url: `${baseUrl}${route.path}.md`,
188+
},
189+
],
190+
});
191+
}
192+
}
193+
194+
return groups;
195+
}
196+
197+
// Check if sidebar uses section headers as primary grouping
198+
function usesSectionHeaders(routes: RouteItem[]): boolean {
199+
return routes.some((r) => r.hasSectionHeader && r.sectionHeader);
200+
}
201+
202+
export const getServerSideProps: GetServerSideProps = async ({res}) => {
203+
const subdomain =
204+
siteConfig.languageCode === 'en' ? '' : siteConfig.languageCode + '.';
205+
const baseUrl = 'https://' + subdomain + 'react.dev';
206+
207+
const lines = [
208+
'# React Documentation',
209+
'',
210+
'> The library for web and native user interfaces.',
211+
];
212+
213+
const sidebars: Sidebar[] = [
214+
sidebarLearn as Sidebar,
215+
sidebarReference as Sidebar,
216+
];
217+
218+
for (const sidebar of sidebars) {
219+
lines.push('');
220+
lines.push(`## ${sidebar.title}`);
221+
222+
if (usesSectionHeaders(sidebar.routes)) {
223+
// API Reference style: section headers define major groups
224+
const sections = extractSectionedRoutes(sidebar.routes, baseUrl);
225+
for (const section of sections) {
226+
if (section.heading) {
227+
lines.push('');
228+
lines.push(`### ${section.heading}`);
229+
}
230+
231+
// Output pages directly under section
232+
for (const page of section.pages) {
233+
lines.push(`- [${page.title}](${page.url})`);
234+
}
235+
236+
// Output sub-groups with #### headings
237+
for (const subGroup of section.subGroups) {
238+
lines.push('');
239+
lines.push(`#### ${subGroup.heading}`);
240+
for (const page of subGroup.pages) {
241+
lines.push(`- [${page.title}](${page.url})`);
242+
}
243+
}
244+
}
245+
} else {
246+
// Learn style: routes with children define groups
247+
const groups = extractGroupedRoutes(sidebar.routes, baseUrl);
248+
for (const group of groups) {
249+
lines.push('');
250+
lines.push(`### ${group.heading}`);
251+
for (const page of group.pages) {
252+
lines.push(`- [${page.title}](${page.url})`);
253+
}
254+
}
255+
}
256+
}
257+
258+
const content = lines.join('\n');
259+
260+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
261+
res.write(content);
262+
res.end();
263+
264+
return {props: {}};
265+
};
266+
267+
export default function LlmsTxt() {
268+
return null;
269+
}

0 commit comments

Comments
 (0)