Skip to content

Commit

Permalink
[MDX] Support YAML frontmatter (#3995)
Browse files Browse the repository at this point in the history
* chore: remove old comment

* deps: add remark-frontmatter

* deps: add remark-mdx-frontmatter

* fix: handle null or undefined frontmatter key

* feat: configure frontmatter plugins with defaults

* test: frontmatter and custom frontmatter name

* docs: add frontmatterOptions config

* docs: add "variables" and "frontmatter" docs

* chore: excessible -> accessible

* chore: changeset

* chore: remove bad mdx comment
  • Loading branch information
bholmesdev committed Jul 21, 2022
1 parent 40a45e3 commit b2b367c
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/silent-clocks-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/mdx': minor
'astro': patch
---

Support YAML frontmatter in MDX files
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function shouldSkipDraft(pageModule: ComponentInstance, astroConfig: AstroConfig
!astroConfig.markdown.drafts &&
// This is a draft post
'frontmatter' in pageModule &&
(pageModule as any).frontmatter.draft === true
(pageModule as any).frontmatter?.draft === true
);
}

Expand Down
87 changes: 87 additions & 0 deletions packages/integrations/mdx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,62 @@ To write your first MDX page in Astro, head to our [UI framework documentation][
Also check our [Astro Integration Documentation][astro-integration] for more on integrations.

### Variables

MDX supports `export` statements to add variables to your templates. These variables are accessible both from the template itself _and_ as named properties when importing the template somewhere else.

For instance, you can export a `title` field from an MDX page or component to use as a heading with `{JSX expressions}`:

```mdx
export const title = 'My first MDX post'

# {title}
```

This `title` will be accessible from `import` and [glob](https://docs.astro.build/en/reference/api-reference/#astroglob) statements as well:

```astro
---
// src/pages/index.astro
const posts = await Astro.glob('./*.mdx');
---
{posts.map(post => <p>{post.title}</p>)}
```

See [the official "how MDX works" guide](https://mdxjs.com/docs/using-mdx/#how-mdx-works) for more on MDX variables.

### Frontmatter

Astro also supports YAML-based frontmatter out-of-the-box using the [remark-mdx-frontmatter](https://github.com/remcohaszing/remark-mdx-frontmatter) plugin. By default, all variables declared in a frontmatter fence (`---`) will be accessible via the `frontmatter` export. See the `frontmatterOptions` configuration to customize this behavior.

For example, we can add a `title` and `publishDate` to an MDX page or component like so:

```mdx
---
title: 'My first MDX post'
publishDate: '21 September 2022'
---

# {frontmatter.title}
```

Now, this `title` and `publishDate` will be accessible from `import` and [glob](https://docs.astro.build/en/reference/api-reference/#astroglob) statements via the `frontmatter` property. This matches the behavior of [plain markdown in Astro](https://docs.astro.build/en/reference/api-reference/#markdown-files) as well!

```astro
---
// src/pages/index.astro
const posts = await Astro.glob('./*.mdx');
---
{posts.map(post => (
<Fragment>
<h2>{post.frontmatter.title}</h2>
<time>{post.frontmatter.publishDate}</time>
</Fragment>
))}
```

## Configuration

<details>
Expand Down Expand Up @@ -140,6 +196,37 @@ export default {
```
</details>

<details>
<summary><strong>frontmatterOptions</strong></summary>

**Default:** `{ name: 'frontmatter' }`

We use [remark-mdx-frontmatter](https://github.com/remcohaszing/remark-mdx-frontmatter) to parse YAML-based frontmatter in your MDX files. If you want to override our default configuration or extend remark-mdx-frontmatter (ex. to [apply a custom frontmatter parser](https://github.com/remcohaszing/remark-mdx-frontmatter#parsers)), you can supply a `frontmatterOptions` configuration.

For example, say you want to access frontmatter as root-level variables without a nested `frontmatter` object. You can override the [`name` configuration option](https://github.com/remcohaszing/remark-mdx-frontmatter#name) like so:

```js
// astro.config.mjs
export default {
integrations: [mdx({
frontmatterOptions: {
name: '',
}
})],
}
```

```mdx
---
title: I'm just a variable now!
---

# {title}
```

See the [remark-mdx-frontmatter README](https://github.com/remcohaszing/remark-mdx-frontmatter#options) for a complete list of options.
</details>

## Examples

- The [Astro MDX example](https://github.com/withastro/astro/tree/latest/examples/with-mdx) shows how to use MDX files in your Astro project.
Expand Down
2 changes: 2 additions & 0 deletions packages/integrations/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"dependencies": {
"@mdx-js/rollup": "^2.1.1",
"es-module-lexer": "^0.10.5",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"remark-mdx-frontmatter": "^2.0.2",
"remark-smartypants": "^2.0.0"
},
"devDependencies": {
Expand Down
28 changes: 23 additions & 5 deletions packages/integrations/mdx/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
import type { AstroIntegration } from 'astro';
import type { RemarkMdxFrontmatterOptions } from 'remark-mdx-frontmatter';
import { parse as parseESM } from 'es-module-lexer';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
import remarkFrontmatter from 'remark-frontmatter';
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
import { getFileInfo } from './utils.js';

type WithExtends<T> = T | { extends: T };

type MdxOptions = {
remarkPlugins?: WithExtends<MdxRollupPluginOptions['remarkPlugins']>;
rehypePlugins?: WithExtends<MdxRollupPluginOptions['rehypePlugins']>;
};
/**
* Configure the remark-mdx-frontmatter plugin
* @see https://github.com/remcohaszing/remark-mdx-frontmatter#options for a full list of options
* @default {{ name: 'frontmatter' }}
*/
frontmatterOptions?: RemarkMdxFrontmatterOptions;
}

const DEFAULT_REMARK_PLUGINS = [remarkGfm, remarkSmartypants];

function handleExtends<T>(
config: WithExtends<T[] | undefined>,
defaults: T[] = []
): T[] | undefined {
defaults: T[] = [],
): T[] {
if (Array.isArray(config)) return config;

return [...defaults, ...(config?.extends ?? [])];
Expand All @@ -35,9 +44,18 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
{
enforce: 'pre',
...mdxPlugin({
remarkPlugins: handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS),
remarkPlugins: [
...handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS),
// Frontmatter plugins should always be applied!
// We can revisit this if a strong use case to *remove*
// YAML frontmatter via config is reported.
remarkFrontmatter,
[remarkMdxFrontmatter, {
name: 'frontmatter',
...mdxOptions.frontmatterOptions,
}],
],
rehypePlugins: handleExtends(mdxOptions.rehypePlugins),
// place these after so the user can't override
jsx: true,
jsxImportSource: 'astro',
// Note: disable `.md` support
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export async function get() {
const mdxPages = await import.meta.glob('./*.mdx', { eager: true });

return {
body: JSON.stringify({
titles: Object.values(mdxPages ?? {}).map(v => v?.customFrontmatter?.title),
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: 'Using YAML frontmatter'
illThrowIfIDontExist: "Oh no, that's scary!"
---

# {customFrontmatter.illThrowIfIDontExist}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export async function get() {
const mdxPages = await import.meta.glob('./*.mdx', { eager: true });

return {
body: JSON.stringify({
titles: Object.values(mdxPages ?? {}).map(v => v?.frontmatter?.title),
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: 'Using YAML frontmatter'
illThrowIfIDontExist: "Oh no, that's scary!"
---

# {frontmatter.illThrowIfIDontExist}
43 changes: 43 additions & 0 deletions packages/integrations/mdx/test/mdx-frontmatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import mdx from '@astrojs/mdx';

import { expect } from 'chai';
import { loadFixture } from '../../../astro/test/test-utils.js';

const FIXTURE_ROOT = new URL('./fixtures/mdx-frontmatter/', import.meta.url);

describe('MDX frontmatter', () => {
it('builds when "frontmatter.property" is in JSX expression', async () => {
const fixture = await loadFixture({
root: FIXTURE_ROOT,
integrations: [mdx()],
});
await fixture.build();
expect(true).to.equal(true);
});

it('extracts frontmatter to "frontmatter" export', async () => {
const fixture = await loadFixture({
root: FIXTURE_ROOT,
integrations: [mdx()],
});
await fixture.build();

const { titles } = JSON.parse(await fixture.readFile('/glob.json'));
expect(titles).to.include('Using YAML frontmatter');
});

it('extracts frontmatter to "customFrontmatter" export when configured', async () => {
const fixture = await loadFixture({
root: new URL('./fixtures/mdx-custom-frontmatter-name/', import.meta.url),
integrations: [mdx({
frontmatterOptions: {
name: 'customFrontmatter',
},
})],
});
await fixture.build();

const { titles } = JSON.parse(await fixture.readFile('/glob.json'));
expect(titles).to.include('Using YAML frontmatter');
});
});
Loading

0 comments on commit b2b367c

Please sign in to comment.