Nextra 3 – Your Favourite MDX Framework, Now on 🧪 Steroids

Dimitri POSTOLOV

Intro

Hi everyone 👋, I am Dimitri POSTOLOV, you may know me previously as B2o5T (boost).

I recently decided to change my nickname to easy-to-remember Dima Machina (for a long time that’s what my team The Guild called me).

Once upon a time… I discovered Nextra and wanted to use it as a base framework for The Guild documentation websites, I found a lot of issues and started to contribute to this library to fix them and to help with the v2 release.

My first PR in Nextra

Nextra 3 is currently available as an alpha release, and I am looking for feedback from the community, choose your favourite package manager and try it:

npm i nextra@alpha            # Install Nextra
npm i nextra-theme-docs@alpha # Install Nextra Theme Docs
npm i nextra-theme-blog@alpha # Install Nextra Theme Blog

What’s New

Starting from releasing v2 in which release I actively contributed, I actively maintained Nextra and received feedback from the community and ideas for future improvements.

So let’s begin what I prepared with a new major release 🎉.

MDX 3

Nextra 3 will be built on top of MDX 3. There are no significant breaking changes (as it was between MDX 1 and MDX 2). More info about what’s new in MDX 3 can be found in the official blog post.

Complete New I18n with Static Exports Support

I18n was rewritten from scratch. The previous i18n was hard to scalable especially when you have a lot of languages, but also had really blocking issues, especially:

  1. only the default locale was produced in sitemap #712

  2. index files were not working for subfolders #1263

  3. build output directory for each locale contained all pages locales #1922

  4. some static files were inaccessible #1354

The original proposal of better i18n by locale folders instead of locale suffix was proposed 2 years ago and in Nextra 3 it was implemented by me.

Comparison of I18n Project Structure for Nextra 2/3

