diff --git a/next.config.js b/next.config.js
index fe88a09a0c..7580eb944c 100644
--- a/next.config.js
+++ b/next.config.js
@@ -19,6 +19,28 @@ const nextConfig = {
scrollRestoration: true,
reactCompiler: true,
},
+ async rewrites() {
+ return [
+ // Serve markdown when Accept header prefers text/markdown
+ // Useful for LLM agents - https://www.skeptrune.com/posts/use-the-accept-header-to-serve-markdown-instead-of-html-to-llms/
+ {
+ source: '/:path*',
+ has: [
+ {
+ type: 'header',
+ key: 'accept',
+ value: '(.*text/markdown.*)',
+ },
+ ],
+ destination: '/api/md/:path*',
+ },
+ // Explicit .md extension also serves markdown
+ {
+ source: '/:path*.md',
+ destination: '/api/md/:path*',
+ },
+ ];
+ },
env: {},
webpack: (config, {dev, isServer, ...options}) => {
if (process.env.ANALYZE) {
diff --git a/package.json b/package.json
index 55fcc0a5b7..8cb257733f 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"classnames": "^2.2.6",
"debounce": "^1.2.1",
"github-slugger": "^1.3.0",
- "next": "15.1.11",
+ "next": "15.1.12",
"next-remote-watch": "^1.0.0",
"parse-numeric-range": "^1.2.0",
"react": "^19.0.0",
diff --git a/src/components/Layout/SidebarNav/SidebarNav.tsx b/src/components/Layout/SidebarNav/SidebarNav.tsx
index 77beb4d729..678d483c14 100644
--- a/src/components/Layout/SidebarNav/SidebarNav.tsx
+++ b/src/components/Layout/SidebarNav/SidebarNav.tsx
@@ -12,7 +12,6 @@
import {Suspense} from 'react';
import * as React from 'react';
import cn from 'classnames';
-import {Feedback} from '../Feedback';
import {SidebarRouteTree} from '../Sidebar/SidebarRouteTree';
import type {RouteItem} from '../getRouteMeta';
@@ -63,9 +62,6 @@ export default function SidebarNav({
-
-
-
diff --git a/src/components/Layout/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx
index ac4bf471c8..15247bb7a7 100644
--- a/src/components/Layout/TopNav/TopNav.tsx
+++ b/src/components/Layout/TopNav/TopNav.tsx
@@ -29,7 +29,6 @@ import {IconHamburger} from 'components/Icon/IconHamburger';
import {IconSearch} from 'components/Icon/IconSearch';
import {Search} from 'components/Search';
import {Logo} from '../../Logo';
-import {Feedback} from '../Feedback';
import {SidebarRouteTree} from '../Sidebar';
import type {RouteItem} from '../getRouteMeta';
import {siteConfig} from 'siteConfig';
@@ -448,9 +447,6 @@ export default function TopNav({
-
-
-
)}
diff --git a/src/pages/api/md/[...path].ts b/src/pages/api/md/[...path].ts
new file mode 100644
index 0000000000..5f80e4e88c
--- /dev/null
+++ b/src/pages/api/md/[...path].ts
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import type {NextApiRequest, NextApiResponse} from 'next';
+import fs from 'fs';
+import path from 'path';
+
+const FOOTER = `
+---
+
+## Sitemap
+
+[Overview of all docs pages](/llms.txt)
+`;
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+ const pathSegments = req.query.path;
+ if (!pathSegments) {
+ return res.status(404).send('Not found');
+ }
+
+ const filePath = Array.isArray(pathSegments)
+ ? pathSegments.join('/')
+ : pathSegments;
+
+ // Block /index.md URLs - use /foo.md instead of /foo/index.md
+ if (filePath.endsWith('/index') || filePath === 'index') {
+ return res.status(404).send('Not found');
+ }
+
+ // Try exact path first, then with /index
+ const candidates = [
+ path.join(process.cwd(), 'src/content', filePath + '.md'),
+ path.join(process.cwd(), 'src/content', filePath, 'index.md'),
+ ];
+
+ for (const fullPath of candidates) {
+ try {
+ const content = fs.readFileSync(fullPath, 'utf8');
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
+ res.setHeader('Cache-Control', 'public, max-age=3600');
+ return res.status(200).send(content + FOOTER);
+ } catch {
+ // Try next candidate
+ }
+ }
+
+ res.status(404).send('Not found');
+}
diff --git a/src/pages/llms.txt.tsx b/src/pages/llms.txt.tsx
new file mode 100644
index 0000000000..23fda9ddf1
--- /dev/null
+++ b/src/pages/llms.txt.tsx
@@ -0,0 +1,269 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import type {GetServerSideProps} from 'next';
+import {siteConfig} from '../siteConfig';
+import sidebarLearn from '../sidebarLearn.json';
+import sidebarReference from '../sidebarReference.json';
+
+interface RouteItem {
+ title?: string;
+ path?: string;
+ routes?: RouteItem[];
+ hasSectionHeader?: boolean;
+ sectionHeader?: string;
+}
+
+interface Sidebar {
+ title: string;
+ routes: RouteItem[];
+}
+
+interface Page {
+ title: string;
+ url: string;
+}
+
+interface SubGroup {
+ heading: string;
+ pages: Page[];
+}
+
+interface Section {
+ heading: string | null;
+ pages: Page[];
+ subGroups: SubGroup[];
+}
+
+// Clean up section header names (remove version placeholders)
+function cleanSectionHeader(header: string): string {
+ return header
+ .replace(/@\{\{version\}\}/g, '')
+ .replace(/-/g, ' ')
+ .split(' ')
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ')
+ .trim();
+}
+
+// Extract routes for sidebars that use hasSectionHeader to define major sections
+// (like the API Reference sidebar)
+function extractSectionedRoutes(
+ routes: RouteItem[],
+ baseUrl: string
+): Section[] {
+ const sections: Section[] = [];
+ let currentSection: Section | null = null;
+
+ for (const route of routes) {
+ // Skip external links
+ if (route.path?.startsWith('http')) {
+ continue;
+ }
+
+ // Start a new section when we hit a section header
+ if (route.hasSectionHeader && route.sectionHeader) {
+ if (currentSection) {
+ sections.push(currentSection);
+ }
+ currentSection = {
+ heading: cleanSectionHeader(route.sectionHeader),
+ pages: [],
+ subGroups: [],
+ };
+ continue;
+ }
+
+ // If no section started yet, skip
+ if (!currentSection) {
+ continue;
+ }
+
+ // Route with children - create a sub-group
+ if (route.title && route.routes && route.routes.length > 0) {
+ const subGroup: SubGroup = {
+ heading: route.title,
+ pages: [],
+ };
+
+ // Include parent page if it has a path
+ if (route.path) {
+ subGroup.pages.push({
+ title: route.title,
+ url: `${baseUrl}${route.path}.md`,
+ });
+ }
+
+ // Add child pages
+ for (const child of route.routes) {
+ if (child.title && child.path && !child.path.startsWith('http')) {
+ subGroup.pages.push({
+ title: child.title,
+ url: `${baseUrl}${child.path}.md`,
+ });
+ }
+ }
+
+ if (subGroup.pages.length > 0) {
+ currentSection.subGroups.push(subGroup);
+ }
+ }
+ // Single page without children
+ else if (route.title && route.path) {
+ currentSection.pages.push({
+ title: route.title,
+ url: `${baseUrl}${route.path}.md`,
+ });
+ }
+ }
+
+ // Don't forget the last section
+ if (currentSection) {
+ sections.push(currentSection);
+ }
+
+ return sections;
+}
+
+// Extract routes for sidebars that use routes with children as the primary grouping
+// (like the Learn sidebar)
+function extractGroupedRoutes(
+ routes: RouteItem[],
+ baseUrl: string
+): SubGroup[] {
+ const groups: SubGroup[] = [];
+
+ for (const route of routes) {
+ // Skip section headers
+ if (route.hasSectionHeader) {
+ continue;
+ }
+
+ // Skip external links
+ if (route.path?.startsWith('http')) {
+ continue;
+ }
+
+ // Route with children - create a group
+ if (route.title && route.routes && route.routes.length > 0) {
+ const pages: Page[] = [];
+
+ // Include parent page if it has a path
+ if (route.path) {
+ pages.push({
+ title: route.title,
+ url: `${baseUrl}${route.path}.md`,
+ });
+ }
+
+ // Add child pages
+ for (const child of route.routes) {
+ if (child.title && child.path && !child.path.startsWith('http')) {
+ pages.push({
+ title: child.title,
+ url: `${baseUrl}${child.path}.md`,
+ });
+ }
+ }
+
+ if (pages.length > 0) {
+ groups.push({
+ heading: route.title,
+ pages,
+ });
+ }
+ }
+ // Single page without children - group under its own heading
+ else if (route.title && route.path) {
+ groups.push({
+ heading: route.title,
+ pages: [
+ {
+ title: route.title,
+ url: `${baseUrl}${route.path}.md`,
+ },
+ ],
+ });
+ }
+ }
+
+ return groups;
+}
+
+// Check if sidebar uses section headers as primary grouping
+function usesSectionHeaders(routes: RouteItem[]): boolean {
+ return routes.some((r) => r.hasSectionHeader && r.sectionHeader);
+}
+
+export const getServerSideProps: GetServerSideProps = async ({res}) => {
+ const subdomain =
+ siteConfig.languageCode === 'en' ? '' : siteConfig.languageCode + '.';
+ const baseUrl = 'https://' + subdomain + 'react.dev';
+
+ const lines = [
+ '# React Documentation',
+ '',
+ '> The library for web and native user interfaces.',
+ ];
+
+ const sidebars: Sidebar[] = [
+ sidebarLearn as Sidebar,
+ sidebarReference as Sidebar,
+ ];
+
+ for (const sidebar of sidebars) {
+ lines.push('');
+ lines.push(`## ${sidebar.title}`);
+
+ if (usesSectionHeaders(sidebar.routes)) {
+ // API Reference style: section headers define major groups
+ const sections = extractSectionedRoutes(sidebar.routes, baseUrl);
+ for (const section of sections) {
+ if (section.heading) {
+ lines.push('');
+ lines.push(`### ${section.heading}`);
+ }
+
+ // Output pages directly under section
+ for (const page of section.pages) {
+ lines.push(`- [${page.title}](${page.url})`);
+ }
+
+ // Output sub-groups with #### headings
+ for (const subGroup of section.subGroups) {
+ lines.push('');
+ lines.push(`#### ${subGroup.heading}`);
+ for (const page of subGroup.pages) {
+ lines.push(`- [${page.title}](${page.url})`);
+ }
+ }
+ }
+ } else {
+ // Learn style: routes with children define groups
+ const groups = extractGroupedRoutes(sidebar.routes, baseUrl);
+ for (const group of groups) {
+ lines.push('');
+ lines.push(`### ${group.heading}`);
+ for (const page of group.pages) {
+ lines.push(`- [${page.title}](${page.url})`);
+ }
+ }
+ }
+ }
+
+ const content = lines.join('\n');
+
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
+ res.write(content);
+ res.end();
+
+ return {props: {}};
+};
+
+export default function LlmsTxt() {
+ return null;
+}
diff --git a/src/styles/index.css b/src/styles/index.css
index 6b2915be48..7bdf4c7659 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -741,13 +741,6 @@ ol.mdx-illustration-block {
}
}
-.exit {
- opacity: 0;
- transition: opacity 500ms ease-out;
- transition-delay: 1s;
- pointer-events: none;
-}
-
.uwu-visible {
display: none;
}
diff --git a/yarn.lock b/yarn.lock
index 775106607d..fdf7958f7b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1259,10 +1259,10 @@
unist-util-visit "^4.0.0"
vfile "^5.0.0"
-"@next/env@15.1.11":
- version "15.1.11"
- resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.11.tgz#599a126f7ce56decc39cea46668cb60d96b66bc6"
- integrity sha512-yp++FVldfLglEG5LoS2rXhGypPyoSOyY0kxZQJ2vnlYJeP8o318t5DrDu5Tqzr03qAhDWllAID/kOCsXNLcwKw==
+"@next/env@15.1.12":
+ version "15.1.12"
+ resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.12.tgz#0821510fa71871f1970cd216963d25a5970fe58a"
+ integrity sha512-EWKXZKsd9ZNn+gLqOtfwH2PQyWuWFmpLldjStw7mZgWgKu56vaqNkAGQrys2g5ER4CNXEDz3Khj2tD1pnsT9Uw==
"@next/eslint-plugin-next@12.0.3":
version "12.0.3"
@@ -5787,12 +5787,12 @@ next-tick@^1.1.0:
resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz"
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
-next@15.1.11:
- version "15.1.11"
- resolved "https://registry.yarnpkg.com/next/-/next-15.1.11.tgz#8a70a236e02d8dd62fb0569bedfd5e4290e7af55"
- integrity sha512-UiVJaOGhKST58AadwbFUZThlNBmYhKqaCs8bVtm4plTxsgKq0mJ0zTsp7t7j/rzsbAEj9WcAMdZCztjByi4EoQ==
+next@15.1.12:
+ version "15.1.12"
+ resolved "https://registry.yarnpkg.com/next/-/next-15.1.12.tgz#6d308fe6cb295faed724481b57f77b8abf4f5468"
+ integrity sha512-fClyhVCGTATGYBnETgKAi7YU5+bSwzM5rqNsY3Dg5wBoBMwE0NSvWA3fzwYj0ijl+LMeiV8P2QAnUFpeqDfTgw==
dependencies:
- "@next/env" "15.1.11"
+ "@next/env" "15.1.12"
"@swc/counter" "0.1.3"
"@swc/helpers" "0.5.15"
caniuse-lite "^1.0.30001579"