withastro / roadmap

Ideas, suggestions, and formal RFC proposals for the Astro project.
292 stars 29 forks source link

RFC: MDX components in layout component #285

Closed kylebutts closed 1 year ago

kylebutts commented 2 years ago

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;

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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!

kylebutts commented 1 year ago

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