Skip to content

Commit 90ae66f

Browse files
committed
refactor: migrate to App Router and enable Cache Components
Migrates the docs site from the Pages Router to the App Router and enables Cache Components (`experimental.cacheComponents` now top-level `cacheComponents: true`). Route tree (`src/app/`): - `layout.tsx` — root layout (theme/uwu init script, fonts, GA, scroll restoration); replaces _document.tsx + _app.tsx - `page.tsx` — home (`section: 'home'`) - `{learn,reference,community,blog}/[[...slug]]/page.tsx` — section- specific catch-all docs pages (each owns its sidebar tree, section literal, and metadata generator) - `warnings/[slug]/page.tsx`, `versions/page.tsx` — flat sections - `errors/page.tsx`, `errors/[errorCode]/page.tsx` — error decoder - `not-found.tsx`, `error.tsx` — error/404 boundaries - `api/md/[...path]/route.ts`, `llms.txt/route.ts` — Route Handlers replacing the old API/static endpoints - `renderSectionPage.tsx`, `DocsPage.tsx`, `clientEffects.tsx` — shared route helpers (server + client respectively) Shared helpers (`src/lib/`, server-only): - `readMarkdownPage.ts` — MDX read + compile from src/content - `collectPaths.ts` — generateStaticParams enumeration - `buildPageMetadata.ts` — canonical URL + hreflang alternates + open graph (replaces the old <Seo> component) - `loadErrorDecoderData.ts` — error-codes fetch + MDX compile Component-level changes: - `next/router` → `next/navigation` across Page.tsx, Search.tsx, PageHeading.tsx, TopNav.tsx, SidebarRouteTree.tsx, ExpandableExample.tsx, Challenges.tsx - `Page.tsx` takes `section` and `pathname` as props instead of sniffing them at runtime via `usePathname`; `<Seo>` and `<Head>` removed (replaced by App Router Metadata API) - `MDXComponents.tsx` is now `'use client'` so the registry can host client-only components (Sandpack etc.); a parallel server-safe `MDXComponentsList.ts` keeps the component name list reachable from `compileMDX.ts` - `useDeserializedMDX.tsx` — shared hook for revival of the serialized React tree on the client - `usePendingRoute.ts` is a no-op (App Router has no equivalent of `router.events.routeChangeStart`; <Link> handles transitions) next.config.js: - `cacheComponents: true` and `reactCompiler: true` lifted out of `experimental` - `serverExternalPackages` lists the Babel + MDX deps that compileMDX loads via runtime `require` (otherwise Next bundles them and breaks inside the cache scope) - `turbopack: {}` placeholder so the build picks the webpack pipeline (the project's existing webpack config + Sandpack `raw-loader` imports aren't Turbopack-compatible yet) Cache Components adoption: - `'use cache'` on every page default export and `generateMetadata` (including `/errors/[errorCode]`) - Zero `export const instant = false` opt-outs - Build output: 816 routes, all Static or Partial Prerender, only `/api/md/[...path]` is dynamic Other: - `use(promise)` polyfill in HomeContent.js dropped in favour of React 19's built-in `use` - `ErrorDecoderContext.tsx` doc comment refreshed to reflect that context now flows from a server component, not getStaticProps - `worker-bundle.dist.js` regenerated by scripts/buildRscWorker.mjs with the updated React 19.2 runtime (much smaller)
1 parent 4ed838c commit 90ae66f

44 files changed

Lines changed: 1358 additions & 33198 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

