Nextra 3 – Your Favourite MDX Framework, Now on 🧪 Steroids
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.
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:
-
only the default locale was produced in sitemap #712
-
index files were not working for subfolders #1263
-
build output directory for each locale contained all pages locales #1922
-
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 2 | Nextra 3 |
---|---|
|
|
You should know also a few notes:
- By default, Next.js doesn’t support project structure like above with the
i18n
property enabled in yournext.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:
- 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.
-
useRouter
fromnext/router
returns incorrectlocale
anddefaultLocale
values (sincei18n
was unset), so instead you should useuseRouter
fromnextra/hooks
which will return correct values. -
Popular i18n libraries like
react-intl
andreact-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 thenextra
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
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
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:
-+ 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
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
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:
-+ 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:
-
buildDynamicMDX
andbuildDynamicMeta
fromnextra/remote
buildDynamicMDX
- compile your MDX data into an evaluable functionbuildDynamicMeta
- create a page map for your catch-all[[...slug]].mdx
route
-
RemoteContent
fromnextra/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 inRemoteContent
’scomponents
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:
[
"configs.mdx",
"custom-rules.mdx",
"getting-started.mdx",
"getting-started/parser-options.mdx",
"getting-started/parser.mdx",
"index.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
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:
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 withoutcss-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
.
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).
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.
-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
-
primaryHue
→color.hue
-
primarySaturation
→color.saturation
-
i18n.text
→i18n.name
-
banner.text
→banner.content
-
editLink.text
→editLink.content
-
footer.text
→footer.content
Removes of several theme.config
options
-
serverSideError
-
useNextSeoProps
(Setup your<head />
tags viahead
option now) -
toc.headingComponent
-
sidebar.titleComponent
(Use JSX elements in your_meta
file instead, see below)
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.