Roma’s Unpolished Posts

Astro MDX Components

Published on:
Categories:
Astro, MDX
Current music:
Sharon Murphy — Can’t Catch Polly
Current drink:
Cold Yunnan tea

Today’s post is sponsored by me not knowing what I’m doing. In other words: fiddling with how my blog is built.

As I mentioned in the first post here, I’m using Astro framework and MDX for building everything.

Given Astro is a relatively new framework, I struggled to find solutions to some probably simple things I wanted to do with it.

In this post, I’ll document what I did find working for me, and maybe this could be of any assistance to others also struggling with Astro & its MDX integration.

Providing MDX Components by Default

The biggest reason I like MDX is because it allows a straightforward way of augmenting your markdown, as it is possible to provide your custom components for every HTML tag it will output.

The best part about MDX with Astro is that it builds everything statically by default, and if we provide any custom components to the underlying MDX, they’d be just rendered as plain HTML (unless we’d enable any dynamic islands).

I want to do numerous things with these custom components, and, the important part: I want to always use them.

The default way of applying custom components to MDX is by first importing right into your .mdx file, and then exporting them back as a components object. This is described in the Astro docs: Assigning Custom Components to HTML elements, and it provides this code snippet as an example:

import Blockquote from '../components/Blockquote.astro';
export const components = {blockquote: Blockquote}

> This quote will be a custom Blockquote

And that’s how I did do it, for every blog post… Not ideal. Is there a better way? When I was looking for it, most suggestions were to somehow pass components via remark plugins, but I’m not entirely sure if that would work with Astro. If it is up to me, I’d better not enter the plugins’ territory unless I really have to.

Finally, after a few attempts to find a solution, I found out that it is possible to provide custom components, but only if you’re using the Content Collections feature.

Basically, when we render a content collection, the final step to render the content to HTML is to do something like this (reduced snippet from the Rendering content to HTML section):

---
const { Content, headings } = await entry.render();
---
<Content />

What I did not find in the docs is that that dynamic Content component accepts a components prop!

That means that all I need to do to provide my default custom components is first import them and create a components object:

import Link from '@components/mdx/Link.astro';
import Paragraph from '@components/mdx/Paragraph.astro';
import ListItem from '@components/mdx/ListItem.astro';
const components = {
	a: Link,
	p: Paragraph,
	li: ListItem,
};

And then pass this object to Content:

<Content components={components} />

And that’s it — now every page rendered this way would automatically apply these components.

Accessing the Slots Content

Now that I have an ability to override the components, one thing that I want to do inside of them — access the content that is passed to them. Then, either do things based on what is inside, or maybe even modify the content. Why did I want this? Perhaps I’ll write about this one day!

In more traditional frameworks like React or Preact, we’d have the children in the props, which we could analyze or modify in some way.

But can we do something similar with .astro components? I did not find this ability initially, so used a bit hacky Preact solution for a while, but I was not happy with the solution I had.

After another attempt to go through the docs, I, finally, got to the solution: Astro.slots.render(). Here is the code example from this doc:

---
const html = await Astro.slots.render('default');
---
<Fragment set:html={html} />

The “default” slot is what would be in every MDX component, so now we can do anything with this html: it is just a rendered string with HTML inside. We could look inside of it, modify, and then pass further into any element via set: html Astro prop.

That’s It

Now I have an ability to do almost anything with the MDX components, without the need to include them every time I write a blog post. And also an ability to just use Astro components without any additional frameworks when all I want is to modify the final generated HTML.

Please share your thoughts about this on Mastodon!