Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e0f0095
chore: upgrade to Next.js 16.3 and React 19.2
aurorascharff Jun 22, 2026
96c103e
refactor: migrate to App Router and enable Cache Components
aurorascharff Jun 22, 2026
85879bd
review: address PR feedback
aurorascharff Jun 22, 2026
b459abc
chore: pin to stable Next.js 16.2.9
aurorascharff Jun 22, 2026
98417e2
perf: move 'use cache' to the data layer with cacheLife('max')
aurorascharff Jun 23, 2026
17d89a9
perf: lazy-load HomeContent so only the home route ships it
aurorascharff Jun 23, 2026
79d988f
perf: prerender the markdown (.md) route handler statically
aurorascharff Jun 23, 2026
155e4ac
fix: return 404 (not 500) for catch-all paths with no content file
aurorascharff Jun 23, 2026
4efe72d
docs: explain the Turbopack opt-out in next.config.js
aurorascharff Jun 23, 2026
423ff30
chore: restore secondary copyright banners
aurorascharff Jun 23, 2026
0b67363
fix: auto-expand deep-linked DeepDive/Example after hydration
aurorascharff Jul 2, 2026
11d5287
Merge remote-tracking branch 'upstream/main' into upgrade-next-16
aurorascharff Jul 9, 2026
171c159
Wire per-page OG images into App Router metadata
aurorascharff Jul 9, 2026
f8733aa
Remove accidentally committed launch.json
aurorascharff Jul 9, 2026
8a242fe
Merge branch 'reactjs:main' into upgrade-next-16
aurorascharff Jul 17, 2026
f415106
format
aurorascharff Jul 17, 2026
b16527d
Merge branch 'upgrade-next-16' of github.com:aurorascharff/react.dev …
aurorascharff Jul 17, 2026
5a4f07b
Force webpack for next.js build to ensure compatibility with Next 16
aurorascharff Jul 17, 2026
18f0169
Update Tailwind CSS content paths for Next.js compatibility
aurorascharff Jul 17, 2026
a1e82f7
Add App Router-aware bundle size analysis script and update workflow
aurorascharff Jul 17, 2026
d50cbcd
Refactor section rendering to use renderSectionContent and add layout…
aurorascharff Jul 17, 2026
91d0864
Enhance Toc component with optimistic scrolling and transition handling
aurorascharff Jul 17, 2026
24638c6
Revert sidebar fix
aurorascharff Jul 17, 2026
6f68601
Add not found boundaries
aurorascharff Jul 17, 2026
37804ab
Restructure components
aurorascharff Jul 17, 2026
5f9a142
Upgrade react-collapse and fix design of scoped not found pages
aurorascharff Jul 17, 2026
b3f3af0
add footer to notfoundpage
aurorascharff Jul 17, 2026
1daeb92
fix notfound layout
aurorascharff Jul 17, 2026
bf050e6
Update Google Analytics configuration to prevent automatic page view …
aurorascharff Jul 17, 2026
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
35 changes: 15 additions & 20 deletions .github/workflows/analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,18 @@ jobs:
key: ${{ runner.os }}-build-${{ env.cache-name }}

- name: Build next.js app
# change this if your site requires a custom build command
run: ./node_modules/.bin/next build

# Here's the first place where next-bundle-analysis' own script is used
# This step pulls the raw bundle stats for the current bundle
# This project pins the webpack pipeline (see next.config.js): the custom
# webpack config and Sandpack's raw-loader imports aren't Turbopack-ready,
# and Next 16's `next build` defaults to Turbopack. Match the `build`/`analyze`
# npm scripts by forcing `--webpack`.
run: ./node_modules/.bin/next build --webpack

# Measure the current build's bundle sizes (App Router-aware).
# See scripts/analyzeBundle.mjs — reads build-manifest.json + .next/static
# and sums gzipped sizes, since nextjs-bundle-analysis only understands the
# Pages Router and reports 0 B for the App Router.
- name: Analyze bundle
run: npx -p [email protected] report
run: node scripts/analyzeBundle.mjs report

