Nextra 4 x App Router. What's New and Migration Guide

Dimitri POSTOLOV

Nextra 4 just released, marking the largest update in its history, with a ton of exciting improvements. Here’s what’s new:

App Router Support

Nextra 4 exclusively uses Next.js App Router. Support for the Pages Router has been discontinued. In Nextra 4, there are two ways to render MDX files using file-based routing:

Trade-offs

  • The catch-all route route may lead to longer compilation times, depending on the number of MDX files.
  • The page file convention works well with colocation, allowing you to keep all the assets for an article in one place.

Using Content Directory Convention

Migrate your Pages Router site with minimal changes using this mode. Follow these steps:

Rename your pages folder to content

You can keep content directory in root of your project, or in src directory.

Set contentDirBasePath option in next.config file (optional)

If you want to serve your content from a different path, you can set contentDirBasePath option:

next.config.mjs
import nextra from 'nextra'
 
const withNextra = nextra({
  contentDirBasePath: '/docs' // Or even nested e.g. `/docs/advanced`
})

Add [[...mdxPath]]/page.jsx file

Place this file in app directory with the following content, you should get the following structure:

    • layout.jsx
      • page.jsx
    • index.mdx
      • index.mdx
      • getting-started.mdx
  • 💡

    Tip

    Consider the single catch-all route [[...mdxPath]]/page.jsx as a gateway to your content directory.

    If you set contentDirBasePath option in next.config file, you should put [[...mdxPath]]/page.jsx in the corresponding directory.

    You are ready to go!

    Note

    Many existing solutions such as Refreshing the Next.js App Router When Your Markdown Content Changes rely on extra dependencies like concurrently and ws. These approaches include Dan Abramov workaround with <AutoRefresh> component and dev web socket server.

    Nextra’s content mode delivers a streamlined solution right out of the box:

    • you don’t need to install unnecessary dependencies
    • you don’t need to restart your server on changes in content directory
    • hot reloading works out of the box
    • you can use import statements in MDX files and static images works as well

    Checkout Nextra’s docs website and i18n website example.

    Using Page File Convention

    The above file-based routing structure, following the Using Content Directory Convention, will appear as follows when using this mode:

    • layout.jsx
    • page.jsx
      • page.mdx
        • page.mdx
  • 💡

    Warning

    All pages page.{jsx,tsx} should export metadata object.

    Note

    See Nextra’s website or Nextra’s blog example as an examples of this mode.

    Turbopack Support

    After being one of our most requested features for over 2 years, Nextra 4 finally supports Turbopack, the Rust-based incremental bundler. Enable it by adding --turbopack to your dev command:

    package.json
    "scripts": {
    -  "dev": "next dev"
    +  "dev": "next dev --turbopack"
    }

    Note

    Without --turbopack flag Next.js under the hood uses Webpack, written in JavaScript.

    ⚠️

    Warning

    At the time of writing this blog only JSON serializable values can be passed to nextra function. You cannot pass custom remarkPlugins, rehypePlugins or recmaPlugins since they are functions.

    next.config.mjs
    import nextra from 'nextra'
     
    const withNextra = nextra({
      mdxOptions: {
        remarkPlugins: [myRemarkPlugin],
        rehypePlugins: [myRehypePlugin],
        recmaPlugins: [myRecmaPlugin]
      }
    })

    If you try to pass them, you’ll get an error from Turbopack:

    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 Support

    Nextra 4 no longer support theme.config files, which were previously used for configuring your theme options, theme and themeConfig option were removed too:

    next.config.mjs
    const withNextra = nextra({
    - theme: 'nextra-theme-docs',
    - themeConfig: './theme.config.tsx'
    })

    Note

    Previously theme config options now should be passed as props for <Layout>, <Navbar>, <Footer>, <Search> and <Banner> components in app/layout.jsx file.

    New Search Engine – Pagefind

    Search engine was migrated from FlexSearch written in JavaScript to Rust-powered Pagefind.

    Benefits

    Pagefind is significantly faster and delivers far superior search results compared to FlexSearch. Here are some examples that previously didn’t work in earlier versions of Nextra:

    1. Indexing remote MDX.

      page.mdx
       import { Callout } from 'nextra/components'
       
       export async function Stars() {
         const response = await fetch('https://api.github.com/repos/shuding/nextra')
         const repo = await response.json()
         const stars = repo.stargazers_count
         return <b>{stars}</b>
       }
       export async function getUpdatedAt() {
         const response = await fetch('https://api.github.com/repos/shuding/nextra')
         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>
    2. Indexing dynamic content written in Markdown/MDX.

      page.mdx
       {/* Current year will be indexed 🎉 */}
       MIT {new Date().getFullYear()} © Nextra.
    3. Indexing imported JavaScript or MDX files in 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 />
    4. Indexing static pages written in JavaScript or TypeScript.

      For JavaScript/TypeScript pages you need to add data-pagefind-body attribute to the tag enclosing the main content area you want indexed.

      You can ignore indexing of specific tags by adding data-pagefind-ignore attribute.

      page.jsx
      export default function Page() {
        return (
          // All content of tags with `data-pagefind-body` attribute will be indexed
          <ul data-pagefind-body>
            <li>Nextra 4 is the best MDX Next.js library</li>
            {/* Except for tags with `data-pagefind-ignore` attribute */}
            <li data-pagefind-ignore>Nextra 3 is the best MDX Next.js library</li>
          </ul>
        )
      }
      💡

      Tip

      For MDX pages while using nextra-theme-docs and nextra-theme-blog you don’t need to add data-pagefind-body attribute.

    Setup

    New search engine requires few steps to setup:

    Instal pagefind as devDependency

    npm i -D pagefind

    Add postbuild script

    Pagefind indexes .html pages and search indexing should be done after building your application:

    package.json
    "scripts": {
      "dev": "next --turbopack",
      "build": "next build",
      "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind",
      // Or if you use Next.js' Static Exports
      "postbuild": "pagefind --site .next/server/app --output-path out/_pagefind"
    }

    Enable pre/post scripts (optional)

    Some package managers like pnpm@8 by default don’t execute pre/post scripts, you should enable it via .npmrc file via enable-pre-post-scripts setting:

    .npmrc
    enable-pre-post-scripts=true

    Note

    pnpm@9 by default run pre/post scripts.

    Add _pagefind/ directory to .gitignore file

    You don’t need to commit _pagefind/ directory to your git repository, he should be always newly generated.

    .gitignore
    node_modules/
    .next/
    _pagefind/

    Use <Search> component in your custom theme

    Search from nextra-theme-docs was exported in nextra/components. With this change nextra-theme-blog received search feature too. And you can benefit from it as well in your custom themes.

    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 <Layout>, <Banner>, <Footer>, etc. components in app/[lang]/layout.jsx.

    Example

    Below an example of server components i18n using nextra-theme-docs, but the same approach should be applied for 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 API
      // https://nextjs.org/docs/app/building-your-application/optimizing/metadata
    }
     
    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 https://github.com/pacocoursey/next-themes#with-app
          suppressHydrationWarning
        >
          <Head />
          <body>
            <Layout
              banner={<Banner storageKey="some-key">{dictionary.banner}</Banner>}
              docsRepositoryBase="https://github.com/shuding/nextra/blob/main/examples/swr-site"
              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 looks 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

    Read more about server components i18n in Next.js docs.
    See working example of i18n website in Nextra’s examples.

    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.

    GitHub Alert Syntax

    nextra-theme-docs and nextra-theme-blog support replacing GitHub alert syntax with <Callout> component for .md/.mdx files.

    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.

    Will be rendered as:

    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.

    Bundle Size Difference with Nextra 3

    Let’s compare how bundle size changed between Nextra 3 and Nextra 4. Similar to Nextra 3 blogpost I am comparing Docs Example which uses nextra-theme-docs and Blog Example which uses nextra-theme-blog.

    Note

    All 4 examples were updated to the latest Next.js 15.1.3 at the moment of writing this blogpost.

    Docs Example

    Theme Docs with Nextra 3
    Example Theme Docs with Nextra 3 (Pages Router)
    Route (pages)                                 Size     First Load JS
    ┌ ○ /                                         1.67 kB         164 kB
    ├   /_app                                     0 B             153 kB
    ├ ● /_meta                                    249 B           154 kB
    ├ ○ /404                                      188 B           154 kB
    ├ ● /advanced/_meta                           256 B           154 kB
    ├ ○ /advanced/code-highlighting               1.92 kB         165 kB
    ├ ● /features/_meta                           256 B           154 kB
    ├ ○ /features/i18n                            2.41 kB         165 kB
    ├ ○ /features/image                           2.64 kB         165 kB
    ├ ○ /features/latex                           2.56 kB         165 kB
    ├ ○ /features/mdx                             4.07 kB         167 kB
    ├ ● /features/ssg (ISR: 60 Seconds) (583 ms)  2.28 kB         165 kB
    ├ ○ /features/themes                          1.35 kB         164 kB
    ├ ○ /get-started                              3.38 kB         171 kB
    ├ ● /themes/_meta                             254 B           154 kB
    ├ ○ /themes/blog                              3.06 kB         166 kB
    ├ ● /themes/blog/_meta                        258 B           154 kB
    ├ ○ /themes/docs                              2.38 kB         170 kB
    ├ ● /themes/docs/_meta                        258 B           154 kB
    ├ ○ /themes/docs/bleed                        2.64 kB         165 kB
    ├ ○ /themes/docs/callout                      2.42 kB         165 kB
    ├ ○ /themes/docs/configuration                6.17 kB         169 kB
    └ ○ /themes/docs/tabs                         5.97 kB         169 kB
    + First Load JS shared by all                 168 kB
      ├ chunks/framework-6b181cec221b9dfa.js      44.8 kB
      ├ chunks/main-c456fe7452186752.js           38.5 kB
      ├ chunks/pages/_app-350c9924bcb3142d.js     68.3 kB
      ├ css/8c65e010692a2141.css                  15.1 kB
      └ other shared chunks (total)               1.71 kB
     
    ○  (Static)  prerendered as static content
    ●  (SSG)     prerendered as static HTML (uses getStaticProps)
       (ISR)     incremental static regeneration (uses revalidate in getStaticProps)
    Theme Docs with Nextra 4
    Example Theme Docs with Nextra 4 (App Router with Content Directory Convention)
    Route (app)                              Size     First Load JS
    ┌ ○ /                                    146 B           106 kB
    ├ ○ /_not-found                          988 B           107 kB
    ├ ○ /apple-icon.png                      0 B                0 B
    ├ ○ /blog                                146 B           106 kB
    ├ ● /docs/[[...mdxPath]]                 299 B           177 kB
    ├   ├ /docs/advanced/code-highlighting
    ├   ├ /docs/features/i18n
    ├   ├ /docs/features/image
    ├   └ [+14 more paths]
    ├ ○ /icon.png                            0 B                0 B
    └ ○ /opengraph-image.png                 0 B                0 B
    + First Load JS shared by all            106 kB
      ├ chunks/5323-beeaeed450ee0972.js      50.7 kB
      ├ chunks/67a2b45f-b45ef6a709f04795.js  53 kB
      └ other shared chunks (total)          2.63 kB
     
     
    ○  (Static)  prerendered as static content
    ●  (SSG)     prerendered as static HTML (uses generateStaticParams)

    First Load JS shared by all is decreased by 36.9% from 168 kB to 106 kB.

    I18n Docs Example

    I18n Theme Docs with Nextra 3
    Example I18n Theme Docs with Nextra 3 (Pages Router)
    Route (pages)                                                                     Size     First Load JS
    ┌   /_app                                                                         0 B             158 kB
    ├ ○ /404                                                                          559 B           158 kB
    ├ ○ /en (600 ms)                                                                  8.8 kB          179 kB
    ├ ● /en/_meta                                                                     254 B           158 kB
    ├ ● /en/about/_meta                                                               257 B           158 kB
    ├ ○ /en/about/a-page                                                              748 B           171 kB
    ├ ○ /en/about/acknowledgement                                                     747 B           171 kB
    ├ ○ /en/about/changelog                                                           1.47 kB         171 kB
    ├ ○ /en/about/team                                                                799 B           171 kB
    ├ ○ /en/blog                                                                      1.54 kB         171 kB
    ├ ● /en/blog/_meta                                                                259 B           158 kB
    ├ ○ /en/blog/swr-v1 (631 ms)                                                      6.28 kB         176 kB
    ├ ● /en/docs/_meta                                                                258 B           158 kB
    ├ ○ /en/docs/404-500 (624 ms)                                                     1.63 kB         172 kB
    ├ ○ /en/docs/advanced (629 ms)                                                    1.13 kB         171 kB
    ├ ● /en/docs/advanced/_meta                                                       264 B           158 kB
    ├ ○ /en/docs/advanced/cache (627 ms)                                              4.77 kB         184 kB
    ├ ○ /en/docs/advanced/code-highlighting (625 ms)                                  1.71 kB         172 kB
    ├ ● /en/docs/advanced/dynamic-markdown-import                                     2.42 kB         172 kB
    ├ ○ /en/docs/advanced/file-name.with.DOTS (626 ms)                                777 B           171 kB
    ├ ○ /en/docs/advanced/images                                                      1.27 kB         171 kB
    ├ ○ /en/docs/advanced/markdown-import                                             9.13 kB         179 kB
    ├ ○ /en/docs/advanced/more/loooooooooooooooooooong-title (624 ms)                 878 B           171 kB
    ├ ● /en/docs/advanced/more/tree/one                                               750 B           171 kB
    ├ ○ /en/docs/advanced/more/tree/three                                             753 B           171 kB
    ├ ○ /en/docs/advanced/more/tree/two                                               733 B           171 kB
    ├ ○ /en/docs/advanced/performance                                                 3.25 kB         173 kB
    ├ ○ /en/docs/advanced/react-native                                                3.32 kB         173 kB
    ├ ○ /en/docs/advanced/scrollbar-x                                                 5.74 kB         176 kB
    ├ ○ /en/docs/arguments                                                            2.66 kB         173 kB
    ├ ○ /en/docs/callout                                                              1.42 kB         171 kB
    ├ ● /en/docs/change-log                                                           1.1 kB          171 kB
    ├ ○ /en/docs/code-block-without-language                                          909 B           171 kB
    ├ ○ /en/docs/conditional-fetching                                                 1.85 kB         172 kB
    ├ ○ /en/docs/custom-header-ids                                                    1.29 kB         171 kB
    ├ ○ /en/docs/data-fetching                                                        2.77 kB         173 kB
    ├ ○ /en/docs/error-handling                                                       3.62 kB         174 kB
    ├ ○ /en/docs/getting-started (316 ms)                                             16 kB           186 kB
    ├ ○ /en/docs/global-configuration (605 ms)                                        2.17 kB         172 kB
    ├ ○ /en/docs/middleware (604 ms)                                                  4.59 kB         175 kB
    ├ ○ /en/docs/mutation (612 ms)                                                    3.67 kB         174 kB
    ├ ○ /en/docs/options (607 ms)                                                     3.15 kB         173 kB
    ├ ○ /en/docs/pagination                                                           6.23 kB         184 kB
    ├ ○ /en/docs/prefetching (604 ms)                                                 2.33 kB         172 kB
    ├ ○ /en/docs/raw-layout (603 ms)                                                  1.5 kB          171 kB
    ├ ○ /en/docs/revalidation (603 ms)                                                5.08 kB         175 kB
    ├ ○ /en/docs/suspense (603 ms)                                                    2.85 kB         173 kB
    ├ ○ /en/docs/typescript                                                           2.68 kB         173 kB
    ├ ○ /en/docs/understanding                                                        3.6 kB          176 kB
    ├ ○ /en/docs/with-nextjs                                                          3.04 kB         173 kB
    ├ ○ /en/docs/wrap-toc-items                                                       1.61 kB         172 kB
    ├ ● /en/examples/_meta                                                            259 B           158 kB
    ├ ○ /en/examples/auth                                                             1.01 kB         171 kB
    ├ ○ /en/examples/basic                                                            1.01 kB         171 kB
    ├ ○ /en/examples/error-handling                                                   1.01 kB         171 kB
    ├ ○ /en/examples/full (604 ms)                                                    1 kB            171 kB
    ├ ○ /en/examples/infinite-loading                                                 1.03 kB         171 kB
    ├ ○ /en/examples/ssr                                                              1.02 kB         171 kB
    ├ ○ /en/foo                                                                       5.07 kB         175 kB
    ├ ● /en/remote/graphql-eslint/_meta                                               272 B           158 kB
    ├ ● /en/remote/graphql-eslint/[[...slug]] (18773 ms)                              5.21 kB         175 kB
    ├   ├ /en/remote/graphql-eslint/configs (4353 ms)
    ├   ├ /en/remote/graphql-eslint/custom-rules (4351 ms)
    ├   ├ /en/remote/graphql-eslint/getting-started (4351 ms)
    ├   ├ /en/remote/graphql-eslint/getting-started/parser-options (4349 ms)
    ├   ├ /en/remote/graphql-eslint/getting-started/parser (948 ms)
    ├   └ /en/remote/graphql-eslint/index (421 ms)
    ├ ● /en/remote/graphql-yoga/_meta                                                 272 B           158 kB
    ├ ● /en/remote/graphql-yoga/[[...slug]] (61009 ms)                                5.22 kB         175 kB
    ├   ├ /en/remote/graphql-yoga/features/error-masking (5851 ms)
    ├   ├ /en/remote/graphql-yoga/features/file-uploads (5846 ms)
    ├   ├ /en/remote/graphql-yoga/features/graphiql (5843 ms)
    ├   ├ /en/remote/graphql-yoga/features/subscriptions (5841 ms)
    ├   ├ /en/remote/graphql-yoga/features/testing (5840 ms)
    ├   ├ /en/remote/graphql-yoga/index (5840 ms)
    ├   ├ /en/remote/graphql-yoga/integrations/integration-with-aws-lambda (5840 ms)
    ├   └ [+16 more paths] (avg 1257 ms)
    ├ ○ /en/test                                                                      1.01 kB         171 kB
    ├ ○ /es (545 ms)                                                                  5.53 kB         173 kB
    ├ ● /es/_meta                                                                     253 B           158 kB
    ├ ● /es/docs/_meta                                                                257 B           158 kB
    ├ ● /es/docs/advanced/_meta                                                       265 B           158 kB
    ├ ○ /es/docs/advanced/file-name.with.DOTS                                         1.71 kB         169 kB
    ├ ○ /es/docs/advanced/performance (545 ms)                                        4.32 kB         171 kB
    ├ ○ /es/docs/arguments (523 ms)                                                   3.63 kB         171 kB
    ├ ● /es/docs/change-log (973 ms)                                                  7.95 kB         175 kB
    ├ ○ /es/docs/conditional-fetching (521 ms)                                        2.8 kB          170 kB
    ├ ○ /es/docs/data-fetching (522 ms)                                               3.67 kB         171 kB
    ├ ○ /es/docs/error-handling (521 ms)                                              4.63 kB         172 kB
    ├ ○ /es/docs/getting-started (521 ms)                                             10 kB           177 kB
    ├ ○ /es/docs/global-configuration                                                 3.12 kB         170 kB
    ├ ○ /es/docs/mutation                                                             4.66 kB         172 kB
    ├ ○ /es/docs/options                                                              4.14 kB         171 kB
    ├ ○ /es/docs/pagination                                                           7.5 kB          182 kB
    ├ ○ /es/docs/prefetching                                                          3.29 kB         170 kB
    ├ ○ /es/docs/revalidation                                                         6.13 kB         173 kB
    ├ ○ /es/docs/suspense (546 ms)                                                    3.79 kB         171 kB
    ├ ○ /es/docs/understanding (552 ms)                                               4.48 kB         174 kB
    ├ ○ /es/docs/with-nextjs                                                          4.02 kB         171 kB
    ├ ○ /es/docs/wrap-toc-items                                                       2.54 kB         170 kB
    ├ ● /es/examples/_meta                                                            259 B           158 kB
    ├ ○ /es/examples/auth                                                             1.91 kB         169 kB
    ├ ○ /es/examples/basic (549 ms)                                                   1.91 kB         169 kB
    ├ ○ /es/examples/error-handling                                                   1.92 kB         169 kB
    ├ ○ /es/examples/infinite-loading                                                 1.93 kB         169 kB
    ├ ○ /ru                                                                           6.66 kB         174 kB
    ├ ● /ru/_meta                                                                     254 B           158 kB
    ├ ○ /ru/blog (545 ms)                                                             3.3 kB          170 kB
    ├ ● /ru/blog/_meta                                                                258 B           158 kB
    ├ ○ /ru/blog/swr-v1 (546 ms)                                                      9.38 kB         177 kB
    ├ ● /ru/docs/_meta                                                                259 B           158 kB
    ├ ● /ru/docs/advanced/_meta                                                       264 B           158 kB
    ├ ○ /ru/docs/advanced/cache                                                       7.72 kB         184 kB
    ├ ○ /ru/docs/advanced/file-name.with.DOTS                                         2.65 kB         170 kB
    ├ ○ /ru/docs/advanced/performance (546 ms)                                        5.71 kB         173 kB
    ├ ○ /ru/docs/advanced/react-native (546 ms)                                       5.54 kB         173 kB
    ├ ○ /ru/docs/arguments                                                            4.7 kB          172 kB
    ├ ● /ru/docs/change-log (972 ms)                                                  8.95 kB         176 kB
    ├ ○ /ru/docs/conditional-fetching                                                 3.88 kB         171 kB
    ├ ○ /ru/docs/data-fetching                                                        4.75 kB         172 kB
    ├ ○ /ru/docs/error-handling                                                       5.99 kB         173 kB
    ├ ○ /ru/docs/getting-started                                                      11.2 kB         178 kB
    ├ ○ /ru/docs/global-configuration                                                 4.24 kB         171 kB
    ├ ○ /ru/docs/middleware                                                           7.14 kB         174 kB
    ├ ○ /ru/docs/mutation                                                             6.18 kB         173 kB
    ├ ○ /ru/docs/options                                                              5.55 kB         173 kB
    ├ ○ /ru/docs/pagination                                                           9.21 kB         184 kB
    ├ ○ /ru/docs/prefetching                                                          4.48 kB         172 kB
    ├ ○ /ru/docs/revalidation                                                         7.53 kB         175 kB
    ├ ○ /ru/docs/suspense (501 ms)                                                    4.97 kB         172 kB
    ├ ○ /ru/docs/understanding (503 ms)                                               5.44 kB         175 kB
    ├ ○ /ru/docs/with-nextjs (506 ms)                                                 5.22 kB         172 kB
    ├ ○ /ru/docs/wrap-toc-items (501 ms)                                              3.49 kB         171 kB
    ├ ● /ru/examples/_meta                                                            260 B           158 kB
    ├ ○ /ru/examples/auth (501 ms)                                                    2.84 kB         170 kB
    ├ ○ /ru/examples/basic (504 ms)                                                   2.85 kB         170 kB
    ├ ○ /ru/examples/error-handling (502 ms)                                          2.85 kB         170 kB
    ├ ○ /ru/examples/infinite-loading                                                 2.86 kB         170 kB
    └ ○ /ru/examples/ssr (501 ms)                                                     2.86 kB         170 kB
    + First Load JS shared by all                                                     173 kB
      ├ chunks/framework-dc66eabaa62619da.js                                          44.8 kB
      ├ chunks/main-6304e7d3c1f52717.js                                               39.2 kB
      ├ chunks/pages/_app-0f8d74dd6fdf7d5b.js                                         71.9 kB
      ├ css/aa443d1a875a7ede.css                                                      15.6 kB
      └ other shared chunks (total)                                                   1.77 kB
     
    ƒ Middleware                                                                      40.3 kB
     
    ○  (Static)  prerendered as static content
    ●  (SSG)     prerendered as static HTML (uses getStaticProps)
    I18n Theme Docs with Nextra 4
    Example I18n Theme Docs with Nextra 4 (App Router with Content Directory Convention)
    Route (app)                                    Size     First Load JS
    ┌ ○ /_not-found                                979 B           107 kB
    ├ ● /[lang]/[[...mdxPath]]                     3.79 kB         184 kB
    ├   ├ /en/about/a-page
    ├   ├ /en/about/acknowledgement
    ├   ├ /en/about/changelog
    ├   └ [+99 more paths]
    ├ ● /[lang]/remote/graphql-eslint/[[...slug]]  144 B           184 kB
    ├ ● /[lang]/remote/graphql-yoga/[[...slug]]    144 B           184 kB
    ├ ○ /apple-icon.png                            0 B                0 B
    ├ ○ /icon.svg                                  0 B                0 B
    └ ○ /manifest.webmanifest                      0 B                0 B
    + First Load JS shared by all                  106 kB
      ├ chunks/323-fcca95ec98243d99.js             50.8 kB
      ├ chunks/67a2b45f-3be7f33ae655c29d.js        52.9 kB
      └ other shared chunks (total)                2 kB
     
     
    ƒ Middleware                                   40.4 kB
     
    ○  (Static)  prerendered as static content
    ●  (SSG)     prerendered as static HTML (uses generateStaticParams)

    First Load JS shared by all is decreased by 38.7% from 173 kB to 106 kB.

    Blog Example

    Theme Blog with Nextra 3
    Example Theme Blog with Nextra 3 (Pages Router)
    Route (pages)                               Size     First Load JS
    ┌ ○ /                                       1.61 kB         115 kB
    ├   /_app                                   0 B             104 kB
    ├ ○ /404                                    188 B           104 kB
    ├ ○ /posts                                  1.08 kB         115 kB
    ├ ○ /posts/aaron-swartz-a-programmable-web  3.9 kB          118 kB
    ├ ○ /posts/callout                          9.77 kB         123 kB
    ├ ○ /posts/code-blocks                      2.16 kB         116 kB
    ├ ○ /posts/draft                            1.28 kB         115 kB
    ├ ○ /posts/lists                            1.22 kB         115 kB
    ├ ○ /posts/table                            1.36 kB         115 kB
    └ ● /tags/[tag] (324 ms)                    1.33 kB         115 kB
        ├ /tags/web development
        ├ /tags/JavaScript
        ├ /tags/GraphQL
        └ [+6 more paths]
    + First Load JS shared by all               114 kB
      ├ chunks/framework-6b181cec221b9dfa.js    44.8 kB
      ├ chunks/main-c456fe7452186752.js         38.5 kB
      ├ chunks/pages/_app-04c2827288399efd.js   19.5 kB
      └ other shared chunks (total)             10.9 kB
     
    ○  (Static)  prerendered as static content
    ●  (SSG)     prerendered as static HTML (uses getStaticProps)
    Theme Blog with Nextra 4
    Example Theme Blog with Nextra 4 (App Router with Page File Convention)
    Route (app)                                 Size     First Load JS
    ┌ ○ /                                       578 B           130 kB
    ├ ○ /_not-found                             979 B           106 kB
    ├ ○ /icon.svg                               0 B                0 B
    ├ ○ /posts                                  144 B           140 kB
    ├ ○ /posts/aaron-swartz-a-programmable-web  231 B           130 kB
    ├ ○ /posts/code-blocks                      231 B           130 kB
    ├ ○ /posts/draft                            231 B           130 kB
    ├ ○ /posts/lists                            231 B           130 kB
    ├ ○ /posts/nextra-components                2.79 kB         137 kB
    ├ ○ /posts/table                            231 B           130 kB
    └ ● /tags/[tag]                             144 B           140 kB
        ├ /tags/web development
        ├ /tags/JavaScript
        ├ /tags/GraphQL
        └ [+6 more paths]
    + First Load JS shared by all               105 kB
      ├ chunks/323-87b56278accb196b.js          50.5 kB
      ├ chunks/67a2b45f-6901f39824410d2a.js     52.9 kB
      └ other shared chunks (total)             1.91 kB
     
     
    ○  (Static)  prerendered as static content
    ●  (SSG)     prerendered as static HTML (uses generateStaticParams)

    First Load JS shared by all is decreased by 7.9% from 114 kB to 105 kB.

    🚀

    Conclusion

    First Load JS is significantly decreased in all above examples.

    Remote Docs

    Remote docs configuration has changed, please refers to official example, Additionally you will need to modify pageMap list in layout file, to properly display sidebar navigation links. Example of modified layout file is here.

    nextra-theme-docs Changes

    Zustand

    All previous React context usages were migrated to zustand except places where we need dependency injection. This applies only to the useConfig and useThemeConfig hooks which needs initialize their state with props.

    Migrated to Tailwind CSS 4

    Theme Docs has been updated to Tailwind CSS 4. The previously used Tailwind CSS prefix _ which helped prevent class name conflicts, has been replaced with x:. If you have overridden Nextra’s theme classes, make sure to update them accordingly.

    my-styles.css
    - ._text-primary-600 { ... }
    + .x\:text-primary-600 { ... }

    New Headings :target State Animation

    All headings now have animation for :target state

    Example animation for headings for `:target` state

    Enhanced <NotFoundPage>

    The built-in <NotFoundPage> now includes a URL for creating a 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

    Choose MDX rendering mode

    Add layout.jsx file

    Example of app/layout.jsx for 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 API
      // https://nextjs.org/docs/app/building-your-application/optimizing/metadata
    }
     
    const banner = <Banner storageKey="some-key">Nextra 4.0 is released 🎉</Banner>
    const navbar = <Navbar logo={<b>Nextra</b>} projectLink="https://github.com/shuding/nextra" />
    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 https://github.com/pacocoursey/next-themes#with-app
          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/shuding/nextra/tree/main/docs"
              editLink="Edit this page on GitHub"
              sidebar={{ defaultMenuCollapseLevel: 1 }}
              footer={footer}
              // ...Your additional theme config options
            >
              {children}
            </Layout>
          </body>
        </html>
      )
    }

    Add an mdx-components.jsx file

    Create an mdx-components.jsx file in the root of your project 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

    Table of migration theme.config options
    Nextra 3Nextra 4
    banner.contentchildren prop in <Banner>1
    banner.dismissibledismissible prop in <Banner>
    banner.keystorageKey prop in <Banner>
    backgroundColor.darkbackgroundColor.dark prop in <Head>2
    backgroundColor.lightbackgroundColor.light prop in <Head>
    chat.iconchatIcon prop in <Navbar>3
    chat.linkchatLink prop in <Navbar>
    components Removed. Provide custom components inside useMDXComponents function
    darkModedarkMode prop in <Layout>4
    direction Removed. Use dir attribute on <html> tag
    docsRepositoryBasedocsRepositoryBase prop in <Layout>
    editLink.componenteditLink prop in <Layout>
    editLink.contentchildren prop in <LastUpdated>5
    faviconGlyphfaviconGlyph prop in <Head>
    feedback.contentfeedback.content prop in <Layout>
    feedback.labelsfeedback.labels prop in <Layout>
    feedback.useLink Removed
    footer.componentfooter prop in <Layout>
    footer.contentchildren prop in <Footer>6
    gitTimestamplastUpdated prop in <Layout>
    head Removed. Use <Head> or Next.js Metadata API
    i18n[number].direction Removed. Use dir attribute on <html> tag
    i18n[number].localei18n[number].locale prop in <Layout>
    i18n[number].namei18n[number].name prop in <Layout>
    logologo prop in <Navbar>
    logoLinklogoLink prop in <Navbar>
    main Removed
    navbar.componentnavbar prop in <Layout>
    navbar.extraContentchildren prop in <Layout>
    navigationnavigation prop in <Layout>
    nextThemesnextThemes prop in <Layout>
    notFound.contentcontent prop in <NotFoundPage>7
    notFound.labelslabels prop in <NotFoundPage>
    color.huecolor.hue prop in <Head>
    color.saturationcolor.saturation prop in <Head>
    project.iconprojectIcon prop in <Navbar>
    project.linkprojectLink prop in <Navbar>
    search.componentsearch prop in <Layout>
    search.emptyResultemptyResult prop in <Search>8
    search.errorerrorText prop in <Search>
    search.loadingloading prop in <Search>
    search.placeholderplaceholder prop in <Search>
    sidebar.autoCollapsesidebar.autoCollapse prop in <Layout>
    sidebar.defaultMenuCollapseLevelsidebar.defaultMenuCollapseLevel prop in <Layout>
    sidebar.toggleButtonsidebar.toggleButton prop in <Layout>
    themeSwitch.component Removed
    themeSwitch.useOptionsthemeSwitch prop in <Layout>
    toc.backToToptoc.backToTop prop in <Layout>
    toc.component Removed
    toc.extraContenttoc.extraContent prop in <Layout>
    toc.floattoc.float prop in <Layout>
    toc.titletoc.title prop in <Layout>

    Dynamic <head> Tags

    Dynamic head tags previously were configured via head theme config option. In Nextra 4 you should use Next.js Metadata API instead.

    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://nextra.site',
        siteName: 'Nextra',
        locale: 'en_US',
        type: 'website'
      }
    }
    app/page.mdx
    ---
    description: Make beautiful websites with Next.js & MDX.
    ---
     
    # Hello Nextra 4

    Front matter title field or first Markdown heading <h1> will set <title> and <meta property="og:title"> and front matter description field will set <meta name="description"> and <meta property="og:description"> in <head> element.

    <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

    The blog theme now includes partial support for _meta files, allowing you to define navbar links directly within an _meta file. The previously used draft: true option in the front matter has been replaced with display: 'hidden' in an _meta file.

    react-cusdis Was Removed

    Previously optional peer dependency react-cusdis was removed, since:

    • this library is no longer maintained
    • has only React 17 specified in peerDependencies

    We use Cusdis SDK directly in <Comments>9 component.

    next-view-transitions Was Added

    Blog received support of View Transitions API by using next-view-transitions package.

    Demo View Transitions API

    nextra-theme-blog v4 is the first version where you can use built-in search. To do it, import <Search> component from nextra/components and place it as children of <Navbar> component, then follow search setup steps.

    app/layout.jsx
    <Navbar pageMap={await getPageMap()}>
      <Search />
      <ThemeSwitch />
    </Navbar>

    Migration Guide

    Choose MDX rendering mode

    Migrate theme.config options

    Add layout.jsx file

    Example of app/layout.jsx for 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 API
      // https://nextjs.org/docs/app/building-your-application/optimizing/metadata
    }
     
    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 https://github.com/pacocoursey/next-themes#with-app
          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 an mdx-components.jsx file

    Create an mdx-components.jsx file in the root of your project 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
      }
    }
    Table of migration theme.config options
    Nextra 3Nextra 4
    commentsProvide your comments component in post layout, e.g. app/posts/(with-comments)/layout.jsx file
    componentsProvide custom components inside useMDXComponents function
    darkModeTo disable theme toggle remove <ThemeSwitch> from <Navbar>
    dateFormatterProvide DateFormatter component inside useMDXComponents function
    footerProvide your <Footer> component as last child of <Layout>
    headUse <Head> or Next.js Metadata API
    navsSetup your navbar links via _meta files
    postFooterProvide your post footer in post layout, e.g. app/posts/(with-comments)/layout.jsx file
    readMoreProvide readMore prop for <PostCard> component
    titleSuffixUse Next.js Metadata API
    New banner prop on <Layout>. E.g. you can now provide Nextra’s <Banner>1
    New nextThemes prop on <Layout>. Configuration options can be found in next-themes docs

    Various Changes

    _meta Files Changes

    1. Your _meta files should be server component files, without 'use client' directive.
    2. zod now parses and transforms _meta files on server, to improve DX and avoid typos.
    3. The _meta’s newWindow field was removed.
    4. All external links declared in _meta files now opens in a new tab with with rel="noreferrer" attribute and have visual suffix icon.
    5. theme.topContent and theme.bottomContent fields were removed.
    6. theme.layout: 'raw' option has been removed. To create pages without a layout, use page.{jsx,tsx} files instead.

    _meta.global File

    You can now define all your pages in a single _meta file, suffixed with .global. The API remains the same as for folder-specific _meta files, with 1 exception: folder items must include an items field.

    Example

    For the following structure, you might use the following _meta files:

    • layout.jsx
    • _meta.js
    • page.jsx
      • page.mdx
      • _meta.js
        • page.mdx
  • app/_meta.js
    export default {
      docs: {
        type: 'page',
        title: 'Documentation'
      }
    }
    app/docs/_meta.js
    export default {
      items: {
        index: 'Getting Started'
      }
    }

    With signle _meta.global file it can be defined as below:

    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 your project.

    Component Migration: Moving UI Elements

    Several components were migrated from nextra-theme-docs to nextra/components to make them accessible for use in your custom themes:

    • <Collapse>
    • <Details>
    • <Summary>
    • <SkipNavContent>
    • <SkipNavLink>
    • <Select>
    • <Bleed>
    • <Head>
    💡

    Tip

    With migration <Head> component to nextra/components, both nextra-theme-blog and your custom themes can now configure:

    • primary color, color prop
    • background color, backgroundColor prop
    • glyph to use as the favicon, faviconGlyph prop
    • color of selection based on primary color

    Previously all of these options were exclusive to nextra-theme-docs only.

    All built-in Nextra components were refactored to be either server or client.

    Table of comparison components from nextra/components

    Here’s the table sorted alphabetically by component name:

    ComponentType
    <Banner>server
    <Bleed>server
    <Button>client
    <Callout>server
    <Cards>/<Cards.Card>server
    <Collapse>client
    <FileTree>/<FileTree.File>server
    <FileTree.Folder>client
    <Head>server
    <Playground>client
    <Popup>/<Popup.Button>/<Popup.Panel>client
    <Search>client
    <Select>client
    <SkipNavContent>server
    <SkipNavLink>client
    <Tabs>/<Tabs.Tab>client
    <Steps>server
    <MDXRemote> (previous <RemoteContent>)server

    Front Matter = Metadata

    All fields from the front matter are now exported as a metadata object in MDX file. If you prefer not to use front matter, you can directly export a metadata from your MDX file, but you can’t use both in same file.

    ✅ Front matter
    ---
    title: Foo
    description: Bar
    ---
     
    {/* Will be compiled to 👇 */}
    export const metadata = {
      title: 'Foo',
      description: 'Bar'
    }
    ✅ export of a metadata
    export const metadata = {
      title: 'Foo',
      description: 'Bar'
    }
    ❌ Invalid
    ---
    title: Foo
    ---
     
    export const metadata = { description: 'Bar' }

    whiteListTagsStyling Option

    Nextra introduces a new option called whiteListTagsStyling, which allows you to whitelist HTML elements to be replaced with components defined in the mdx-components.js file. By default, Nextra only replaces <details> and <summary>. With this option, you can extend the behavior to replace any HTML element.

    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 Changes

    In Nextra 2 and 3, the structure shown below is referred to as folders with index pages . Notice the presence of both a docs folder and a docs.mdx file at the same level:

      • getting-started.mdx
    • docs.mdx
  • This structure is incompatible with the page file convention, to fix it a new front matter option called asIndexPage has been introduced. You should set asIndexPage: true in the front matter of docs/page.mdx to achieve the same effect.

    • layout.jsx
      • page.mdx
        • page.mdx
  • When using the content directory convention, you can achieve the same result by setting asIndexPage: true in the front matter of docs/index.mdx

      • index.mdx
      • getting-started.mdx
  • List Subpages

    It’s now possible to automatically list all subpages of a given route as cards.

    1. Import createIndexPage and getPageMap from nextra/page-map.
    2. Use MDXRemote from nextra/mdx-remote to render list of subpages
    3. Replace '/my-route' with the route whose subpages you want to display
    4. To show card’s icon, use icon front matter field in your subpages (optional)
    5. Provide Cards and your icons to 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

    Subpages Example from nextra.site/docs/advanced

    In Nextra 4, the sidebar title is now determined by the following order:

    1. A non-empty title from the _meta file.
    2. sidebarTitle in the front matter.
    3. title in the front matter.
    4. New in Nextra 4: The title derived from the first h1 Markdown heading (e.g. # Dima Machina).
    5. If none of the above are available, it falls back to the filename of the page, formatted according to The Chicago Manual of Style.

    Code Block Icons Changes

    Customizing Icons

    You can now customize icons for code blocks. Here’s an example for nextra-theme-docs, but the same approach applies to custom themes:

    1. Import withIcons HOC function from nextra/components.
    2. Wrap the Pre component with withIcons, passing your custom icon for the desired 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: The jsx and tsx languages now display the React icon for better visual identification.
    • Diff icons: For code blocks with the diff language, if a filename attribute is provided, the icon will automatically correspond to the file extension of the specified filename.

    All external Markdown links declared in MDX files now opens in a new tab with with rel="noreferrer" attribute and have visual suffix icon. (Was inspired from Next.js docs)

    Example:

    example.mdx
    [dimaMachina](https://github.com/dimaMachina)

    Will compiles to:

    <a href="https://github.com/dimaMachina" target="_blank" rel="noreferrer">
      dimaMachina&thinsp;
      <LinkArrowIcon height="16" className="_inline _align-baseline" />
    </a>

    ::selection Styles

    Variant of primary color which is set via color prop on <Head> component is now used for ::selection styles.

    <Table> Changes

    The <Th>, <Tr> and <Td> components have been removed and are now attached directly into <Table> component. This change improves the DX, as these components are typically used together within a <Table>.

    Example of 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 function now returns a Promise<string> instead of a 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

    1. The <RemoteContent> component has been renamed to <MDXRemote>.
    2. He has been removed from nextra/components and moved to nextra/mdx-remote
    3. You no longer need to manually pass your default components to <MDXRemote>. They will now be automatically provided from your mdx-components.jsx file.

    Optimized Imports

    Imports from nextra/components, nextra-theme-docs and nextra-theme-blog are now optimized internally with Next.js’ optimizePackageImports option resulting in improved bundle size.

    useRouter Removed

    Nextra’s useRouter hook was removed, use Next.js’ useRouter from next/navigation instead now.

    - import { useRouter } from 'nextra/hooks'
    + import { useRouter } from 'next/navigation'

    Bump Minimal Next.js to V14

    Nextra 4 requires at least Next.js 14 to be installed in your project.

    Update Your tsconfig.json

    If you are using TypeScript and have moduleResolution set to node, you will receive type errors:

    Type error: Cannot find module 'nextra/components' or its corresponding type declarations.

    "typesVersions" fields from package.json’s of Nextra packages were removed. You need to set "moduleResolution": "bundler" in your tsconfig.json.

    tsconfig.json
    {
      "compilerOptions": {
    -   "moduleResolution": "node"
    +   "moduleResolution": "bundler"
      }
    }

    Conclusion. Why Prefer Nextra 4 over Others?

    Nextra 4 introduces several key features that enhance its functionality and performance:

    • App Router Support: Nextra 4 uses Next.js App Router, discontinuing support for the Pages Router. This change aligns with the latest Next.js API such as Metadata API.

    • Turbopack Support: Improved development experience, addressing a long-standing community request.

    • New Search Engine: New Rust-powered search engine Pagefind provides faster performance and significantly better search results.

    • RSC I18n Support: Enhanced internationalization by supporting React Server Components (RSC) with built-in i18n capabilities, facilitating the creation of multilingual websites.

    • Enhanced by React Compiler: Compiling official Nextra packages source code with the React Compiler in leads to optimized performance, resulting in a better user experience.

    • Reduced Bundle Size: Through various optimizations, the smallest bundle size ever for a Nextra-powered website was achieved, enhancing load times and overall performance.

    • Improved Pages Collection: Structure of pages called “Page Map” was improved and now collects not only static md/mdx pages, but any static jsx/tsx pages from app/ directory as well.

    • Powerful Table of Content: The improved Table of Content (TOC) from Nextra 3 shows exact same content from your Markdown headings, including interpolated content, inlined code-blocks, JSX elements, React components and even rendered math formulas LaTeX.

    • Remote MDX Rendering: Remote rendering of MDX content added in Nextra 3 allows you to render your content from any source, and rendering proper sidebar navigation links.

    These features make Nextra 4 a more powerful and efficient framework for building content-focused websites.

    Next Steps

    Nextra 4 is a major release with a lot of breaking changes. I already created a roadmap for next major release Nextra 5 optional using Rust-powered MDX mdxjs-rs parser. Share your thoughts as well.

    Credits

    A sincere thank for the most active Nextra contributor, I can even name him as a perfect example of a great Open Source contributor Oscar Xie! His amazing contributions previously pushed me a lot to release Nextra 3!

    Huge thanks for Nextra sponsors:

    ❤️

    The Nextra 4 documentation is still a work in progress, and we need your help to improve it!
    Check out the recent updates in this blog post, and feel free to submit your contribution PRs to improve Nextra docs.

    Spread the word about Nextra on 𝕏 using the #Nextra hashtag. You can also follow me on 𝕏 and GitHub to stay updated!


    Footnotes

    1. import { Banner } from 'nextra/components' 2

    2. import { Head } from 'nextra/components'

    3. import { Navbar } from 'nextra-theme-docs'

    4. import { Layout } from 'nextra-theme-docs'

    5. import { LastUpdated } from 'nextra-theme-docs'

    6. import { Footer } from 'nextra-theme-docs'

    7. import { NotFoundPage } from 'nextra-theme-docs'

    8. import { Search } from 'nextra/components'

    9. import { Comments } from 'nextra-theme-blog'

    Join our newsletter

    Want to hear from us when there's something new?
    Sign up and stay up to date!

    *By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.