From 3938fbfac5b6e6a2700001ee397a71c8430387fc Mon Sep 17 00:00:00 2001 From: Ricky Date: Tue, 27 Jan 2026 21:58:22 -0500 Subject: [PATCH 1/5] Update deps (#8268) --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) 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/yarn.lock b/yarn.lock index a1ce77d117..b3c69fcac8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1225,10 +1225,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" @@ -5797,12 +5797,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" busboy "1.6.0" From dcc5deb2f767e20c7c3dd9e6c95a98c0b442a9bc Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 28 Jan 2026 03:12:02 +0000 Subject: [PATCH 2/5] Add llms.txt (#8267) Co-authored-by: artimath --- next.config.js | 8 ++++ src/pages/api/md/[...path].ts | 45 +++++++++++++++++++ src/pages/llms.txt.tsx | 85 +++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 src/pages/api/md/[...path].ts create mode 100644 src/pages/llms.txt.tsx diff --git a/next.config.js b/next.config.js index fe88a09a0c..c9dc3ead80 100644 --- a/next.config.js +++ b/next.config.js @@ -19,6 +19,14 @@ const nextConfig = { scrollRestoration: true, reactCompiler: true, }, + async rewrites() { + return [ + { + source: '/:path*.md', + destination: '/api/md/:path*', + }, + ]; + }, env: {}, webpack: (config, {dev, isServer, ...options}) => { if (process.env.ANALYZE) { diff --git a/src/pages/api/md/[...path].ts b/src/pages/api/md/[...path].ts new file mode 100644 index 0000000000..9c0e214285 --- /dev/null +++ b/src/pages/api/md/[...path].ts @@ -0,0 +1,45 @@ +/** + * 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'; + +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); + } 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..a8cae128b6 --- /dev/null +++ b/src/pages/llms.txt.tsx @@ -0,0 +1,85 @@ +/** + * 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'; +import sidebarBlog from '../sidebarBlog.json'; + +interface RouteItem { + title?: string; + path?: string; + routes?: RouteItem[]; +} + +interface Sidebar { + title: string; + routes: RouteItem[]; +} + +function extractRoutes( + routes: RouteItem[], + baseUrl: string +): {title: string; url: string}[] { + const result: {title: string; url: string}[] = []; + + for (const route of routes) { + if (route.title && route.path) { + result.push({ + title: route.title, + url: `${baseUrl}${route.path}.md`, + }); + } + if (route.routes) { + result.push(...extractRoutes(route.routes, baseUrl)); + } + } + + return result; +} + +const sidebars: Sidebar[] = [ + sidebarLearn as Sidebar, + sidebarReference as Sidebar, + sidebarBlog as Sidebar, +]; + +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.', + ]; + + for (const sidebar of sidebars) { + lines.push(''); + lines.push(`## ${sidebar.title}`); + lines.push(''); + + const routes = extractRoutes(sidebar.routes, baseUrl); + for (const route of routes) { + lines.push(`- [${route.title}](${route.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; +} From 61b1f512830d1d4750f38f10685d6c563b89e81d Mon Sep 17 00:00:00 2001 From: Ricky Date: Wed, 28 Jan 2026 11:43:24 -0500 Subject: [PATCH 3/5] Add sections to llms.txt and sitemap footer to *.md (#8270) * Add sections to llms.txt * Also add sitemap footer --- src/pages/api/md/[...path].ts | 10 +- src/pages/llms.txt.tsx | 220 +++++++++++++++++++++++++++++++--- 2 files changed, 211 insertions(+), 19 deletions(-) diff --git a/src/pages/api/md/[...path].ts b/src/pages/api/md/[...path].ts index 9c0e214285..5f80e4e88c 100644 --- a/src/pages/api/md/[...path].ts +++ b/src/pages/api/md/[...path].ts @@ -9,6 +9,14 @@ 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) { @@ -35,7 +43,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { 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); + return res.status(200).send(content + FOOTER); } catch { // Try next candidate } diff --git a/src/pages/llms.txt.tsx b/src/pages/llms.txt.tsx index a8cae128b6..23fda9ddf1 100644 --- a/src/pages/llms.txt.tsx +++ b/src/pages/llms.txt.tsx @@ -9,12 +9,13 @@ import type {GetServerSideProps} from 'next'; import {siteConfig} from '../siteConfig'; import sidebarLearn from '../sidebarLearn.json'; import sidebarReference from '../sidebarReference.json'; -import sidebarBlog from '../sidebarBlog.json'; interface RouteItem { title?: string; path?: string; routes?: RouteItem[]; + hasSectionHeader?: boolean; + sectionHeader?: string; } interface Sidebar { @@ -22,32 +23,181 @@ interface Sidebar { routes: RouteItem[]; } -function extractRoutes( +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 -): {title: string; url: string}[] { - const result: {title: string; url: string}[] = []; +): Section[] { + const sections: Section[] = []; + let currentSection: Section | null = null; for (const route of routes) { - if (route.title && route.path) { - result.push({ + // 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`, }); } - if (route.routes) { - result.push(...extractRoutes(route.routes, baseUrl)); + } + + // 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 result; + return groups; } -const sidebars: Sidebar[] = [ - sidebarLearn as Sidebar, - sidebarReference as Sidebar, - sidebarBlog as Sidebar, -]; +// 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 = @@ -60,14 +210,48 @@ export const getServerSideProps: GetServerSideProps = async ({res}) => { '> 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}`); - lines.push(''); - const routes = extractRoutes(sidebar.routes, baseUrl); - for (const route of routes) { - lines.push(`- [${route.title}](${route.url})`); + 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})`); + } + } } } From d340c41ba50d44e34a59488207692e58debe1f37 Mon Sep 17 00:00:00 2001 From: Ricky Date: Wed, 28 Jan 2026 11:44:02 -0500 Subject: [PATCH 4/5] Remove feedback (#8271) --- src/components/Layout/Feedback.tsx | 105 ------------------ .../Layout/SidebarNav/SidebarNav.tsx | 4 - src/components/Layout/TopNav/TopNav.tsx | 4 - src/styles/index.css | 7 -- 4 files changed, 120 deletions(-) delete mode 100644 src/components/Layout/Feedback.tsx diff --git a/src/components/Layout/Feedback.tsx b/src/components/Layout/Feedback.tsx deleted file mode 100644 index fe92725170..0000000000 --- a/src/components/Layout/Feedback.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * 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. - */ - -/* - * Copyright (c) Facebook, Inc. and its affiliates. - */ - -import {useState} from 'react'; -import {useRouter} from 'next/router'; -import cn from 'classnames'; - -export function Feedback({onSubmit = () => {}}: {onSubmit?: () => void}) { - const {asPath} = useRouter(); - const cleanedPath = asPath.split(/[\?\#]/)[0]; - // Reset on route changes. - return ; -} - -const thumbsUpIcon = ( - - - -); - -const thumbsDownIcon = ( - - - -); - -function sendGAEvent(isPositive: boolean) { - const category = isPositive ? 'like_button' : 'dislike_button'; - const value = isPositive ? 1 : 0; - // Fragile. Don't change unless you've tested the network payload - // and verified that the right events actually show up in GA. - // @ts-ignore - gtag('event', 'feedback', { - event_category: category, - event_label: window.location.pathname, - event_value: value, - }); -} - -function SendFeedback({onSubmit}: {onSubmit: () => void}) { - const [isSubmitted, setIsSubmitted] = useState(false); - return ( -
-

- {isSubmitted ? 'Thank you for your feedback!' : 'Is this page useful?'} -

- {!isSubmitted && ( - - )} - {!isSubmitted && ( - - )} -
- ); -} 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 148098933d..efc90ed2c7 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/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; } From a2a19bae5f3ea54496915979fcfd01a9738d07c3 Mon Sep 17 00:00:00 2001 From: Joseph Date: Wed, 28 Jan 2026 21:48:01 +0100 Subject: [PATCH 5/5] feat: Add Accept header content negotiation for markdown (#8272) --- next.config.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/next.config.js b/next.config.js index c9dc3ead80..7580eb944c 100644 --- a/next.config.js +++ b/next.config.js @@ -21,6 +21,20 @@ const nextConfig = { }, 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*',