Using Astro, I have a layout file: /src/layouts/MdxLayout.astro where I try to set allowed components Button and Link to be rendered if passed in from a page file.
---
import BaseLayout from '@/layouts/Base/BaseLayout.astro';
import Button from '@/components/Button/Button';
import Link from "@/components/Link/Link";
---
<BaseLayout components={{ Button: Button, Link: Link }}>
<slot />
</BaseLayout>
Hoping this would work in /src/pages/mdx.mdx:
---
layout: '@/layouts/MdxLayout.astro'
title: "Hello, World!"
---
# Lorem ipsum
<Button>Click me</Button>
So I will not have to add imports to all mdx files where I wish to use these components, (ie: import Button from '@/components/Button/Button'
). Without this import though, I am faced with the error:
Expected component Button to be defined: you likely forgot to import, pass, or provide it.
My question: is there any way to predefine components in a layout file that can then be used in a mdx file that uses this layout without having to import them in the mdx file?
It is possible but not natively. You have three options:
Using Remark or Rehype
You can create a custom Remark/Rehype plugin to update the tree with the import statements for your components. Something like:
import type { Root } from 'hast';
import type { Plugin as UnifiedPlugin } from 'unified';
import { visit } from 'unist-util-visit';
export const rehypeAutoImportComponents: UnifiedPlugin<[], Root> =
() => (tree, file) => {
const importsStatements = [];
visit(tree, 'mdxJsxFlowElement', (node, index, nodeParent) => {
if (node.name === 'Button') importsStatements.push('import Button from "@/components/Button/Button"');
});
tree.children.unshift(...importsStatements);
};
You'll need to figure out how to resolve your path aliases and how to avoid duplicated import statements.
Then update your Astro configuration file:
import { rehypeAutoImportComponents } from './src/utils/rehype-auto-import-components';
export default defineConfig({
markdown: {
rehypePlugins: [
rehypeAutoImportComponents
],
},
});
You can also add an option to your plugin to pass the components directly in your Astro config file instead of hardcoding them in your plugin.
Note: with this solution, you can remove the components
property in <BaseLayout ... />
.
Existing libraries
I haven't test them but you can look for astro-auto-import
or Astro-M²DX
for example.
Mapping HTML tags to custom components
Another alternative is to map HTML tags to custom components:
---
import BaseLayout from '@/layouts/Base/BaseLayout.astro';
import Button from '@/components/Button/Button';
import Link from "@/components/Link/Link";
---
<BaseLayout components={{ a: Link, button: Button }}>
<slot />
</BaseLayout>
However to be able to map HTML tags (ie. <button />
) you need a plugin to disable the default behavior of MDXJS (ignoring HTML tags). Something like:
import type { Root } from 'hast';
import type { Plugin as UnifiedPlugin } from 'unified';
import { visit } from 'unist-util-visit';
export const rehypeDisableExplicitJsx: UnifiedPlugin<[], Root> =
() => (tree) => {
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
if (node.data && '_mdxExplicitJsx' in node.data) {
delete node.data._mdxExplicitJsx;
}
});
};
Then you can add this plugin to your Astro config:
import { rehypeDisableExplicitJsx } from './src/utils/rehype-disable-explicit-jsx';
export default defineConfig({
markdown: {
rehypePlugins: [
rehypeDisableExplicitJsx
],
},
});
With this, your MDX file can look like this:
---
layout: '@/layouts/MdxLayout.astro'
title: "Hello, World!"
---
# Lorem ipsum
<button>Click me</button>
The caveat is that for more complex components (when a HTML tag does not exist for it) you'll need extra logic. You could map a div to a custom component where you check for a particular class/attribute to return the right component.