Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MDX] Support YAML frontmatter #3995

Merged
merged 11 commits into from
Jul 21, 2022
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remark-mdx-frontmatter applies export const frontmatter = undefined when no frontmatter is present. This handles that case!

);
}

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