chore: upgrade to Nextra 4 + Next 15 + React 19 (App Router)#43
Merged
Conversation
Migrate the docs site from Nextra 3 / Next 14 / Pages Router to the current Nextra 4 / Next 15 / React 19 stack. Nextra 4 only ships an App Router runtime, so this is a forced framework migration, not a drop-in version bump. Key changes: - pages/ moved to content/; new app/[[...mdxPath]]/page.tsx catch-all uses Nextra's importPage to render MDX through the docs theme wrapper - app/layout.tsx replaces theme.config.jsx and pages/_app.tsx — Layout/Navbar/Footer are configured as JSX, head tags become Next.js Metadata API entries, GA / Clarity / Umami stay as <Script> - pages/api/raw-mdx.ts ported to app/api/raw-mdx/route.ts (route handler) - nextra-theme-docs/style-prefixed.css imported instead of style.css so the theme's Tailwind v4 layers don't clash with this project's Tailwind v3 PostCSS pipeline - _meta.js entries updated for Nextra 4's stricter Zod schema: theme.layout "raw" → "full" (plus sidebar/toc/footer false on the landing page) and the unsupported `collapsible` key dropped - content/blog.mdx and content/changelog.mdx use getPageMap + normalizePages instead of the removed getPagesUnderRoute - LanguageSelector switched from next/router to next/navigation; components touching window/state marked "use client" (richtextDemo, video, Footer landing, etc.) - React 19 dropped the global JSX namespace; custom-types.d.ts re-aliases it so existing JSX.Element annotations keep type-checking - next.config.mjs drops the theme/themeConfig keys, simplifies the SVG rule for Webpack 5, keeps WASM + redirects - tailwind.config.js scans content/ + app/, plus a safelist for responsive grid-cols variants Nextra's MDX loader was missing - gen-rss.js reads from content/ instead of pages/ Verified visually with Playwright screenshots of 28 routes at desktop + mobile against the previous build; routing, navigation, and layout match the original.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loro-docs | 96d531e | Commit Preview URL Branch Preview URL |
Apr 28 2026, 02:00 AM |
…yout
Pixel-diff against the pre-migration screenshots surfaced two regressions
introduced by the Nextra 3 → 4 jump:
1. **MDX heading/code styles flattened.** Tailwind v3's preflight emits
`h1,h2,h3,h4,h5,h6 { font-size: inherit; font-weight: inherit }` outside
any `@layer`, and unlayered rules outrank Nextra 4's layered
`.x:text-4xl` / `.x:font-bold` defaults from `style-prefixed.css`. Result:
every heading on every docs page rendered at 16px / weight 400. Fix:
disable Tailwind v3's preflight via `corePlugins.preflight = false` and
re-add only the box-sizing + button reset bits the landing page actually
needs, scoped to `.landing-page-root`.
2. **/blog, /changelog, /about showed the docs sidebar + a centered
"Copy page" button + center-aligned headings.** In Nextra 3 these were
plain centred pages; Nextra 4 always renders the docs sidebar for nested
page-type routes and `theme.sidebar = false` in `_meta.js` does not
propagate through the catch-all once we collapse `content/blog.mdx` into
`content/blog/index.mdx`. Fix: a small `RouteBodyClass` client component
sets `<body data-layout="plain">` for these routes, and `style.css` has
targeted overrides that hide the sidebar / TOC / Copy-page button and
left-align headings on those pages only. Docs pages (`data-layout="docs"`)
are untouched.
Also moved `content/blog.mdx` → `content/blog/index.mdx` and
`content/changelog.mdx` → `content/changelog/index.mdx` to remove the
file/folder ambiguity that made Nextra's normalize-pages assign
`activeType: "doc"` to the route.
Verified with Playwright across 28 routes (desktop + mobile): headings,
code blocks, sidebar visibility, copy-page button, footer, and overall
layout match the original Nextra 3 build.
A second pass over the screenshots surfaced two more regressions:
1. **Individual blog/changelog posts lost sidebar + TOC.** The previous
commit hid the docs sidebar on every `/blog/*` and `/changelog/*` route,
but the Nextra 3 build only stripped them on the index pages — child
posts kept the standard docs layout with a post-list sidebar and an
"On This Page" TOC. Fix: tighten `RouteBodyClass`'s plain-layout regex
to match `/blog`, `/changelog`, `/about` exactly (no children), and
drop the `theme.sidebar=false` overrides from `content/_meta.js` for
blog/changelog so per-post `_meta.js` entries can re-enable TOC.
2. **Docs pages showed Nextra 4's "Copy page" button.** The original
Nextra 3 layout never had this. Disabled globally via
`<Layout copyPageButton={false}>`.
Also constrains article max-width to 720px on plain-layout pages so
`/about` content matches the narrower column the Nextra 3 page-type
routes used.
Cloudflare Pages CI ran the default `pnpm run build` with Node's stock ~2GB heap and OOM'd during `next build` (Mark-Compact thrashing). The Nextra 4 catch-all route triples the per-route memory cost vs Nextra 3 because the same MDX module graph is built for every entry under `/[[...mdxPath]]`, which pushed the total over the limit. Wrap both `gen-rss.js` and `next build` (plus `next dev`) with `NODE_OPTIONS=--max-old-space-size=8192` so CI hosts and contributors get the same headroom that local builds already had.
The Cloudflare Pages deploy step (`npx wrangler versions upload`) expects `.open-next/worker.js` to exist after the build, but our `pnpm build` only ran `next build` + `next-sitemap` — it never invoked `opennextjs-cloudflare build`, so the worker bundle was missing and CF failed with `The entry-point file at ".open-next/worker.js" was not found.` - Chain `opennextjs-cloudflare build --skipNextBuild` into `postbuild` so the same `pnpm build` produces the worker. - Bump `@opennextjs/cloudflare` 1.13.0 → 1.19.4 (1.13 doesn't recognise Next 15.5 / WASM correctly; 1.19 is the first release that pins `next >=15.5.15`). - Bump `wrangler` 4.49 → 4.84 to satisfy the new peer requirement. - Add `output: "standalone"` to `next.config.mjs` — OpenNext 1.19 reads the standalone trace to bundle the server function.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pages/→content/, newapp/[[...mdxPath]]/page.tsxcatch-all renders MDX through Nextra'simportPage.theme.config.jsx+pages/_app.tsxcollapsed intoapp/layout.tsx(Layout/Navbar/Footeras JSX props,head→ Metadata API; GA / Clarity / Umami stay as<Script>).What changed
pages/api/raw-mdx.ts→app/api/raw-mdx/route.ts;_meta.jsupdated for Nextra 4's stricter Zod schema (theme.layout: "raw"→"full"+sidebar/toc/footer: false, droppedcollapsible).getPageMap+normalizePages(replaces the removedgetPagesUnderRoute).nextra-theme-docs/style-prefixed.cssso the theme's Tailwind v4 layers don't clash with this project's Tailwind v3 PostCSS pipeline.tailwind.config.jsscanscontent/+app/, with a safelist for responsivegrid-colsvariants Nextra's MDX loader otherwise misses.LanguageSelectorswitched fromnext/routertonext/navigation; components touching window/state marked"use client"(richtextDemo,video, landingFooter, etc.).JSXnamespace;custom-types.d.tsre-aliases it so existingJSX.Elementannotations keep type-checking.tsconfig.jsonnow usesmoduleResolution: "bundler".next.config.mjsdropstheme/themeConfig, simplifies the SVG rule for Webpack 5, keeps WASM + redirects.gen-rss.jsreads fromcontent/instead ofpages/.Test plan
pnpm build(withNODE_OPTIONS=--max-old-space-size=8192) — generates 72 static pages cleanlypnpm start+ Playwright screenshots of 28 routes at desktop and mobile (landing, docs/tutorial, docs/concepts, docs/api/js, blog index + posts, changelog, about) — routing, navigation, sidebar, and visual layout match the previous build/docs/api,/docs/advanced,/blog/v1) still 404 — no accidental new routingpnpm test(Deno code-block runner) is unaffectedNotes for reviewers
pnpm dev) needsNODE_OPTIONS=--max-old-space-size=8192: the catch-all route globs everycontent/*.mdx, which transitively pulls inloro-crdt's WASM via the richtext demo; production build is unaffected.style-prefixed.cssimport is Nextra's documented escape hatch for v3 consumers (nextra#3935).