Gathering detailed insights and metrics for next-mdx-remote-client
Gathering detailed insights and metrics for next-mdx-remote-client
Gathering detailed insights and metrics for next-mdx-remote-client
Gathering detailed insights and metrics for next-mdx-remote-client
next-mdx-remote
utilities for loading mdx from any remote source as data, rather than as a local import
@storybook/addon-docs
Document component usage and properties in Markdown
rehype-prism-plus
rehype plugin to highlight code blocks in HTML with Prism (via refractor) with line highlighting and line numbers
@next/mdx
Use [MDX](https://github.com/mdx-js/mdx) with [Next.js](https://github.com/vercel/next.js)
A wrapper of the `@mdx-js/mdx` for the `nextjs` applications in order to load MDX content. It is a fork of `next-mdx-remote`.
npm install next-mdx-remote-client
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
51 Stars
38 Commits
3 Forks
1 Branches
Updated on 21 Nov 2024
Minified
Minified + Gzipped
TypeScript (97.13%)
JavaScript (2.87%)
Cumulative downloads
Total Downloads
Last day
-14.7%
139
Compared to previous day
Last week
-3.4%
1,321
Compared to previous week
Last month
116.1%
6,091
Compared to previous month
Last year
0%
23,358
Compared to previous year
8
27
The next-mdx-remote-client
is a wrapper of @mdx-js/mdx
for nextjs
applications in order to load MDX content. It is a fork of next-mdx-remote
.
See some blog applications in which next-mdx-remote-client
is used:
app
router, visit source code or living web sitepages
router, visit source code or living web siteapp
and pages
router, visit source code or living web sitenext-mdx-remote-client
?I started to create the next-mdx-remote-client
in line with the mindset of the @mdx-js/mdx
in early 2024 considering next-mdx-remote had not been updated for a long time, and finally, a brand new package emerged.
The next-mdx-remote-client
serves as a viable alternative to next-mdx-remote
having more features.
I would like to highlight some main features:
import statements
and export statements
in MDX source, which can be disabled as well.vfile.data
into the scope
.getFrontmatter
.@mdx-js/mdx
so as you don't need to install.Let's compare the features of next-mdx-remote
and next-mdx-remote-client
.
Features | next-mdx-remote | next-mdx-remote-client |
---|---|---|
support MDX version 3 | ✅ (as of v5) | ✅ |
ensure internal error handling mechanism in app router | ❌ | ✅ |
ensure internal error handling mechanism in pages router | ❌ | ✅ |
support export-from-MDX in app router | ❌ | ✅ |
support export-from-MDX in pages router | ❌ | ✅ |
support import-into-MDX in app router | ❌ | ✅ |
support import-into-MDX in pages router | ❌ | ❌ |
get frontmatter and mutated scope in app router | ❌ | ✅ |
get frontmatter and mutated scope in pages router | ✅ | ✅ |
support options for disabling imports and exports in MDX | ✅ | ✅ |
support passing vfile.data into the scope | ❌ | ✅ |
provide utility for getting frontmatter without compiling | ❌ | ✅ |
expose MDXProvider from @mdx-js/mdx | ❌ | ✅ |
provide option for disabling parent MDXProvider contexts | ❌ | ✅ |
expose the necessary types from mdx/types | ❌ | ✅ |
[!IMPORTANT] You will see a lot the abbreviatons
csr
andrsc
. Pay attention to the both are spelled backwards.
csr
stands for "client side rendering" which is related withpages
router
rsc
stands for "react server component" which is related withapp
router
import statements
and export statements
in the MDXapp
and pages
routerapp
router[!IMPORTANT] Imported modules in MDX with relative path should be transpiled into javascript before or during build process, otherwise will not work. I believe the community can find a solution to import reqular
.jsx
or.tsx
modules into MDX. With the support of thenext/mdx
, it is viable to import.mdx
into the MDX, but not tested yet.
This package is ESM only, requires Node.js (version 18.17+).
1npm install next-mdx-remote-client
or
1yarn add next-mdx-remote-client
[!WARNING]
Thenext-mdx-remote
users may follow the migration guide.
The main entry point /
also refers to /csr
subpath.
1// main entry point, which is related "pages" router 2import /* */ from "next-mdx-remote-client"; 3 4// isolated subpath for the "serialize" function 5import /* */ from "next-mdx-remote-client/serialize"; 6 7// sub entry point related with "pages" router 8import /* */ from "next-mdx-remote-client/csr"; 9 10// sub entry point related with "app" router 11import /* */ from "next-mdx-remote-client/rsc"; 12 13// isolated subpath for the utils 14import /* */ from "next-mdx-remote-client/utils";
app
routerGo to the part associated with Next.js pages router
The next-mdx-remote-client
exposes evaluate
function and MDXRemote
component for "app" router.
1import { evaluate, MDXRemote } from "next-mdx-remote-client/rsc";
[!TIP] If you need to get the exports from MDX --> use
evaluate
If you don't need --> useMDXRemote
If you need to get the frontmatter and the mutated scope --> useevaluate
If you don't need --> useMDXRemote
Let's give some examples how to use next-mdx-remote-client
in "app" router first, then explain the exposed function and component.
app
routerSee a demo application with app
router, visit source code or living web site
javascript
1import { Suspense } from "react"; 2import { MDXRemote } from "next-mdx-remote-client/rsc"; 3 4import { ErrorComponent, LoadingComponent } from "../components"; 5import { Test } from '../mdxComponents'; 6 7const components = { 8 Test, 9 wrapper: ({ children }) => <div className="mdx-wrapper">{children}</div>, 10} 11 12export default async function Page() { 13 const source = "Some **bold text** in MDX, with a component <Test />"; 14 15 return ( 16 <Suspense fallback={<LoadingComponent />}> 17 <MDXRemote 18 source={source} 19 components={components} 20 onError={ErrorComponent} 21 /> 22 </Suspense> 23 ); 24};
typescript
, parsing frontmatter and providing custom data with scope1import { Suspense } from "react"; 2import { MDXRemote } from "next-mdx-remote-client/rsc"; 3import type { MDXRemoteOptions, MDXComponents } from "next-mdx-remote-client/rsc"; 4 5import { calculateSomeHow, getSourceSomeHow } from "../utils"; 6import { ErrorComponent, LoadingComponent } from "../components"; 7import { Test } from '../mdxComponents'; 8 9const components: MDXComponents = { 10 Test, 11 wrapper: function ({ children }: React.ComponentPropsWithoutRef<"div">) { 12 return <div className="mdx-wrapper">{children}</div>; 13 }, 14} 15 16export default async function Page() { 17 const source = await getSourceSomeHow(); 18 19 if (!source) { 20 return <ErrorComponent error="The source could not found !" />; 21 } 22 23 const options: MDXRemoteOptions = { 24 mdxOptions: { 25 // ... 26 }, 27 parseFrontmatter: true, 28 scope: { 29 readingTime: calculateSomeHow(source), 30 }, 31 }; 32 33 return ( 34 <Suspense fallback={<LoadingComponent />}> 35 <MDXRemote 36 source={source} 37 options={options} 38 components={components} 39 onError={ErrorComponent} 40 /> 41 </Suspense> 42 ); 43}
I assume you have a MDX file having <TableOfContentComponent />
inside; and you provide some MDX components which have an entry for TableOfContentComponent: (props) => { ... }
.
1--- 2title: My Article 3--- 4# {frontmatter.title} 5 6<TableOfContentComponent toc={toc} /> 7 8rest of the article...
You can have a look at an example TOC component in the demo application.
In order to create a table of contents (TOC) I use remark-flexible-toc
in the remark plugin list and pass the table of contents objects vFile.data.toc
into the scope
via the option vfileDataIntoScope
. That's it ! So easy !
1import { Suspense } from "react"; 2import { MDXRemote, type MDXRemoteOptions } from "next-mdx-remote-client/rsc"; 3import remarkFlexibleToc from "remark-flexible-toc"; // <--------- 4 5import { calculateSomeHow, getSourceSomeHow } from "../utils"; 6import { ErrorComponent, LoadingComponent } from "../components"; 7import { components } from '../mdxComponents'; 8 9export default async function Page() { 10 const source = await getSourceSomeHow(); 11 12 if (!source) { 13 return <ErrorComponent error="The source could not found !" />; 14 } 15 16 const options: MDXRemoteOptions = { 17 mdxOptions: { 18 remarkPlugins: [ 19 // ... 20 remarkFlexibleToc, // <--------- 21 ], 22 }, 23 parseFrontmatter: true, 24 scope: { 25 readingTime: calculateSomeHow(source), 26 }, 27 vfileDataIntoScope: "toc", // <--------- 28 }; 29 30 return ( 31 <Suspense fallback={<LoadingComponent />}> 32 <MDXRemote 33 source={source} 34 options={options} 35 components={components} 36 onError={ErrorComponent} 37 /> 38 </Suspense> 39 ); 40}
1import { Suspense } from "react"; 2import { evaluate, type EvaluateOptions } from "next-mdx-remote-client/rsc"; 3import remarkFlexibleToc, { type TocItem } from "remark-flexible-toc"; 4 5import { calculateSomeHow, getSourceSomeHow } from "../utils"; 6import { ErrorComponent, LoadingComponent, TableOfContentComponent } from "../components"; 7import { components } from "../mdxComponents"; 8 9type Scope = { 10 readingTime: string; 11 toc?: TocItem[]; 12}; 13 14type Frontmatter = { 15 title: string; 16 author: string; 17}; 18 19export default async function Page() { 20 const source = await getSourceSomeHow(); 21 22 if (!source) { 23 return <ErrorComponent error="The source could not found !" />; 24 } 25 26 const options: EvaluateOptions<Scope> = { 27 mdxOptions: { 28 remarkPlugins: [ 29 // ... 30 remarkFlexibleToc, 31 ], 32 }, 33 parseFrontmatter: true, 34 scope: { 35 readingTime: calculateSomeHow(source), 36 }, 37 vfileDataIntoScope: "toc", 38 }; 39 40 const { content, frontmatter, scope, error } = await evaluate<Frontmatter, Scope>({ 41 source, 42 options, 43 components, 44 }); 45 46 if (error) { 47 return <ErrorComponent error={error} />; 48 } 49 50 return ( 51 <> 52 <h1>{frontmatter.title}</h1> 53 <p>Written by {frontmatter.author}; read in {scope.readingTime}</p> 54 <TableOfContentComponent toc={scope.toc} /> 55 <Suspense fallback={<LoadingComponent />}> 56 {content} 57 </Suspense> 58 </> 59 ); 60}
Actually, you may not need to access the "frontmatter" and "scope" in JSX, you can use them within MDX directly, and return just content
only.
1// ... 2export default async function Page({ source }: Props) { 3 // ... 4 return ( 5 <Suspense fallback={<LoadingComponent />}> 6 {content} 7 </Suspense> 8 ); 9}
article.mdx
1# {frontmatter.title} 2 3Written by {frontmatter.author}; read in {readingTime} 4 5<TableOfContentComponent toc={toc} /> 6 7rest of the article...
After the examples given, let's dive into the exposed function and component by next-mdx-remote-client
for "app" router.
evaluate
functionGo to the MDXRemote component
The evaluate
function is used for compiling the MDX source, constructing the compiled source, getting exported information from MDX and returning MDX content to be rendered on the server side, as a react server component.
1async function evaluate(props: EvaluateProps): Promise<EvaluateResult> {}
The evaluate
function takes EvaluateProps
and returns EvaluateResult
as a promise.
Props of the evaluate
function
1type EvaluateProps<TScope> = { 2 source: Compatible; 3 options?: EvaluateOptions<TScope>; 4 components?: MDXComponents; 5};
Result of the evaluate
function
1type EvaluateResult<TFrontmatter, TScope> = { 2 content: JSX.Element; 3 mod: Record<string, unknown>; 4 frontmatter: TFrontmatter; 5 scope: TScope; 6 error?: Error; 7};
The evaluate
has internal error handling mechanism as much as it can, in order to do so, it returns an error
object if it is catched.
[!CAUTION] The eval of the compiled source returns a module
MDXModule
, and does not throw errors except syntax errors. Some errors throw during the render process which needs you to use an ErrorBoundary.
1import { Suspense } from "react"; 2import { evaluate, type EvaluateOptions } from "next-mdx-remote-client/rsc"; 3 4import { ErrorComponent, LoadingComponent, TableOfContentComponent } from "../components"; 5import { components } from "../mdxComponents"; 6import type { Frontmatter, Scope } from "../types" 7 8export default async function MDXComponent({ source }: {source?: string}) { 9 if (!source) { 10 return <ErrorComponent error="The source could not found !" />; 11 } 12 13 const options: EvaluateOptions = { 14 /* */ 15 }; 16 17 const { content, mod, frontmatter, scope, error } = await evaluate<Frontmatter, Scope>({ 18 source, 19 options, 20 components, 21 }); 22 23 if (error) { 24 return <ErrorComponent error={error} />; 25 } 26 27 /** 28 * Use "mod", "frontmatter" and "scope" as you wish 29 * 30 * "mod" object is for exported information from MDX 31 * "frontmatter" is available even if a MDX syntax error occurs 32 * "scope" is for mutated scope if the `vfileDataIntoScope` option is used 33 */ 34 35 return ( 36 <> 37 <h1>{frontmatter.title}</h1> 38 <div><em>{mod.something}</em></div> 39 <TableOfContentComponent toc={scope.toc} /> 40 <Suspense fallback={<LoadingComponent />}> 41 {content} 42 </Suspense> 43 </> 44 ); 45};
If you provide the generic type parameters like await evaluate<Frontmatter, Scope>(){}
, the frontmatter
and the scope
get the types, otherwise Record<string, unknown>
by default for both.
[!WARNING] Pay attention to the order of the generic type parameters.
The type parametersFrontmatter
andScope
should extendRecord<string, unknown>
. You should usetype
instead ofinterface
for type parameters otherwise, you will receive an error sayingType 'Xxxx' does not satisfy the constraint 'Record<string, unknown>'.
See this issue for more explanation.
In the above example, I assume you use remark-flexible-toc
remark plugin in order to collect the headings from the MDX content, and you pass that information into the scope
via vfileDataIntoScope
option.
EvaluateOptions
)All options are optional.
1type EvaluateOptions<TScope> = { 2 mdxOptions?: EvaluateMdxOptions; 3 disableExports?: boolean; 4 disableImports?: boolean; 5 parseFrontmatter?: boolean; 6 scope?: TScope; 7 vfileDataIntoScope?: VfileDataIntoScope; 8};
mdxOptions
It is an EvaluateMdxOptions
option to be passed to the @mdx-js/mdx
compiler.
1import { type EvaluateOptions as OriginalEvaluateOptions } from "@mdx-js/mdx"; 2 3type EvaluateMdxOptions = Omit< 4 OriginalEvaluateOptions, 5 | "Fragment" 6 | "jsx" 7 | "jsxs" 8 | "jsxDEV" 9 | "useMDXComponents" 10 | "providerImportSource" 11 | "outputFormat" 12>;
As you see, some of the options are omitted and opinionated within the package. For example the outputFormat
is always function-body
by default. Visit https://mdxjs.com/packages/mdx/#evaluateoptions for available mdxOptions.
1const options: EvaluateOptions = { 2 // ... 3 mdxOptions: { 4 format: "mdx", 5 baseUrl: import.meta.url, 6 development: true, 7 remarkPlugins: [/* */], 8 rehypePlugins: [/* */], 9 recmaPlugins: [/* */], 10 remarkRehypeOptions: {handlers: {/* */}}, 11 // ... 12 }; 13};
For more information see the MDX documentation.
disableExports
It is a boolean option whether or not stripping the export statements
out from the MDX source.
By default it is false, meaningly the export statements
work as expected.
1const options: EvaluateOptions = { 2 disableExports: true; 3};
Now, the export statements
will be stripped out from the MDX.
disableImports
It is a boolean option whether or not stripping the import statements
out from the MDX source.
By default it is false, meaningly the import statements
work as expected.
1const options: EvaluateOptions = { 2 disableImports: true; 3};
Now, the import statements
will be stripped out from the MDX.
parseFrontmatter
It is a boolean option whether or not the frontmatter should be parsed out of the MDX.
By default it is false, meaningly the frontmatter
will not be parsed and extracted.
1const options: EvaluateOptions = { 2 parseFrontmatter: true; 3};
Now, the frontmatter
part of the MDX file is parsed and extracted from the MDX source; and will be supplied into the MDX file so as you to use it within the javascript statements.
[!NOTE] Frontmatter is a way to identify metadata in Markdown files. Metadata can literally be anything you want it to be, but often it's used for data elements your page needs and you don't want to show directly.
1--- 2title: "My Article" 3author: "ipikuka" 4--- 5# {frontmatter.title} 6 7It is written by {frontmatter.author}
The package uses the vfile-matter
internally to parse the frontmatter.
scope
It is an Record<string, unknown>
option which is an arbitrary object of data which will be supplied to the MDX. For example, in cases where you want to provide template variables to the MDX, like my name is {name}
, you could provide scope as { name: "ipikuka" }
.
Here is another example:
1const options: EvaluateOptions = { 2 scope: { 3 readingTime: calculateSomeHow(source) 4 }; 5};
Now, the scope
will be supplied into the MDX file so as you to use it within the statements.
1# My article 2 3read in {readingTime} min.
The variables within the expression in the MDX content should be valid javascript variable names. Therefore, each key of the scope must be a valid variable name.
1My name is {name} valid expression. 2My name is {my-name} is not valid expression, which will throw error
So, we can say for the scope
, here:
1const options: EvaluateOptions = { 2 scope: { 3 name: "ipikuka", // valid usage 4 "my-name": "ipikuka", // is not valid and error prone for the MDX content !!! 5 }; 6};
[!TIP] The scope variables can be consumed not only as a property of a component, but also within the texts.
1my name is {name} 2 3<BarComponent name={name} />
vfileDataIntoScope
It is an union type option. It is for passing some fields of vfile.data
into the scope
by mutating the scope
.
[!IMPORTANT]
It provides referencial copy for objects and arrays. If thescope
has the same key already,vfile.data
overrides it.
The reason behind of this option is that vfile.data
may hold some extra information added by some remark plugins. Some fields of the vfile.data
may be needed to pass into the scope
so as you to use in the MDX.
1type VfileDataIntoScope = 2 | true // all fields from vfile.data 3 | string // one specific field 4 | { name: string; as: string } // one specific field but change the key as 5 | Array<string | { name: string; as: string }>; // more than one field
1const options: EvaluateOptions = { 2 // Let's assume you use "remark-flexible-toc" plugin which composes 3 // the table of content (TOC) within the 'vfile.data.toc' 4 vfileDataIntoScope: "toc"; // or fileDataIntoScope: ["toc"]; 5};
Now, vfile.data.toc
is copied into the scope as scope["toc"]
, and will be supplied to the MDX via scope
.
1# My article 2 3<TableOfContentComponent toc={toc} />
If you need to change the name of the field, specify it for example { name: "toc", as: "headings" }
.
1const options: EvaluateOptions = { 2 vfileDataIntoScope: { name: "toc", as: "headings" }; 3};
1# My article 2 3<TableOfContentComponent headings={headings} />
If you need to pass all the fields from vfile.data
, specify it as true
1const options: EvaluateOptions = { 2 vfileDataIntoScope: true; 3};
MDXRemote
componentGo to the evaluate function
The MDXRemote
component is used for rendering the MDX content on the server side. It is a react server component.
1async function MDXRemote(props: MDXRemoteProps): Promise<JSX.Element> {}
The MDXRemote
component takes MDXRemoteProps
and returns JSX.Element
as a promise.
Props of the MDXRemote
component
1type MDXRemoteProps<TScope> = { 2 source: Compatible; 3 options?: MDXRemoteOptions<TScope>; 4 components?: MDXComponents; 5 onError?: React.ComponentType<{ error: Error }> 6};
The MDXRemote
has internal error handling mechanism as much as it can, in order to do so, it takes onError
prop in addition to evaluate
function.
[!CAUTION] The eval of the compiled source returns a module
MDXModule
, and does not throw errors except syntax errors. Some errors throw during the render process which needs you to use an ErrorBoundary.
1import { Suspense } from "react"; 2import { MDXRemote, type MDXRemoteOptions } from "next-mdx-remote-client/rsc"; 3 4import { ErrorComponent, LoadingComponent } from "../components"; 5import { components } from "../mdxComponents"; 6 7export default async function MDXComponent({ source }: {source?: string}) { 8 if (!source) { 9 return <ErrorComponent error="The source could not found !" />; 10 } 11 12 const options: MDXRemoteOptions = { 13 /* */ 14 }; 15 16 return ( 17 <Suspense fallback={<LoadingComponent />}> 18 <MDXRemote 19 source={source} 20 options={options} 21 components={components} 22 onError={ErrorComponent} 23 /> 24 </Suspense> 25 ); 26};
MDXRemoteOptions
)All options are optional.
1type MDXRemoteOptions<TScope> = { 2 mdxOptions?: EvaluateMdxOptions; 3 disableExports?: boolean; 4 disableImports?: boolean; 5 parseFrontmatter?: boolean; 6 scope?: TScope; 7 vfileDataIntoScope?: VfileDataIntoScope; 8};
The details are the same with the EvaluateOptions.
pages
routerGo to the part associated with Next.js app router
The next-mdx-remote-client
exposes serialize
, hydrate
functions and MDXClient
component for "pages" router.
The serialize
function is used on the server side in "pages" router, while as the hydrate
and the MDXClient
are used on the client side in "pages" router. That is why the "serialize" function is purposefully isolated considering it is intended to run on the server side.
Let's give some examples how to use next-mdx-remote-client
in "pages" router first, then explain the exposed functions and component.
pages
routerSee a demo application with pages
router, visit source code or living web site
javascript
1import { serialize } from 'next-mdx-remote-client/serialize'; 2import { MDXClient } from 'next-mdx-remote-client'; 3 4import ErrorComponent from '../components/ErrorComponent'; 5import Test from '../mdxComponents/Test'; 6 7const components = { 8 Test, 9 wrapper: ({children}) => <div className="mdx-wrapper">{children}</div>, 10} 11 12export default function Page({ mdxSource }) { 13 if ("error" in mdxSource) { 14 return <ErrorComponent error={mdxSource.error} />; 15 } 16 17 return <MDXClient {...mdxSource} components={components} />; 18} 19 20export async function getStaticProps() { 21 const source = "Some **bold text** in MDX, with a component <Test />"; 22 23 const mdxSource = await serialize({source}); 24 25 return { props: { mdxSource } }; 26}
typescript
, parsing frontmatter and providing custom data with scope1import { MDXClient, type MDXComponents } from 'next-mdx-remote-client'; 2import { serialize } from "next-mdx-remote-client/serialize"; 3import type { SerializeOptions, SerializeResult } from "next-mdx-remote-client/serialize"; 4 5import { calculateSomeHow, getSourceSomeHow } from "../utils"; 6import ErrorComponent from '../components/ErrorComponent'; 7import Test from '../mdxComponents/Test'; 8 9type Scope = { 10 readingTime: string; 11}; 12 13type Frontmatter = { 14 title: string; 15 author: string; 16}; 17 18const components: MDXComponents = { 19 Test, 20 wrapper: function ({ children }: React.ComponentPropsWithoutRef<"div">) { 21 return <div className="mdx-wrapper">{children}</div>; 22 }, 23} 24 25type Props = { 26 mdxSource?: SerializeResult<Frontmatter, Scope>; 27} 28 29export default function Page({ mdxSource }: Props) { 30 if (!mdxSource) { 31 return <ErrorComponent error="The source could not found !" />; 32 } 33 34 if ("error" in mdxSource) { 35 return <ErrorComponent error={mdxSource.error} />; 36 } 37 38 return ( 39 <> 40 <h1>{mdxSource.frontmatter.title}</h1> 41 <p>Written by {mdxSource.frontmatter.author}; read in {mdxSource.scope.readingTime}</p> 42 <MDXClient {...mdxSource} components={components} /> 43 </> 44 ); 45} 46 47export async function getStaticProps() { 48 const source = await getSourceSomeHow(); 49 50 if (!source) return { props: {} }; 51 52 const options: SerializeOptions<Scope> = { 53 disableImports: true, 54 mdxOptions: { 55 // ... 56 }, 57 parseFrontmatter: true, 58 scope: { 59 readingTime: calculateSomeHow(source), 60 }, 61 }; 62 63 const mdxSource = await serialize<Frontmatter, Scope>({source, options}); 64 65 return { props: { mdxSource } }; 66}
I assume you have a MDX file having <TableOfContentComponent />
inside; and you provide some MDX components which have an entry for TableOfContentComponent: (props) => { ... }
.
1--- 2title: My Article 3--- 4# {frontmatter.title} 5 6<TableOfContentComponent toc={toc} /> 7 8rest of the article...
You can have a look at an example TOC component in the demo application.
In order to create a table of contents (TOC) I use remark-flexible-toc
in the remark plugin list and pass the table of contents objects vFile.data.toc
into the scope
via the option vfileDataIntoScope
. That's it ! So easy !
1import { MDXClient, type MDXComponents } from 'next-mdx-remote-client'; 2import { serialize } from "next-mdx-remote-client/serialize"; 3import type { SerializeOptions, SerializeResult } from "next-mdx-remote-client/serialize"; 4import remarkFlexibleToc, {type TocItem} from "remark-flexible-toc"; // <--------- 5 6import { calculateSomeHow, getSourceSomeHow } from "../utils"; 7import { ErrorComponent, TableOfContentComponent } from '../components'; 8import { Test } from '../mdxComponents'; 9 10type Scope = { 11 readingTime: string; 12}; 13 14type Frontmatter = { 15 title: string; 16 author: string; 17}; 18 19const components: MDXComponents = { 20 Test, 21 wrapper: function ({ children }: React.ComponentPropsWithoutRef<"div">) { 22 return <div className="mdx-wrapper">{children}</div>; 23 }, 24} 25 26type Props = { 27 mdxSource?: SerializeResult<Frontmatter, Scope & {toc: TocItem[]}>; 28} 29 30export default function Page({ mdxSource }: Props) { 31 if (!mdxSource) { 32 return <ErrorComponent error="The source could not found !" />; 33 } 34 35 if ("error" in mdxSource) { 36 return <ErrorComponent error={mdxSource.error} />; 37 } 38 39 return ( 40 <> 41 <h1>{mdxSource.frontmatter.title}</h1> 42 <p>Written by {mdxSource.frontmatter.author}; read in {mdxSource.scope.readingTime}</p> 43 <TableOfContentComponent toc={mdxSource.scope.toc /* <----- here added TOC */} /> 44 <MDXClient {...mdxSource} components={components} /> 45 </> 46 ); 47} 48 49export async function getStaticProps() { 50 const source = await getSourceSomeHow(); 51 52 if (!source) return { props: {} }; 53 54 const options: SerializeOptions<Scope> = { 55 disableImports: true, 56 mdxOptions: { 57 remarkPlugins: [ 58 // ... 59 remarkFlexibleToc, // <--------- 60 ], 61 }, 62 parseFrontmatter: true, 63 scope: { 64 readingTime: calculateSomeHow(source), 65 }, 66 vfileDataIntoScope: "toc", // <--------- 67 }; 68 69 const mdxSource = await serialize<Frontmatter, Scope>({source, options}); 70 71 return { props: { mdxSource } }; 72}
Actually, you may not need to access the "frontmatter" and "scope" in JSX, you can use them within MDX directly, and return just <MDXClient />
only.
1// ... 2const components: MDXComponents = { 3 TableOfContentComponent, // <--------- 4 wrapper: function ({ children }: React.ComponentPropsWithoutRef<"div">) { 5 return <div className="mdx-wrapper">{children}</div>; 6 }, 7} 8// ... 9export default function Page({ mdxSource }: Props) { 10 // ... 11 return ( 12 <MDXClient {...mdxSource} components={components} /> 13 ); 14}
article.mdx
1# {frontmatter.title} 2 3Written by {frontmatter.author}; read in {readingTime} 4 5<TableOfContentComponent toc={toc} /> 6 7rest of the article...
After the examples given, let's dive into the exposed functions and component by next-mdx-remote-client
for "pages" router.
serialize
functionGo to the hydrate function or the MDXClient component
1import { serialize } from "next-mdx-remote-client/serialize";
The serialize
function is used for compiling the MDX source, in other words, producing the compiled source from MDX source, intended to run on server side at build time.
[!WARNING] The
serialize
function is asyncronous and to be used within thegetStaticProps
or thegetServerSideProps
on the server side. (Off the record, it can be used within anuseEffect
as well, but this is not recommended because it is a heavy function as having more dependencies).
1async function serialize(props: SerializeProps): Promise<SerializeResult> {}
The serialize
function takes SerializeProps
and returns SerializeResult
as a promise.
Props of the serialize
function
1type SerializeProps<TScope> = { 2 source: Compatible; 3 options?: SerializeOptions<TScope>; 4};
Result of the serialize
function
Either the compiledSource
or the error
exists, in addition to frontmatter
and scope
.
1type SerializeResult<TFrontmatter, TScope> = 2({ compiledSource: string } | { error: Error }) 3& { 4 frontmatter: TFrontmatter; 5 scope: TScope; 6};
The serialize
function has internal error handling mechanism for the MDX syntax errors. The catched error is serialized via serialize-error
package and attached into the serialize results, further you can deserialize the error on the client, if necessary. You don't need to implement error handling by yourself.
1import { serialize, type SerializeOptions } from "next-mdx-remote-client/serialize"; 2import type { Frontmatter, Scope } from "./types" 3 4export async function getStaticProps() { 5 const source = await getSourceSomeHow(); 6 7 if (!source) { 8 return { props: {} }; 9 } 10 11 const options: SerializeOptions = { 12 /* */ 13 }; 14 15 const mdxSource = await serialize<Frontmatter, Scope>({ 16 source, 17 options, 18 }); 19 20 return { 21 props: { 22 mdxSource, 23 }, 24 }; 25}
If you provide the generic type parameters like await serialize<Frontmatter, Scope>(){}
, the frontmatter
and the scope
get the types, otherwise Record<string, unknown>
by default for both.
[!WARNING] Pay attention to the order of the generic type parameters.
The type parametersFrontmatter
andScope
should extendRecord<string, unknown>
. You should usetype
instead ofinterface
for type parameters otherwise, you will receive an error sayingType 'Xxxx' does not satisfy the constraint 'Record<string, unknown>'.
See this issue for more explanation.
The nextjs
will send the mdxSource
((compiledSource
or error
) + frontmatter
+ scope
) to client side.
On client side, you need first to narrow the mdxSource
by checking if ("error" in mdxSource) {}
.
1type Props = { 2 mdxSource?: SerializeResult<Frontmatter, Scope>; 3} 4 5export default function Page({ mdxSource }: Props) { 6 // ... 7 8 if ("error" in mdxSource) { 9 return <ErrorComponent error={mdxSource.error} />; 10 } 11 12 // ... 13};
SerializeOptions
)All options are optional.
1type SerializeOptions<TScope> = { 2 mdxOptions?: SerializeMdxOptions; 3 disableExports?: boolean; 4 disableImports?: boolean; 5 parseFrontmatter?: boolean; 6 scope?: TScope; 7 vfileDataIntoScope?: VfileDataIntoScope; 8};
Except the mdxOptions
, the details are the same with the EvaluateOptions.
mdxOptions
It is a SerializeMdxOptions
option to be passed to the @mdx-js/mdx
compiler.
1import { type CompileOptions as OriginalCompileOptions } from "@mdx-js/mdx"; 2 3type SerializeMdxOptions = Omit< 4 OriginalCompileOptions, 5 "outputFormat" | "providerImportSource" 6>;
As you see, some of the options are omitted and opinionated within the package. For example the outputFormat
is always function-body
by default. Visit https://mdxjs.com/packages/mdx/#compileoptions for available mdxOptions.
1const options: SerializeOptions = { 2 // ... 3 mdxOptions: { 4 format: "mdx", 5 baseUrl: import.meta.url, 6 development: true, 7 remarkPlugins: [/* */], 8 rehypePlugins: [/* */], 9 recmaPlugins: [/* */], 10 remarkRehypeOptions: {handlers: {/* */}}, 11 // ... 12 }; 13};
[!WARNING] Here I need to mention about the
scope
option again for theserialize
.
scope
Actually, theserialize
doesn't do so much with thescope
except you provide the optionvfileDataIntoScope
for passing data fromvfile.data
into thescope
. Since thescope
is passed from the server to the client bynextjs
, thescope
must be serializable. Thescope
can not hold function, component , Date, undefined, Error object etc.
If the scope has to have unserializable information or if you don't need or don't want to pass thescope
into theserialize
, you can pass it intohydrate
orMDXClient
directly on the client side.
hydrate
functionGo to the serialize function or the MDXClient component
1import { hydrate } from "next-mdx-remote-client/csr";
The hydrate
function is used for constructing the compiled source, getting exported information from MDX and returning MDX content to be rendered on the client side.
1function hydrate(props: HydrateProps): HydrateResult {}
The hydrate
function takes HydrateProps
and returns HydrateResult
. The hydrate
has no "options" parameter.
Props of the hydrate
function
1type HydrateProps = { 2 compiledSource: string; 3 frontmatter?: Record<string, unknown>; 4 scope?: Record<string, unknown>; 5 components?: MDXComponents; 6 disableParentContext?: boolean; 7};
The option disableParentContext
is a feature of @mdx-js/mdx
. If it is false
, the mdx components provided by parent MDXProvider
s are going to be disregarded.
Result of the hydrate
function
1type HydrateResult = { 2 content: JSX.Element; 3 mod: Record<string, unknown>; 4 error?: Error; 5};
The mod
object is for exported information from MDX source.
[!TIP] If you need to get the exports from MDX --> use
hydrate
If you don't need --> useMDXClient
The hydrate
has internal error handling mechanism as much as it can, in order to do so, it returns an error
object if it is catched.
[!CAUTION] The eval of the compiled source returns a module
MDXModule
, and does not throw errors except syntax errors. Some errors throw during the render process which needs you to use an ErrorBoundary.
1import { hydrate, type SerializeResult } from "next-mdx-remote-client/csr"; 2 3import { ErrorComponent, TableOfContentComponent } from "../components"; 4import { components } from "../mdxComponents"; 5import type { Frontmatter, Scope } from "../types" 6 7type Props = { 8 mdxSource?: SerializeResult<Frontmatter, Scope>; 9} 10 11export default function Page({ mdxSource }: Props) { 12 if (!mdxSource) { 13 return <ErrorComponent error="The source could not found !" />; 14 } 15 16 if ("error" in mdxSource) { 17 return <ErrorComponent error={mdxSource.error} />; 18 } 19 20 // Now, mdxSource has {compiledSource, frontmatter, scope} 21 22 const { content, mod, error } = hydrate({ ...mdxSource, components }); 23 24 if (error) { 25 return <ErrorComponent error={error} />; 26 } 27 28 // You can use the "mod" object for exported information from the MDX as you wish 29 30 return ( 31 <> 32 <h1>{mdxSource.frontmatter.title}</h1> 33 <div><em>{mod.something}</em></div> 34 <TableOfContentComponent toc={mdxSource.scope.toc} /> 35 {content} 36 </> 37 ); 38};
In the above example, I assume you use remark-flexible-toc
remark plugin in order to collect the headings from the MDX content, and you pass that information into the scope
via vfileDataIntoScope
option within the serialize on the server side.
MDXClient
componentGo to the serialize function or the hydrate function
1import { MDXClient } from "next-mdx-remote-client/csr";
The MDXClient
component is used for rendering the MDX content on the client side.
1function MDXClient(props: MDXClientProps): JSX.Element {}
The MDXClient
component takes MDXClientProps
and returns JSX.Element
. The MDXClient
has no "options" parameter like hydrate
.
Props of the MDXClient
component
1type MDXClientProps = { 2 compiledSource: string; 3 frontmatter?: Record<string, unknown>; 4 scope?: Record<string, unknown>; 5 components?: MDXComponents; 6 disableParentContext?: boolean; 7 onError?: React.ComponentType<{ error: Error }> 8};
The option disableParentContext
is a feature of @mdx-js/mdx
. If it is false
, the mdx components provided by parent MDXProvider
s are going to be disregarded.
[!TIP] If you need to get the exports from MDX --> use
hydrate
If you don't need --> useMDXClient
The MDXClient
has internal error handling mechanism as much as it can, in order to do so, it takes onError
prop in addition to hydrate
function.
[!CAUTION] The eval of the compiled source returns a module
MDXModule
, and does not throw errors except syntax errors. Some errors throw during the render process which needs you to use an ErrorBoundary.
1import { MDXClient, type SerializeResult } from "next-mdx-remote-client/csr"; 2 3import { ErrorComponent, TableOfContentComponent } from "../components"; 4import { components } from "../mdxComponents"; 5import type { Frontmatter, Scope } from "../types" 6 7type Props = { 8 mdxSource?: SerializeResult<Frontmatter, Scope>; 9} 10 11export default function Page({ mdxSource }: Props) { 12 if (!mdxSource) { 13 return <ErrorComponent error="The source could not found !" />; 14 } 15 16 if ("error" in mdxSource) { 17 return <ErrorComponent error={mdxSource.error} />; 18 } 19 20 // Now, mdxSource has {compiledSource, frontmatter, scope} 21 22 return ( 23 <> 24 <h1>{mdxSource.frontmatter.title}</h1> 25 <TableOfContentComponent toc={mdxSource.scope.toc} /> 26 <MDXClient 27 {...mdxSource} 28 components={components} 29 onError={ErrorComponent} 30 /> 31 </> 32 ); 33};
In the above example, I assume you use remark-flexible-toc
remark plugin in order to collect the headings from the MDX content, and you pass that information into the scope
via vfileDataIntoScope
option within the serialize on the server side.
hydrateLazy
function and the MDXClientLazy
componentThe next-mdx-remote-client
exports additional versions, say, the hydrateLazy
and the MDXClientLazy
, which both have the same functionality, props, results with the hydrate
and the MDXClient
, correspondently.
The only difference is the hydration process takes place lazily on the browser within a window.requestIdleCallback
in a useEffect. You can use hydrateLazy
or MDXClientLazy
in order to defer hydration of the content and immediately serve the static markup.
1import { hydrateLazy, MDXClientLazy } from "next-mdx-remote-client/csr";
When you use hydrateLazy
, and want to get the exports from MDX via mod
object, please be aware that the mod
object is always empty {}
at first render, then it will get actual exports at second render.
[!NOTE] Lazy hydration defers hydration of the components on the client. This is an optimization technique to improve the initial load of the application, but may introduce unexpected delays in interactivity for any dynamic content within the MDX content.
This will add an additional wrapping div around the rendered MDX, which is necessary to avoid hydration mismatches during render.
For further explanation about the lazy hydration seenext-mdx-remote
notes.
hydrateAsync
function and the MDXClientAsync
componentThe next-mdx-remote-client
exports additional versions, say, the hydrateAsync
and the MDXClientAsync
.
These have additional props and options, but here, I don't want to give the details since I created them for experimental to show the import statements
on the client side don't work. You can have a look at the github repository for the code and the tests.
The main difference is that the eval of the compiled source takes place in a useEffect on the browser, since the compile source has await
keyword for import statements
.
1import { hydrateAsync, MDXClientAsync } from "next-mdx-remote-client/csr";
[!NOTE]
I believe, it is viable somehow usingdynamic
API if thevercel
supports for a solution for thepages
router viaimport.meta
APIs. During the compilation of the MDX in theserialize
, a remark/recma plugin can register the imported modules into theimport.meta.url
via a nextjs API (needs support of vercel) for them will be available to download/import on the client side viadynamic
api. This is my imagination.
MDXProvider
componentThe package exports the MDXProvider
from @mdx-js/react
, in order the developers don't need to install the @mdx-js/react
.
1import { MDXProvider } from "next-mdx-remote-client/csr";
The <MDXProvider />
makes the mdx components available to any <MDXClient />
or hydrate's { content }
being rendered in the application, as a child status of that provider.
For example, you can wrap the whole application so as you do not need to supply the mdx components into any <MDXClient />
or hydrate's { content }
.
1import { MDXProvider } from 'next-mdx-remote-client'; 2 3import { components } from "../mdxComponents"; 4 5export default function App({ Component, pageProps }) { 6 return ( 7 <MDXProvider components={components}> 8 <Component {...pageProps} /> 9 </MDXProvider> 10 ) 11}
[!NOTE] How this happens, because the
next-mdx-remote-client
injects theuseMdxComponents
context hook from@mdx-js/react
during the function construction of the compiled source, internally. Pay attention that it is valid for onlyMDXClient
andhydrate
functions.
[!CAUTION] Since
MDXRemote
as a react server component can not read the context,MDXProvider
is effectless when used within the nextjsapp
router forMDXRemote
, which is also forevaluate
.
You can provide a map of custom MDX components, which is a feature of @mdx-js/mdx
, in order to replace HTML tags (see the list of markdown syntax and equivalent HTML tags) with the custom components.
Typescript users can use MDXComponents
from mdx/types
, which is exported by this package as well.
../mdxComponents/index.ts
1import { type MDXComponents } from "next-mdx-remote-client"; 2 3import dynamic from "next/dynamic"; 4import Image from "next/image"; 5import Link from "next/link"; 6 7import { Typography } from "@material-ui/core"; 8import { motion } from 'framer-motion' 9 10import Hello from "./Hello"; 11import CountButton from "./CountButton"; 12import BlockQuote, { default as blockquote } from "./BlockQuote"; 13import pre from "./pre"; 14 15export const mdxComponents: MDXComponents = { 16 Hello, 17 CountButton, 18 Dynamic: dynamic(() => import("./dynamic")), 19 Image, 20 Link, 21 motion: { div: () => <div>Hello world</div> }, 22 h2: (props: React.ComponentPropsWithoutRef<"h2">) => ( 23 <Typography variant="h2" {...props} /> 24 ), 25 strong: (props: React.ComponentPropsWithoutRef<"strong">) => ( 26 <strong className="custom-strong" {...props} /> 27 ), 28 em: (props: React.ComponentPropsWithoutRef<"em">) => ( 29 <em className="custom-em" {...props} /> 30 ), 31 pre, 32 blockquote, 33 BlockQuote, 34 wrapper: (props: { children: any }) => { 35 return <div id="mdx-layout">{props.children}</div>; 36 } 37};
[!NOTE] The
wrapper
is a special key, if you want to wrap the MDX content with a HTML container element.
./data/my-article.mdx
1--- 2title: "My Article" 3author: "ipikuka" 4--- 5_Read in {readingTime}, written by <Link href="#">**{frontmatter.author}**</Link>_ 6 7# {frontmatter.title} 8 9## Sub heading for custom components 10 11<Hello name={foo} /> 12 13<CountButton /> 14 15<Dynamic /> 16 17<Image src="/images/cover.png" alt="cover" width={180} height={40} /> 18 19<BlockQuote> 20 I am blackquote content 21</BlockQuote> 22 23<motion.div animate={{ x: 100 }} /> 24 25## Sub heading for some markdown elements 26 27![cover](/images/cover.png) 28 29Here is _italic text_ and **strong text** 30 31> I am blackquote content
getFrontmatter
The package exports one utility getFrontmatter
which is for getting frontmatter without compiling the source. You can get the fronmatter and the stripped source by using the getFrontmatter
which employs the same frontmatter extractor vfile-matter
used within the package.
1import { getFrontmatter } from "next-mdx-remote-client/utils"; 2 3const { frontmatter, strippedSource } = getFrontmatter<TFrontmatter>(source);
If you provide the generic type parameter, it ensures the frontmatter
gets the type, otherwise Record<string, unknown>
by default.
If there is no frontmatter in the source, the frontmatter
will be empty object {}
.
[!IMPORTANT]
If you usenext-mdx-remote
and want to getfrontmatter
without compiling the source !
The subpathnext-mdx-remote-client/utils
is isolated from other features of the package and it does cost minimum. So, anyone can usenext-mdx-remote-client/utils
while usingnext-mdx-remote
.
The next-mdx-remote-client
is fully typed with TypeScript.
The package exports the types for server side (rsc):
EvaluateProps
EvaluateOptions
EvaluateResult
MDXRemoteProps
MDXRemoteOptions
The package exports the types for client side (csr):
HydrateProps
HydrateResult
MDXClientProps
SerializeResult
The package exports the types for the serialize function:
SerializeProps
SerializeOptions
SerializeResult
In addition, the package exports the types from mdx/types
so that developers do not need to import mdx/types
:
MDXComponents
MDXContent
MDXProps
MDXModule
The next-mdx-remote-client
works with unified version 6+ ecosystem since it is compatible with MDX version 3.
Allowance of the export declarations
and the import declarations
in MDX source, if you don't have exact control on the content, may cause vulnerabilities and harmful activities. The next-mdx-remote-client gives options for disabling them.
But, you need to use a custom recma plugin for disabiling the import expressions
like await import("xyz")
since the next-mdx-remote-client doesn't touch the import expressions.
Eval
a string of JavaScript can be a dangerous and may cause enabling XSS attacks, which is how the next-mdx-remote-client APIs do. Please, take your own measures while passing the user input.
If there is a Content Security Policy (CSP) on the website that disallows code evaluation via eval
or new Function()
, it is needed to loosen that restriction in order to utilize next-mdx-remote-client
, which can be done using unsafe-eval.
I like to contribute the Unified / Remark / MDX ecosystem, so I recommend you to have a look my plugins.
remark-flexible-code-titles
– Remark plugin to add titles or/and containers for the code blocks with customizable propertiesremark-flexible-containers
– Remark plugin to add custom containers with customizable properties in markdownremark-ins
– Remark plugin to add ins
element in markdownremark-flexible-paragraphs
– Remark plugin to add custom paragraphs with customizable properties in markdownremark-flexible-markers
– Remark plugin to add custom mark
element with customizable properties in markdownremark-flexible-toc
– Remark plugin to expose the table of contents via Vfile.data or via an option referenceremark-mdx-remove-esm
– Remark plugin to remove import and/or export statements (mdxjsEsm)rehype-pre-language
– Rehype plugin to add language information as a property to pre
elementrecma-mdx-escape-missing-components
– Recma plugin to set the default value () => null
for the Components in MDX in case of missing or not provided so as not to throw an errorrecma-mdx-change-props
– Recma plugin to change the props
parameter into the _props
in the function _createMdxContent(props) {/* */}
in the compiled source in order to be able to use {props.foo}
like expressions. It is useful for the next-mdx-remote
or next-mdx-remote-client
users in nextjs
applications.MPL 2.0 License © ipikuka
🟩 @mdx-js 🟩 next/mdx 🟩 next-mdx-remote 🟩 next-mdx-remote-client
No vulnerabilities found.
No security vulnerabilities found.