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

feat: support layout in MDX frontmatter #4088

Merged
merged 12 commits into from
Jul 29, 2022
5 changes: 5 additions & 0 deletions .changeset/witty-crews-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/mdx': minor
---

Support "layout" frontmatter property
42 changes: 33 additions & 9 deletions packages/integrations/mdx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,37 @@ const posts = await Astro.glob('./*.mdx');

### Layouts

You can use the [MDX layout component](https://mdxjs.com/docs/using-mdx/#layout) to specify a layout component to wrap all page content. This is done with a default export statement at the end of your `.mdx` file:
Layouts can be applied [in the same way as standard Astro Markdown](https://docs.astro.build/en/guides/markdown-content/#markdown-layouts). You can add a `layout` to [your frontmatter](#frontmatter) like so:

```mdx
// src/pages/my-page.mdx
```yaml
---
layout: '../layouts/BaseLayout.astro'
title: 'My Blog Post'
---
```

Then, you can retrieve all other frontmatter properties from your layout via the `content` property, and render your MDX using the default [`<slot />`](https://docs.astro.build/en/core-concepts/astro-components/#slots):

export {default} from '../../layouts/BaseLayout.astro';
```astro
---
// src/layouts/BaseLayout.astro
const { content } = Astro.props;
---
<html>
<head>
<title>{content.title}</title>
</head>
<body>
<h1>{content.title}</h1>
<!-- Rendered MDX will be passed into the default slot. -->
<slot />
</body>
</html>
```

You can also import and use a [`<Layout />` component](/en/core-concepts/layouts/) for your MDX page content, and pass all the variables declared in frontmatter as props.
#### Importing layouts manually

You may need to pass information to your layouts that does not (or cannot) exist in your frontmatter. In this case, you can import and use a [`<Layout />` component](https://docs.astro.build/en/core-concepts/layouts/) like any other component:

```mdx
---
Expand All @@ -155,9 +177,11 @@ publishDate: '21 September 2022'
---
import BaseLayout from '../layouts/BaseLayout.astro';

<BaseLayout {...frontmatter}>
# {frontmatter.title}
function fancyJsHelper() {
return "Try doing that with YAML!";
}

<BaseLayout title={frontmatter.title} fancyJsHelper={fancyJsHelper}>
Welcome to my new Astro blog, using MDX!
</BaseLayout>
```
Expand All @@ -166,12 +190,12 @@ Then, your values are available to you through `Astro.props` in your layout, and
```astro
---
// src/layouts/BaseLayout.astro
const { title, publishDate } = Astro.props;
const { title, fancyJsHelper } = Astro.props;
---
<!-- -->
<h1>{title}</h1>
<slot />
<p>Published on {publishDate}</p>
<p>{fancyJsHelper()}</p>
<!-- -->
```

Expand Down
7 changes: 4 additions & 3 deletions packages/integrations/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@
"@mdx-js/mdx": "^2.1.2",
"@mdx-js/rollup": "^2.1.1",
"es-module-lexer": "^0.10.5",
"gray-matter": "^4.0.3",
"prismjs": "^1.28.0",
"rehype-raw": "^6.1.1",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"remark-mdx-frontmatter": "^2.0.2",
"remark-shiki-twoslash": "^3.1.0",
"remark-smartypants": "^2.0.0",
"shiki": "^0.10.1",
"unist-util-visit": "^4.1.0",
"remark-frontmatter": "^4.0.1",
"remark-mdx-frontmatter": "^2.0.2"
"unist-util-visit": "^4.1.0"
},
"devDependencies": {
"@types/chai": "^4.3.1",
Expand Down
48 changes: 36 additions & 12 deletions packages/integrations/mdx/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Plugin as VitePlugin } from 'vite';
import { nodeTypes } from '@mdx-js/mdx';
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
import type { AstroIntegration } from 'astro';
Expand All @@ -10,7 +11,7 @@ import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
import remarkShikiTwoslash from 'remark-shiki-twoslash';
import remarkSmartypants from 'remark-smartypants';
import remarkPrism from './remark-prism.js';
import { getFileInfo } from './utils.js';
import { getFileInfo, getFrontmatter } from './utils.js';

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

Expand Down Expand Up @@ -68,24 +69,47 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
},
]);

const configuredMdxPlugin = mdxPlugin({
remarkPlugins,
rehypePlugins,
jsx: true,
jsxImportSource: 'astro',
// Note: disable `.md` support
format: 'mdx',
mdExtensions: [],
})

updateConfig({
vite: {
plugins: [
{
enforce: 'pre',
...mdxPlugin({
remarkPlugins,
rehypePlugins,
jsx: true,
jsxImportSource: 'astro',
// Note: disable `.md` support
format: 'mdx',
mdExtensions: [],
}),
...configuredMdxPlugin,
// Override transform to inject layouts before MDX compilation
async transform(this, code, id) {
if (!id.endsWith('.mdx')) return;

const mdxPluginTransform = configuredMdxPlugin.transform?.bind(this);
// If user overrides our default YAML parser,
// do not attempt to parse the `layout` via gray-matter
if (mdxOptions.frontmatterOptions?.parsers) {
return mdxPluginTransform?.(code, id);
}
const frontmatter = getFrontmatter(code, id);
if (frontmatter.layout) {
const { layout, ...content } = frontmatter;
code += `\nexport default async function({ children }) {\nconst Layout = (await import(${
JSON.stringify(frontmatter.layout)
})).default;\nreturn <Layout content={${
JSON.stringify(content)
}}>{children}</Layout> }`
}
return mdxPluginTransform?.(code, id);
}
},
{
name: '@astrojs/mdx',
transform(code: string, id: string) {
transform(code, id) {
if (!id.endsWith('.mdx')) return;
const [, moduleExports] = parseESM(code);

Expand Down Expand Up @@ -113,7 +137,7 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
return code;
},
},
],
] as VitePlugin[],
},
});
},
Expand Down
23 changes: 22 additions & 1 deletion packages/integrations/mdx/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AstroConfig } from 'astro';
import type { AstroConfig, SSRError } from 'astro';
import matter from 'gray-matter';

function appendForwardSlash(path: string) {
return path.endsWith('/') ? path : path + '/';
Expand Down Expand Up @@ -37,3 +38,23 @@ export function getFileInfo(id: string, config: AstroConfig): FileInfo {
}
return { fileId, fileUrl };
}

/**
* Match YAML exception handling from Astro core errors
* @see 'astro/src/core/errors.ts'
*/
export function getFrontmatter(code: string, id: string) {
try {
return matter(code).data;
} catch (e: any) {
if (e.name === 'YAMLException') {
const err: SSRError = e;
err.id = id;
err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column };
err.message = e.reason;
throw err;
} else {
throw e;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
const { content = { title: "Didn't work" } } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{content.title}</title>
</head>
<body>
<h1>{content.title}</h1>
<p data-layout-rendered>Layout rendered!</p>
<slot />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: 'Using YAML frontmatter'
layout: '../layouts/Base.astro'
illThrowIfIDontExist: "Oh no, that's scary!"
---

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

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

const FIXTURE_ROOT = new URL('./fixtures/mdx-frontmatter/', import.meta.url);
Expand All @@ -26,6 +27,36 @@ describe('MDX frontmatter', () => {
expect(titles).to.include('Using YAML frontmatter');
});

it('renders layout from "layout" frontmatter property', async () => {
const fixture = await loadFixture({
root: FIXTURE_ROOT,
integrations: [mdx()],
});
await fixture.build();

const html = await fixture.readFile('/index.html');
const { document } = parseHTML(html);

const layoutParagraph = document.querySelector('[data-layout-rendered]');

expect(layoutParagraph).to.not.be.null;
});

it('passes frontmatter to layout via "content" prop', async () => {
const fixture = await loadFixture({
root: FIXTURE_ROOT,
integrations: [mdx()],
});
await fixture.build();

const html = await fixture.readFile('/index.html');
const { document } = parseHTML(html);

const h1 = document.querySelector('h1');

expect(h1.textContent).to.equal('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),
Expand Down
4 changes: 3 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.