- name: Upload bundle
uses: actions/upload-artifact@v4
Expand All @@ -65,22 +70,12 @@ jobs:
name: bundle_analysis.json
path: .next/analyze/base/bundle

# And here's the second place - this runs after we have both the current and
# base branch bundle stats, and will compare them to determine what changed.
# There are two configurable arguments that come from package.json:
#
# - budget: optional, set a budget (bytes) against which size changes are measured
# it's set to 350kb here by default, as informed by the following piece:
# https://infrequently.org/2021/03/the-performance-inequality-gap/
#
# - red-status-percentage: sets the percent size increase where you get a red
# status indicator, defaults to 20%
#
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
# entry in your package.json file.
# Compare the current build against the base-branch stats downloaded above
# and write the Markdown comment body. Degrades gracefully when the base
# branch has no stats yet (or still has the old format).
- name: Compare with base branch bundle
if: success() && github.event.number
run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
run: node scripts/analyzeBundle.mjs compare

- name: Upload analysis comment
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public/rss.xml
# claude local settings
.claude/*.local.*
.claude/react/
.claude/launch.json

# worktrees
.worktrees/
Expand Down
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
41 changes: 20 additions & 21 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 && node ./scripts/generateOgImages.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 && node ./scripts/generateOgImages.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-collapsed": "4.0.4",
"react-dom": "^19.0.0",
"react": "19.2.7",
"react-collapsed": "^4.2.0",
"react-dom": "19.2.7",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1"
},
Expand All @@ -57,10 +56,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 @@ -70,7 +70,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 @@ -110,16 +110,15 @@
"webpack-bundle-analyzer": "^4.5.0"
},
"engines": {
"node": ">=16.8.0"
},
"nextBundleAnalysis": {
"budget": null,
"budgetPercentIncreaseRed": 10,
"showDetails": true
"node": ">=20.9.0"
},
"lint-staged": {
"*.{js,ts,jsx,tsx,css}": "yarn prettier",
"src/**/*.md": "yarn fix-headings"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"resolutions": {
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3"
}
}
169 changes: 169 additions & 0 deletions scripts/analyzeBundle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* 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.
*/

/*
* App Router-aware bundle-size analysis.
*
* Modeled on Next.js's own `next-stats-action`: sum the gzipped sizes of
* groups of build-output files and compare a PR build against the base branch.
* Unlike `nextjs-bundle-analysis`, this reads the real build output
* (`build-manifest.json` + `.next/static`) rather than the Pages-Router-only
* `build-manifest.json.pages`, which the App Router leaves empty.
*
* Usage:
* node scripts/analyzeBundle.mjs report # writes the current build's stats
* node scripts/analyzeBundle.mjs compare # diffs against the base-branch stats
*
* `report` -> .next/analyze/__bundle_analysis.json (uploaded as an artifact)
* `compare` -> .next/analyze/__bundle_analysis_comment.txt (posted by
* analyze_comment.yml). The base-branch artifact is expected under
* .next/analyze/base/bundle/ (downloaded by analyze.yml).
*/

import fs from 'fs';
import path from 'path';
import zlib from 'zlib';
import {fileURLToPath} from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..');
const nextDir = path.join(root, '.next');
const analyzeDir = path.join(nextDir, 'analyze');
const statsFile = path.join(analyzeDir, '__bundle_analysis.json');
const baseDir = path.join(analyzeDir, 'base', 'bundle');
const commentFile = path.join(analyzeDir, '__bundle_analysis_comment.txt');

function walk(dir) {
if (!fs.existsSync(dir)) return [];
let out = [];
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
const p = path.join(dir, entry.name);
if (entry.isDirectory()) out = out.concat(walk(p));
else out.push(p);
}
return out;
}

