Nextra 4 Migration Guide
Nextra 4 just released, marking the largest update in its history, with a ton of improvements.
At a Glance (Migration Checklist)
- Choose MDX rendering mode: content directory vs. page files
- Remove theme.config: pass props via
<Layout>,<Navbar>,<Footer>,<Search>,<Banner> - Enable Turbopack (optional): faster dev (
next dev --turbopack) - Migrate search to Pagefind: postbuild indexing, ignore
_pagefind, wire<Search> - Use RSC i18n if multilingual: load dictionaries server-side and pass to theme components
- Docs theme changes: Tailwind v4 prefix to
x:, Zustand init from props,<NotFoundPage> - Adopt Metadata API for titles/OG; use front matter for page title/description
- Update tables: use
Table.Tr/Table.Thinstead of standaloneTr/Th/Td - Remote MDX: use
<MDXRemote>andnextra/mdx-remotepath - Replace
useRouterwithnext/navigation - TypeScript config: set
moduleResolutiontobundler
App Router Support
Nextra 4 exclusively uses the Next.js App Router. Support for the Pages Router has been discontinued. There are two ways to render MDX files using file-based routing:
- Content directory convention: use a catch-all route that loads MDX files from a content directory.
- Page file convention: follow the App Router’s page conventions with enhanced page extensions (
page.{md,mdx}).
App Router essentials:
- Routes live under the
app/directory and are defined by folders (segments) and files. - A
page.mdx(orpage.jsx/tsx) inside a folder makes that folder a route. - Dynamic segments like
[slug]match one level; catch‑all segments like[[...mdxPath]]match nested paths.
Trade-offs
- The catch-all route can lead to longer compilation times, depending on the number of MDX files.
- The page file convention works well with colocation, keeping all assets for an article together.
See also: colocate prose, assets, and code for each page.
Using Content Directory Convention
Migrate your Pages Router site with minimal changes using this mode. Steps:
- Rename your
pagesfolder tocontent(can be at project root orsrc). - Set
contentDirBasePathinnext.config.mjs(optional) if you want a different served path.
next.config.mjs
import nextra from 'nextra'
const withNextra = nextra({
contentDirBasePath: '/docs' // Or even nested, e.g. `/docs/advanced`
})
- Add
[[...mdxPath]]/page.jsxunderapp/with the content route loader.
[TIP]
Consider the single catch-all route[[...mdxPath]]/page.jsxas a gateway to your content directory. If you setcontentDirBasePathinnext.config.mjs, put[[...mdxPath]]/page.jsxunder the corresponding directory.
[NOTE]
Many existing solutions (e.g. “Refreshing the Next.js App Router When Your Markdown Content Changes”) rely on extra dependencies likeconcurrentlyandws, or a dev websocket server and an<AutoRefresh>workaround. Nextra’s content mode works out of the box:
- No extra dependencies to install
- No server restarts for content changes
- Hot reloading works out of the box
importin MDX files and static images work
- Static image imports in MDX are supported.
See Nextra’s docs website and the i18n example.
Tip in practice: content mode works without extra tooling; noconcurrently/websocket hacks, no restarts, and MDX imports and static images work out of the box.
Using Page File Convention
The same file-based routing from the content convention can be expressed with page files.
[WARNING]
Allpage.{jsx,tsx}must export ametadataobject.
[NOTE]
See Nextra’s website or the Nextra blog example for this mode in practice.
You can implement this mode by colocatingpage.{md,mdx}next to content; this keeps prose, assets, and code in one place.
Turbopack Support
After being one of the most requested features for over 2 years, Nextra 4 supports Turbopack (the Rust-based incremental bundler). Enable it by adding --turbopack to your dev command:
What Turbopack changes:
- Much faster incremental builds and HMR during development.
- Uses Rust for the dev bundler; production build remains the same unless otherwise configured.
- When not enabled (
--turbopackomitted), Next.js falls back to Webpack.
package.json
"scripts": {
- "dev": "next dev"
+ "dev": "next dev --turbopack"
}
[NOTE]
Without the--turbopackflag, Next.js uses Webpack (JavaScript) under the hood.
[WARNING]
At the time of writing, only JSON-serializable values can be passed tonextra(...). You cannot pass customremarkPlugins,rehypePlugins, orrecmaPlugins(functions) when using Turbopack.
next.config.mjs
import nextra from 'nextra'
const withNextra = nextra({
mdxOptions: {
remarkPlugins: [myRemarkPlugin],
rehypePlugins: [myRehypePlugin],
recmaPlugins: [myRecmaPlugin]
}
})
If you pass functions, Turbopack will error:
Error: loader nextra/loader for match "./{src/app,app}/**/page.{md,mdx}" does not have serializable options.
Ensure that options passed are plain JavaScript objects and values.
Discontinuing theme.config
Nextra 4 no longer supports theme.config files, and the theme and themeConfig options were removed.
next.config.mjs
const withNextra = nextra({
- theme: 'nextra-theme-docs',
- themeConfig: './theme.config.tsx'
})
[NOTE]
Previously theme config options should now be passed as props to<Layout>,<Navbar>,<Footer>,<Search>, and<Banner>inapp/layout.jsx.
New Search Engine – Pagefind
Search has migrated from FlexSearch (JS) to Pagefind (Rust).
Benefits
Pagefind is significantly faster and delivers far superior search results. Examples that previously didn’t work now index correctly:
Indexing remote MDX
page.mdx
import { Callout } from 'nextra/components'
export async function Stars() {
const response = await fetch('https://api.example.com/repos/owner/repo')
const repo = await response.json()
const stars = repo.stargazers_count
return <b>{stars}</b>
}
export async function getUpdatedAt() {
const response = await fetch('https://api.example.com/repos/owner/repo')
const repo = await response.json()
const updatedAt = repo.updated_at
return new Date(updatedAt).toLocaleDateString()
}
<Callout emoji="🏆">
{/* Stars count will be indexed 🎉 */}
Nextra has <Stars /> stars on GitHub!
{/* Last update time will be indexed 🎉 */}
Last repository update _{await getUpdatedAt()}_.
</Callout>
Indexing dynamic Markdown/MDX content
page.mdx
{/* Current year will be indexed 🎉 */}
MIT {new Date().getFullYear()} © Nextra.
Indexing imported JS/MDX content in an MDX page
../path/to/your/reused-js-component.js
export function ReusedJsComponent() {
return <strong>My content will be indexed</strong>
}
../path/to/your/reused-mdx-component.mdx
**My content will be indexed as well**
page.mdx
import { ReusedJsComponent } from '../path/to/your/reused-js-component.js'
import ReusedMdxComponent from '../path/to/your/reused-mdx-component.mdx'
<ReusedJsComponent />
<ReusedMdxComponent />
Indexing static JS/TS pages
For JS/TS pages, add data-pagefind-body to the element wrapping the content you want indexed. You can ignore specific subtrees with data-pagefind-ignore.
page.jsx
export default function Page() {
return (
// All content within `data-pagefind-body` will be indexed
<ul data-pagefind-body>
<li>Nextra 4 is the best MDX Next.js library</li>
{/* Except elements with `data-pagefind-ignore` */}
<li data-pagefind-ignore>Nextra 3 is the best MDX Next.js library</li>
</ul>
)
}
[TIP]
For MDX pages while usingnextra-theme-docsornextra-theme-blog, you don’t need to adddata-pagefind-body.
Pagefind basics:
- Indexes your built HTML, not your source; run indexing after
next build. - Scope the indexed area by adding
data-pagefind-bodyto the container you want indexed. - Exclude subtrees with
data-pagefind-ignore.
Setup
New search engine requires a few steps:
1) Install Pagefind as a dev dependency
npm i -D pagefind
2) Add a postbuild script (Pagefind indexes built .html pages)
package.json
{
"scripts": {
"postbuild": "pagefind --site .next/server/app --output-path public/_pagefind"
}
}
3) Enable pre/post scripts (optional; e.g. pnpm@8 disables them by default)
.npmrc
enable-pre-post-scripts=true
[NOTE]
pnpm@9 runs pre/post scripts by default.
pnpm@8 requires enabling pre/post scripts via.npmrc(enable-pre-post-scripts=true); pnpm@9 runs them by default.
4) Ignore generated index output in Git
.gitignore
node_modules/
.next/
_pagefind/
Using <Search> in a custom theme
Search from nextra-theme-docs is exported via nextra/components. nextra-theme-blog also supports it; custom themes can use it too.
app/layout.jsx
import { Search } from 'nextra/components'
export function RootLayout({ children }) {
return (
<html>
<body>
<header>
<Search />
</header>
<main>{children}</main>
</body>
</html>
)
}
RSC I18n Support
Thanks to server components, we no longer need to ship to client translation dictionary files, e.g ./dictionaries/en.json. We can dynamically load translations in server components and pass according translation as props to , ,
Below is an example of server components i18n using nextra-theme-docs, but the same approach should be applied to your custom theme:
app/[lang]/layout.jsx
import { Footer, LastUpdated, Layout, Navbar } from 'nextra-theme-docs'
import { Banner, Head, Search } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
import { getDictionary, getDirection } from '../path/to/your/get-dictionary'
// Required for theme styles, previously was imported under the hood
import 'nextra-theme-docs/style.css'
export const metadata = {
// ... your metadata (Next.js Metadata API)
}
export default async function RootLayout({ children, params }) {
const { lang } = await params
const pageMap = await getPageMap(lang)
const direction = getDirection(lang)
const dictionary = await getDictionary(lang)
return (
<html
lang={lang}
// Required to be set
dir={direction}
// Suggested by `next-themes` package
suppressHydrationWarning
>
<Head />
<body>
<Layout
banner={<Banner storageKey="some-key">{dictionary.banner}</Banner>}
docsRepositoryBase="https://github.com/your-org/your-repo/path"
editLink={dictionary.editPage}
feedback={{ content: dictionary.feedback }}
footer={<Footer>{dictionary.footer}</Footer>}
i18n={[
{ locale: 'en', name: 'English' },
{ locale: 'fr', name: 'Français' },
{ locale: 'ru', name: 'Русский' }
]}
lastUpdated={<LastUpdated>{dictionary.lastUpdated}</LastUpdated>}
navbar={<Navbar logo={<MyLogo />} />}
pageMap={pageMap}
search={
<Search
emptyResult={dictionary.searchEmptyResult}
errorText={dictionary.searchError}
loading={dictionary.searchLoading}
placeholder={dictionary.searchPlaceholder}
/>
}
themeSwitch={{
dark: dictionary.dark,
light: dictionary.light,
system: dictionary.system
}}
toc={{
backToTop: dictionary.backToTop,
title: dictionary.tocTitle
}}
>
{children}
</Layout>
</body>
</html>
)
}
Where get-dictionary file may look like:
../path/to/your/get-dictionary.js
// Ensure this file is always called in server component
import 'server-only'
// Enumerate all dictionaries
const dictionaries = {
en: () => import('./en.json'),
fr: () => import('./fr.json'),
ru: () => import('./ru.json')
}
export async function getDictionary(locale) {
const { default: dictionary } = await (dictionaries[locale] || dictionaries.en)()
return dictionary
}
export function getDirection(locale) {
switch (locale) {
case 'he':
return 'rtl'
default:
return 'ltr'
}
}
[NOTE]
App Router i18n uses[lang]route segments; the example above loads dictionaries and direction server-side and passes them to theme components.
Enhanced by React Compiler
The source code for nextra, nextra-theme-docs and nextra-theme-blog has been optimized using the React Compiler. All Nextra’s components and hooks are optimized under the hood by React Compiler and all internal usages of useCallback, useMemo and memo were removed.
What this means in practice:
- Components are compiled to be more memo-friendly automatically.
- Many manual
useMemo,useCallback, ormemousages become unnecessary. - You don’t change your code’s behavior; compilation optimizes it behind the scenes.
GitHub Alert Syntax
nextra-theme-docs and nextra-theme-blog support replacing GitHub alert syntax with component for .md/.mdx files.
// GitHub alert syntax is converted to Nextra Callout components as shown above.
Markdown
> [!NOTE]
>
> Useful information that users should know, even when skimming content.
> [!TIP]
>
> Helpful advice for doing things better or more easily.
> [!IMPORTANT]
>
> Key information users need to know to achieve their goal.
> [!WARNING]
>
> Urgent info that needs immediate user attention to avoid problems.
> [!CAUTION]
>
> Advises about risks or negative outcomes of certain actions.
Rendered
- Note: Useful information that users should know.
- Tip: Helpful advice for doing things better or more easily.
- Important: Key information needed to achieve a goal.
- Warning: Urgent info to avoid problems.
- Caution: Advises about risks or negative outcomes.
Bundle Size Difference with Nextra 3
Let’s compare bundle size changes between Nextra 3 and 4 (Docs and Blog examples).
[NOTE]
All four examples use Next.js 15.1.3 at the time of writing.
- Docs Example: First Load JS shared by all decreased 36.9% (168 kB → 106 kB).
- I18n Docs Example: First Load JS shared by all decreased 38.7% (173 kB → 106 kB).
- Blog Example: First Load JS shared by all decreased 7.9% (114 kB → 105 kB).
Overall, First Load JS is reduced across all examples.
Remote Docs
Remote docs configuration has changed. Refer to the official example. Also update the pageMap list in your layout to display sidebar navigation links correctly. An example modified layout is available in the repository.
nextra-theme-docs Changes
Zustand
All previous React context usages were migrated to zustand except places where dependency injection is needed. This applies only to the useConfig and useThemeConfig hooks, which need to initialize state with props.
Tip: useConfig and useThemeConfig stores read initial state from props passed at layout time so that server-provided data is available immediately without extra client fetches.
Migrated to Tailwind CSS 4
The theme has been updated to Tailwind CSS 4. The previously used Tailwind CSS prefix _ (to avoid class name conflicts) has been replaced with x:. Update overridden classes accordingly.
Notes:
- Tailwind v4 simplifies config and layers. If you relied on a global
_prefix to avoid collisions, switch tox:and update any overrides.
my-styles.css
- ._text-primary-600 { ... }
+ .x\:text-primary-600 { ... }
New Headings :target Animation
All headings now have an animation for the :target state.
Enhanced <NotFoundPage>
The built-in <NotFoundPage> now includes a URL for creating an issue with the referrer URL included. This makes it easier to identify the broken page that led the user to the 404 error.
Migration Guide (Docs Theme)
- Choose MDX rendering mode
- Add
layout.jsx
Example app/layout.jsx for the Nextra documentation theme:
app/layout.jsx
import { Footer, Layout, Navbar } from 'nextra-theme-docs'
import { Banner, Head } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
// Required for theme styles, previously was imported under the hood
import 'nextra-theme-docs/style.css'
export const metadata = {
// ... your metadata (Next.js Metadata API)
}
const banner = <Banner storageKey="some-key">Nextra 4.0 is released 🎉</Banner>
const navbar = <Navbar logo={<b>Nextra</b>} projectLink="https://github.com/your-org/your-repo" />
const footer = (
<Footer className="flex-col items-center md:items-start">
MIT {new Date().getFullYear()} © Nextra.
</Footer>
)
export default async function RootLayout({ children }) {
return (
<html
// Not required, but good for SEO
lang="en"
// Required to be set
dir="ltr"
// Suggested by `next-themes` package
suppressHydrationWarning
>
<Head
backgroundColor={{
dark: 'rgb(15, 23, 42)',
light: 'rgb(254, 252, 232)'
}}
color={{
hue: { dark: 120, light: 0 },
saturation: { dark: 100, light: 100 }
}}
>
{/* Your additional tags should be passed as `children` of `<Head>` element */}
</Head>
<body>
<Layout
banner={banner}
navbar={navbar}
pageMap={await getPageMap()}
docsRepositoryBase="https://github.com/your-org/your-repo/docs"
editLink="Edit this page on GitHub"
sidebar={{ defaultMenuCollapseLevel: 1 }}
footer={footer}
// ...Your additional theme config options
>
{children}
</Layout>
</body>
</html>
)
}
Add mdx-components.jsx file
Create an mdx-components.jsx file in the project root to define global MDX components:
mdx-components.jsx
import { useMDXComponents as getDocsMDXComponents } from 'nextra-theme-docs'
const docsComponents = getDocsMDXComponents()
export function useMDXComponents(components) {
return {
...docsComponents,
...components
// ... your additional components
}
}
Migrate theme.config options
| Nextra 3 | Nextra 4 |
|---|---|
banner.content |
children prop in <Banner> |
banner.dismissible |
dismissible prop in <Banner> |
banner.key |
storageKey prop in <Banner> |
backgroundColor.dark |
backgroundColor.dark prop in <Head> |
backgroundColor.light |
backgroundColor.light prop in <Head> |
chat.icon |
chatIcon prop in <Navbar> |
chat.link |
chatLink prop in <Navbar> |
components |
Removed. Provide custom components inside useMDXComponents |
darkMode |
darkMode prop in <Layout> |
direction |
Removed. Use dir attribute on <html> |
docsRepositoryBase |
docsRepositoryBase prop in <Layout> |
editLink.component |
editLink prop in <Layout> |
editLink.content |
children prop in <LastUpdated> |
faviconGlyph |
faviconGlyph prop in <Head> |
feedback.content |
feedback.content prop in <Layout> |
feedback.labels |
feedback.labels prop in <Layout> |
feedback.useLink |
Removed |
footer.component |
footer prop in <Layout> |
footer.content |
children prop in <Footer> |
gitTimestamp |
lastUpdated prop in <Layout> |
head |
Removed. Use <Head> or Next.js Metadata API |
i18n[number].direction |
Removed. Use dir attribute on <html> |
i18n[number].locale |
i18n[number].locale prop in <Layout> |
i18n[number].name |
i18n[number].name prop in <Layout> |
logo |
logo prop in <Navbar> |
logoLink |
logoLink prop in <Navbar> |
main |
Removed |
navbar.component |
navbar prop in <Layout> |
navbar.extraContent |
children prop in <Layout> |
navigation |
navigation prop in <Layout> |
nextThemes |
nextThemes prop in <Layout> |
notFound.content |
content prop in <NotFoundPage> |
notFound.labels |
labels prop in <NotFoundPage> |
color.hue |
color.hue prop in <Head> |
color.saturation |
color.saturation prop in <Head> |
project.icon |
projectIcon prop in <Navbar> |
project.link |
projectLink prop in <Navbar> |
search.component |
search prop in <Layout> |
search.emptyResult |
emptyResult prop in <Search> |
search.error |
errorText prop in <Search> |
search.loading |
loading prop in <Search> |
search.placeholder |
placeholder prop in <Search> |
sidebar.autoCollapse |
sidebar.autoCollapse prop in <Layout> |
sidebar.defaultMenuCollapseLevel |
sidebar.defaultMenuCollapseLevel prop in <Layout> |
sidebar.toggleButton |
sidebar.toggleButton prop in <Layout> |
themeSwitch.component |
Removed |
themeSwitch.useOptions |
themeSwitch prop in <Layout> |
toc.backToTop |
toc.backToTop prop in <Layout> |
toc.component |
Removed |
toc.extraContent |
toc.extraContent prop in <Layout> |
toc.float |
toc.float prop in <Layout> |
toc.title |
toc.title prop in <Layout> |
### Dynamic <head> Tags |
|
Dynamic head tags were previously configured via the head theme config option. In Nextra 4 use the Next.js Metadata API instead. |
// Use the Next.js Metadata API to set titles and Open Graph fields at the layout level; front matter provides page-level title/description.
Nextra 3 — theme.config.jsx
export default {
head() {
const config = useConfig()
const { route } = useRouter()
const title = config.title + (route === '/' ? '' : ' | Nextra')
return (
<>
<title>{title}</title>
<meta property="og:title" content={title} />
</>
)
}
}
Nextra 4 — app/layout.jsx
export const metadata = {
title: {
default: 'Nextra – Next.js Static Site Generator',
template: '%s | Nextra'
},
openGraph: {
url: 'https://example.com',
siteName: 'Nextra',
locale: 'en_US',
type: 'website'
}
}
app/page.mdx
---
description: Make beautiful websites with Next.js & MDX.
---
# Hello Nextra 4
Front matter `title` or the first Markdown `<h1>` sets `<title>` and `<meta property="og:title">`. The `description` field sets `<meta name="description">` and `<meta property="og:description">` in `<head>`.
<head>
<title>Hello Nextra 4 | Nextra</title>
<meta property="og:title" content="Hello Nextra 4 | Nextra" />
<meta name="description" content="Make beautiful websites with Next.js & MDX." />
<meta property="og:description" content="Make beautiful websites with Next.js & MDX." />
</head>
nextra-theme-blog Changes
Support for _meta files
Partial support for _meta files allows defining navbar links directly there. The front matter option draft: true is replaced by display: 'hidden' in an _meta file.
react-cusdis removed
The optional peer dependency react-cusdis was removed because it is unmaintained and pinned to React 17. The <Comments> component uses the Cusdis SDK directly.
How to integrate Cusdis SDK (simplified):
// In your post layout or page
export function Comments() {
return (
<div
id="cusdis_thread"
data-host="https://cusdis.example.com"
data-app-id="YOUR_APP_ID"
data-page-id={usePathname()}
data-page-url={typeof window !== 'undefined' ? window.location.href : ''}
data-page-title={typeof document !== 'undefined' ? document.title : ''}
/>
)
}
// Load once globally (e.g. in layout)
// Include the Cusdis embed script globally (e.g. in layout)
next-view-transitions added
Blog theme supports the View Transitions API via next-view-transitions.
Minimal usage example:
// app/layout.jsx
import { ViewTransitions } from 'next-view-transitions'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ViewTransitions>{children}</ViewTransitions>
</body>
</html>
)
}
// Use the Link from next-view-transitions for animated navigations
import { Link } from 'next-view-transitions'
function Navbar() {
return <Link href="/posts/hello">Read more</Link>
}
Use built-in Nextra <Search>
nextra-theme-blog v4 supports built-in search. Import <Search> from nextra/components and place it inside <Navbar>, then follow the search setup steps.
app/layout.jsx
<Navbar pageMap={await getPageMap()}>
<Search />
<ThemeSwitch />
</Navbar>
Migration Guide (Blog Theme)
- Choose MDX rendering mode
- Migrate
theme.configoptions - Add
layout.jsx
Example app/layout.jsx for the Nextra blog theme:
app/layout.jsx
import { Footer, Layout, Navbar, ThemeSwitch } from 'nextra-theme-blog'
import { Banner, Head, Search } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
// Required for theme styles, previously was imported under the hood
import 'nextra-theme-blog/style.css'
export const metadata = {
// ... your metadata (Next.js Metadata API)
}
const banner = <Banner storageKey="some-key">Nextra 4.0 is released 🎉</Banner>
export default async function RootLayout({ children }) {
return (
<html
// Not required, but good for SEO
lang="en"
// Suggested by `next-themes` package
suppressHydrationWarning
>
<Head backgroundColor={{ dark: '#0f172a', light: '#fefce8' }} />
<body>
<Layout banner={banner}>
<Navbar pageMap={await getPageMap()}>
<Search />
<ThemeSwitch />
</Navbar>
{children}
<Footer>{new Date().getFullYear()} © Dimitri Postolov.</Footer>
</Layout>
</body>
</html>
)
}
Add mdx-components.jsx file
Create an mdx-components.jsx file in the root to define global MDX components:
mdx-components.jsx
import { useMDXComponents as getBlogMDXComponents } from 'nextra-theme-blog'
const blogComponents = getBlogMDXComponents()
export function useMDXComponents(components) {
return {
...blogComponents,
...components
// ... your additional components
}
}
Migrate theme.config options
| Nextra 3 | Nextra 4 |
|---|---|
comments |
Provide your comments component in post layout (e.g. app/posts/(with-comments)/layout.jsx) |
components |
Provide custom components inside useMDXComponents |
darkMode |
To disable theme toggle, remove <ThemeSwitch> from <Navbar> |
dateFormatter |
Provide DateFormatter component inside useMDXComponents |
footer |
Provide your <Footer> as the last child of <Layout> |
head |
Use <Head> or Next.js Metadata API |
navs |
Set up your navbar links via _meta files |
postFooter |
Provide your post footer in post layout (e.g. app/posts/(with-comments)/layout.jsx) |
readMore |
Provide readMore prop for <PostCard> |
titleSuffix |
Use Next.js Metadata API |
Various Changes
_meta files changes
_metafiles should be server component files (no'use client').zodnow parses and transforms_metafiles on the server, improving DX and avoiding typos.- The
_meta.newWindowfield was removed. - External links in
_metanow open in a new tab withrel="noreferrer"and show a ↗ suffix icon. theme.topContentandtheme.bottomContentwere removed.theme.layout: 'raw'was removed. For pages without a layout, usepage.{jsx,tsx}files.
_meta.global file
Define all pages in a single _meta.global file. API is the same as folder-specific _meta files, except folder items must include an items field.
Example:
app/_meta.js
export default {
docs: {
type: 'page',
title: 'Documentation'
}
}
app/docs/_meta.js
export default {
items: {
index: 'Getting Started'
}
}
With a single _meta.global file, it becomes:
app/_meta.global.js
export default {
docs: {
type: 'page',
title: 'Documentation',
items: {
index: 'Getting Started'
}
}
}
[WARNING]
You can’t use both_meta.globaland_metafiles in the same project.
Component Migration
Several components were migrated from nextra-theme-docs to nextra/components so custom themes can use them:
CollapseDetailsSummarySkipNavContentSkipNavLinkSelectBleedHead
[TIP]
With<Head>moved tonextra/components, bothnextra-theme-blogand custom themes can configure:
- primary color (
colorprop)- background color (
backgroundColorprop)- favicon glyph (
faviconGlyphprop)- selection color based on primary color
Previously these options were exclusive to
nextra-theme-docs.
All built-in Nextra components were refactored to be either server or client.
Front Matter = Metadata
All fields from front matter are now exported as a metadata object in an MDX file. If you prefer not to use front matter, you can export metadata directly — but you can’t use both in the same file.
✅ Front matter
---
title: Foo
description: Bar
---
{/* Will be compiled to 👇 */}
export const metadata = {
title: 'Foo',
description: 'Bar'
}
✅ Export metadata
export const metadata = {
title: 'Foo',
description: 'Bar'
}
❌ Invalid (don’t mix)
---
title: Foo
---
export const metadata = { description: 'Bar' }
whiteListTagsStyling Option
Whitelist HTML elements to be replaced with components from mdx-components.js. By default, Nextra only replaces <details> and <summary>. Extend it to other elements as needed.
next.config.mjs
import nextra from 'nextra'
const withNextra = nextra({
whiteListTagsStyling: ['h1']
})
Example of replacing <h1>
# Hello
{/* This will be replaced as well */}
<h1>World</h1>
Folders with Index Pages
In Nextra 2 and 3, a structure with both a docs/ folder and a docs.mdx file at the same level is called “folders with index pages”.
// Known in Nextra docs as “folders with index page”.
This structure is incompatible with the page file convention. A new front matter option asIndexPage achieves the same effect:
- For page file convention: set
asIndexPage: trueindocs/page.mdx. - For content directory convention: set
asIndexPage: trueindocs/index.mdx.
List Subpages
Automatically list all subpages of a route as cards.
Steps:
- Import
createIndexPageandgetPageMapfromnextra/page-map. - Use
MDXRemotefromnextra/mdx-remoteto render the list. - Replace
/my-routewith your target route. - To show a card icon, set the
iconfront matter in subpages (optional). - Provide
Cardsand your icons viaMDXRemote’scomponentsprop.
app/my-route/page.mdx
import { Cards } from 'nextra/components'
import { MDXRemote } from 'nextra/mdx-remote'
import { createIndexPage, getPageMap } from 'nextra/page-map'
import { MyIcon } from '../path/to/your/icons'
<MDXRemote
compiledSource={
await createIndexPage(
await getPageMap('/my-route')
)
}
components={{
Cards,
MyIcon
}}
/>
app/my-route/demo/page.mdx
---
icon: MyIcon
---
# My Subpage
Subpages Example: see nextra.site/docs/advanced
Sidebar Title Priority Changes
The sidebar title is determined in this order:
- Non-empty title from the
_metafile sidebarTitlein front mattertitlein front matter- First Markdown
#heading (new in Nextra 4) - Otherwise, filename formatted per The Chicago Manual of Style
Code Block Icons Changes
Customizing icons
You can customize code block icons. Example (docs theme; same approach for custom themes):
- Import
withIconsHOC fromnextra/components. - Wrap the
Precomponent withwithIcons, passing your custom icon for the language.
mdx-components.jsx
import { useMDXComponents as useDocsMDXComponents } from 'nextra-theme-docs'
import { Pre, withIcons } from 'nextra/components'
const docsComponents = getDocsMDXComponents({
pre: withIcons(Pre, { js: MyCustomIcon })
})
export function useMDXComponents(components) {
return {
...docsComponents,
...components
// ... your additional components
}
}
Icons updates
- JSX icons:
jsxandtsxnow display the React icon. - Diff icons: For
diffcode blocks with afilenameattribute, the icon corresponds to the file extension.
Markdown Links Changes
All external Markdown links in MDX open in a new tab with rel="noreferrer" and show a ↗ suffix icon (inspired by Next.js docs).
Example:
example.mdx
[Author](https://example.com/author)
Compiles to:
<a href="https://example.com/author" target="_blank" rel="noreferrer">
Author 
<LinkArrowIcon height="16" className="_inline _align-baseline" />
</a>
::selection Styles
The color prop from <Head> now drives the ::selection styles (variant of the primary color).
<Table> Changes
<Th>, <Tr>, and <Td> were removed and are now attached to <Table> directly, improving DX.
Example migration:
- import { Table, Th, Tr, Td } from 'nextra/components'
+ import { Table } from 'nextra/components'
<Table>
<thead>
- <Tr>
+ <Table.Tr>
- <Th>Items</Th>
+ <Table.Th>Items</Table.Th>
- </Tr>
+ </Table.Tr>
</thead>
<tbody>
- <Tr>
+ <Table.Tr>
- <Th>Donuts</Th>
+ <Table.Th>Donuts</Table.Th>
- </Tr>
+ </Table.Tr>
</tbody>
<tfoot>
- <Tr>
+ <Table.Tr>
- <Th>Totals</Th>
+ <Table.Th>Totals</Table.Th>
- </Tr>
+ </Table.Tr>
</tfoot>
</Table>
compileMdx Changes
compileMdx now returns a Promise<string> instead of Promise<object>.
import { compileMdx } from 'nextra/compile'
const rawMdx = '# Hello Nextra 4'
- const { result: rawJs } = await compileMdx(rawMdx)
+ const rawJs = await compileMdx(rawMdx)
<RemoteContent> Changes
<RemoteContent>was renamed to<MDXRemote>.- Moved from
nextra/componentstonextra/mdx-remote. - Default components are auto-provided from your
mdx-components.jsx— no need to pass them manually.
Optimized Imports
Imports from nextra/components, nextra-theme-docs, and nextra-theme-blog are optimized with Next.js optimizePackageImports, which rewrites and hoists package sub-imports to reduce client bundle size without code changes on your side.
useRouter Removed
Use Next.js useRouter from next/navigation instead of Nextra’s hook.
- import { useRouter } from 'nextra/hooks'
+ import { useRouter } from 'next/navigation'
Minimal Next.js v14
Nextra 4 requires Next.js 14 or newer.
Update your tsconfig.json
If you use TypeScript with moduleResolution: node, you may see:
Type error: Cannot find module 'nextra/components' or its corresponding type declarations.
typesVersions fields were removed from Nextra packages. Set "moduleResolution": "bundler":
tsconfig.json
{
"compilerOptions": {
- "moduleResolution": "node"
+ "moduleResolution": "bundler"
}
}
Conclusion
Nextra 4 introduces key improvements:
- App Router support (aligned with latest Next.js APIs)
- Turbopack support for faster development
- Pagefind search engine with better results
- RSC i18n support for multilingual sites
- React Compiler optimization across packages
- Reduced bundle size across examples
- Improved Page Map collects static jsx/tsx in
app/ - More capable TOC with interpolated content and math
- Robust remote MDX rendering with proper nav
'JS,TS,NPM' 카테고리의 다른 글
| [Vite] React 하나의 index.html로 빌드 (1) | 2024.11.20 |
|---|---|
| [Next] App router 방식에서 Edge Function 적용법 (1) | 2024.11.06 |
| [JS] 브라우저에서 유저의 언어 식별 (2) | 2024.11.04 |
| [Node] class 혹은 function의 file path를 찾아보자 (0) | 2023.11.16 |
| [TypeScript] Decorator를 이용한 코드 변경 (0) | 2023.11.07 |
댓글