diff --git a/.changeset/witty-crews-worry.md b/.changeset/witty-crews-worry.md
new file mode 100644
index 000000000000..767b96bf4202
--- /dev/null
+++ b/.changeset/witty-crews-worry.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/mdx': minor
+---
+
+Support "layout" frontmatter property
diff --git a/packages/integrations/mdx/README.md b/packages/integrations/mdx/README.md
index fe68738044a5..6d9876ee891a 100644
--- a/packages/integrations/mdx/README.md
+++ b/packages/integrations/mdx/README.md
@@ -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 [``](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;
+---
+
+
+ {content.title}
+
+
+ {content.title}
+
+
+
+
```
-You can also import and use a [`` 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 [`` component](https://docs.astro.build/en/core-concepts/layouts/) like any other component:
```mdx
---
@@ -155,9 +177,11 @@ publishDate: '21 September 2022'
---
import BaseLayout from '../layouts/BaseLayout.astro';
-
- # {frontmatter.title}
+function fancyJsHelper() {
+ return "Try doing that with YAML!";
+}
+
Welcome to my new Astro blog, using MDX!
```
@@ -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;
---
{title}
-Published on {publishDate}
+{fancyJsHelper()}
```
diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json
index 8f9a1357a866..d2d97bd919e1 100644
--- a/packages/integrations/mdx/package.json
+++ b/packages/integrations/mdx/package.json
@@ -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",
diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts
index cb3f7c3fe906..9313d10eeae5 100644
--- a/packages/integrations/mdx/src/index.ts
+++ b/packages/integrations/mdx/src/index.ts
@@ -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';
@@ -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 | { extends: T };
@@ -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 {children} }`
+ }
+ return mdxPluginTransform?.(code, id);
+ }
},
{
name: '@astrojs/mdx',
- transform(code: string, id: string) {
+ transform(code, id) {
if (!id.endsWith('.mdx')) return;
const [, moduleExports] = parseESM(code);
@@ -113,7 +137,7 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
return code;
},
},
- ],
+ ] as VitePlugin[],
},
});
},
diff --git a/packages/integrations/mdx/src/utils.ts b/packages/integrations/mdx/src/utils.ts
index 6eb7a3570260..97bc72d7420c 100644
--- a/packages/integrations/mdx/src/utils.ts
+++ b/packages/integrations/mdx/src/utils.ts
@@ -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 + '/';
@@ -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;
+ }
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro
new file mode 100644
index 000000000000..0d18725f282d
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro
@@ -0,0 +1,18 @@
+---
+const { content = { title: "Didn't work" } } = Astro.props;
+---
+
+
+
+
+
+
+
+ {content.title}
+
+
+ {content.title}
+ Layout rendered!
+
+
+
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx
index 333026f85404..e8815b9c3954 100644
--- a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx
@@ -1,6 +1,7 @@
---
title: 'Using YAML frontmatter'
+layout: '../layouts/Base.astro'
illThrowIfIDontExist: "Oh no, that's scary!"
---
-# {frontmatter.illThrowIfIDontExist}
+{frontmatter.illThrowIfIDontExist}
diff --git a/packages/integrations/mdx/test/mdx-frontmatter.test.js b/packages/integrations/mdx/test/mdx-frontmatter.test.js
index 3021f926fb89..4d25e0ed33f1 100644
--- a/packages/integrations/mdx/test/mdx-frontmatter.test.js
+++ b/packages/integrations/mdx/test/mdx-frontmatter.test.js
@@ -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);
@@ -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),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 881cc0da5589..37792b3291dd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2174,6 +2174,7 @@ importers:
astro-scripts: workspace:*
chai: ^4.3.6
es-module-lexer: ^0.10.5
+ gray-matter: ^4.0.3
linkedom: ^0.14.12
mocha: ^9.2.2
prismjs: ^1.28.0
@@ -2191,6 +2192,7 @@ importers:
'@mdx-js/mdx': 2.1.2
'@mdx-js/rollup': 2.1.2
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
@@ -9836,7 +9838,7 @@ packages:
dev: true
/concat-map/0.0.1:
- resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
/concurrently/7.3.0:
resolution: {integrity: sha512-IiDwm+8DOcFEInca494A8V402tNTQlJaYq78RF2rijOrKEk/AOHTxhN4U1cp7GYKYX5Q6Ymh1dLTBlzIMN0ikA==}