Nextra 2Nextra 3
    • index.en.mdx
    • index.fr.mdx
    • index.ru.mdx
    • getting-started.en.mdx
    • getting-started.fr.mdx
    • getting-started.ru.mdx
    • _meta.en.json
    • _meta.fr.json
    • _meta.ru.json
      • index.mdx
      • getting-started.mdx
      • _meta.js
      • index.mdx
      • getting-started.mdx
      • _meta.js
      • index.mdx
      • getting-started.mdx
      • _meta.js
  • You should know also a few notes:

    1. By default, Next.js doesn’t support project structure like above with the i18n property enabled in your next.config

    If without Nextra you’ll try to create this project structure, and you try to access http://localhost:3000/en or http://localhost:3000/en/getting-started you’ll get a 404 error. To fix it Nextra set the i18n property in next.config to undefined. With i18n enabled and with Nextra you’ll see the following console warning:

    Terminal
    - warn [nextra] Next.js doesn't support i18n by locale folder names.
    When i18n enabled, Nextra unset nextConfig.i18n to `undefined`, use `useRouter` from `nextra/hooks`
    if you need `locale` or `defaultLocale` values.
    1. useRouter from next/router returns incorrect locale and defaultLocale values (since i18n was unset), so instead you should use useRouter from nextra/hooks which will return correct values.

    2. Popular i18n libraries like react-intl and react-i18next or others will not work either.

    ⚠️

    Keep in mind that, you may not need the above i18n libraries since maintaining docs and keeping them updated for multiple languages is really hard, and you may just use some platform like Crowdin that will translate docs for you once the default language is changed. Also Crowdin is free for Open Source 😍.

    🎉

    On the other hand, since Nextra under the hood disables i18n in next.config your i18n docs website could have static exports!

    New _meta.{js,jsx,ts,tsx} Files with JSX Support

    After one year of creating my proposal of using _meta.js instead of _meta.json, I finally found a way of implementing the trickiest feature of Nextra 3 and _meta.json files are no longer supported.

    This means that:

    • you can use either _meta.js or _meta.jsx with JSX support or _meta.{ts,tsx} for TypeScript projects instead
    • you can use ESLint’s built-in rule sort-keys with /* eslint sort-keys: error */ comment to sort your sidebar items alphabetically
    • you can export and import everything that you want in your _meta files
    • you can have typesafe _meta.{ts,tsx} files while asserting type definition from the nextra package
    • you can add a banner/footer for your repeated content in _meta files, for individual pages, or for all nested pages in * key
    ⚠️

    In the case of using next-sitemap, you probably need to add exclude: ['*/_meta'] in your next-sitemap.config.js since it’s tricky to exclude _meta files from the build.

    More Powerful TOC

    There is no MDX framework where TOC can be better than in Nextra 3 (Correct me if I am wrong 🙂).

    ❤️

    If you wanna this feature to be part of your MDX framework or Open Source, ping me and I’ll try my best to publish it as my separate Open Source package.

    Previously TOC could show only static content from your headings, and it showed unexpected results for dynamic content #2411 #1955 #2076 #1884.

    With Nextra 3 all issues are fixed, and everything that is inserted in your main content is properly rendered in TOC too 😍.

    Better Bundle Size

    A lot of work was done to make Nextra’s bundle size smaller, I deep-dived into the generated _app file and took a look what is included and what can be removed.

    Let’s do some calculations! 🧮

    💡

    Note: all 4 examples for v2 and v3 were updated on the latest Next.js 13.5.6 at the moment of writing this blog post.

    Build Output of Blog Example Website

    with Nextra 2

    https://github.com/shuding/nextra/tree/66798f8e7f92cca80f2d62d19f9db5667bcc62ef/examples/blog

    Terminal
    Route (pages)                               Size     First Load JS
    ┌ ○ /                                       1.08 kB         119 kB
    ├   /_app                                   0 B             114 kB
    ├ ○ /404                                    180 B           115 kB
    ├ ○ /posts                                  588 B           119 kB
    ├ ○ /posts/aaron-swartz-a-programmable-web  3.53 kB         122 kB
    ├ ○ /posts/callout                          1.98 kB         120 kB
    ├ ○ /posts/code-blocks                      1.53 kB         120 kB
    ├ ○ /posts/draft                            795 B           119 kB
    ├ ○ /posts/table                            844 B           119 kB
    └ ● /tags/[tag] (716 ms)                    788 B           119 kB
        ├ /tags/web development
        ├ /tags/JavaScript
        ├ /tags/GraphQL
        └ [+6 more paths]
    + First Load JS shared by all               123 kB
      ├ chunks/framework-bce0fd2bcc8d4c85.js    45.3 kB
      ├ chunks/main-48077aa82984b023.js         37.6 kB
      ├ chunks/pages/_app-83deac736929a1bc.js   29.6 kB
      ├ chunks/webpack-109eb0aced926db3.js      1.81 kB
      └ css/e187e49945a86f29.css                8.61 kB
     
    ○  (Static)  automatically rendered as static HTML (uses no initial props)
    ●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
    with Nextra 3

    https://github.com/shuding/nextra/tree/c2ad837d3dacd87e1aa3ea3b9839f18062ae1c25/examples/blog

    Terminal
    Route (pages)                               Size     First Load JS
    ┌ ○ /                                       1.61 kB         108 kB
    ├ ○ /404                                    180 B           101 kB
    ├ ○ /posts                                  1.12 kB         107 kB
    ├ ○ /posts/aaron-swartz-a-programmable-web  3.93 kB         110 kB
    ├ ○ /posts/callout                          11.2 kB         117 kB
    ├ ○ /posts/code-blocks                      2.02 kB         108 kB
    ├ ○ /posts/draft                            1.28 kB         107 kB
    ├ ○ /posts/table                            1.35 kB         107 kB
    └ ● /tags/[tag] (872 ms)                    1.34 kB         107 kB
        ├ /tags/web development
        ├ /tags/JavaScript
        ├ /tags/GraphQL
        └ [+6 more paths]
    + First Load JS shared by all               101 kB
      ├ chunks/framework-bce0fd2bcc8d4c85.js    45.3 kB
      ├ chunks/main-c12ab09220a28f5f.js         37.8 kB
      ├ chunks/pages/_app-f6bbbcf4ee8f890e.js   16.2 kB
      └ chunks/webpack-d4712bf1c9b5b172.js      1.81 kB
     
    ○  (Static)  automatically rendered as static HTML (uses no initial props)
    ●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

    Let’s compare everything starting from the First Load JS shared by all lines:

    Comparison of bundle size for Nextra 2/3
    -+ First Load JS shared by all               123 kB
    ++ First Load JS shared by all               101 kB
       ├ chunks/framework-bce0fd2bcc8d4c85.js    45.3 kB
    -  ├ chunks/main-48077aa82984b023.js         37.6 kB
    +  ├ chunks/main-c12ab09220a28f5f.js         37.8 kB
    -  ├ chunks/pages/_app-83deac736929a1bc.js   29.6 kB
    +  ├ chunks/pages/_app-f6bbbcf4ee8f890e.js   16.2 kB
       ├ chunks/webpack-109eb0aced926db3.js      1.81 kB
    -  └ css/e187e49945a86f29.css                8.61 kB

    We can assume that the first load is decreased by 17.89% and _app file is decreased by 45.27% for nextra-theme-blog 🤯.

    ⚠️

    Note: the .css file is not included in the bundle report for Nextra 3, because the v3 blog example just doesn’t contain a custom _app file, and importing CSS from node_modules is not possible for pages router, so instead .css file is imported in each MDX page (in Nextra’s webpack loader).

    Build Output of I18n Docs Website with LaTeX Enabled

    with Nextra 2

    https://github.com/shuding/nextra/tree/66798f8e7f92cca80f2d62d19f9db5667bcc62ef/examples/swr-site

    Terminal
    Route (pages)                                                Size     First Load JS
    ┌   /_app                                                    0 B             187 kB
    ├ ○ /404                                                     560 B           191 kB
    ├ ○ /500                                                     555 B           191 kB
    ├ ○ /about/a-page.en-US                                      527 B           191 kB
    ├ ○ /about/acknowledgement.en-US                             526 B           191 kB
    ├ ○ /about/changelog.en-US                                   1.22 kB         192 kB
    ├ ○ /about/team.en-US                                        574 B           191 kB
    ├ ○ /blog.en-US                                              1.5 kB          192 kB
    ├ ○ /blog.ru                                                 1.5 kB          192 kB
    ├ ○ /blog/swr-v1.en-US                                       6.32 kB         197 kB
    ├ ○ /blog/swr-v1.ru                                          8.1 kB          199 kB
    ├ ○ /docs/404-500.en-US                                      1.44 kB         192 kB
    ├ ○ /docs/advanced.en-US                                     909 B           192 kB
    ├ ○ /docs/advanced/cache.en-US                               4.46 kB         204 kB
    ├ ○ /docs/advanced/cache.ru                                  5.74 kB         205 kB
    ├ ○ /docs/advanced/code-highlighting.en-US                   1.39 kB         192 kB
    ├ ○ /docs/advanced/dynamic-markdown-import.en-US             2.92 kB         194 kB
    ├ ○ /docs/advanced/file-name.with.DOTS.en-US                 557 B           191 kB
    ├ ○ /docs/advanced/file-name.with.DOTS.es-ES                 568 B           191 kB
    ├ ○ /docs/advanced/file-name.with.DOTS.ru                    621 B           191 kB
    ├ ○ /docs/advanced/images.en-US                              892 B           192 kB
    ├ ○ /docs/advanced/markdown-import.en-US                     4.69 kB         195 kB
    ├ ○ /docs/advanced/more/loooooooooooooooooooong-title.en-US  637 B           191 kB
    ├ ● /docs/advanced/more/tree/one.en-US                       588 B           191 kB
    ├ ○ /docs/advanced/more/tree/three.en-US                     532 B           191 kB
    ├ ● /docs/advanced/more/tree/two.en-US                       587 B           191 kB
    ├ ○ /docs/advanced/performance.en-US                         2.97 kB         194 kB
    ├ ○ /docs/advanced/performance.es-ES                         3.15 kB         194 kB
    ├ ○ /docs/advanced/performance.ru                            3.79 kB         195 kB
    ├ ○ /docs/advanced/react-native.en-US                        2.51 kB         193 kB
    ├ ○ /docs/advanced/react-native.ru                           3.08 kB         194 kB
    ├ ○ /docs/advanced/scrollbar-x.en-US                         1.72 kB         192 kB
    ├ ○ /docs/arguments.en-US                                    1.9 kB          193 kB
    ├ ○ /docs/arguments.es-ES                                    1.98 kB         193 kB
    ├ ○ /docs/arguments.ru                                       2.23 kB         193 kB
    ├ ○ /docs/callout.en-US                                      693 B           191 kB
    ├ ● /docs/change-log.en-US                                   898 B           192 kB
    ├ ● /docs/change-log.es-ES                                   928 B           197 kB
    ├ ● /docs/change-log.ru                                      1.04 kB         197 kB
    ├ ○ /docs/code-block-without-language.en-US                  718 B           191 kB
    ├ ○ /docs/conditional-fetching.en-US                         1.56 kB         192 kB
    ├ ○ /docs/conditional-fetching.es-ES                         1.62 kB         192 kB
    ├ ○ /docs/conditional-fetching.ru                            1.87 kB         193 kB
    ├ ○ /docs/custom-header-ids.en-US                            1.02 kB         192 kB
    ├ ○ /docs/data-fetching.en-US                                1.97 kB         193 kB
    ├ ○ /docs/data-fetching.es-ES                                2.02 kB         193 kB
    ├ ○ /docs/data-fetching.ru                                   2.33 kB         193 kB
    ├ ○ /docs/error-handling.en-US                               2.81 kB         194 kB
    ├ ○ /docs/error-handling.es-ES                               2.96 kB         194 kB
    ├ ○ /docs/error-handling.ru                                  3.55 kB         194 kB
    ├ ○ /docs/getting-started.en-US                              8.96 kB         200 kB
    ├ ○ /docs/getting-started.es-ES                              7.88 kB         199 kB
    ├ ○ /docs/getting-started.ru                                 8.58 kB         199 kB
    ├ ○ /docs/global-configuration.en-US                         1.85 kB         193 kB
    ├ ○ /docs/global-configuration.es-ES                         1.9 kB          193 kB
    ├ ○ /docs/global-configuration.ru                            2.22 kB         193 kB
    ├ ○ /docs/middleware.en-US                                   3.9 kB          195 kB
    ├ ○ /docs/middleware.ru                                      4.75 kB         196 kB
    ├ ○ /docs/mutation.en-US                                     3.41 kB         194 kB
    ├ ○ /docs/mutation.es-ES                                     3.48 kB         194 kB
    ├ ○ /docs/mutation.ru                                        4.21 kB         195 kB
    ├ ○ /docs/options.en-US                                      2.42 kB         193 kB
    ├ ○ /docs/options.es-ES                                      2.57 kB         193 kB
    ├ ○ /docs/options.ru                                         3.21 kB         194 kB
    ├ ○ /docs/pagination.en-US                                   6.02 kB         204 kB
    ├ ○ /docs/pagination.es-ES                                   6.42 kB         204 kB
    ├ ○ /docs/pagination.ru                                      7.38 kB         205 kB
    ├ ○ /docs/prefetching.en-US                                  2.05 kB         193 kB
    ├ ○ /docs/prefetching.es-ES                                  2.14 kB         193 kB
    ├ ○ /docs/prefetching.ru                                     2.56 kB         193 kB
    ├ ○ /docs/raw-layout.en-US                                   1.24 kB         192 kB
    ├ ○ /docs/revalidation.en-US                                 2.76 kB         198 kB
    ├ ○ /docs/revalidation.es-ES                                 2.9 kB          198 kB
    ├ ○ /docs/revalidation.ru                                    3.5 kB          199 kB
    ├ ○ /docs/suspense.en-US                                     2.08 kB         193 kB
    ├ ○ /docs/suspense.es-ES                                     2.15 kB         193 kB
    ├ ○ /docs/suspense.ru                                        2.53 kB         193 kB
    ├ ○ /docs/typescript.en-US                                   2.37 kB         193 kB
    ├ ○ /docs/understanding.en-US                                3.88 kB         199 kB
    ├ ○ /docs/understanding.es-ES                                3.88 kB         199 kB
    ├ ○ /docs/understanding.ru                                   3.88 kB         199 kB
    ├ ○ /docs/with-nextjs.en-US                                  2.25 kB         193 kB
    ├ ○ /docs/with-nextjs.es-ES                                  2.39 kB         193 kB
    ├ ○ /docs/with-nextjs.ru                                     2.82 kB         194 kB
    ├ ○ /docs/wrap-toc-items.en-US                               1.39 kB         192 kB
    ├ ○ /docs/wrap-toc-items.es-ES                               1.39 kB         192 kB
    ├ ○ /docs/wrap-toc-items.ru                                  1.39 kB         192 kB
    ├ ○ /examples/auth.en-US                                     830 B           192 kB
    ├ ○ /examples/auth.es-ES                                     834 B           192 kB
    ├ ○ /examples/auth.ru                                        865 B           192 kB
    ├ ○ /examples/basic.en-US                                    832 B           192 kB
    ├ ○ /examples/basic.es-ES                                    834 B           192 kB
    ├ ○ /examples/basic.ru                                       875 B           192 kB
    ├ ○ /examples/error-handling.en-US                           839 B           192 kB
    ├ ○ /examples/error-handling.es-ES                           842 B           192 kB
    ├ ○ /examples/error-handling.ru                              875 B           192 kB
    ├ ○ /examples/full.en-US                                     815 B           192 kB
    ├ ○ /examples/infinite-loading.en-US                         851 B           192 kB
    ├ ○ /examples/infinite-loading.es-ES                         853 B           192 kB
    ├ ○ /examples/infinite-loading.ru                            896 B           192 kB
    ├ ○ /examples/ssr.en-US                                      840 B           192 kB
    ├ ○ /examples/ssr.ru                                         838 B           192 kB
    ├ ○ /foo.en-US                                               954 B           192 kB
    ├ ○ /index.en-US                                             4.34 kB         195 kB
    ├ ○ /index.es-ES                                             3.99 kB         195 kB
    ├ ○ /index.ru                                                4.38 kB         195 kB
    ├ ○ /remote/_meta                                            225 B           187 kB
    ├ ○ /remote/graphql-eslint/_meta                             237 B           187 kB
    ├ ○ /remote/graphql-yoga/_meta                               236 B           187 kB
    └ ○ /test.en-US                                              786 B           192 kB
    + First Load JS shared by all                                203 kB
      ├ chunks/framework-733009b6474116fd.js                     45.3 kB
      ├ chunks/main-1dbce7e00d8751d5.js                          38.4 kB
      ├ chunks/pages/_app-f3b27ac2556aba2f.js                    101 kB
      ├ chunks/webpack-e7e126cbd02a5d11.js                       1.95 kB
      └ css/86d65edbc5de703f.css                                 16.1 kB
     
    ƒ Middleware 26.8 kB
     
    ○  (Static)  automatically rendered as static HTML (uses no initial props)
    ●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
    with Nextra 3

    https://github.com/shuding/nextra/tree/c2ad837d3dacd87e1aa3ea3b9839f18062ae1c25/examples/swr-site

    Terminal
    Route (pages)                                                            Size     First Load JS
    ┌   /_app                                                                0 B             148 kB
    ├ ○ /404                                                                 183 B           149 kB
    ├ ○ /en                                                                  8.34 kB         164 kB
    ├ ● /en/_meta                                                            247 B           149 kB
    ├ ● /en/about/_meta                                                      251 B           149 kB
    ├ ○ /en/about/a-page                                                     472 B           156 kB
    ├ ○ /en/about/acknowledgement                                            471 B           156 kB
    ├ ○ /en/about/changelog                                                  1.17 kB         157 kB
    ├ ○ /en/about/team                                                       518 B           156 kB
    ├ ○ /en/blog                                                             1.22 kB         157 kB
    ├ ● /en/blog/_meta                                                       250 B           149 kB
    ├ ○ /en/blog/swr-v1                                                      6.18 kB         162 kB
    ├ ● /en/docs/_meta                                                       250 B           149 kB
    ├ ○ /en/docs/404-500                                                     1.28 kB         157 kB
    ├ ○ /en/docs/advanced                                                    855 B           157 kB
    ├ ● /en/docs/advanced/_meta                                              258 B           149 kB
    ├ ○ /en/docs/advanced/cache                                              4.46 kB         170 kB
    ├ ○ /en/docs/advanced/code-highlighting                                  1.38 kB         157 kB
    ├ ● /en/docs/advanced/dynamic-markdown-import                            2.89 kB         159 kB
    ├ ○ /en/docs/advanced/file-name.with.DOTS                                502 B           156 kB
    ├ ○ /en/docs/advanced/images                                             829 B           157 kB
    ├ ○ /en/docs/advanced/markdown-import                                    8.66 kB         165 kB
    ├ ○ /en/docs/advanced/more/loooooooooooooooooooong-title                 591 B           157 kB
    ├ ● /en/docs/advanced/more/tree/one                                      507 B           156 kB
    ├ ○ /en/docs/advanced/more/tree/three                                    475 B           156 kB
    ├ ○ /en/docs/advanced/more/tree/two                                      492 B           156 kB
    ├ ○ /en/docs/advanced/performance                                        2.95 kB         159 kB
    ├ ○ /en/docs/advanced/react-native                                       3.02 kB         159 kB
    ├ ○ /en/docs/advanced/scrollbar-x                                        5.33 kB         161 kB
    ├ ○ /en/docs/arguments                                                   2.35 kB         158 kB
    ├ ○ /en/docs/callout                                                     1.13 kB         157 kB
    ├ ● /en/docs/change-log                                                  810 B           157 kB
    ├ ○ /en/docs/code-block-without-language                                 604 B           157 kB
    ├ ○ /en/docs/conditional-fetching                                        1.54 kB         157 kB
    ├ ○ /en/docs/custom-header-ids                                           987 B           157 kB
    ├ ○ /en/docs/data-fetching                                               2.45 kB         158 kB
    ├ ○ /en/docs/error-handling                                              3.3 kB          159 kB
    ├ ○ /en/docs/getting-started                                             14.4 kB         170 kB
    ├ ○ /en/docs/global-configuration                                        1.86 kB         158 kB
    ├ ○ /en/docs/middleware                                                  4.28 kB         160 kB
    ├ ○ /en/docs/mutation                                                    3.36 kB         159 kB
    ├ ○ /en/docs/options                                                     2.86 kB         159 kB
    ├ ○ /en/docs/pagination                                                  5.9 kB          170 kB
    ├ ○ /en/docs/prefetching                                                 2.02 kB         158 kB
    ├ ○ /en/docs/raw-layout                                                  1.2 kB          157 kB
    ├ ○ /en/docs/revalidation                                                2.71 kB         164 kB
    ├ ○ /en/docs/suspense                                                    2.52 kB         158 kB
    ├ ○ /en/docs/typescript                                                  2.36 kB         158 kB
    ├ ○ /en/docs/understanding                                               3.85 kB         165 kB
    ├ ○ /en/docs/with-nextjs                                                 2.71 kB         159 kB
    ├ ○ /en/docs/wrap-toc-items                                              1.31 kB         157 kB
    ├ ● /en/examples/_meta                                                   254 B           149 kB
    ├ ○ /en/examples/auth                                                    738 B           157 kB
    ├ ○ /en/examples/basic                                                   738 B           157 kB
    ├ ○ /en/examples/error-handling                                          747 B           157 kB
    ├ ○ /en/examples/full                                                    729 B           157 kB
    ├ ○ /en/examples/infinite-loading                                        753 B           157 kB
    ├ ○ /en/examples/ssr                                                     754 B           157 kB
    ├ ○ /en/foo                                                              4.68 kB         161 kB
    ├ ● /en/remote/graphql-eslint/_meta                                      264 B           149 kB
    ├ ● /en/remote/graphql-eslint/[[...slug]] (1853 ms)                      4.86 kB         161 kB
    ├   ├ /en/remote/graphql-eslint/custom-rules (464 ms)
    ├   ├ /en/remote/graphql-eslint/getting-started (373 ms)
    ├   ├ /en/remote/graphql-eslint/getting-started/parser-options (345 ms)
    ├   ├ /en/remote/graphql-eslint/configs
    ├   ├ /en/remote/graphql-eslint/getting-started/parser
    ├   └ /en/remote/graphql-eslint/index
    ├ ● /en/remote/graphql-yoga/_meta                                        263 B           149 kB
    ├ ● /en/remote/graphql-yoga/[[...slug]] (32819 ms)                       4.88 kB         161 kB
    ├   ├ /en/remote/graphql-yoga/features/subscriptions (2996 ms)
    ├   ├ /en/remote/graphql-yoga/features/context (2885 ms)
    ├   ├ /en/remote/graphql-yoga/features/file-uploads (2777 ms)
    ├   ├ /en/remote/graphql-yoga/features/apollo-federation (2738 ms)
    ├   ├ /en/remote/graphql-yoga/features/graphiql (2724 ms)
    ├   ├ /en/remote/graphql-yoga/features/cors (2674 ms)
    ├   ├ /en/remote/graphql-yoga/features/testing (2560 ms)
    ├   └ [+16 more paths] (avg 842 ms)
    ├ ○ /en/test                                                             699 B           157 kB
    ├ ○ /es                                                                  5.56 kB         159 kB
    ├ ● /es/_meta                                                            247 B           149 kB
    ├ ● /es/docs/_meta                                                       251 B           149 kB
    ├ ● /es/docs/advanced/_meta                                              256 B           149 kB
    ├ ○ /es/docs/advanced/file-name.with.DOTS                                1.78 kB         155 kB
    ├ ○ /es/docs/advanced/performance                                        4.39 kB         157 kB
    ├ ○ /es/docs/arguments                                                   3.68 kB         157 kB
    ├ ● /es/docs/change-log (1096 ms)                                        2.13 kB         161 kB
    ├ ○ /es/docs/conditional-fetching                                        2.86 kB         156 kB
    ├ ○ /es/docs/data-fetching                                               3.73 kB         157 kB
    ├ ○ /es/docs/error-handling                                              4.69 kB         158 kB
    ├ ○ /es/docs/getting-started                                             9.69 kB         163 kB
    ├ ○ /es/docs/global-configuration                                        3.18 kB         156 kB
    ├ ○ /es/docs/mutation                                                    4.72 kB         158 kB
    ├ ○ /es/docs/options                                                     4.21 kB         157 kB
    ├ ○ /es/docs/pagination                                                  7.57 kB         168 kB
    ├ ○ /es/docs/prefetching                                                 3.34 kB         156 kB
    ├ ○ /es/docs/revalidation                                                4.09 kB         162 kB
    ├ ○ /es/docs/suspense                                                    3.85 kB         157 kB
    ├ ○ /es/docs/understanding                                               5.1 kB          163 kB
    ├ ○ /es/docs/with-nextjs                                                 4.07 kB         157 kB
    ├ ○ /es/docs/wrap-toc-items                                              2.6 kB          156 kB
    ├ ● /es/examples/_meta                                                   253 B           149 kB
    ├ ○ /es/examples/auth                                                    1.99 kB         155 kB
    ├ ○ /es/examples/basic                                                   2 kB            155 kB
    ├ ○ /es/examples/error-handling                                          2 kB            155 kB
    ├ ○ /es/examples/infinite-loading                                        2 kB            155 kB
    ├ ○ /ru                                                                  6.18 kB         159 kB
    ├ ● /ru/_meta                                                            248 B           149 kB
    ├ ○ /ru/blog                                                             2.85 kB         156 kB
    ├ ● /ru/blog/_meta                                                       251 B           149 kB
    ├ ○ /ru/blog/swr-v1                                                      9.12 kB         162 kB
    ├ ● /ru/docs/_meta                                                       250 B           149 kB
    ├ ● /ru/docs/advanced/_meta                                              258 B           149 kB
    ├ ○ /ru/docs/advanced/cache                                              7.15 kB         169 kB
    ├ ○ /ru/docs/advanced/file-name.with.DOTS                                2.15 kB         155 kB
    ├ ○ /ru/docs/advanced/performance                                        5.2 kB          158 kB
    ├ ○ /ru/docs/advanced/react-native                                       5.05 kB         158 kB
    ├ ○ /ru/docs/arguments                                                   4.21 kB         157 kB
    ├ ● /ru/docs/change-log (1101 ms)                                        2.56 kB         161 kB
    ├ ○ /ru/docs/conditional-fetching                                        3.36 kB         156 kB
    ├ ○ /ru/docs/data-fetching                                               4.26 kB         157 kB
    ├ ○ /ru/docs/error-handling                                              5.49 kB         159 kB
    ├ ○ /ru/docs/getting-started                                             10.6 kB         164 kB
    ├ ○ /ru/docs/global-configuration                                        3.73 kB         157 kB
    ├ ○ /ru/docs/middleware                                                  6.59 kB         160 kB
    ├ ○ /ru/docs/mutation                                                    5.65 kB         159 kB
    ├ ○ /ru/docs/options                                                     5.05 kB         158 kB
    ├ ○ /ru/docs/pagination                                                  8.66 kB         169 kB
    ├ ○ /ru/docs/prefetching                                                 3.97 kB         157 kB
    ├ ○ /ru/docs/revalidation                                                4.91 kB         163 kB
    ├ ○ /ru/docs/suspense                                                    4.47 kB         158 kB
    ├ ○ /ru/docs/understanding                                               5.5 kB          164 kB
    ├ ○ /ru/docs/with-nextjs                                                 4.73 kB         158 kB
    ├ ○ /ru/docs/wrap-toc-items                                              2.98 kB         156 kB
    ├ ● /ru/examples/_meta                                                   253 B           149 kB
    ├ ○ /ru/examples/auth                                                    2.37 kB         155 kB
    ├ ○ /ru/examples/basic                                                   2.37 kB         155 kB
    ├ ○ /ru/examples/error-handling                                          2.38 kB         155 kB
    ├ ○ /ru/examples/infinite-loading                                        2.38 kB         155 kB
    └ ○ /ru/examples/ssr                                                     2.38 kB         155 kB
    + First Load JS shared by all                                            163 kB
      ├ chunks/framework-733009b6474116fd.js                                 45.3 kB
      ├ chunks/main-3f7e5ffcd26ef392.js                                      37.9 kB
      ├ chunks/pages/_app-163bf496e5a12010.js                                63.4 kB
      ├ chunks/webpack-958fb7687230bd78.js                                   1.89 kB
      └ css/a30520c2298a663c.css                                             14.6 kB
     
    ○  (Static)  automatically rendered as static HTML (uses no initial props)
    ●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

    Let’s compare everything starting from the First Load JS shared by all lines:

    Comparison of bundle size for Nextra 2/3
    -+ First Load JS shared by all                                203 kB
    ++ First Load JS shared by all                                163 kB
       ├ chunks/framework-733009b6474116fd.js                     45.3 kB
    -  ├ chunks/main-1dbce7e00d8751d5.js                          38.4 kB
    +  ├ chunks/main-3f7e5ffcd26ef392.js                          37.9 kB
    -  ├ chunks/pages/_app-f3b27ac2556aba2f.js                    101 kB
    +  ├ chunks/pages/_app-163bf496e5a12010.js                    63.4 kB
    -  ├ chunks/webpack-e7e126cbd02a5d11.js                       1.95 kB
    +  ├ chunks/webpack-958fb7687230bd78.js                       1.89 kB
    -  └ css/86d65edbc5de703f.css                                 16.1 kB
    +  └ css/a30520c2298a663c.css                                 14.6 kB

    We can assume that the first load is decreased by 19.7% and the _app file is decreased by 37.23% for nextra-theme-docs 🤯.

    Remote Docs Support

    Nextra 2 already had undocumented remote docs support, and now remote docs are officially supported. You can do it with a few lines of code 🤯. To render your remote docs, you’ll need 2 exports:

    1. buildDynamicMDX and buildDynamicMeta from nextra/remote

    2. RemoteContent from nextra/components - React component which evaluates your compiled remote MDX.

      ⚠️

      All imports/exports expressions are stripped by the buildDynamicMDX function since in remote MDX it’s impossible to use them, imported components in your remote MDX should be passed instead in RemoteContent’s components prop.

    An Example of Remote Docs Fetched from GitHub

    Where for current example:

    • user: "dimaMachina"
    • repo: "graphql-eslint"
    • branch: "master"
    • docsPath: "website/src/pages/docs/"
    • filePaths: An array of available docs file paths of your remote doc, for example:
    graphql-eslint's filePaths array
    [
      "configs.mdx",
      "custom-rules.mdx",
      "getting-started.mdx",
      "getting-started/parser-options.mdx",
      "getting-started/parser.mdx",
      "index.mdx"
    ]
    pages/graphql-eslint/docs/[[...slug]].mdx
    import { Callout, RemoteContent } from 'nextra/components'
    import { buildDynamicMDX, buildDynamicMeta } from 'nextra/remote'
    import { branch, docsPath, filePaths, repo, user } from '@somewhere/graphq-eslint-data'
     
    export async function getStaticProps({ params }) {
      const path = params.slug?.join('/') ?? 'index'
      const foundPath = filePaths.find(filePath => filePath.replace(/\.mdx?/, '') === path)
      const url = `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${docsPath}${foundPath}`
      const response = await fetch(url)
      const data = await response.text()
      const { __nextra_pageMap } = await buildDynamicMeta()
      const dynamicMdx = await buildDynamicMDX(data, { defaultShowCopyCode: true })
      return {
        props: {
          __nextra_pageMap,
          ...dynamicMdx
        }
      }
    }
     
    export const getStaticPaths = () => ({
      fallback: 'blocking',
      paths: filePaths.map(filePath => ({
        params: { slug: filePath.replace(/\.mdx?$/, '').split('/') }
      }))
    })
     
    <RemoteContent components={{ Callout }} />

    An Example of Remote Docs’ _meta.js That Will Fill Sidebar Items

    pages/graphql-eslint/docs/_meta.js
    import { createCatchAllMeta } from 'nextra/catch-all'
    // filePaths: An array of available docs file paths of your remote doc
    import { filePaths } from '@somewhere/graphq-eslint-data'
     
    // Note: for remote MDX `_meta.js` should return a function instead of an object
    export default () => createCatchAllMeta(filePaths)

    Click to see the final result.

    _app.mdx Is Removed

    _app.mdx was an undocumented file, a custom workaround that was added after the v2 release to avoid OOM (Out of Memory) problems for projects with a lot of MDX pages, he worked pretty well and showed incredible results in improving bundle size #1448 and #1463.

    But he confused people since no type definition can be used inside him, and no variable declarations in your MDX too (only export const ... or export let ...).

    With the v3 release, I’m happy to announce that your bundle size is improved with the regular _app.{js,jsx,ts,tsx} file or even without 🤯!

    💡

    A good thing to know is that if you don’t use custom _app/_document/_error in your project, Next.js anyway will use some default ones 🤓.

    MathJax Support

    In addition to KaTeX, Nextra 3 can render LaTeX expressions with MathJax to dynamically render math in the browser.

    MathJax rendering is enabled by setting renderer: 'mathjax' in your Nextra config:

    next.config.mjs
    const withNextra = nextra({
      latex: {
        renderer: 'mathjax'
      }
    })

    With KaTeX, math is pre-rendered which means flicker-free and faster page loads. However, KaTeX does not support all of the features of MathJax, especially features related to accessibility.

    Because of MathJax’s accessibility features, the LaTeX formula is tab-accessible and has a context menu that helps screen readers reprocess math for the visually impaired.

    Huge thanks to siefkenj who created the initial PR and updated docs for the MathJax section ❤️

    Hello ESM, Goodbye CJS

    Nextra 2 was a mix of CJS/ESM bundles, Nextra 3 now is built as an ESM-only package, and the CJS next.config.js file is no longer supported.

    💡

    To use Nextra 3 you should use an ESM next.config.mjs or add "type": "module" in your package.json file and use an ESM next.config.js with js extension.

    Bumping Minimal Node.js to 18

    Node.js 18 is now the minimum supported version since Node.js 16 is EOL and no longer supported 👋.

    NextraConfig In next.config.js Now Validated by Zod

    To improve the development experience and avoid mistakes when users pass NextConfig option in NextraConfig nextra config is now validated by Zod.

    New Code Blocks for nextra-theme-docs and nextra-theme-blog

    Code block styles were inspired by the Next.js docs website, also now they contain icons for some program languages.

    Replacement shiki by shikiji

    Recently Anthony Fu released shikiji an ESM-focused rewrite of shiki.

    The downside of shiki is also that without css-variables theme, your dual themes are always rendered twice and an unneeded theme should be hidden with CSS, which I found very bad 😮‍💨.

    With Nextra 3 shikiji is used instead of shiki. github-light and github-dark themes are used by default instead of the previously custom css-variables theme.


    Migration Guide to Nextra 3

    nextra

    Ensure to use Node.js >= 18

    Node.js 16 is EOL and no longer supported.

    Ensure to use React.js >= 18

    Ensure to use Next.js >= 13

    CJS next.config.js is no longer supported

    Use ESM next.config.mjs or add "type": "module" in your package.json file and use ESM next.config.js.

    next.config.mjs
    import nextra from 'nextra'
     
    const withNextra = nextra({
      theme: 'nextra-theme-docs',
      themeConfig: './theme.config.jsx'
      // ... your Nextra config
    })
     
    export default withNextra({
      // ... your Next.js config
    })

    _app.mdx is no longer supported

    Use _app.{js,jsx} or _app.{ts,tsx} for TypeScript projects instead.

    _meta.json is no longer supported

    Use _meta.{js,jsx,ts,tsx} instead.

    nextra/ssg export was removed

    useSSG was renamed to useData and moved to nextra/hooks, RemoteContent was moved to nextra/components.

    - import { useSSG, RemoteContent } from 'nextra/data'
    + import { useData } from 'nextra/hooks'
    + import { RemoteContent } from 'nextra/components'

    pageOpts.headings was removed

    To retrieve TOC now you can only via toc prop in the wrapper component which defines the layout (but a local layout takes precedence).

    my-custom-nextra-theme.js
    import { MDXProvider } from 'nextra/mdx'
    import { MySidebar, MyTOC } from '@somewhere/my-components'
     
    export default function MyTheme({ children, pageOpts, themeConfig }) {
      return (
        <>
          <MySidebar />
    -     <MDXProvider>{children}</MDXProvider>
    -     <MyTOC toc={pageOpts.headings} />
    +     <MDXProvider components={{ wrapper: MyWrapper }}>{children}</MDXProvider>
        </>
      )
    }
     
    +function MyWrapper({ children, toc }) {
    +  return (
    +    <>
    +      {children}
    +      <MyTOC toc={toc} />
    +    </>
    +  )
    +}

    nextra/filter-route-locale and nextra/locales exports were removed

    With implementing new i18n there is no need to use filterRouteLocale helper function and locales middleware that previously redirected to a correct route based on locale suffix.

    Tab, Card exports from nextra/components were removed

    Use Tabs.Tab and Cards.Card instead.

    page.mdx
    -import { Tabs, Cards, Card, Tab } from 'nextra/components'
    +import { Tabs, Cards } from 'nextra/components'
     
    <Tabs items={[]}>
    -  <Tab>...</Tab>
    +  <Tabs.Tab>...</Tabs.Tab>
    </Tabs>
     
    <Cards>
    -  <Card title="..." href="..." />
    +  <Cards.Card title="..." href="..." />
    </Cards>

    Import md/mdx files that are outside the working directory no longer supported

    Use symlinks instead now.

    nextraConfig.flexsearch was renamed to nextraConfig.search

    $$ syntax no longer works for LaTeX

    remark-math was upgraded to v6 where the support of $$ syntax was removed. You probably should receive the following error:

    TypeError: Cannot read properties of undefined (reading 'mathFlowInside')

    To fix it math code block language should be used instead of $$:

    - $$
    + ```math
      x^2
    - $$
    + ```

    nextra-theme-docs

    Ensure to use Next.js >= 13

    Steps, Callout, Tabs, Cards and FileTree exports were removed

    Import them now from nextra/components instead.

    -import { Steps, Callout, Tabs, Cards, FileTree } from 'nextra-theme-docs'
    +import { Steps, Callout, Tabs, Cards, FileTree } from 'nextra/components'

    Tailwind CSS classes prefixes now have _ prefix instead of nx-

    If you have to select these classes in your custom CSS, consider updating them to new ones.

    useConfig hook was split into two hooks useConfig / useThemeConfig

    -import { useConfig } from 'nextra-theme-docs'
    +import { useThemeConfig } from 'nextra-theme-docs'
     
    function MyComponent() {
      // Get options from `theme.config` file
    -  const { banner, sidebar, toc, ... } = useConfig()
    +  const { banner, sidebar, toc, ... } = useThemeConfig()
    }

    Renames of several theme.config options

    • primaryHuecolor.hue

    • primarySaturationcolor.saturation

    • i18n.texti18n.name

    • banner.textbanner.content

    • editLink.texteditLink.content

    • footer.textfooter.content

    Removes of several theme.config options

    • serverSideError

    • useNextSeoProps (Setup your <head /> tags via head option now)

    • toc.headingComponent

    • sidebar.titleComponent (Use JSX elements in your _meta file instead, see below)

    _meta.jsx
    import { MySeparator } from '@somewhere/my-separator'
     
    export default {
      // ...
      '-- Machina': {
        title: <MySeparator>Machina</MySeparator>,
        type: 'separator'
      }
    }

    <MatchSorterSearch /> was removed

    git-url-parse and next-seo were removed

    This was made to make the bundle smaller.

    sidebar.toggleButton is set to true by default

    toc.backToTop should use a JSX element or a React component

    To disable it, you can set toc.backToTop to null.

    nextra-theme-blog

    Ensure to use Next.js >= 13

    cusdis option in the theme.config file was removed

    You need to pass cusdis options as props in Cusdis component.

    Conclusion

    I’m very proud and happy to lead Nextra into the future. So many things have improved and so many more improvements are coming in the future!

    If you want to help Nextra – spread the word about Nextra in X with #nextra hashtag and subscribe on me in X and GitHub.

    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.