function sumGroup(files) {
let raw = 0;
let gzip = 0;
for (const file of files) {
const buf = fs.readFileSync(file);
raw += buf.length;
gzip += zlib.gzipSync(buf).length;
}
return {raw, gzip, count: files.length};
}

function report() {
const manifest = JSON.parse(
fs.readFileSync(path.join(nextDir, 'build-manifest.json'), 'utf8')
);
// The shared bundle that loads on every page (App Router: rootMainFiles).
const globalFiles = [
...(manifest.rootMainFiles || []),
...(manifest.polyfillFiles || []),
]
.map((f) => path.join(nextDir, f))
.filter((f) => fs.existsSync(f));
const jsFiles = walk(path.join(nextDir, 'static', 'chunks')).filter((f) =>
f.endsWith('.js')
);
const cssFiles = walk(path.join(nextDir, 'static', 'css')).filter((f) =>
f.endsWith('.css')
);

const stats = {
'Global (loads on every page)': sumGroup(globalFiles),
'Total JS': sumGroup(jsFiles),
'Total CSS': sumGroup(cssFiles),
};

fs.mkdirSync(analyzeDir, {recursive: true});
fs.writeFileSync(statsFile, JSON.stringify(stats, null, 2));
console.log('Wrote', path.relative(root, statsFile));
for (const [name, v] of Object.entries(stats)) {
console.log(` ${name}: ${formatBytes(v.gzip)} gzip (${v.count} files)`);
}
}

function formatBytes(bytes) {
if (Math.abs(bytes) >= 1024 * 1024) {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
return `${(bytes / 1024).toFixed(2)} KB`;
}

function formatDelta(cur, base) {
const d = cur - base;
if (d === 0) return 'no change';
return `${d > 0 ? '🔺 +' : '🟢 -'}${formatBytes(Math.abs(d))}`;
}

// New format: every value is {raw, gzip, count}. The old nextjs-bundle-analysis
// artifact was {"/_app": {raw, gzip}, "__global": {...}} with no `count`.
function isNewFormat(obj) {
const vals = obj && typeof obj === 'object' ? Object.values(obj) : [];
return (
vals.length > 0 &&
vals.every((v) => v && typeof v.gzip === 'number' && typeof v.count === 'number')
);
}

function loadBaseStats() {
if (!fs.existsSync(baseDir)) return null;
const jsons = fs.readdirSync(baseDir).filter((f) => f.endsWith('.json'));
if (jsons.length === 0) return null;
try {
return JSON.parse(fs.readFileSync(path.join(baseDir, jsons[0]), 'utf8'));
} catch {
return null;
}
}

function compare() {
const cur = JSON.parse(fs.readFileSync(statsFile, 'utf8'));
const base = loadBaseStats();

let md;
if (base && isNewFormat(base)) {
md =
'| Metric | Size (gzip) | Change vs base |\n|---|---|---|\n' +
Object.entries(cur)
.map(([name, v]) => {
const b = base[name];
const change = b ? formatDelta(v.gzip, b.gzip) : '— (new)';
return `| ${name} | ${formatBytes(v.gzip)} | ${change} |`;
})
.join('\n') +
'\n';
} else {
md =
'_No comparable base-branch data yet — the base branch has not produced ' +
'stats in this format. Showing current sizes only; deltas will appear on ' +
'the next run after this lands on the base branch._\n\n' +
'| Metric | Size (gzip) |\n|---|---|\n' +
Object.entries(cur)
.map(([name, v]) => `| ${name} | ${formatBytes(v.gzip)} |`)
.join('\n') +
'\n';
}

fs.mkdirSync(analyzeDir, {recursive: true});
fs.writeFileSync(commentFile, md);
console.log(md);
}

const mode = process.argv[2];
if (mode === 'report') {
report();
} else if (mode === 'compare') {
compare();
} else {
console.error('Usage: node scripts/analyzeBundle.mjs <report|compare>');
process.exit(1);
}
Loading
Loading