Skip to content

RFC: MDX components in layout component#285

Closed
kylebutts wants to merge 1 commit into
withastro:mainfrom
kylebutts:main
Closed

RFC: MDX components in layout component#285
kylebutts wants to merge 1 commit into
withastro:mainfrom
kylebutts:main

Conversation

@kylebutts
Copy link
Copy Markdown

@kylebutts kylebutts commented Sep 5, 2022

Summary

Include custom MDX components (e.g. h1-6, p, ol, etc.) in layout components.

Example

If the proposal involves a new or changed API, then;

  • include a basic code example; otherwise,
  • omit this section if it's not applicable.

Motivation

Currently, if you want to have a set of custom components for your MDX documents, you need to import (and export) them at the start of each file:

import H1 from "../components/H1.astro"
import Paragraph from '../components/Paragraph.astro';
import Blockquote from '../components/Blockquote.astro';
export const components = { h1: H1, p: Paragraph, blockquote: Blockquote };

This either (i) creates a lot of boilerplate that needs to be copied to each new file, (ii) style content (in a somewhat constrained way) in the layout component by using css with tag selectors, or (iii) requires programming a custom router page like:

---
// src/pages/[slug].astro
import path from "node:path";
import Layout from "../layouts/Layout.astro";
import Heading from "../components/Heading.astro";

export async function getStaticPaths() {
  const posts = await Astro.glob('../content/*.mdx');
  return posts.map(post => ({
    params: { slug: path.parse(post.file).name },
    props: post,
  }));
}
---

<Layout>
  <Astro.props.default components={{ h1: Heading }} />
</Layout>

Detailed design

I propose to include a components option in a layout component's frontmatter. Currently, Astro already implements a layout option for MDX in the frontmatter, which seems like a natural place to bundle both the structure of the page and the components that would be rendered with MDX.

For example, an API could be something like:

---
// src/layouts/MdxLayout.astro
import BaseLayout from './BaseLayout.astro';
import H1 from "../components/H1.astro";
import Paragraph from '../components/Paragraph.astro';
import Blockquote from '../components/Blockquote.astro';

export const components = { h1: H1, p: Paragraph, blockquote: Blockquote };
---

<BaseLayout>
  <slot />
</BaseLayout>

Then, when the MDX uses the layout, these components could be used to render the appropriate tags.

I'm not exactly sure how to implement this feature. It seems like the way layout components are used is in a rehype plugin (which occurs after MDX processes the file). It could be possible to inject the import and export code during this step but that might be difficult in the proposed API since the exported components object is the actual components, and not strings. I suspect doing it similar to the way the custom router above does it is the best way, but I really don't know enough about the underlying code powering Astro to say exactly what the best strategy would be.

Drawbacks

One potential drawback is that this is an MDX specific feature, which could be confusing if for example, people expect their HTML tags (e.g. <h1>) to be transformed in their .astro pages when using their <MdxLayout> layout. That of course could be mitigated with proper documentation of the feature.

Alternatives

Three alternatives were described above:

(i) Import and export components in each mdx file.

(ii) Style content (in a somewhat constrained way) in the layout component by using css with tag selectors. For example,

---
// src/layouts/MdxLayout.astro
---

<style>
h1 {
  font-size: 1.5rem;
  margin-bottom: 0.75rem;
}
</style>

<slot />

This way works well for styling, but doesn't allow you to do any processing or wrap in custom components.

(iii) requires programming a custom router:

---
// src/pages/[slug].astro
import path from "node:path";
import Layout from "../layouts/Layout.astro";
import Heading from "../components/Heading.astro";

export async function getStaticPaths() {
  const posts = await Astro.glob('../content/*.mdx');
  return posts.map(post => ({
    params: { slug: path.parse(post.file).name },
    props: post,
  }));
}
---

<Layout>
  <Astro.props.default components={{ h1: Heading }} />
</Layout>

Adoption strategy

Unresolved questions

I'm not sure what the best path forward in actual implementation of this feature which is obviously an important question to get right.

@wassfila
Copy link
Copy Markdown

wassfila commented Dec 22, 2022

in the example, there is a glob for all of the content, '../content/*.mdx'. It is rare that markdown has one .astro file per .md file, so simply shifting the components from where its used to the layout, might not improve visibility.
Also, the layout, is no special component, but any component can have slots. The interesting concept here is how to create a context for child slot component without having to pass them in each step (avoids prop drilling), and that could be solved in a generic way not just for a specific component variable.

@kizu
Copy link
Copy Markdown

kizu commented Feb 11, 2023

Want to +1 here, and also mention another flawed alternative: it is possible to bulk export the whole components object with all the component associations, making it a single line you'd need to use in your .mdx:

export { components } from '../components/index.js';

This works for regular components, but, playing with the code a bit (just starting with astro, so might be mistaken) there seems to be no way to create an index file like this and export multiple astro components into it? So this method would work only for non-astro components, and the main issue with this is that it won't then be possible to use client directives for those components.

Having an easy way to export a list of mdx components from a layout component would be a very welcomed feature!

@natemoo-re
Copy link
Copy Markdown
Member

Thanks for opening this proposal @kylebutts! Sorry it took us so long to respond.


With the new Content Collections feature, Astro is moving away from the magic layout pattern.

While it's still possible to use, we'd recommend that new projects use Content Collections and declaratively pass their components to the Content component as a prop.

We're hopeful that this pattern addresses the limitations of the previous pattern that this proposal was attempting to solve. If you'd like to continue this discussion, we encourage you to open a new Stage 1 Proposal and link back to this proposal!

@natemoo-re natemoo-re closed this Mar 23, 2023
@kylebutts
Copy link
Copy Markdown
Author

Thanks @natemoo-re; I think Content Collections works wonderfully!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants