Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ src/
│ ├── reference/ # API reference docs
│ ├── blog/ # Blog posts
│ └── community/ # Community pages
├── app/ # Next.js App Router routes
├── components/ # React components
├── pages/ # Next.js pages
├── lib/ # Server-only helpers (MDX loading, metadata)
├── hooks/ # Custom React hooks
├── utils/ # Utility functions
└── styles/ # CSS/Tailwind styles
Expand All @@ -50,3 +51,11 @@ For Sandpack code examples, invoke `/docs-sandpack`.
See `.claude/docs/react-docs-patterns.md` for comprehensive style guidelines.

Prettier is used for formatting (config in `.prettierrc`).

<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.

**Keep this block, including in commits.** It is part of the project's agent setup, maintained by `next dev` for every agent that works here. If it appears as an uncommitted change, that is intentional — commit it as-is. Do not remove it to clean up a diff; it will be regenerated.
<!-- END:nextjs-agent-rules -->
3 changes: 2 additions & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
25 changes: 21 additions & 4 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,27 @@
const nextConfig = {
pageExtensions: ['jsx', 'js', 'ts', 'tsx', 'mdx', 'md'],
reactStrictMode: true,
experimental: {
scrollRestoration: true,
reactCompiler: true,
},
reactCompiler: true,
cacheComponents: true,
serverExternalPackages: [
'@babel/core',
'@babel/plugin-transform-modules-commonjs',
'@babel/preset-react',
'@mdx-js/mdx',
'metro-cache',
'gray-matter',
'unist-util-visit',
'remark-gfm',
'remark-frontmatter',
],
// Next 16 defaults to Turbopack, but this project still relies on the custom
// `webpack()` config below (module aliases, IgnorePlugin, and Sandpack's
// `raw-loader` imports) which Turbopack can't run yet. The build scripts pass
// `--webpack` to force the webpack pipeline; this empty `turbopack` object
// silences the "webpack config without turbopack config" warning in the
// meantime.
// TODO: port the webpack customizations to Turbopack and drop `--webpack`.
turbopack: {},
async rewrites() {
return {
beforeFiles: [
Expand Down
34 changes: 19 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"private": true,
"license": "CC",
"scripts": {
"analyze": "ANALYZE=true next build",
"dev": "next-remote-watch ./src/content",
"analyze": "ANALYZE=true next build --webpack",
"dev": "next dev --webpack",
"prebuild:rsc": "node scripts/buildRscWorker.mjs",
"build": "node scripts/buildRscWorker.mjs && next build && node --experimental-modules ./scripts/downloadFonts.mjs",
"lint": "next lint && eslint \"src/content/**/*.md\"",
"lint:fix": "next lint --fix && eslint \"src/content/**/*.md\" --fix",
"build": "node scripts/buildRscWorker.mjs && next build --webpack && node --experimental-modules ./scripts/downloadFonts.mjs",
"lint": "eslint \"{src,plugins}/**/*.{js,jsx,ts,tsx}\" && eslint \"src/content/**/*.md\"",
"lint:fix": "eslint --fix \"{src,plugins}/**/*.{js,jsx,ts,tsx}\" && eslint --fix \"src/content/**/*.md\"",
"format:source": "prettier --config .prettierrc --write \"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\"",
"nit:source": "prettier --config .prettierrc --list-different \"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\"",
"prettier": "yarn format:source",
Expand All @@ -36,13 +36,12 @@
"classnames": "^2.2.6",
"debounce": "^1.2.1",
"github-slugger": "^1.3.0",
"next": "15.1.12",
"next-remote-watch": "^1.0.0",
"next": "16.2.9",
"parse-numeric-range": "^1.2.0",
"raw-loader": "^4.0.2",
"react": "^19.0.0",
"react": "19.2.7",
"react-collapsed": "4.0.4",
"react-dom": "^19.0.0",
"react-dom": "19.2.7",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1"
},
Expand All @@ -56,10 +55,11 @@
"@types/debounce": "^1.2.1",
"@types/github-slugger": "^1.3.0",
"@types/mdx-js__react": "^1.5.2",
"@types/node": "^14.6.4",
"@types/node": "^20",
"@types/parse-numeric-range": "^0.0.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/prop-types": "^15.7.15",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"asyncro": "^3.0.0",
Expand All @@ -69,7 +69,7 @@
"chalk": "4.1.2",
"esbuild": "^0.24.0",
"eslint": "7.x",
"eslint-config-next": "12.0.3",
"eslint-config-next": "14",
"eslint-config-react-app": "^5.2.1",
"eslint-plugin-flowtype": "4.x",
"eslint-plugin-import": "2.x",
Expand Down Expand Up @@ -108,7 +108,7 @@
"webpack-bundle-analyzer": "^4.5.0"
},
"engines": {
"node": ">=16.8.0"
"node": ">=20.9.0"
},
"nextBundleAnalysis": {
"budget": null,
Expand All @@ -119,5 +119,9 @@
"*.{js,ts,jsx,tsx,css}": "yarn prettier",
"src/**/*.md": "yarn fix-headings"
},
"packageManager": "yarn@1.22.22"
"packageManager": "yarn@1.22.22",
"resolutions": {
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3"
}
}
35 changes: 35 additions & 0 deletions src/app/DocsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* 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.
*/

'use client';

import {Page, type PageSection} from 'components/Layout/Page';
import {useDeserializedMDX} from 'components/Layout/useDeserializedMDX';
import type {RouteItem} from 'components/Layout/getRouteMeta';
import type {PageData} from 'lib/readMarkdownPage';

interface DocsPageProps {
data: PageData;
pathname: string;
section: PageSection;
routeTree: RouteItem;
}

export function DocsPage({data, pathname, section, routeTree}: DocsPageProps) {
const {parsedContent, parsedToc} = useDeserializedMDX(data.content, data.toc);
return (
<Page
toc={parsedToc}
routeTree={routeTree}
meta={data.meta}
section={section}
pathname={pathname}
languages={data.languages}>
{parsedContent}
</Page>
);
}
77 changes: 77 additions & 0 deletions src/app/api/md/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* 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 fs from 'fs';
import path from 'path';
import {NextResponse} from 'next/server';
import {cacheLife} from 'next/cache';
import {collectAllContentPaths} from 'lib/collectPaths';

const FOOTER = `
---

## Sitemap

[Overview of all docs pages](/llms.txt)
`;

// The content set is fixed at build time, so prerender every `.md` endpoint
// instead of running a function per request. With Cache Components, this means
// generateStaticParams + a cached body (no `dynamic`/`dynamicParams` configs,
// which are disallowed when cacheComponents is on).
export async function generateStaticParams() {
const paths = await collectAllContentPaths();
return paths.map((segments) => ({path: segments}));
}

/**
* Read a markdown file for the given URL segments. Cached so prerendered
* `.md` endpoints don't re-read from disk per request. Returns null when no
* matching file exists.
*/
async function readContentMarkdown(
pathSegments: string[] | undefined
): Promise<string | null> {
'use cache';
cacheLife('max');
if (!pathSegments || pathSegments.length === 0) return null;

const filePath = pathSegments.join('/');
// Block /index.md URLs - use /foo.md instead of /foo/index.md
if (filePath.endsWith('/index') || filePath === 'index') return null;

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 {
return fs.readFileSync(fullPath, 'utf8');
} catch {
// Try next candidate
}
}
return null;
}

export async function GET(
_req: Request,
ctx: {params: Promise<{path: string[]}>}
) {
const {path: pathSegments} = await ctx.params;
const content = await readContentMarkdown(pathSegments);
if (content == null) {
return new NextResponse('Not found', {status: 404});
}
return new NextResponse(content + FOOTER, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
},
});
}
39 changes: 39 additions & 0 deletions src/app/blog/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* 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 {Metadata} from 'next';
import sidebarBlog from '../../../sidebarBlog.json';
import type {RouteItem} from 'components/Layout/getRouteMeta';
import {collectSectionPaths} from 'lib/collectPaths';
import {renderSectionPage, sectionPageMetadata} from '../../renderSectionPage';

