본문 바로가기
JS,TS,NPM

[Nextra] Migrate Nextra 3 to the Nextra 4 framework(Nextra3 -> 4 마이그레이션 문서)

by Nhahan 2025. 8. 29.

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.Th instead of standalone Tr/Th/Td
  • Remote MDX: use <MDXRemote> and nextra/mdx-remote path
  • Replace useRouter with next/navigation
  • TypeScript config: set moduleResolution to bundler

 

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 (or page.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 pages folder to content (can be at project root or src).
  • Set contentDirBasePath in next.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.jsx under app/ with the content route loader.

[TIP]
Consider the single catch-all route [[...mdxPath]]/page.jsx as a gateway to your content directory. If you set contentDirBasePath in next.config.mjs, put [[...mdxPath]]/page.jsx under the corresponding directory.

[NOTE]
Many existing solutions (e.g. “Refreshing the Next.js App Router When Your Markdown Content Changes”) rely on extra dependencies like concurrently and ws, 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
  • import in 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; no concurrently/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]
All page.{jsx,tsx} must export a metadata object.

[NOTE]
See Nextra’s website or the Nextra blog example for this mode in practice.
You can implement this mode by colocating page.{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 (--turbopack omitted), Next.js falls back to Webpack.

package.json

"scripts": {
-  "dev": "next dev"
+  "dev": "next dev --turbopack"
}

[NOTE]
Without the --turbopack flag, Next.js uses Webpack (JavaScript) under the hood.

[WARNING]
At the time of writing, only JSON-serializable values can be passed to nextra(...). You cannot pass custom remarkPlugins, rehypePlugins, or recmaPlugins (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> in app/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 using nextra-theme-docs or nextra-theme-blog, you don’t need to add data-pagefind-body.

Pagefind basics:

  • Indexes your built HTML, not your source; run indexing after next build.
  • Scope the indexed area by adding data-pagefind-body to 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 , ,

, etc. components in app/[lang]/layout.jsx.

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, or memo usages 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 to x: 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 &amp; MDX." />
  <meta property="og:description" content="Make beautiful websites with Next.js &amp; 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.config options
  • 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

  • _meta files should be server component files (no 'use client').
  • zod now parses and transforms _meta files on the server, improving DX and avoiding typos.
  • The _meta.newWindow field was removed.
  • External links in _meta now open in a new tab with rel="noreferrer" and show a ↗ suffix icon.
  • theme.topContent and theme.bottomContent were removed.
  • theme.layout: 'raw' was removed. For pages without a layout, use page.{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.global and _meta files in the same project.

Component Migration

Several components were migrated from nextra-theme-docs to nextra/components so custom themes can use them:

  • Collapse
  • Details
  • Summary
  • SkipNavContent
  • SkipNavLink
  • Select
  • Bleed
  • Head

[TIP]
With <Head> moved to nextra/components, both nextra-theme-blog and custom themes can configure:

  • primary color (color prop)
  • background color (backgroundColor prop)
  • favicon glyph (faviconGlyph prop)
  • 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: true in docs/page.mdx.
  • For content directory convention: set asIndexPage: true in docs/index.mdx.

List Subpages

Automatically list all subpages of a route as cards.

Steps:

  • Import createIndexPage and getPageMap from nextra/page-map.
  • Use MDXRemote from nextra/mdx-remote to render the list.
  • Replace /my-route with your target route.
  • To show a card icon, set the icon front matter in subpages (optional).
  • Provide Cards and your icons via MDXRemote’s components prop.

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 _meta file
  • sidebarTitle in front matter
  • title in 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 withIcons HOC from nextra/components.
  • Wrap the Pre component with withIcons, 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: jsx and tsx now display the React icon.
  • Diff icons: For diff code blocks with a filename attribute, 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&thinsp;
  <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/components to nextra/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

댓글