# Layout that imports components for you instead of each individual MDX file
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?
## Answer 1
It is possible but not natively. You have three options:
1. use a custom Remark or Rehype plugin
2. use an existing library that offers this functionality
3. map existing HTML tags to custom components
### 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.
## Answer 2
Thanks to Armand's answer, I made it work with this rehype plugin:
```
import { parse, resolve } from 'node:path';
import { parse as parseJs } from 'acorn';
import type { VFile } from 'vfile';
const resolveModulePath = (path: string) => {
// Resolve relative paths
if (path.startsWith('.')) return resolve(path);
// Don’t resolve other paths (e.g. npm modules)
return path;
};
type NamedImportConfig = string | [from: string, as: string];
type ImportsConfig = (string | Record<string, string | NamedImportConfig[]>)[];
/**
* Use a filename to generate a default import name.
* @example
* getDefaultImportName('/path/to/cool-component.astro');
* // => coolcomponent
*/
function getDefaultImportName(path: string): string {
return parse(path).name.replaceAll(/[^\w\d]/g, '');
}
/**
* Create an import statement.
* @param imported Stuff to import (e.g. `Thing` or `{ Named }`)
* @param module Module to import from (e.g. `module-thing`)
*/
function formatImport(imported: string, module: string): string {
return `import ${imported} from ${JSON.stringify(module)};`;
}
/** Get the parts for a named import statement from config. */
function formatNamedImports(namedImport: NamedImportConfig[]): string {
const imports: string[] = [];
for (const imp of namedImport) {
if (typeof imp === 'string') {
imports.push(imp);
} else {
const [from, as] = imp;
imports.push(`${from} as ${as}`);
}
}
return `{ ${imports.join(', ')} }`;
}
/** Generate imports from a full imports config array. */
function processImportsConfig(config: ImportsConfig) {
const imports = [];
for (const option of config) {
if (typeof option === 'string') {
imports.push(formatImport(getDefaultImportName(option), resolveModulePath(option)));
} else {
for (const path in option) {
const namedImportsOrNamespace = option[path];
if (typeof namedImportsOrNamespace === 'string') {
imports.push(formatImport(`* as ${namedImportsOrNamespace}`, resolveModulePath(path)));
} else {
const importString = formatNamedImports(namedImportsOrNamespace);
imports.push(formatImport(importString, resolveModulePath(path)));
}
}
}
}
return imports;
}
/** Get an MDX node representing a block of imports based on user config. */
function generateImportsNode(config: ImportsConfig) {
const imports = processImportsConfig(config);
const js = imports.join('\n');
return {
type: 'mdxjsEsm',
value: '',
data: {
estree: {
...parseJs(js, { ecmaVersion: 'latest', sourceType: 'module' }),
type: 'Program',
sourceType: 'module',
},
},
};
}
type MfxFile = VFile & { data?: { astro?: { frontmatter?: { layout?: string } } } };
type PluginConfig = {
[layoutName: string]: string[];
};
export function AutoImportComponentsPerLayout(pluginConfig: PluginConfig) {
return function (tree: { children: unknown[] }, vfile: MfxFile) {
const fileLayout: string =
vfile?.data?.astro?.frontmatter?.layout?.split('/').pop()?.split('.')[0] || '';
if (!fileLayout) {
return;
}
// for each key in PluginConfig loop and add the array of imports to the imports array
for (const key in pluginConfig) {
if (Object.prototype.hasOwnProperty.call(pluginConfig, key)) {
if (key === fileLayout) {
const imports: string[] = [];
const components: string[] = pluginConfig[key];
components.forEach((component: string) => {
imports.push(`@/components/${component}/${component}.tsx`);
});
const importsNode = generateImportsNode(imports);
tree?.children.unshift(importsNode);
}
}
}
};
}
```
Which can be used like so:
```
integrations: [
mdx({
rehypePlugins: [[AutoImportComponentsPerLayout, { BaseLayout: ["Button", "Card"], AnotherLayout: ["Button"] } ]]
})
],
```