Skip to content

Commit 0eb0f88

Browse files
feat: migrate React.dev to the App Router (#7437)
* update version to latest + move folders around * getting home page working * make the mdx setup work * bypass mdxname * split out mdx components * re-add meta mdx logic * replace mdxName usage * fix code blocks * fix max width * convert mdx post processing to actual plugins * fix tailwind * fix incorrect iframe props * cleanup mdx dic * make it actually build * align fonts * fix uwu script * fix search * remove _app * make it actually build * replace next-watch-remote with custom setup * clean up logs + clean up inline scripts * add rss handler * remove rss generation * remove rss generation * support MDX components for TOC * clean up log + bump cache * fix toc * add back translations + add new overlay * use MDX link instead of Next.js links for translation * fix analytics * add myself to the contributors * fix blinking sidebar * avoid rendering toc on the client * plugin metadata * simplify metadata * fix title * clean up metadata * add back error decoder * Update src/content/learn/index.md --------- Co-authored-by: Ricky <[email protected]>
1 parent 3bb7a4e commit 0eb0f88

Some content is hidden

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

70 files changed

+1805
-1635
lines changed

.eslintrc

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}],
99
"react-hooks/exhaustive-deps": "error",
1010
"react/no-unknown-property": ["error", {"ignore": ["meta"]}],
11-
"react-compiler/react-compiler": "error"
11+
"react-compiler/react-compiler": "error",
12+
"@next/next/no-img-element": "off",
13+
"@next/next/no-html-link-for-pages": "off"
1214
},
1315
"env": {
1416
"node": true,

next-env.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
/// <reference types="next/image-types/global" />
33

44
// NOTE: This file should not be edited
5-
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
5+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

next.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ const nextConfig = {
1111
experimental: {
1212
scrollRestoration: true,
1313
reactCompiler: true,
14+
newDevOverlay: true,
1415
},
16+
1517
env: {},
18+
serverExternalPackages: [],
1619
webpack: (config, {dev, isServer, ...options}) => {
1720
if (process.env.ANALYZE) {
1821
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');

package.json

+8-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"license": "CC",
66
"scripts": {
77
"analyze": "ANALYZE=true next build",
8-
"dev": "next-remote-watch ./src/content",
8+
"dev": "next dev",
99
"build": "next build && node --experimental-modules ./scripts/downloadFonts.mjs",
1010
"lint": "next lint",
1111
"lint:fix": "next lint --fix",
@@ -15,32 +15,33 @@
1515
"prettier:diff": "yarn nit:source",
1616
"lint-heading-ids": "node scripts/headingIdLinter.js",
1717
"fix-headings": "node scripts/headingIdLinter.js --fix",
18-
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss",
18+
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids",
1919
"tsc": "tsc --noEmit",
2020
"start": "next start",
2121
"postinstall": "is-ci || husky install .husky",
22-
"check-all": "npm-run-all prettier lint:fix tsc rss",
23-
"rss": "node scripts/generateRss.js"
22+
"check-all": "npm-run-all prettier lint:fix tsc"
2423
},
2524
"dependencies": {
2625
"@codesandbox/sandpack-react": "2.13.5",
2726
"@docsearch/css": "^3.8.3",
2827
"@docsearch/react": "^3.8.3",
2928
"@headlessui/react": "^1.7.0",
3029
"@radix-ui/react-context-menu": "^2.1.5",
30+
"@types/mdast": "^4.0.4",
3131
"body-scroll-lock": "^3.1.3",
3232
"classnames": "^2.2.6",
3333
"date-fns": "^2.16.1",
3434
"debounce": "^1.2.1",
3535
"github-slugger": "^1.3.0",
36-
"next": "15.1.0",
36+
"next": "^15.2.0-canary.33",
3737
"next-remote-watch": "^1.0.0",
3838
"parse-numeric-range": "^1.2.0",
3939
"react": "^19.0.0",
4040
"react-collapsed": "4.0.4",
4141
"react-dom": "^19.0.0",
4242
"remark-frontmatter": "^4.0.1",
43-
"remark-gfm": "^3.0.1"
43+
"remark-gfm": "^3.0.1",
44+
"unist-builder": "^4.0.0"
4445
},
4546
"devDependencies": {
4647
"@babel/core": "^7.12.9",
@@ -62,6 +63,7 @@
6263
"autoprefixer": "^10.4.2",
6364
"babel-eslint": "10.x",
6465
"babel-plugin-react-compiler": "19.0.0-beta-e552027-20250112",
66+
"chokidar": "^4.0.3",
6567
"eslint": "7.x",
6668
"eslint-config-next": "12.0.3",
6769
"eslint-config-react-app": "^5.2.1",

scripts/generateRss.js

-6
This file was deleted.

src/app/[[...markdownPath]]/page.tsx

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import fs from 'fs/promises';
2+
import path from 'path';
3+
import {Page} from 'components/Layout/Page';
4+
import sidebarHome from '../../sidebarHome.json';
5+
import sidebarLearn from '../../sidebarLearn.json';
6+
import sidebarReference from '../../sidebarReference.json';
7+
import sidebarCommunity from '../../sidebarCommunity.json';
8+
import sidebarBlog from '../../sidebarBlog.json';
9+
import {generateMDX} from '../../utils/generateMDX';
10+
11+
import {generateMetadata as generateSeoMetadata} from '../../utils/generateMetadata';
12+
13+
import {getRouteMeta, RouteItem} from 'components/Layout/getRouteMeta';
14+
import {LanguageItem} from 'components/MDX/LanguagesContext';
15+
import {cache} from 'react';
16+
17+
function getActiveSection(pathname: string) {
18+
if (pathname === '/') {
19+
return 'home';
20+
} else if (pathname.startsWith('/reference')) {
21+
return 'reference';
22+
} else if (pathname.startsWith('/learn')) {
23+
return 'learn';
24+
} else if (pathname.startsWith('/community')) {
25+
return 'community';
26+
} else if (pathname.startsWith('/blog')) {
27+
return 'blog';
28+
} else {
29+
return 'unknown';
30+
}
31+
}
32+
33+
async function getRouteTree(section: string) {
34+
switch (section) {
35+
case 'home':
36+
case 'unknown':
37+
return sidebarHome;
38+
case 'learn':
39+
return sidebarLearn;
40+
case 'reference':
41+
return sidebarReference;
42+
case 'community':
43+
return sidebarCommunity;
44+
case 'blog':
45+
return sidebarBlog;
46+
default:
47+
throw new Error(`Unknown section: ${section}`);
48+
}
49+
}
50+
51+
const getPageContent = cache(async function getPageContent(
52+
markdownPath: any[]
53+
) {
54+
const rootDir = path.join(process.cwd(), 'src/content');
55+
let mdxPath = markdownPath?.join('/') || 'index';
56+
let mdx;
57+
58+
try {
59+
mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8');
60+
} catch {
61+
mdx = await fs.readFile(path.join(rootDir, mdxPath, 'index.md'), 'utf8');
62+
}
63+
64+
return await generateMDX(mdx, mdxPath, {});
65+
});
66+
67+
// This replaces getStaticPaths
68+
export async function generateStaticParams() {
69+
const rootDir = path.join(process.cwd(), 'src/content');
70+
71+
async function getFiles(dir: string): Promise<string[]> {
72+
const entries = await fs.readdir(dir, {withFileTypes: true});
73+
const files = await Promise.all(
74+
entries.map(async (entry) => {
75+
const res = path.resolve(dir, entry.name);
76+
return entry.isDirectory()
77+
? getFiles(res)
78+
: res.slice(rootDir.length + 1);
79+
})
80+
);
81+
82+
return files
83+
.flat()
84+
.filter(
85+
(file: string) => file.endsWith('.md') && !file.startsWith('errors/')
86+
);
87+
}
88+
89+
function getSegments(file: string) {
90+
let segments = file.slice(0, -3).replace(/\\/g, '/').split('/');
91+
if (segments[segments.length - 1] === 'index') {
92+
segments.pop();
93+
}
94+
return segments;
95+
}
96+
97+
const files = await getFiles(rootDir);
98+
99+
return files.map((file: any) => ({
100+
markdownPath: getSegments(file),
101+
}));
102+
}
103+
104+
export default async function WrapperPage({
105+
params,
106+
}: {
107+
params: Promise<{markdownPath: string[]}>;
108+
}) {
109+
const {markdownPath} = await params;
110+
111+
// Get the MDX content and associated data
112+
const {content, toc, meta} = await getPageContent(markdownPath);
113+
114+
const pathname = '/' + (markdownPath?.join('/') || '');
115+
const section = getActiveSection(pathname);
116+
const routeTree = await getRouteTree(section);
117+
118+
// Load the list of translated languages conditionally.
119+
let languages: Array<LanguageItem> | null = null;
120+
if (pathname.endsWith('/translations')) {
121+
languages = await (
122+
await fetch(
123+
'https://raw.githubusercontent.com/reactjs/translations.react.dev/main/langs/langs.json'
124+
)
125+
).json(); // { code: string; name: string; enName: string}[]
126+
}
127+
128+
// Pass the content and TOC directly, as `getPageContent` should already return them in the correct format
129+
return (
130+
<Page
131+
toc={toc} // Pass the TOC directly without parsing
132+
routeTree={routeTree as RouteItem}
133+
meta={meta}
134+
section={section}
135+
pathname={pathname}
136+
languages={languages}>
137+
{content}
138+
</Page>
139+
);
140+
}
141+
// Configure dynamic segments to be statically generated
142+
export const dynamicParams = false;
143+
144+
export async function generateMetadata({
145+
params,
146+
}: {
147+
params: Promise<{markdownPath: string[]}>;
148+
}) {
149+
const {markdownPath} = await params;
150+
const pathname = '/' + (markdownPath?.join('/') || '');
151+
const section = getActiveSection(pathname);
152+
const routeTree = await getRouteTree(section);
153+
const {route, order} = getRouteMeta(pathname, routeTree as RouteItem);
154+
const {
155+
title = route?.title || '',
156+
description = route?.description || '',
157+
titleForTitleTag,
158+
} = await getPageContent(markdownPath).then(({meta}) => meta);
159+
160+
return generateSeoMetadata({
161+
title,
162+
isHomePage: pathname === '/',
163+
path: pathname,
164+
description,
165+
titleForTitleTag,
166+
image: `/images/og-${section}.png`,
167+
searchOrder:
168+
section === 'learn' || (section === 'blog' && pathname !== '/blog')
169+
? order
170+
: undefined,
171+
});
172+
}

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1+
'use client';
2+
13
/*
24
* Copyright (c) Facebook, Inc. and its affiliates.
35
*/
46

57
import {Page} from 'components/Layout/Page';
68
import {MDXComponents} from 'components/MDX/MDXComponents';
79
import sidebarLearn from '../sidebarLearn.json';
10+
import {RouteItem} from 'components/Layout/getRouteMeta';
11+
import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata';
812

913
const {Intro, MaxWidth, p: P, a: A} = MDXComponents;
1014

11-
export default function NotFound() {
15+
export default function Error() {
1216
return (
1317
<Page
18+
section="unknown"
1419
toc={[]}
15-
routeTree={sidebarLearn}
20+
pathname="/500"
21+
routeTree={sidebarLearn as RouteItem}
1622
meta={{title: 'Something Went Wrong'}}>
1723
<MaxWidth>
1824
<Intro>
@@ -29,3 +35,11 @@ export default function NotFound() {
2935
</Page>
3036
);
3137
}
38+
39+
export async function generateMetadata({}: {}) {
40+
return generateSeoMetadata({
41+
title: 'Something Went Wrong',
42+
isHomePage: false,
43+
path: '/500',
44+
});
45+
}

0 commit comments

Comments
 (0)