Repository: ipikuka/next-mdx-remote-client
Branch: main
Commit: 2bf52150eb4d
Files: 64
Total size: 281.1 KB
Directory structure:
gitextract_yhdnptxl/
├── .editorconfig
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── main.yml
│ └── publish.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── codecov.yml
├── eslint.config.js
├── migration_guide.md
├── package.json
├── src/
│ ├── csr/
│ │ ├── MDXClient.tsx
│ │ ├── MDXClientAsync.tsx
│ │ ├── MDXClientLazy.tsx
│ │ ├── hydrate.tsx
│ │ ├── hydrateAsync.tsx
│ │ ├── hydrateLazy.tsx
│ │ ├── idle-callback-polyfill.js
│ │ ├── index.ts
│ │ ├── requestIdleCallback.d.ts
│ │ ├── serialize.ts
│ │ └── types.ts
│ ├── lib/
│ │ ├── compile.ts
│ │ ├── prepare.ts
│ │ ├── run.ts
│ │ ├── types.ts
│ │ └── util.ts
│ ├── rsc/
│ │ ├── MDXRemote.tsx
│ │ ├── evaluate.tsx
│ │ ├── index.ts
│ │ └── types.ts
│ └── utils/
│ ├── getFrontmatter.ts
│ └── index.ts
├── tests/
│ ├── ErrorBoundaryClassic.jsx
│ ├── ErrorBoundaryForTests.tsx
│ ├── context/
│ │ ├── ExampleForm.mjs
│ │ ├── ExampleFormTransformedAutomatic.mjs
│ │ ├── ExampleFormTransformedClassic.mjs
│ │ ├── HelloDave.mjs
│ │ ├── components.js
│ │ └── data.js
│ ├── test.MDXClient.spec.tsx
│ ├── test.MDXClientAsync.esm.spec.tsx
│ ├── test.MDXClientLazy.spec.tsx
│ ├── test.MDXProvider.spec.tsx
│ ├── test.MDXRemote.esm.spec.tsx
│ ├── test.MDXRemote.spec.tsx
│ ├── test.compile.spec.tsx
│ ├── test.core.spec.ts
│ ├── test.evaluate.esm.spec.tsx
│ ├── test.evaluate.spec.tsx
│ ├── test.original.compile.spec.tsx
│ ├── test.run.spec.tsx
│ ├── test.serialize-hydrate.spec.tsx
│ ├── test.utils.spec.ts
│ ├── test.wrapper.spec.tsx
│ └── utils/
│ └── index.ts
├── tsconfig.json
├── vite.config.js
└── vitest-setup-tests.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# Stop the editor from looking for .editorconfig files in the parent directories
root = true
[*]
# Non-configurable Prettier behaviors
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
# Configurable Prettier behaviors
end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 96
[*.md]
trim_trailing_whitespace = false
max_line_length = off
================================================
FILE: .github/FUNDING.yml
================================================
github: [ipikuka]
================================================
FILE: .github/workflows/main.yml
================================================
name: main
on:
pull_request:
branches:
- "**"
push:
branches:
- "**"
jobs:
test:
name: Test (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [20, 22, 24]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node ${{ matrix.node }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
- run: npm run format
coverage:
name: Code Coverage (Node 22)
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node 22
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test-coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
directory: ./coverage/
files: ./coverage.json
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
slug: ipikuka/next-mdx-remote-client
================================================
FILE: .github/workflows/publish.yml
================================================
name: publish to npm
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # for provenance
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node 22
uses: actions/setup-node@v6
with:
node-version: 22
registry-url: https://registry.npmjs.org/
cache: npm
- run: npm ci
- run: npm run build
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
.DS_Store
.vscode
dist
node_modules
archive
coverage
================================================
FILE: .npmignore
================================================
dist/*.tsbuildinfo
================================================
FILE: .npmrc
================================================
ignore-scripts=true
================================================
FILE: .prettierignore
================================================
.DS_Store
.vscode
dist
node_modules
package-lock.json
archive
coverage
README.md
migration_guide.md
================================================
FILE: .prettierrc.json
================================================
{
"singleQuote": false,
"bracketSpacing": true,
"trailingComma": "all",
"tabWidth": 2,
"useTabs": false,
"semi": true,
"arrowParens": "always",
"endOfLine": "lf",
"printWidth": 96,
"objectWrap": "preserve"
}
================================================
FILE: LICENSE
================================================
# Mozilla Public License Version 2.0
Copyright (c) 2026 @talatkuyuk AKA @ipikuka
1. Definitions
---
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
---
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
---
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
---
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
---
- *
- 6. Disclaimer of Warranty \*
- ------------------------- \*
- *
- Covered Software is provided under this License on an "as is" \*
- basis, without warranty of any kind, either expressed, implied, or \*
- statutory, including, without limitation, warranties that the \*
- Covered Software is free of defects, merchantable, fit for a \*
- particular purpose or non-infringing. The entire risk as to the \*
- quality and performance of the Covered Software is with You. \*
- Should any Covered Software prove defective in any respect, You \*
- (not any Contributor) assume the cost of any necessary servicing, \*
- repair, or correction. This disclaimer of warranty constitutes an \*
- essential part of this License. No use of any Covered Software is \*
- authorized under this License except under this disclaimer. \*
- *
---
---
- *
- 7. Limitation of Liability \*
- -------------------------- \*
- *
- Under no circumstances and under no legal theory, whether tort \*
- (including negligence), contract, or otherwise, shall any \*
- Contributor, or anyone who distributes Covered Software as \*
- permitted above, be liable to You for any direct, indirect, \*
- special, incidental, or consequential damages of any character \*
- including, without limitation, damages for lost profits, loss of \*
- goodwill, work stoppage, computer failure or malfunction, or any \*
- and all other commercial damages or losses, even if such party \*
- shall have been informed of the possibility of such damages. This \*
- limitation of liability shall not apply to liability for death or \*
- personal injury resulting from such party's negligence to the \*
- extent applicable law prohibits such limitation. Some \*
- jurisdictions do not allow the exclusion or limitation of \*
- incidental or consequential damages, so this exclusion and \*
- limitation may not apply to You. \*
- *
---
8. Litigation
---
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
---
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
## Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
## Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
================================================
FILE: README.md
================================================
**A robust Next.js newsletter `Next.js Weekly` is sponsoring me** 💖
[](https://nextjsweekly.com/)
### [Become a sponsor](https://github.com/sponsors/ipikuka/) 🚀
If you find **`next-mdx-remote-client`** useful in your projects, consider supporting my work.
Your sponsorship means a lot 💖
My sponsors are going to be featured here and on [my sponsor wall](https://github.com/sponsors/ipikuka).
A warm thanks 🙌 to [@ErfanEbrahimnia](https://github.com/ErfanEbrahimnia), [@recepkyk](https://github.com/recepkyk), and [@LSeaburg](https://github.com/LSeaburg) for the support!
Thank you for supporting open source! 🙌
# next-mdx-remote-client
[![npm version][badge-npm-version]][url-npm-package]
[![npm downloads][badge-npm-download]][url-npm-package]
[![publish to npm][badge-publish-to-npm]][url-publish-github-actions]
[![code-coverage][badge-codecov]][url-codecov]
[![type-coverage][badge-type-coverage]][url-github-package]
[![typescript][badge-typescript]][url-typescript]
[![license][badge-license]][url-license]
**`next-mdx-remote-client`** is recommended in the [`Next.js` official documentation](https://nextjs.org/docs/15/app/guides/mdx#remote-mdx).
> [!IMPORTANT]
> **If you are using `react18`, use ver.1 of `next-mdx-remote-client`, currently `v1.1`**
>
> **If you are using `react19`, use ver.2 of `next-mdx-remote-client`, currently `v2.1`**
>
> *The both serve the same features and APIs. I am going to maintain both.*
**`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:
+ for a **`app` router demo application** visit [source code](https://github.com/talatkuyuk/next-mdx-remote-client-in-app-router) or [living web site](https://nmrc-in-app-router.vercel.app/),
+ for a **`pages` router demo application** visit [source code](https://github.com/talatkuyuk/next-mdx-remote-client-in-pages-router) or [living web site](https://nmrc-in-pages-router.vercel.app/),
+ for a **testing application** which uses **both `app` and `pages` router**, visit [source code](https://github.com/talatkuyuk/testing-app-for-next-mdx-remote-client) or [living web site](https://testing-app-for-nmrc.vercel.app/).
## Why `next-mdx-remote-client` ?
I started to create **`next-mdx-remote-client`** in line with the mindset of **`@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.
**`next-mdx-remote-client`** serves as a **viable alternative** to **`next-mdx-remote`** having **more features**.
**I would like to highlight some main features:**
+ It supports MDX version 3.
+ It provides well designed components and functions for both "pages" router and "app" router, which completely isolated from eachother.
+ It provides internal error handling mechanism.
+ It supports `import statements` and `export statements` in MDX source, which can be disabled as well.
+ Creating table of contents (TOC) is so easy since it supports passing `vfile.data` into the `scope`.
+ You can get frontmatter without compiling the source for example for listing articles/posts via `getFrontmatter`.
+ It exports some components and types from `@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 | ✅ | ✅ |
| 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` | ❌ | ✅ |
| injects `React` instance into runtime options | ❌ | ✅ |
> [!IMPORTANT]
> You will see a lot the abbreviatons **`csr`** and **`rsc`**. _Pay attention to the both are spelled backwards._\
> \
> **`csr`** stands for "client side rendering" which is related with **`pages`** router\
> **`rsc`** stands for "react server component" which is related with **`app`** router
## General considerations about development
- It is ESM only package
- Needs `react` version 19.1+, works with `next@15` and `next@16` versions (tested)
- Needs `node` version 20.9.0+ in line with `Next.js` does
- Vitest is used instead of jest for testing
- Rollup is removed for bundling
- Test coverage is 100%
- Type coverage is 100%
- The parts client side (csr) and server side (rsc) are completely isolated from each other
- Exported a small utility to get frontmatter without compiling the source
- **All functions take named parameters**
- Supports `import statements` and `export statements` in MDX
- Export statements in MDX work for **both** `app` and `pages` router
- Import statements in MDX work for **only** `app` 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 the **`next/mdx`**, it is viable to import **`.mdx`** into MDX, but not tested yet.
## Installation
This package is ESM only, requires Node.js (version 18.18+).
```bash
# in general
npm install next-mdx-remote-client
# specifically for react18 users
npm install next-mdx-remote-client@^1
# specifically for react19 users
npm install next-mdx-remote-client@^2
```
or
```bash
yarn add next-mdx-remote-client
```
> [!WARNING]
> **`next-mdx-remote`** users may follow the [migration guide](/migration_guide.md).
## Initial Security Concerns
Before diving into **`next-mdx-remote-client`**, it’s important to highlight the security risks associated with rendering MDX content.
Because MDX supports JavaScript expressions, it introduces serious security considerations. If the content is not fully trusted and controlled, it can enable **cross-site scripting (XSS)** attacks and even lead to **remote code execution (RCE)**. In the worst-case scenario, attackers could steal sensitive data, inject malicious scripts, install malware, or compromise your server.
> **Never render user-supplied MDX without proper sanitization.**
MDX provides three powerful constructs: **JSX syntax**, **JavaScript expressions**, and **ESM blocks (`mdxjsEsm`)** such as `import` and `export`. **`next-mdx-remote-client`** provides options to disable **ESM blocks (`mdxjsEsm`)**. However, it does **not** take responsibility for securing MDX content itself, as **it operates purely at the rendering layer**. Content sanitization and JS expression control should be handled earlier in the processing pipeline (e.g., at the remark/recma/compilation stage).
If you do not have full control over the MDX source, you should at minimum strip **dangerous JavaScript expressions** before rendering. A recommended approach is to use the remark plugin [**`remark-mdx-remove-expressions`**](https://github.com/ipikuka/remark-mdx-remove-expressions), which removes executable/dangerous MDX expressions at the syntax level.
For further security concerns you can visit [Security section](#security).
## The package's exported subpaths
The main entry point **`/`** also refers to **`/csr`** subpath.
```typescript
// main entry point, which is related "pages" router
import /* */ from "next-mdx-remote-client";
// isolated subpath for the "serialize" function
import /* */ from "next-mdx-remote-client/serialize";
// sub entry point related with "pages" router
import /* */ from "next-mdx-remote-client/csr";
// sub entry point related with "app" router
import /* */ from "next-mdx-remote-client/rsc";
// isolated subpath for the utils
import /* */ from "next-mdx-remote-client/utils";
```
## The part associated with Next.js `app` router
_Go to [the part associated with Next.js pages router](#the-part-associated-with-nextjs-pages-router)_
**`next-mdx-remote-client`** exposes **`evaluate`** function and **`MDXRemote`** component for **"app" router**.
```typescript
import { 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 --> use **`MDXRemote`**\
> \
> If you need to get the **frontmatter** and the **mutated scope** --> use **`evaluate`**\
> If you don't need --> use **`MDXRemote`**
Let's give some examples how to use **`next-mdx-remote-client`** in "app" router first, then explain the exposed function and component.
### Examples for `app` router
See a **demo application** with **`app` router**, visit [source code](https://github.com/talatkuyuk/next-mdx-remote-client-in-app-router) or [living web site](https://nmrc-in-app-router.vercel.app/).
#### An example with `javascript`
```jsx
import { Suspense } from "react";
import { MDXRemote } from "next-mdx-remote-client/rsc";
import { ErrorComponent, LoadingComponent } from "../components";
import { Test } from '../mdxComponents';
const components = {
Test,
wrapper: ({ children }) =>
{children}
,
}
export default async function Page() {
const source = "Some **bold text** in MDX, with a component ";
return (
}>
);
};
```
#### An example with `typescript`, parsing frontmatter and providing custom data with scope
```tsx
import { Suspense } from "react";
import { MDXRemote } from "next-mdx-remote-client/rsc";
import type { MDXRemoteOptions, MDXComponents } from "next-mdx-remote-client/rsc";
import { calculateSomeHow, getSourceSomeHow } from "../utils";
import { ErrorComponent, LoadingComponent } from "../components";
import { Test } from '../mdxComponents';
const components: MDXComponents = {
Test,
wrapper: function ({ children }: React.ComponentPropsWithoutRef<"div">) {
return
{children}
;
},
}
export default async function Page() {
const source = await getSourceSomeHow();
if (!source) {
return ;
}
const options: MDXRemoteOptions = {
mdxOptions: {
// ...
},
parseFrontmatter: true,
scope: {
readingTime: calculateSomeHow(source),
},
};
return (
}>
);
}
```
#### An example with creating a table of contents (TOC)
I assume you have a MDX content having `` inside; and you've provided it in MDX components.
```markdown
---
title: My Article
---
# {frontmatter.title}
rest of the article...
```
You can have a look at an example [TableOfContentComponent](https://github.com/talatkuyuk/next-mdx-remote-client-in-app-router/blob/main/mdxComponents/Toc.tsx) in the demo application.
In order to create a table of contents (TOC) I use **`remark-flexible-toc`** in the remark plugins and pass the table of contents objects `vFile.data.toc` into the `scope` via the option `vfileDataIntoScope`.
That's it! So easy!
```tsx
import { Suspense } from "react";
import { MDXRemote, type MDXRemoteOptions } from "next-mdx-remote-client/rsc";
import remarkFlexibleToc from "remark-flexible-toc"; // <---------
import { calculateSomeHow, getSourceSomeHow } from "../utils";
import { ErrorComponent, LoadingComponent } from "../components";
import { components } from '../mdxComponents';
export default async function Page() {
const source = await getSourceSomeHow();
if (!source) {
return ;
}
const options: MDXRemoteOptions = {
mdxOptions: {
remarkPlugins: [
// ...
remarkFlexibleToc, // <---------
],
},
parseFrontmatter: true,
scope: {
readingTime: calculateSomeHow(source),
},
vfileDataIntoScope: "toc", // <---------
};
return (
}>
);
}
```
#### An example with using "frontmatter" and "scope" in JSX in "app" router
```tsx
import { Suspense } from "react";
import { evaluate, type EvaluateOptions } from "next-mdx-remote-client/rsc";
import remarkFlexibleToc, { type TocItem } from "remark-flexible-toc";
import { calculateSomeHow, getSourceSomeHow } from "../utils";
import { ErrorComponent, LoadingComponent, TableOfContentComponent } from "../components";
import { components } from "../mdxComponents";
type Scope = {
readingTime: string;
toc?: TocItem[];
};
type Frontmatter = {
title: string;
author: string;
};
export default async function Page() {
const source = await getSourceSomeHow();
if (!source) {
return ;
}
const options: EvaluateOptions = {
mdxOptions: {
remarkPlugins: [
// ...
remarkFlexibleToc,
],
},
parseFrontmatter: true,
scope: {
readingTime: calculateSomeHow(source),
},
vfileDataIntoScope: "toc",
};
const { content, frontmatter, scope, error } = await evaluate({
source,
options,
components,
});
if (error) {
return ;
}
return (
<>
{frontmatter.title}
Written by {frontmatter.author}; read in {scope.readingTime}
}>
{content}
>
);
}
```
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.
```tsx
// ...
export default async function Page({ source }: Props) {
// ...
return (
}>
{content}
);
}
```
_article.mdx_
```markdown
# {frontmatter.title}
Written by {frontmatter.author}; read in {readingTime}
rest of the article...
```
After the examples given, let's dive into the exposed function and component by **`next-mdx-remote-client`** for "app" router.
### The `evaluate` function
_Go to the [MDXRemote](#the-mdxremote-component) component_
The `evaluate` function is used for **compiling** MDX source, **constructing compiled source**, getting exported information from MDX and returning MDX content to be rendered on the server side, as a react server component.
```typescript
async function evaluate(props: EvaluateProps): Promise {}
```
The `evaluate` function takes `EvaluateProps` and returns `EvaluateResult` as a promise.
**Props of the `evaluate` function**
```typescript
type EvaluateProps = {
source: Compatible;
options?: EvaluateOptions;
components?: MDXComponents;
};
```
**Result of the `evaluate` function**
```typescript
type EvaluateResult = {
content: React.JSX.Element;
mod: Record;
frontmatter: TFrontmatter;
scope: TScope;
error?: Error;
};
```
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**.
```tsx
import { Suspense } from "react";
import { evaluate, type EvaluateOptions } from "next-mdx-remote-client/rsc";
import { ErrorComponent, LoadingComponent, TableOfContentComponent } from "../components";
import { components } from "../mdxComponents";
import type { Frontmatter, Scope } from "../types"
export default async function MDXComponent({ source }: {source?: string}) {
if (!source) {
return ;
}
const options: EvaluateOptions = {
/* */
};
const { content, mod, frontmatter, scope, error } = await evaluate({
source,
options,
components,
});
if (error) {
return ;
}
/**
* Use "mod", "frontmatter" and "scope" as you wish
*
* "mod" object is for exported information from MDX
* "frontmatter" is available even if a MDX syntax error occurs
* "scope" is for mutated scope if the `vfileDataIntoScope` option is used
*/
return (
<>
{frontmatter.title}
{mod.something}
}>
{content}
>
);
};
```
If you provide **the generic type parameters** like `await evaluate(){}`, the `frontmatter` and the `scope` get the types, otherwise `Record` by default for both.
> [!WARNING]
> Pay attention to the order of the generic type parameters.\
> \
> The type parameters `Frontmatter` and `Scope` should extend `Record`. You should use **`type`** instead of **`interface`** for type parameters otherwise, you will receive an error saying `Type 'Xxxx' does not satisfy the constraint 'Record'.` See this [issue](https://github.com/ipikuka/next-mdx-remote-client/issues/2) for more explanation.
In the above example, I assume you use **`remark-flexible-toc`** remark plugin in order to collect the headings from MDX content, and you pass that information into the `scope` via `vfileDataIntoScope` option.
### The evaluate options (`EvaluateOptions`)
All options are optional.
```typescript
type EvaluateOptions = {
mdxOptions?: EvaluateMdxOptions;
disableExports?: boolean;
disableImports?: boolean;
parseFrontmatter?: boolean;
scope?: TScope;
vfileDataIntoScope?: VfileDataIntoScope;
};
```
#### `mdxOptions`
It is an **`EvaluateMdxOptions`** option to be passed to **`@mdx-js/mdx`** compiler.
```typescript
import { type EvaluateOptions as OriginalEvaluateOptions } from "@mdx-js/mdx";
type EvaluateMdxOptions = Omit<
OriginalEvaluateOptions,
| "Fragment"
| "jsx"
| "jsxs"
| "jsxDEV"
| "useMDXComponents"
| "providerImportSource"
| "outputFormat"
>;
```
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.
```typescript
const options: EvaluateOptions = {
// ...
mdxOptions: {
format: "mdx",
baseUrl: import.meta.url,
development: true,
remarkPlugins: [/* */],
rehypePlugins: [/* */],
recmaPlugins: [/* */],
remarkRehypeOptions: {handlers: {/* */}},
// ...
};
};
```
For more information see [the MDX documentation](https://github.com/mdx-js/mdx/blob/master/packages/mdx/index.js).
#### `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.
```typescript
const options: EvaluateOptions = {
disableExports: true;
};
```
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.
```typescript
const options: EvaluateOptions = {
disableImports: true;
};
```
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.
```typescript
const options: EvaluateOptions = {
parseFrontmatter: true;
};
```
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.
```mdx
---
title: "My Article"
author: "ipikuka"
---
# {frontmatter.title}
It is written by {frontmatter.author}
```
The package uses the `vfile-matter` internally to parse the frontmatter.
#### `scope`
It is an **`Record`** 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:
```typescript
const options: EvaluateOptions = {
scope: {
readingTime: calculateSomeHow(source)
};
};
```
Now, the `scope` will be supplied into the MDX file so as you to use it within the statements.
```markdown
# My article
read 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.**
```markdown
My name is {name} valid expression.
My name is {my-name} is not valid expression, which will throw error
```
So, we can say for the `scope`, here:
```typescript
const options: EvaluateOptions = {
scope: {
name: "ipikuka", // valid usage
"my-name": "ipikuka", // is not valid and error prone for the MDX content !!!
};
};
```
> [!TIP]
> The scope variables can be consumed not only as a property of a component, but also within the texts.
```mdx
my name is {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 the `scope` 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.
```typescript
type VfileDataIntoScope =
| true // all fields from vfile.data
| string // one specific field
| { name: string; as: string } // one specific field but change the key as
| Array; // more than one field
```
```typescript
const options: EvaluateOptions = {
// Let's assume you use "remark-flexible-toc" plugin which composes
// the table of content (TOC) within the 'vfile.data.toc'
vfileDataIntoScope: "toc"; // or fileDataIntoScope: ["toc"];
};
```
Now, `vfile.data.toc` is copied into the scope as `scope["toc"]`, and will be supplied to the MDX via `scope`.
```mdx
# My article
```
If you need to change the name of the field, specify it for example `{ name: "toc", as: "headings" }`.
```typescript
const options: EvaluateOptions = {
vfileDataIntoScope: { name: "toc", as: "headings" };
};
```
```mdx
# My article
```
If you need to pass all the fields from `vfile.data`, specify it as `true`
```typescript
const options: EvaluateOptions = {
vfileDataIntoScope: true;
};
```
### The `MDXRemote` component
_Go to the [evaluate](#the-evaluate-function) function_
The `MDXRemote` component is used for rendering the MDX content on the server side. It is a react server component.
```typescript
async function MDXRemote(props: MDXRemoteProps): Promise {}
```
The `MDXRemote` component takes `MDXRemoteProps` and returns `React.JSX.Element` as a promise.
**Props of the `MDXRemote` component**
```typescript
type MDXRemoteProps = {
source: Compatible;
options?: MDXRemoteOptions;
components?: MDXComponents;
onError?: React.ComponentType<{ error: Error }>
};
```
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**.
```tsx
import { Suspense } from "react";
import { MDXRemote, type MDXRemoteOptions } from "next-mdx-remote-client/rsc";
import { ErrorComponent, LoadingComponent } from "../components";
import { components } from "../mdxComponents";
export default async function MDXComponent({ source }: {source?: string}) {
if (!source) {
return ;
}
const options: MDXRemoteOptions = {
/* */
};
return (
}>
);
};
```
### The MDXRemote options (`MDXRemoteOptions`)
All options are optional.
```typescript
type MDXRemoteOptions = {
mdxOptions?: EvaluateMdxOptions;
disableExports?: boolean;
disableImports?: boolean;
parseFrontmatter?: boolean;
scope?: TScope;
vfileDataIntoScope?: VfileDataIntoScope;
};
```
The details are the same with the [EvaluateOptions](#the-evaluate-options-evaluateoptions).
## The part associated with Next.js `pages` router
_Go to [the part associated with Next.js app router](#the-part-associated-with-nextjs-app-router)_
**`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.
### Examples for `pages` router
See a **demo application** with **`pages` router**, visit [source code](https://github.com/talatkuyuk/next-mdx-remote-client-in-pages-router) or [living web site](https://nmrc-in-pages-router.vercel.app/).
#### An example with `javascript`
```jsx
import { serialize } from 'next-mdx-remote-client/serialize';
import { MDXClient } from 'next-mdx-remote-client';
import ErrorComponent from '../components/ErrorComponent';
import Test from '../mdxComponents/Test';
const components = {
Test,
wrapper: ({children}) =>
{children}
,
}
export default function Page({ mdxSource }) {
if ("error" in mdxSource) {
return ;
}
return ;
}
export async function getStaticProps() {
const source = "Some **bold text** in MDX, with a component ";
const mdxSource = await serialize({source});
return { props: { mdxSource } };
}
```
#### An example with `typescript`, parsing frontmatter and providing custom data with scope
```tsx
import { MDXClient, type MDXComponents } from 'next-mdx-remote-client';
import { serialize } from "next-mdx-remote-client/serialize";
import type { SerializeOptions, SerializeResult } from "next-mdx-remote-client/serialize";
import { calculateSomeHow, getSourceSomeHow } from "../utils";
import ErrorComponent from '../components/ErrorComponent';
import Test from '../mdxComponents/Test';
type Scope = {
readingTime: string;
};
type Frontmatter = {
title: string;
author: string;
};
const components: MDXComponents = {
Test,
wrapper: function ({ children }: React.ComponentPropsWithoutRef<"div">) {
return
{children}
;
},
}
type Props = {
mdxSource?: SerializeResult;
}
export default function Page({ mdxSource }: Props) {
if (!mdxSource) {
return ;
}
if ("error" in mdxSource) {
return ;
}
return (
<>
{mdxSource.frontmatter.title}
Written by {mdxSource.frontmatter.author}; read in {mdxSource.scope.readingTime}
>
);
}
export async function getStaticProps() {
const source = await getSourceSomeHow();
if (!source) return { props: {} };
const options: SerializeOptions = {
disableImports: true,
mdxOptions: {
// ...
},
parseFrontmatter: true,
scope: {
readingTime: calculateSomeHow(source),
},
};
const mdxSource = await serialize({source, options});
return { props: { mdxSource } };
}
```
#### An example with creating a table of contents (TOC)
I assume you have a MDX content having `` inside; and you've provided it in MDX components.
```markdown
---
title: My Article
---
# {frontmatter.title}
rest of the article...
```
You can have a look at an example [TableOfContentComponent](https://github.com/talatkuyuk/next-mdx-remote-client-in-pages-router/blob/main/mdxComponents/Toc.tsx) in the demo application.
In order to create a table of contents (TOC) I use **`remark-flexible-toc`** in the remark plugins and pass the table of contents objects `vFile.data.toc` into the `scope` via the option `vfileDataIntoScope`.
That's it! So easy!
```tsx
import { MDXClient, type MDXComponents } from 'next-mdx-remote-client';
import { serialize } from "next-mdx-remote-client/serialize";
import type { SerializeOptions, SerializeResult } from "next-mdx-remote-client/serialize";
import remarkFlexibleToc, {type TocItem} from "remark-flexible-toc"; // <---------
import { calculateSomeHow, getSourceSomeHow } from "../utils";
import { ErrorComponent, TableOfContentComponent } from '../components';
import { Test } from '../mdxComponents';
type Scope = {
readingTime: string;
};
type Frontmatter = {
title: string;
author: string;
};
const components: MDXComponents = {
Test,
wrapper: function ({ children }: React.ComponentPropsWithoutRef<"div">) {
return
{children}
;
},
}
type Props = {
mdxSource?: SerializeResult;
}
export default function Page({ mdxSource }: Props) {
if (!mdxSource) {
return ;
}
if ("error" in mdxSource) {
return ;
}
return (
<>
{mdxSource.frontmatter.title}
Written by {mdxSource.frontmatter.author}; read in {mdxSource.scope.readingTime}
>
);
}
export async function getStaticProps() {
const source = await getSourceSomeHow();
if (!source) return { props: {} };
const options: SerializeOptions = {
disableImports: true,
mdxOptions: {
remarkPlugins: [
// ...
remarkFlexibleToc, // <---------
],
},
parseFrontmatter: true,
scope: {
readingTime: calculateSomeHow(source),
},
vfileDataIntoScope: "toc", // <---------
};
const mdxSource = await serialize({source, options});
return { props: { mdxSource } };
}
```
Actually, you may not need to access the "frontmatter" and "scope" in JSX, you can use them within MDX directly, and return just `` only.
```tsx
// ...
const components: MDXComponents = {
TableOfContentComponent, // <---------
wrapper: function ({ children }: React.ComponentPropsWithoutRef<"div">) {
return
{children}
;
},
}
// ...
export default function Page({ mdxSource }: Props) {
// ...
return (
);
}
```
_article.mdx_
```markdown
# {frontmatter.title}
Written by {frontmatter.author}; read in {readingTime}
rest of the article...
```
After the examples given, let's dive into the exposed functions and component by **`next-mdx-remote-client`** for "pages" router.
### The `serialize` function
_Go to the [hydrate](#the-hydrate-function) function_
_or the [MDXClient](#the-mdxclient-component) component_
```typescript
import { serialize } from "next-mdx-remote-client/serialize";
```
The `serialize` function is used for **compiling** MDX source, in other words **constructing 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 the `getStaticProps` or the `getServerSideProps` on the server side. (Off the record, it can be used within an `useEffect` as well, but this is not recommended because it is a heavy function as having more dependencies).
```typescript
async function serialize(props: SerializeProps): Promise {}
```
The `serialize` function takes `SerializeProps` and returns `SerializeResult` as a promise.
**Props of the `serialize` function**
```typescript
type SerializeProps = {
source: Compatible;
options?: SerializeOptions;
};
```
**Result of the `serialize` function**
Either the `compiledSource` or the `error` exists, in addition to `frontmatter` and `scope`.
```typescript
type SerializeResult =
({ compiledSource: string } | { error: Error })
& {
frontmatter: TFrontmatter;
scope: TScope;
};
```
The `serialize` function has **internal error handling mechanism** for the MDX syntax errors. The catched error is serialized via **`serialize-error`** 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.**
```tsx
import { serialize, type SerializeOptions } from "next-mdx-remote-client/serialize";
import type { Frontmatter, Scope } from "./types"
export async function getStaticProps() {
const source = await getSourceSomeHow();
if (!source) {
return { props: {} };
}
const options: SerializeOptions = {
/* */
};
const mdxSource = await serialize({
source,
options,
});
return {
props: {
mdxSource,
},
};
}
```
If you provide **the generic type parameters** like `await serialize(){}`, the `frontmatter` and the `scope` get the types, otherwise `Record` by default for both.
> [!WARNING]
> Pay attention to the order of the generic type parameters.\
> \
> The type parameters `Frontmatter` and `Scope` should extend `Record`. You should use **`type`** instead of **`interface`** for type parameters otherwise, you will receive an error saying `Type 'Xxxx' does not satisfy the constraint 'Record'.` See this [issue](https://github.com/ipikuka/next-mdx-remote-client/issues/2) 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) {}`.**
```tsx
type Props = {
mdxSource?: SerializeResult;
}
export default function Page({ mdxSource }: Props) {
// ...
if ("error" in mdxSource) {
return ;
}
// ...
};
```
### The serialize options (`SerializeOptions`)
All options are optional.
```typescript
type SerializeOptions = {
mdxOptions?: SerializeMdxOptions;
disableExports?: boolean;
disableImports?: boolean;
parseFrontmatter?: boolean;
scope?: TScope;
vfileDataIntoScope?: VfileDataIntoScope;
};
```
Except the `mdxOptions`, the details are the same with the [EvaluateOptions](#the-evaluate-options-evaluateoptions).
#### `mdxOptions`
It is a **`SerializeMdxOptions`** option to be passed to **`@mdx-js/mdx`** compiler.
```typescript
import { type CompileOptions as OriginalCompileOptions } from "@mdx-js/mdx";
type SerializeMdxOptions = Omit<
OriginalCompileOptions,
"outputFormat" | "providerImportSource"
>;
```
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.
```typescript
const options: SerializeOptions = {
// ...
mdxOptions: {
format: "mdx",
baseUrl: import.meta.url,
development: true,
remarkPlugins: [/* */],
rehypePlugins: [/* */],
recmaPlugins: [/* */],
remarkRehypeOptions: {handlers: {/* */}},
// ...
};
};
```
> [!WARNING]
> Here I need to mention about the `scope` option again for the `serialize`.\
> \
> **scope**\
> \
> Actually, the `serialize` doesn't do so much with the `scope` except you provide the option `vfileDataIntoScope` for passing data from `vfile.data` into the `scope`. Since the `scope` is passed from the server to the client by `nextjs`, the `scope` must be serializable. The `scope` 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 the `scope` into the `serialize`, you can pass it into `hydrate` or `MDXClient` directly on the client side.
### The `hydrate` function
_Go to the [serialize](#the-serialize-function) function_
_or the [MDXClient](#the-mdxclient-component) component_
```typescript
import { hydrate } from "next-mdx-remote-client/csr";
```
The `hydrate` function is used for **constructing compiled source**, getting exported information from MDX and returning MDX content to be rendered on the client side.
```typescript
function hydrate(props: HydrateProps): HydrateResult {}
```
The `hydrate` function takes `HydrateProps` and returns `HydrateResult`. The `hydrate` has no "options" parameter.
**Props of the `hydrate` function**
```typescript
type HydrateProps = {
compiledSource: string;
frontmatter?: Record;
scope?: Record;
components?: MDXComponents;
disableParentContext?: boolean;
};
```
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**
```typescript
type HydrateResult = {
content: React.JSX.Element;
mod: Record;
error?: Error;
};
```
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 --> use **`MDXClient`**
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**.
```tsx
import { hydrate, type SerializeResult } from "next-mdx-remote-client/csr";
import { ErrorComponent, TableOfContentComponent } from "../components";
import { components } from "../mdxComponents";
import type { Frontmatter, Scope } from "../types"
type Props = {
mdxSource?: SerializeResult;
}
export default function Page({ mdxSource }: Props) {
if (!mdxSource) {
return ;
}
if ("error" in mdxSource) {
return ;
}
// Now, mdxSource has {compiledSource, frontmatter, scope}
const { content, mod, error } = hydrate({ ...mdxSource, components });
if (error) {
return ;
}
// You can use the "mod" object for exported information from the MDX as you wish
return (
<>
{mdxSource.frontmatter.title}
{mod.something}
{content}
>
);
};
```
In the above example, I assume you use **`remark-flexible-toc`** remark plugin in order to collect the headings from MDX content, and you pass that information into the `scope` via `vfileDataIntoScope` option within the serialize on the server side.
### The `MDXClient` component
_Go to the [serialize](#the-serialize-function) function_
_or the [hydrate](#the-hydrate-function) function_
```typescript
import { MDXClient } from "next-mdx-remote-client/csr";
```
The `MDXClient` component is used for rendering the MDX content on the client side.
```typescript
function MDXClient(props: MDXClientProps): React.JSX.Element {}
```
The `MDXClient` component takes `MDXClientProps` and returns `React.JSX.Element`. The `MDXClient` has no "options" parameter like `hydrate`.
**Props of the `MDXClient` component**
```typescript
type MDXClientProps = {
compiledSource: string;
frontmatter?: Record;
scope?: Record;
components?: MDXComponents;
disableParentContext?: boolean;
onError?: React.ComponentType<{ error: Error }>
};
```
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 --> use **`MDXClient`**
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**.
```tsx
import { MDXClient, type SerializeResult } from "next-mdx-remote-client/csr";
import { ErrorComponent, TableOfContentComponent } from "../components";
import { components } from "../mdxComponents";
import type { Frontmatter, Scope } from "../types"
type Props = {
mdxSource?: SerializeResult;
}
export default function Page({ mdxSource }: Props) {
if (!mdxSource) {
return ;
}
if ("error" in mdxSource) {
return ;
}
// Now, mdxSource has {compiledSource, frontmatter, scope}
return (
<>
{mdxSource.frontmatter.title}
>
);
};
```
In the above example, I assume you use **`remark-flexible-toc`** remark plugin in order to collect the headings from MDX content, and you pass that information into the `scope` via `vfileDataIntoScope` option within the serialize on the server side.
### The `hydrateLazy` function and the `MDXClientLazy` component
**`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.
```typescript
import { 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 see [`next-mdx-remote`](https://github.com/hashicorp/next-mdx-remote) notes._
### The `hydrateAsync` function and the `MDXClientAsync` component
**`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`.
```typescript
import { hydrateAsync, MDXClientAsync } from "next-mdx-remote-client/csr";
```
> [!NOTE]
> I believe, it is viable somehow using `dynamic` API if the `vercel` supports for a solution for the `pages` router via `import.meta` APIs. During the compilation of the MDX in the `serialize`, a remark/recma plugin can register the imported modules into the `import.meta.url` via a nextjs API (needs support of vercel) for them will be available to download/import on the client side via `dynamic` api. This is my imagination.
### The `MDXProvider` component
The package exports the `MDXProvider` from **`@mdx-js/react`**, in order the developers don't need to install **`@mdx-js/react`**.
```typescript
import { MDXProvider } from "next-mdx-remote-client/csr";
```
The `` makes the mdx components available to any `` 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 `` or `hydrate's { content }`.**
```tsx
import { MDXProvider } from 'next-mdx-remote-client';
import { components } from "../mdxComponents";
export default function App({ Component, pageProps }) {
return (
)
}
```
> [!NOTE]
> How this happens, because **`next-mdx-remote-client`** injects the `useMdxComponents` context hook from **`@mdx-js/react`** during the function construction of the compiled source, internally. Pay attention that it is valid for only `MDXClient` and `hydrate` functions.
> [!CAUTION]
> Since `MDXRemote` as a react server component can not read the context, **`MDXProvider` is effectless when used within the nextjs `app` router** for `MDXRemote`, which is also for `evaluate`.
## MDX Components
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](https://mdxjs.com/table-of-components)) with the custom components.
Typescript users can use `MDXComponents` from `mdx/types`, which is exported by this package as well.
`../mdxComponents/index.ts`
```tsx
import { type MDXComponents } from "next-mdx-remote-client";
import dynamic from "next/dynamic";
import Image from "next/image";
import Link from "next/link";
import { Typography } from "@material-ui/core";
import { motion } from 'framer-motion'
import Hello from "./Hello";
import CountButton from "./CountButton";
import BlockQuote, { default as blockquote } from "./BlockQuote";
import pre from "./pre";
export const mdxComponents: MDXComponents = {
Hello,
CountButton,
Dynamic: dynamic(() => import("./dynamic")),
Image,
Link,
motion: { div: () =>
;
}
};
```
> [!NOTE]
> The **`wrapper`** is a special key, if you want to wrap the MDX content with a HTML container element.
`./data/my-article.mdx`
```markdown
---
title: "My Article"
author: "ipikuka"
---
_Read in {readingTime}, written by **{frontmatter.author}**_
# {frontmatter.title}
## Sub heading for custom components
I am blackquote content
## Sub heading for some markdown elements

Here is _italic text_ and **strong text**
> I am blackquote content
```
## Utility `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.
```typescript
import { getFrontmatter } from "next-mdx-remote-client/utils";
const { frontmatter, strippedSource } = getFrontmatter(source);
```
If you provide **the generic type parameter**, it ensures the `frontmatter` gets the type, otherwise `Record` by default.
**If there is no frontmatter** in the source, the `frontmatter` will be **empty object `{}`**.
> [!IMPORTANT]
> \
> If you use **`next-mdx-remote`** and want to get `frontmatter` without compiling the source !
> \
> The subpath **`next-mdx-remote-client/utils`** is isolated from other features of the package and it does cost minimum. **So, anyone can use `next-mdx-remote-client/utils` while using `next-mdx-remote`.**
## Types
**`next-mdx-remote-client`** is fully typed with [TypeScript][url-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`
## Compatibility
**`next-mdx-remote-client`** works with unified version 6+ ecosystem since it is compatible with MDX version 3.
## Security
The security model of **`next-mdx-remote-client`** is fundamentally the same as other MDX integrations built on top of **`@mdx-js/mdx`**, including **`next-mdx-remote`**, **`@next/mdx`**, and similar solutions.
MDX supports **JSX syntax**; **JavaScript expressions** (`{1 + 1}`, `{someVar}`, `{fn()}`); and **ESM blocks (`mdxjsEsm`)** such as `import` and `export`.
Because MDX compiles to executable JavaScript, rendering **untrusted MDX** can inherently introduce **Cross-Site Scripting (XSS)** and **Remote Code Execution (RCE)**.
This is not unique to **`next-mdx-remote-client`**; it is intrinsic to how MDX works and has long been documented across the MDX ecosystem.
### Trust Model
If MDX content is **not fully trusted**, then **RCE** is inherently possible. If you completely block all JavaScript expressions, you effectively reduce MDX to *Markdown with JSX*. That may be acceptable for certain use cases, but it changes the expressive nature of MDX.
A more balanced approach is to **block only dangerous expressions** rather than disabling all expressions. The remark plugin [**`remark-mdx-remove-expressions`**](https://github.com/ipikuka/remark-mdx-remove-expressions) can remove executable or dangerous MDX expressions at the syntax level:
```ts
import remarkMdxRemoveExpressions from "remark-mdx-remove-expressions";
{
mdxOptions: {
remarkPlugins: [
[remarkMdxRemoveExpressions, { onlyDangerousExpressions: true }]
]
}
}
```
### ESM (`import` / `export`) in MDX
Allowing **`import` declarations** and **`export` declarations** inside MDX can introduce serious risks if the content is not strictly controlled. **`next-mdx-remote-client`** provides options to disable ESM blocks (`mdxjsEsm`) for this reason.
But *dynamic import expressions*, such as `await import("some-module")` are JavaScript expressions and require a **custom recma plugin** to block or transform them. [**`remark-mdx-remove-expressions`**](https://github.com/ipikuka/remark-mdx-remove-expressions) can also remove import expressions, **`next-mdx-remote-client`** does not modify or restrict these automatically.
### Runtime Evaluation (`eval` / `new Function`)
Like most MDX runtimes, **`next-mdx-remote-client`** evaluates compiled JavaScript using runtime evaluation `Reflect.construct` similar with `eval` or `new Function()`.
Evaluating JavaScript from a string can enable **XSS** attacks or **RCE** if the input is not trusted. You are responsible for ensuring either the MDX source is trusted, or the source is properly sanitized and restricted before evaluation. Please, take your own measures while passing the user input.
### Content Security Policy (CSP)
If your site uses a strict **Content Security Policy (CSP)** that disallows dynamic code evaluation via `eval` or `new Function()`, you will need to loosen the `script-src` policy to include `unsafe-eval`. Otherwise, MDX runtime evaluation will fail.
Carefully assess the security implications before enabling [`unsafe-eval`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#common_sources), especially in high-security environments.
### Final Security Recommendations
- Treat MDX as executable code.
- Never evaluate/render untrusted MDX without applying proper restrictions or sanitization.
- Prefer removing dangerous expressions at the **remark/recma stage**, not at the rendering layer.
- Disable ESM blocks if the content is not fully controlled.
- Understand your CSP implications before deploying to production.
## Some Plugins
I like to contribute the Unified / Remark / MDX ecosystem, so I recommend you to have a look my plugins.
### My Remark Plugins
- [`remark-flexible-code-titles`](https://www.npmjs.com/package/remark-flexible-code-titles)
– Remark plugin to add titles or/and containers for the code blocks with customizable properties
- [`remark-flexible-containers`](https://www.npmjs.com/package/remark-flexible-containers)
– Remark plugin to add custom containers with customizable properties in markdown
- [`remark-ins`](https://www.npmjs.com/package/remark-ins)
– Remark plugin to add `ins` element in markdown
- [`remark-flexible-paragraphs`](https://www.npmjs.com/package/remark-flexible-paragraphs)
– Remark plugin to add custom paragraphs with customizable properties in markdown
- [`remark-flexible-markers`](https://www.npmjs.com/package/remark-flexible-markers)
– Remark plugin to add custom `mark` element with customizable properties in markdown
- [`remark-flexible-toc`](https://www.npmjs.com/package/remark-flexible-toc)
– Remark plugin to expose the table of contents via Vfile.data or via an option reference
- [`remark-mdx-remove-esm`](https://www.npmjs.com/package/remark-mdx-remove-esm)
– Remark plugin to remove import and/or export statements (mdxjsEsm)
- [`remark-mdx-remove-expressions`](https://www.npmjs.com/package/remark-mdx-remove-expressions)
– Remark plugin to remove MDX expressions within curlybraces {} in MDX content
### My Rehype Plugins
- [`rehype-pre-language`](https://www.npmjs.com/package/rehype-pre-language)
– Rehype plugin to add language information as a property to `pre` element
- [`rehype-highlight-code-lines`](https://www.npmjs.com/package/rehype-highlight-code-lines)
– Rehype plugin to add line numbers to code blocks and allow highlighting of desired code lines
- [`rehype-code-meta`](https://www.npmjs.com/package/rehype-code-meta)
– Rehype plugin to copy `code.data.meta` to `code.properties.metastring`
- [`rehype-image-toolkit`](https://www.npmjs.com/package/rehype-image-toolkit)
– Rehype plugin to enhance Markdown image syntax `![]()` and Markdown/MDX media elements (``, `