next.config.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,22 @@
1515
const nextConfig = {
1616
pageExtensions: ['jsx', 'js', 'ts', 'tsx', 'mdx', 'md'],
1717
reactStrictMode: true,
18-
experimental: {
19-
scrollRestoration: true,
20-
reactCompiler: true,
21-
},
18+
reactCompiler: true,
19+
cacheComponents: true,
20+
serverExternalPackages: [
21+
'@babel/core',
22+
'@babel/plugin-transform-modules-commonjs',
23+
'@babel/preset-react',
24+
'@mdx-js/mdx',
25+
'metro-cache',
26+
'gray-matter',
27+
'unist-util-visit',
28+
'remark-gfm',
29+
'remark-frontmatter',
30+
],
31+
// Custom webpack config: opt out of Turbopack for builds.
32+
// TODO: migrate webpack tweaks to Turbopack config.
33+
turbopack: {},
2234
async rewrites() {
2335
return {
2436
beforeFiles: [

src/app/DocsPage.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
'use client';
9+
10+
import {Page, type PageSection} from 'components/Layout/Page';
11+
import {useDeserializedMDX} from 'components/Layout/useDeserializedMDX';
12+
import type {RouteItem} from 'components/Layout/getRouteMeta';
13+
import type {PageData} from 'lib/readMarkdownPage';
14+
15+
interface DocsPageProps {
16+
data: PageData;
17+
pathname: string;
18+
section: PageSection;
19+
routeTree: RouteItem;
20+
}
21+
22+
export function DocsPage({data, pathname, section, routeTree}: DocsPageProps) {
23+
const {parsedContent, parsedToc} = useDeserializedMDX(data.content, data.toc);
24+
return (
25+
<Page
26+
toc={parsedToc}
27+
routeTree={routeTree}
28+
meta={data.meta}
29+
section={section}
30+
pathname={pathname}
31+
languages={data.languages}>
32+
{parsedContent}
33+
</Page>
34+
);
35+
}
Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import type {NextApiRequest, NextApiResponse} from 'next';
98
import fs from 'fs';
109
import path from 'path';
10+
import {NextResponse} from 'next/server';
1111

1212
const FOOTER = `
1313
---
@@ -17,22 +17,22 @@ const FOOTER = `
1717
[Overview of all docs pages](/llms.txt)
1818
`;
1919

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');
20+
export async function GET(
21+
_req: Request,
22+
ctx: {params: Promise<{path: string[]}>}
23+
) {
24+
const {path: pathSegments} = await ctx.params;
25+
if (!pathSegments || pathSegments.length === 0) {
26+
return new NextResponse('Not found', {status: 404});
2427
}
2528

26-
const filePath = Array.isArray(pathSegments)
27-
? pathSegments.join('/')
28-
: pathSegments;
29+
const filePath = pathSegments.join('/');
2930

3031
// Block /index.md URLs - use /foo.md instead of /foo/index.md
3132
if (filePath.endsWith('/index') || filePath === 'index') {
32-
return res.status(404).send('Not found');
33+
return new NextResponse('Not found', {status: 404});
3334
}
3435

35-
// Try exact path first, then with /index
3636
const candidates = [
3737
path.join(process.cwd(), 'src/content', filePath + '.md'),
3838
path.join(process.cwd(), 'src/content', filePath, 'index.md'),
@@ -41,13 +41,17 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
4141
for (const fullPath of candidates) {
4242
try {
4343
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);
44+
return new NextResponse(content + FOOTER, {
45+
status: 200,
46+
headers: {
47+
'Content-Type': 'text/plain; charset=utf-8',
48+
'Cache-Control': 'public, max-age=3600',
49+
},
50+
});
4751
} catch {
4852
// Try next candidate
4953
}
5054
}
5155

52-
res.status(404).send('Not found');
56+
return new NextResponse('Not found', {status: 404});
5357
}

src/app/blog/[[...slug]]/page.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 {Metadata} from 'next';
9+
import sidebarBlog from '../../../sidebarBlog.json';
10+
import type {RouteItem} from 'components/Layout/getRouteMeta';
11+
import {collectSectionPaths} from 'lib/collectPaths';
12+
import {renderSectionPage, sectionPageMetadata} from '../../renderSectionPage';
13+
14+
interface PageProps {
15+
params: Promise<{slug?: string[]}>;
16+
}
17+
18+
export async function generateStaticParams() {
19+
const paths = await collectSectionPaths('blog');
20+
return paths.map((slug) => ({slug}));
21+
}
22+
23+
export async function generateMetadata({params}: PageProps): Promise<Metadata> {
24+
'use cache';
25+
const {slug} = await params;
26+
return sectionPageMetadata({
27+
section: 'blog',
28+
segments: ['blog', ...(slug ?? [])],
29+
});
30+
}
31+
32+
export default async function BlogPage({params}: PageProps) {
33+
'use cache';
34+
const {slug} = await params;
35+
return renderSectionPage({
36+
section: 'blog',
37+
segments: ['blog', ...(slug ?? [])],
38+
routeTree: sidebarBlog as RouteItem,
39+
});
40+
}

src/app/clientEffects.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
'use client';
9+
10+
import {useEffect} from 'react';
11+
import {usePathname} from 'next/navigation';
12+
13+
declare const gtag: (...args: any[]) => void;
14+
15+
export function AnalyticsTracker() {
16+
const pathname = usePathname();
17+
18+
useEffect(() => {
19+
if (typeof window === 'undefined' || typeof gtag === 'undefined') return;
20+
gtag('event', 'pageview', {event_label: pathname});
21+
}, [pathname]);
22+
23+
useEffect(() => {
24+
if (typeof window === 'undefined') return;
25+
const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'unload';
26+
const handler = () => {
27+
if (typeof gtag !== 'undefined') {
28+
gtag('event', 'timing', {
29+
event_label: 'JS Dependencies',
30+
event: 'unload',
31+
});
32+
}
33+
};
34+
window.addEventListener(terminationEvent, handler);
35+
return () => window.removeEventListener(terminationEvent, handler);
36+
}, []);
37+
38+
return null;
39+
}
40+
41+
export function ScrollRestoration() {
42+
useEffect(() => {
43+
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
44+
if (isSafari) {
45+
history.scrollRestoration = 'auto';
46+
}
47+
}, []);
48+
return null;
49+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 {Metadata} from 'next';
9+
import sidebarCommunity from '../../../sidebarCommunity.json';
10+
import type {RouteItem} from 'components/Layout/getRouteMeta';
11+
import {collectSectionPaths} from 'lib/collectPaths';
12+
import {renderSectionPage, sectionPageMetadata} from '../../renderSectionPage';
13+
14+
interface PageProps {
15+
params: Promise<{slug?: string[]}>;
16+
}
17+
18+
export async function generateStaticParams() {
19+
const paths = await collectSectionPaths('community');
20+
return paths.map((slug) => ({slug}));
21+
}
22+
23+
export async function generateMetadata({params}: PageProps): Promise<Metadata> {
24+
'use cache';
25+
const {slug} = await params;
26+
return sectionPageMetadata({
27+
section: 'community',
28+
segments: ['community', ...(slug ?? [])],
29+
});
30+
}
31+
32+
export default async function CommunityPage({params}: PageProps) {
33+
'use cache';
34+
const {slug} = await params;
35+
return renderSectionPage({
36+
section: 'community',
37+
segments: ['community', ...(slug ?? [])],
38+
routeTree: sidebarCommunity as RouteItem,
39+
});
40+
}

src/pages/500.js renamed to src/app/error.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,32 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
/*
9-
* Copyright (c) Facebook, Inc. and its affiliates.
10-
*/
8+
'use client';
119

10+
import {useEffect} from 'react';
1211
import {Page} from 'components/Layout/Page';
1312
import {MDXComponents} from 'components/MDX/MDXComponents';
1413
import sidebarLearn from '../sidebarLearn.json';
14+
import type {RouteItem} from 'components/Layout/getRouteMeta';
1515

1616
const {Intro, MaxWidth, p: P, a: A} = MDXComponents;
1717

18-
export default function NotFound() {
18+
export default function GlobalError({
19+
error,
20+
}: {
21+
error: Error & {digest?: string};
22+
reset: () => void;
23+
}) {
24+
useEffect(() => {
25+
console.error(error);
26+
}, [error]);
27+
1928
return (
2029
<Page
2130
toc={[]}
22-
routeTree={sidebarLearn}
31+
routeTree={sidebarLearn as RouteItem}
32+
section="unknown"
33+
pathname="/500"
2334
meta={{title: 'Something Went Wrong'}}>
2435
<MaxWidth>
2536
<Intro>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
'use client';
9+
10+
import {Page} from 'components/Layout/Page';
11+
import {useDeserializedMDX} from 'components/Layout/useDeserializedMDX';
12+
import {ErrorDecoderContext} from 'components/ErrorDecoderContext';
13+
import sidebarLearn from '../../sidebarLearn.json';
14+
import type {RouteItem} from 'components/Layout/getRouteMeta';
15+
import type {ErrorDecoderData} from 'lib/loadErrorDecoderData';
16+
17+
interface ErrorDecoderViewProps {
18+
data: ErrorDecoderData;
19+
pathname: string;
20+
}
21+
22+
export function ErrorDecoderView({data, pathname}: ErrorDecoderViewProps) {
23+
const {parsedContent} = useDeserializedMDX(data.content, data.toc);
24+
return (
25+
<ErrorDecoderContext
26+
value={{errorMessage: data.errorMessage, errorCode: data.errorCode}}>
27+
<Page
28+
toc={[]}
29+
meta={{
30+
title: data.errorCode
31+
? `Minified React error #${data.errorCode}`
32+
: 'Minified Error Decoder',
33+
}}
34+
routeTree={sidebarLearn as RouteItem}
35+
section="unknown"
36+
pathname={pathname}>
37+
<div>{parsedContent}</div>
38+
</Page>
39+
</ErrorDecoderContext>
40+
);
41+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 {Metadata} from 'next';
9+
import {listErrorCodes, loadErrorDecoderData} from 'lib/loadErrorDecoderData';
10+
import {ErrorDecoderView} from '../ErrorDecoderView';
11+
12+
interface PageProps {
13+
params: Promise<{errorCode: string}>;
14+
}
15+
16+
export async function generateStaticParams() {
17+
const codes = await listErrorCodes();
18+
return codes.map((errorCode) => ({errorCode}));
19+
}
20+
21+
export async function generateMetadata({params}: PageProps): Promise<Metadata> {
22+
'use cache';
23+
const {errorCode} = await params;
24+
return {title: `Minified React error #${errorCode}`};
25+
}
26+
27+
export default async function ErrorDecoderPage({params}: PageProps) {
28+
'use cache';
29+
const {errorCode} = await params;
30+
const data = await loadErrorDecoderData(errorCode);
31+
return <ErrorDecoderView data={data} pathname={`/errors/${errorCode}`} />;
32+
}

src/app/errors/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 {Metadata} from 'next';
9+
import {loadErrorDecoderData} from 'lib/loadErrorDecoderData';
10+
import {ErrorDecoderView} from './ErrorDecoderView';
11+
12+
export const metadata: Metadata = {
13+
title: 'Minified Error Decoder',
14+
};
15+
16+
export default async function ErrorDecoderIndex() {
17+
'use cache';
18+
const data = await loadErrorDecoderData(null);
19+
return <ErrorDecoderView data={data} pathname="/errors" />;
20+
}

0 commit comments

Comments
 (0)