Skip to content

Commit 7188278

Browse files
author
Dimitri POSTOLOV
authoredSep 7, 2023
[v3] insert frontMatter as export node in remark plugin (#2265)

27 files changed

+643
-84
lines changed
 

‎.changeset/brave-ties-train.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'nextra': major
3+
---
4+
5+
- insert `frontMatter` as export node via custom remark plugin
6+
7+
- remove `frontMatter.mdxOptions` support

‎docs/pages/docs/guide/image.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ just use the `![]()` Markdown syntax:
3030
This loads the `demo.png` file inside the `public` folder, and automatically
3131
wraps it with Next.js `<Image>`.
3232

33-
<Callout>
33+
<Callout type="info">
3434
You can also use `![](../public/demo.png)` to load the image from a relative
3535
path, if you don't want to host it via `public`.
3636
</Callout>

‎docs/pages/docs/guide/ssg.mdx

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useData } from 'nextra/data'
2+
13
# Next.js SSG
24

35
With Next.js, you can pre-render your page using
@@ -7,10 +9,8 @@ can also be cached by a CDN to maximize the performance.
79

810
This is supported by Nextra too. Here's an example:
911

10-
import { useData } from 'nextra/data'
11-
12-
export const getStaticProps = ({ params }) => {
13-
return fetch(`https://api.github.com/repos/shuding/nextra`)
12+
export function getStaticProps() {
13+
return fetch('https://api.github.com/repos/shuding/nextra')
1414
.then(res => res.json())
1515
.then(repo => ({
1616
props: {
@@ -25,7 +25,7 @@ export const getStaticProps = ({ params }) => {
2525
}))
2626
}
2727

28-
export const Stars = () => {
28+
export function Stars() {
2929
// Get the data from SSG, and render it as a component.
3030
const { stars } = useData()
3131
return <strong>{stars}</strong>
@@ -46,8 +46,8 @@ Here's the MDX code for the example above:
4646
```mdx
4747
import { useData } from 'nextra/data'
4848

49-
export const getStaticProps = ({ params }) => {
50-
return fetch(`https://api.github.com/repos/shuding/nextra`)
49+
export function getStaticProps() {
50+
return fetch('https://api.github.com/repos/shuding/nextra')
5151
.then(res => res.json())
5252
.then(repo => ({
5353
props: {
@@ -62,7 +62,7 @@ export const getStaticProps = ({ params }) => {
6262
}))
6363
}
6464

65-
export const Stars = () => {
65+
export function Stars() {
6666
// Get the data from SSG, and render it as a component.
6767
const { stars } = useData()
6868
return <strong>{stars}</strong>

‎examples/swr-site/pages/en/docs/arguments.md renamed to ‎examples/swr-site/pages/en/docs/arguments.mdx

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Callout } from 'nextra/components'
2+
13
# Arguments
24

35
By default, `key` will be passed to `fetcher` as the argument. So the following
@@ -34,13 +36,13 @@ key will also be associated with `token` now.
3436

3537
## Passing Objects
3638

37-
import { Callout } from 'nextra/components'
38-
3939
<Callout>
40-
Since SWR 1.1.0, object-like keys will be serialized under the hood automatically.
40+
Since SWR 1.1.0, object-like keys will be serialized under the hood
41+
automatically.
4142
</Callout>
42-
43-
Say you have another function that fetches data with a user scope: `fetchWithUser(api, user)`. You can do the following:
43+
44+
Say you have another function that fetches data with a user scope:
45+
`fetchWithUser(api, user)`. You can do the following:
4446

4547
```js
4648
const { data: user } = useSWR(['/api/user', token], fetchWithToken)
@@ -60,5 +62,5 @@ const { data: orders } = useSWR({ url: '/api/orders', args: user }, fetcher)
6062
```
6163

6264
<Callout emoji="⚠️">
63-
In older versions (< 1.1.0), SWR **shallowly** compares the arguments on every render, and triggers revalidation if any of them has changed.
65+
In older versions (< 1.1.0), SWR **shallowly** compares the arguments on every render, and triggers revalidation if any of them has changed.
6466
</Callout>

‎examples/swr-site/pages/en/docs/middleware.md renamed to ‎examples/swr-site/pages/en/docs/middleware.mdx

+6-5
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { Callout } from 'nextra/components'
22

33
# Middleware
44

5-
<Callout>
6-
Upgrade to the latest version (≥ 1.0.0) to use this feature.
7-
</Callout>
5+
<Callout>Upgrade to the latest version (≥ 1.0.0) to use this feature.</Callout>
86

97
The middleware feature is a new addition in SWR 1.0 that enables you to execute
108
logic before and after SWR hooks.
@@ -182,7 +180,8 @@ const { data, isLagging, resetLaggy } = useSWR(key, fetcher, { use: [laggy] })
182180
### Serialize Object Keys
183181

184182
<Callout>
185-
Since SWR 1.1.0, object-like keys will be serialized under the hood automatically.
183+
Since SWR 1.1.0, object-like keys will be serialized under the hood
184+
automatically.
186185
</Callout>
187186

188187
<Callout emoji="⚠️">
@@ -213,5 +212,7 @@ serialized to the same string, and the fetcher will still receive those object
213212
arguments.
214213

215214
<Callout>
216-
Furthermore, you can use libs like [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) instead of `JSON.stringify` — faster and stabler.
215+
Furthermore, you can use libs like
216+
[fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify)
217+
instead of `JSON.stringify` — faster and stabler.
217218
</Callout>

‎examples/swr-site/pages/en/docs/with-nextjs.md renamed to ‎examples/swr-site/pages/en/docs/with-nextjs.mdx

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,6 @@ fully powered by SWR on the client side. The data can be dynamic and
6767
self-updated over time.
6868

6969
<Callout>
70-
The `Article` component will render the pre-generated data first, and after the page is hydrated, it will fetch the latest data again to keep it refresh.
70+
The `Article` component will render the pre-generated data first, and after
71+
the page is hydrated, it will fetch the latest data again to keep it refresh.
7172
</Callout>

‎examples/swr-site/pages/en/remote/graphql-eslint/[[...slug]].mdx

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ export async function getStaticProps({ params }) {
1717
...(await buildDynamicMDX(data, {
1818
defaultShowCopyCode: true
1919
}))
20-
},
21-
revalidate: 10
20+
}
2221
}
2322
}
2423

‎examples/swr-site/pages/en/remote/graphql-yoga/[[...slug]].mdx

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ export async function getStaticProps({ params }) {
1717
...(await buildDynamicMDX(data, {
1818
defaultShowCopyCode: true
1919
}))
20-
},
21-
revalidate: 10
20+
}
2221
}
2322
}
2423

‎examples/swr-site/pages/en/test.md

-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
---
2-
mdxOptions: { format: 'md' }
3-
---
4-
51
# Hello!
62

73
This is an MD file instead of MDX, which means you can use syntax like this:

‎examples/swr-site/pages/es/docs/arguments.md renamed to ‎examples/swr-site/pages/es/docs/arguments.mdx

+6-4
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ la key del caché también estará asociada al `token`.
3838
import { Callout } from 'nextra/components'
3939

4040
<Callout>
41-
Since SWR 1.1.0, object-like keys will be serialized under the hood automatically.
41+
Since SWR 1.1.0, object-like keys will be serialized under the hood
42+
automatically.
4243
</Callout>
43-
44-
Say you have another function that fetches data with a user scope: `fetchWithUser(api, user)`. You can do the following:
44+
45+
Say you have another function that fetches data with a user scope:
46+
`fetchWithUser(api, user)`. You can do the following:
4547

4648
```js
4749
const { data: user } = useSWR(['/api/user', token], fetchWithToken)
@@ -61,5 +63,5 @@ const { data: orders } = useSWR({ url: '/api/orders', args: user }, fetcher)
6163
```
6264

6365
<Callout emoji="⚠️">
64-
In older versions (< 1.1.0), SWR **shallowly** compares the arguments on every render, and triggers revalidation if any of them has changed.
66+
In older versions (< 1.1.0), SWR **shallowly** compares the arguments on every render, and triggers revalidation if any of them has changed.
6567
</Callout>

‎examples/swr-site/pages/es/docs/with-nextjs.md renamed to ‎examples/swr-site/pages/es/docs/with-nextjs.mdx

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,6 @@ fully powered by SWR on the client side. The data can be dynamic and
7171
self-updated over time.
7272

7373
<Callout>
74-
The `Article` component will render the pre-generated data first, and after the page is hydrated, it will fetch the latest data again to keep it refresh.
74+
The `Article` component will render the pre-generated data first, and after
75+
the page is hydrated, it will fetch the latest data again to keep it refresh.
7576
</Callout>

‎examples/swr-site/pages/ru/docs/arguments.md renamed to ‎examples/swr-site/pages/ru/docs/arguments.mdx

+6-4
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ const { data: user } = useSWR(['/api/user', token], fetchWithToken)
3737
import { Callout } from 'nextra/components'
3838

3939
<Callout>
40-
Since SWR 1.1.0, object-like keys will be serialized under the hood automatically.
40+
Since SWR 1.1.0, object-like keys will be serialized under the hood
41+
automatically.
4142
</Callout>
42-
43-
Say you have another function that fetches data with a user scope: `fetchWithUser(api, user)`. You can do the following:
43+
44+
Say you have another function that fetches data with a user scope:
45+
`fetchWithUser(api, user)`. You can do the following:
4446

4547
```js
4648
const { data: user } = useSWR(['/api/user', token], fetchWithToken)
@@ -60,5 +62,5 @@ const { data: orders } = useSWR({ url: '/api/orders', args: user }, fetcher)
6062
```
6163

6264
<Callout emoji="⚠️">
63-
In older versions (< 1.1.0), SWR **shallowly** compares the arguments on every render, and triggers revalidation if any of them has changed.
65+
In older versions (< 1.1.0), SWR **shallowly** compares the arguments on every render, and triggers revalidation if any of them has changed.
6466
</Callout>

‎examples/swr-site/pages/ru/docs/middleware.md renamed to ‎examples/swr-site/pages/ru/docs/middleware.mdx

+5-2
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ const { data, isLagging, resetLaggy } = useSWR(key, fetcher, { use: [laggy] })
183183
### Сериализация ключей объекта
184184

185185
<Callout>
186-
Since SWR 1.1.0, object-like keys will be serialized under the hood automatically.
186+
Since SWR 1.1.0, object-like keys will be serialized under the hood
187+
automatically.
187188
</Callout>
188189

189190
<Callout emoji="⚠️">
@@ -214,5 +215,7 @@ useSWR(['/api/user', { id: '73' }], fetcher, { use: [serialize] })
214215
аргументы объекта.
215216

216217
<Callout>
217-
Кроме того, вы можете использовать такие библиотеки, как [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) вместо `JSON.stringify` — быстрее и стабильнее.
218+
Кроме того, вы можете использовать такие библиотеки, как
219+
[fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify)
220+
вместо `JSON.stringify` — быстрее и стабильнее.
218221
</Callout>

‎examples/swr-site/pages/ru/docs/with-nextjs.md renamed to ‎examples/swr-site/pages/ru/docs/with-nextjs.mdx

+3-1
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,7 @@ export default function Page({ fallback }) {
7070
Данные могут быть динамическими и автоматически обновляться с течением времени.
7171

7272
<Callout>
73-
Компонент `Article` сначала отрендерит предварительно сгенерированные данные, а после гидратации страницы он снова получит последние данные, чтобы они были актуальными.
73+
Компонент `Article` сначала отрендерит предварительно сгенерированные данные,
74+
а после гидратации страницы он снова получит последние данные, чтобы они были
75+
актуальными.
7476
</Callout>

‎packages/nextra/__test__/__snapshots__/compile.test.ts.snap

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ exports[`Process heading > code-h1 1`] = `
55
"frontMatter": {},
66
"result": "/*@jsxRuntime automatic @jsxImportSource react*/
77
import {useMDXComponents as _provideComponents} from \\"nextra/mdx\\";
8+
export const frontMatter = {};
89
export const __toc = [];
910
function _createMdxContent(props) {
1011
const _components = Object.assign({
@@ -28,6 +29,7 @@ exports[`Process heading > code-with-text-h1 1`] = `
2829
"frontMatter": {},
2930
"result": "/*@jsxRuntime automatic @jsxImportSource react*/
3031
import {useMDXComponents as _provideComponents} from \\"nextra/mdx\\";
32+
export const frontMatter = {};
3133
export const __toc = [];
3234
function _createMdxContent(props) {
3335
const _components = Object.assign({
@@ -52,6 +54,7 @@ exports[`Process heading > dynamic-h1 1`] = `
5254
"hasJsxInH1": true,
5355
"result": "/*@jsxRuntime automatic @jsxImportSource react*/
5456
import {useMDXComponents as _provideComponents} from \\"nextra/mdx\\";
57+
export const frontMatter = {};
5558
import {useRouter} from 'next/router';
5659
export const TagName = () => {
5760
const {tag} = useRouter().query;
@@ -79,6 +82,7 @@ exports[`Process heading > no-h1 1`] = `
7982
"frontMatter": {},
8083
"result": "/*@jsxRuntime automatic @jsxImportSource react*/
8184
import {useMDXComponents as _provideComponents} from \\"nextra/mdx\\";
85+
export const frontMatter = {};
8286
export const __toc = [{
8387
depth: 2,
8488
value: \\"H2\\",
@@ -104,6 +108,7 @@ exports[`Process heading > static-h1 1`] = `
104108
"frontMatter": {},
105109
"result": "/*@jsxRuntime automatic @jsxImportSource react*/
106110
import {useMDXComponents as _provideComponents} from \\"nextra/mdx\\";
111+
export const frontMatter = {};
107112
export const __toc = [];
108113
function _createMdxContent(props) {
109114
const _components = Object.assign({

‎packages/nextra/__test__/__snapshots__/page-map.test.ts.snap

-5
Original file line numberDiff line numberDiff line change
@@ -571,11 +571,6 @@ exports[`Page Process > should match i18n site page maps 1`] = `
571571
"route": "/en/remote",
572572
},
573573
{
574-
"frontMatter": {
575-
"mdxOptions": {
576-
"format": "md",
577-
},
578-
},
579574
"kind": "MdxPage",
580575
"name": "test",
581576
"route": "/en/test",

‎packages/nextra/__test__/compile.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import Last from './three.mdx'
9696
expect(result).toMatchInlineSnapshot(`
9797
"/*@jsxRuntime automatic @jsxImportSource react*/
9898
import {useMDXComponents as _provideComponents} from \\"nextra/mdx\\";
99+
export const frontMatter = {};
99100
import FromMdx, {__toc as __toc0} from './one.mdx';
100101
import FromMarkdown, {__toc as __toc1} from './two.md';
101102
import IgnoreMe from './foo';

‎packages/nextra/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
"clean": "rimraf ./dist ./style.css",
109109
"dev": "tsup --watch",
110110
"prepublishOnly": "pnpm build",
111-
"test": "vitest run",
111+
"test": "vitest",
112112
"types": "tsup --dts-only",
113113
"types:check": "tsc --noEmit"
114114
},
@@ -125,6 +125,7 @@
125125
"@theguild/remark-mermaid": "^0.0.4",
126126
"@theguild/remark-npm2yarn": "0.2.0-alpha-20230904225308-5ba1c7b",
127127
"clsx": "^2.0.0",
128+
"estree-util-value-to-estree": "^3.0.1",
128129
"github-slugger": "^2.0.0",
129130
"graceful-fs": "^4.2.11",
130131
"gray-matter": "^4.0.3",
@@ -135,6 +136,7 @@
135136
"rehype-katex": "^6.0.3",
136137
"rehype-pretty-code": "0.9.11",
137138
"rehype-raw": "^7.0.0",
139+
"remark-frontmatter": "^4.0.1",
138140
"remark-gfm": "^3.0.1",
139141
"remark-math": "^5.1.1",
140142
"remark-reading-time": "^2.0.1",
@@ -143,6 +145,7 @@
143145
"title": "^3.5.3",
144146
"unist-util-remove": "^4.0.0",
145147
"unist-util-visit": "^5.0.0",
148+
"yaml": "^2.3.2",
146149
"zod": "^3.22.2",
147150
"zod-validation-error": "^1.5.0"
148151
},

‎packages/nextra/src/compile.ts

+17-12
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { createProcessor } from '@mdx-js/mdx'
44
import type { Processor } from '@mdx-js/mdx/lib/core'
55
import { remarkMermaid } from '@theguild/remark-mermaid'
66
import { remarkNpm2Yarn } from '@theguild/remark-npm2yarn'
7-
import grayMatter from 'gray-matter'
87
import rehypeKatex from 'rehype-katex'
98
import type { Options as RehypePrettyCodeOptions } from 'rehype-pretty-code'
109
import rehypePrettyCode from 'rehype-pretty-code'
1110
import rehypeRaw from 'rehype-raw'
11+
import remarkFrontmatter from 'remark-frontmatter'
1212
import remarkGfm from 'remark-gfm'
1313
import remarkMath from 'remark-math'
1414
import remarkReadingTime from 'remark-reading-time'
@@ -27,12 +27,14 @@ import {
2727
remarkHeadings,
2828
remarkLinkRewrite,
2929
remarkMdxDisableExplicitJsx,
30+
remarkMdxFrontMatter,
3031
remarkRemoveImports,
3132
remarkStaticImage,
3233
remarkStructurize
3334
} from './mdx-plugins'
3435
import theme from './theme.json'
3536
import type {
37+
FrontMatter,
3638
LoaderOptions,
3739
PageOpts,
3840
ReadingTime,
@@ -104,15 +106,12 @@ export async function compileMdx(
104106
defaultShowCopyCode,
105107
route = '',
106108
locale,
107-
mdxOptions,
109+
mdxOptions = {},
108110
filePath = '',
109111
useCachedCompiler,
110112
isPageImport = true
111113
}: CompileMdxOptions = {}
112114
) {
113-
// Extract frontMatter information if it exists
114-
const { data: frontMatter, content } = grayMatter(source)
115-
116115
let searchIndexKey: string | null = null
117116
if (ERROR_ROUTES.has(route)) {
118117
/* skip */
@@ -136,11 +135,7 @@ export async function compileMdx(
136135
remarkPlugins,
137136
rehypePlugins,
138137
rehypePrettyCodeOptions
139-
}: MdxOptions = {
140-
...mdxOptions,
141-
// You can override MDX options in the frontMatter too.
142-
...frontMatter.mdxOptions
143-
}
138+
}: MdxOptions = mdxOptions
144139

145140
const format =
146141
_format === 'detect' ? (filePath.endsWith('.mdx') ? 'mdx' : 'md') : _format
@@ -165,7 +160,7 @@ export async function compileMdx(
165160

166161
try {
167162
const vFile = await processor.process(
168-
filePath ? { value: content, path: filePath } : content
163+
filePath ? { value: source, path: filePath } : source
169164
)
170165

171166
const { title, hasJsxInH1, readingTime, structurizedData } = vFile.data as {
@@ -176,13 +171,21 @@ export async function compileMdx(
176171
// https://github.com/shuding/nextra/issues/1032
177172
const result = String(vFile).replaceAll('__esModule', '_\\_esModule')
178173

174+
const frontMatter = (vFile.data.frontMatter || {}) as FrontMatter
175+
176+
if (frontMatter.mdxOptions) {
177+
throw new Error('`frontMatter.mdxOptions` is no longer supported')
178+
}
179+
179180
return {
180181
result,
181182
...(title && { title }),
182183
...(hasJsxInH1 && { hasJsxInH1 }),
183184
...(readingTime && { readingTime }),
184185
...(searchIndexKey !== null && { searchIndexKey, structurizedData }),
185-
...(isRemoteContent && { headings: vFile.data.headings }),
186+
...(isRemoteContent && {
187+
headings: vFile.data.headings
188+
}),
186189
frontMatter
187190
}
188191
} catch (err) {
@@ -208,6 +211,8 @@ export async function compileMdx(
208211
}
209212
] satisfies Pluggable,
210213
isRemoteContent && remarkRemoveImports,
214+
remarkFrontmatter, // parse and attach yaml node
215+
[remarkMdxFrontMatter, { isRemoteContent }] satisfies Pluggable,
211216
remarkGfm,
212217
format !== 'md' &&
213218
([

‎packages/nextra/src/layout.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,25 @@ export default function Nextra({
77
__nextra_dynamic_opts,
88
...props
99
}: any): ReactElement {
10-
const { Layout, themeConfig, Content, pageOpts } = useInternals()
10+
const { Layout, themeConfig, Content, ...rest } = useInternals()
11+
12+
let { pageOpts } = rest
1113

1214
if (__nextra_pageMap) {
13-
pageOpts.pageMap = __nextra_pageMap
15+
pageOpts = {
16+
...pageOpts,
17+
pageMap: __nextra_pageMap
18+
}
1419
}
1520

1621
if (__nextra_dynamic_opts) {
1722
const { headings, title, frontMatter } = JSON.parse(__nextra_dynamic_opts)
18-
Object.assign(pageOpts, {
23+
pageOpts = {
24+
...pageOpts,
1925
headings,
2026
title,
2127
frontMatter
22-
})
28+
}
2329
}
2430
return (
2531
<Layout themeConfig={themeConfig} pageOpts={pageOpts}>

‎packages/nextra/src/loader.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,13 @@ ${themeConfigImport && '__nextra_internal__.themeConfig = __themeConfig'}`
168168
const locale =
169169
locales[0] === '' ? '' : mdxPath.replace(PAGES_DIR, '').split('/')[1]
170170

171-
// todo: rethink to save to fileMap dynamic pages
172-
const route = mdxPath.includes('[')
173-
? '/' +
174-
path.relative(PAGES_DIR, mdxPath).replace(MARKDOWN_EXTENSION_REGEX, '')
175-
: fileMap[mdxPath].route
171+
const route =
172+
'/' +
173+
path
174+
.relative(PAGES_DIR, mdxPath)
175+
.replace(MARKDOWN_EXTENSION_REGEX, '')
176+
.replace(/(^|\/)index$/, '')
177+
176178
const {
177179
result,
178180
title,
@@ -205,13 +207,14 @@ ${themeConfigImport && '__nextra_internal__.themeConfig = __themeConfig'}`
205207
if (!isPageImport) {
206208
return result
207209
}
208-
209210
// Logic for resolving the page title (used for search and as fallback):
210211
// 1. If the frontMatter has a title, use it.
211212
// 2. Use the first h1 heading if it exists.
212213
// 3. Use the fallback, title-cased file name.
213214
const fallbackTitle =
214-
frontMatter.title || title || pageTitleFromFilename(fileMap[mdxPath].name)
215+
frontMatter.title ||
216+
title ||
217+
pageTitleFromFilename(path.parse(mdxPath).name)
215218

216219
if (searchIndexKey && frontMatter.searchable !== false) {
217220
// Store all the things in buildInfo.
@@ -238,7 +241,6 @@ ${themeConfigImport && '__nextra_internal__.themeConfig = __themeConfig'}`
238241

239242
let pageOpts: Partial<PageOpts> = {
240243
filePath: slash(path.relative(CWD, mdxPath)),
241-
...(Object.keys(frontMatter).length > 0 && { frontMatter }),
242244
hasJsxInH1,
243245
timestamp,
244246
readingTime,
@@ -255,7 +257,7 @@ ${themeConfigImport && '__nextra_internal__.themeConfig = __themeConfig'}`
255257

256258
const stringifiedPageOpts =
257259
JSON.stringify(pageOpts).slice(0, -1) +
258-
',headings:__toc,pageMap:__nextraPageMap}'
260+
',headings:__toc,pageMap:__nextraPageMap,frontMatter}'
259261
const stringifiedChecksum = IS_PRODUCTION
260262
? "''"
261263
: JSON.stringify(hashFnv32a(stringifiedPageOpts))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { compile } from '@mdx-js/mdx'
2+
import type { VFile } from '@mdx-js/mdx/lib/compile'
3+
import remarkFrontmatter from 'remark-frontmatter'
4+
import { remarkMdxFrontMatter } from '../remark-mdx-frontmatter'
5+
6+
function process(content: string, isRemoteContent = false): Promise<VFile> {
7+
return compile(content, {
8+
jsx: true,
9+
remarkPlugins: [
10+
remarkFrontmatter,
11+
[remarkMdxFrontMatter, { isRemoteContent }]
12+
]
13+
})
14+
}
15+
16+
const YAML_FRONTMATTER = '---\nfoo: bar\n---'
17+
const ESM_FRONTMATTER = "export const frontMatter = { foo: 'bar' }"
18+
19+
function trim(value: VFile): string {
20+
const string = String(value)
21+
return string
22+
.slice(0, string.indexOf('function _createMdxContent'))
23+
.replace('/*@jsxRuntime automatic @jsxImportSource react*/', '')
24+
.trim()
25+
}
26+
27+
describe('remarkMdxFrontMatter', () => {
28+
it('should throw error if both yaml/esm frontmatter are used', () => {
29+
const processor = process(`${YAML_FRONTMATTER}\n${ESM_FRONTMATTER}`)
30+
expect(() => processor).rejects.toThrowError(
31+
"Both yaml frontMatter and esm export frontMatter aren't supported. Keep only 1."
32+
)
33+
})
34+
35+
describe('yaml frontmatter', () => {
36+
describe('not remote content', async () => {
37+
const file = await process(YAML_FRONTMATTER)
38+
39+
it.skip('should not add file.data', () => {
40+
expect(file.data).toEqual({})
41+
})
42+
43+
it('should export yaml frontmatter', () => {
44+
expect(trim(file)).toMatchInlineSnapshot(`
45+
"export const frontMatter = {
46+
\\"foo\\": \\"bar\\"
47+
};"
48+
`)
49+
})
50+
})
51+
52+
describe('remote content', async () => {
53+
const file = await process(YAML_FRONTMATTER, true)
54+
55+
it('should add data.frontMatter', () => {
56+
expect(file.data).toEqual({ frontMatter: { foo: 'bar' } })
57+
})
58+
59+
it('should not export yaml frontmatter', () => {
60+
expect(trim(file)).toMatchInlineSnapshot('""')
61+
})
62+
})
63+
})
64+
65+
describe('esm frontmatter', () => {
66+
describe('not remote content', async () => {
67+
const file = await process(ESM_FRONTMATTER)
68+
it.skip('should not add file.data', () => {})
69+
70+
it('should export esm frontmatter', () => {
71+
expect(trim(file)).toMatchInlineSnapshot(`
72+
"export const frontMatter = {
73+
foo: 'bar'
74+
};"
75+
`)
76+
})
77+
})
78+
79+
describe('remote content', async () => {
80+
const file = await process(ESM_FRONTMATTER, true)
81+
it('should not export esm frontmatter', () => {
82+
expect(trim(file)).toMatchInlineSnapshot('""')
83+
})
84+
})
85+
})
86+
})

‎packages/nextra/src/mdx-plugins/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { parseMeta, attachMeta } from './rehype'
22
export { remarkCustomHeadingId } from './remark-custom-heading-id'
3+
export { remarkMdxFrontMatter } from './remark-mdx-frontmatter'
34
export { remarkHeadings } from './remark-headings'
45
export { remarkRemoveImports } from './remark-remove-imports'
56
export {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { valueToEstree } from 'estree-util-value-to-estree'
2+
import type { Parent, Root } from 'mdast'
3+
import type { Plugin } from 'unified'
4+
import { parse as parseYaml } from 'yaml'
5+
6+
function createNode(data: Record<string, unknown>): any {
7+
return {
8+
type: 'mdxjsEsm',
9+
data: {
10+
estree: {
11+
body: [
12+
{
13+
type: 'ExportNamedDeclaration',
14+
specifiers: [],
15+
declaration: {
16+
type: 'VariableDeclaration',
17+
kind: 'const',
18+
declarations: [
19+
{
20+
type: 'VariableDeclarator',
21+
id: { type: 'Identifier', name: 'frontMatter' },
22+
init: valueToEstree(data)
23+
}
24+
]
25+
}
26+
}
27+
]
28+
}
29+
}
30+
}
31+
}
32+
33+
export const remarkMdxFrontMatter: Plugin<
34+
[{ isRemoteContent?: boolean }?],
35+
Root
36+
> =
37+
({ isRemoteContent } = {}) =>
38+
(ast: Parent, file) => {
39+
const yamlNodeIndex = ast.children.findIndex(node => node.type === 'yaml')
40+
const esmNodeIndex = ast.children.findIndex((node: any) => {
41+
if (node.type !== 'mdxjsEsm') return
42+
const name =
43+
node.data.estree.body[0].declaration?.declarations?.[0].id.name
44+
return name === 'frontMatter'
45+
})
46+
const hasYaml = yamlNodeIndex !== -1
47+
const hasEsm = esmNodeIndex !== -1
48+
49+
if (hasYaml) {
50+
if (hasEsm) {
51+
throw new Error(
52+
"Both yaml frontMatter and esm export frontMatter aren't supported. Keep only 1."
53+
)
54+
}
55+
56+
const raw = (ast.children[yamlNodeIndex] as { value: string }).value
57+
const data = parseYaml(raw)
58+
59+
file.data.frontMatter = data
60+
if (!isRemoteContent) {
61+
ast.children[yamlNodeIndex] = createNode(data)
62+
}
63+
return
64+
}
65+
66+
if (hasEsm) {
67+
if (isRemoteContent) {
68+
const [node] = ast.children.splice(esmNodeIndex, 1) as any
69+
70+
// TODO: attach data to file.data.frontMatter
71+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
72+
const objectNode =
73+
node.data.estree.body[0].declaration.declarations[0].init
74+
}
75+
return
76+
}
77+
78+
// Attach dummy node
79+
ast.children.unshift(createNode({}))
80+
}

‎packages/nextra/src/types.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { GrayMatterFile } from 'gray-matter'
21
import type { Heading as MDASTHeading } from 'mdast'
32
import type { NextConfig } from 'next'
43
import type { FC, ReactNode } from 'react'
@@ -49,7 +48,7 @@ export type DynamicMetaJsonFile = {
4948
data: DynamicMeta
5049
}
5150

52-
export type FrontMatter = GrayMatterFile<string>['data']
51+
export type FrontMatter = Record<string, any>
5352
export type Meta = string | Record<string, any>
5453

5554
export type MdxFile<FrontMatterType = FrontMatter> = {

‎packages/nextra/src/use-internals.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function useInternals() {
1212
NEXTRA_INTERNAL
1313
]
1414
const { route } = useRouter()
15+
console.log({route})
1516
const rerender = useState({})[1]
1617

1718
// The HMR handling logic is not needed for production builds, the condition

‎pnpm-lock.yaml

+370-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.