interface PageProps {
params: Promise<{slug?: string[]}>;
}

export async function generateStaticParams() {
const paths = await collectSectionPaths('blog');
return paths.map((slug) => ({slug}));
}

export async function generateMetadata({params}: PageProps): Promise<Metadata> {
const {slug} = await params;
return sectionPageMetadata({
section: 'blog',
segments: ['blog', ...(slug ?? [])],
routeTree: sidebarBlog as RouteItem,
});
}

export default async function BlogPage({params}: PageProps) {
const {slug} = await params;
return renderSectionPage({
section: 'blog',
segments: ['blog', ...(slug ?? [])],
routeTree: sidebarBlog as RouteItem,
});
}
49 changes: 49 additions & 0 deletions src/app/clientEffects.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* 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.
*/

'use client';

import {useEffect} from 'react';
import {usePathname} from 'next/navigation';

declare const gtag: (...args: any[]) => void;

export function AnalyticsTracker() {
const pathname = usePathname();

useEffect(() => {
if (typeof window === 'undefined' || typeof gtag === 'undefined') return;
gtag('event', 'pageview', {event_label: pathname});
}, [pathname]);

useEffect(() => {
if (typeof window === 'undefined') return;
const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'unload';
const handler = () => {
if (typeof gtag !== 'undefined') {
gtag('event', 'timing', {
event_label: 'JS Dependencies',
event: 'unload',
});
}
};
window.addEventListener(terminationEvent, handler);
return () => window.removeEventListener(terminationEvent, handler);
}, []);

return null;
}

export function ScrollRestoration() {
useEffect(() => {
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari) {
history.scrollRestoration = 'auto';
}
}, []);
return null;
}
38 changes: 38 additions & 0 deletions src/app/community/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* 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 {Metadata} from 'next';
import sidebarCommunity from '../../../sidebarCommunity.json';
import type {RouteItem} from 'components/Layout/getRouteMeta';
import {collectSectionPaths} from 'lib/collectPaths';
import {renderSectionPage, sectionPageMetadata} from '../../renderSectionPage';

interface PageProps {
params: Promise<{slug?: string[]}>;
}

export async function generateStaticParams() {
const paths = await collectSectionPaths('community');
return paths.map((slug) => ({slug}));
}

export async function generateMetadata({params}: PageProps): Promise<Metadata> {
const {slug} = await params;
return sectionPageMetadata({
section: 'community',
segments: ['community', ...(slug ?? [])],
});
}

export default async function CommunityPage({params}: PageProps) {
const {slug} = await params;
return renderSectionPage({
section: 'community',
segments: ['community', ...(slug ?? [])],
routeTree: sidebarCommunity as RouteItem,
});
}
Loading
Loading