Custom markdown components with remark and web components

published

Recently, I ported my portfolio (again) to Astro from Hugo. While I really liked Hugo, I missed the choice JS gave me (plus, I was bored) so here we go! gif

Porting Data

Everything is markdown in Hugo (mostly), so transitioning to and from was pretty simple; just import the data, build the collections and we’re off to the races! The one thing I had to deal with was importing the cover images for the opengraph images, but that wasn’t hard and went on without many issues.

Where are my shortcodes ?

One thing that I really like about Hugo is how it handles shortcodes. This is how I could generate a stackblitz embed using Hugo: {{<stackblitz url goes here>}} That line right there would then be mapped to a component called stackblitz where I could describe the html element and I was done! But unfortunately, Astro does not handle shortcodes (or similar) natively, so I had to get creative.

Astro uses remark and rehype to transform your markdown collections into html.

I’ve already written about the unified ecosystem, you can checkout that article here

That means that we can hopefully build something within this pipeline and recreate the feeling of shortcodes.

Enter Remark Directive

Turns out we (kind of) can! There’s a remark plugin called remark-directive that enables remark to parse directives (basically shortcodes, but maybe more powerful). With this plugin, we can then write a custom function that looks for these directives and “does something with them”.

What do we do with a remark directive ?

Let’s take a small look at the source of one of my articles:

Astro uses [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/rehypejs/rehype) to transform your markdown collections into html.

:::callout(type="info")
I've already written about the unified ecosystem, you can checkout that article [here](/blog/markdown-editor)
:::

The thing with the 3 : is a remark directive. When I traverse the tree of the article (an abstract syntax tree describing the repo), I can check whether the node I’m looking at is a directive. If I find one, I can then check the type of directive (there are 3) and finally get its attributes.

For example, the following snippet

::stackblitz{#mg-fbr-express}

Contains a node that is a leafDirective with an id of mg-fbr-express

the attributes section of the directive supports multiple elements separated by a whitespace like this: ::codesandbox{projectType="devbox" projectid="cwmtww"}

Getting the node’s attributes is pretty straightforward from there: they’re in

node.attributes

And now we have everything we need to create our custom elements! gif

Web Components

Web Components are not new (they have been baseline available since 2021) but I don’t think I’ve ever seen them much out in the wild. They have lots of advantages and will work pretty well in this use case, but they also come with some drawbacks.

The Good

Web components, as stated above, are pretty easy to get up and running and put in a page. Here’s the code for my youtube embed web component:

class Youtube extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this.render()
    }

    render() {
        const iframe = document.createElement('iframe');
        iframe.src = `https://www.youtube.com/embed/${this.getAttribute('videoid')}`;
        this.innerHTML = `<div style="width: 100%; min-height: 500px;"><iframe class="w-full min-h-[500px]" src="https://www.youtube.com/embed/${this.getAttribute('videoId')}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe></div>`;
    }   
}

customElements.define('wc-youtube', Youtube)

This little snippet does all the heavy lifting to generate an iframe tag with the provided videoid and all the necessary styles and (potentially) logic.

Notice the last line; that’s where we tell the browser that there’s a new valid tag (called wc-youtube) that is represented by the Youtube class.

gif

The Bad

Well, if web components are so nice, why is everyone bothering with frontend frameworks ?

Well, because it’s not all roses. There are some design principles that you need to take into account if you want to use web components. If I had to choose one thing that “grinds my gears” about web components is that they are encapsulated; while this has some benefits in my use case this caused quite some headaches.

I tried for quite some time to have TailwindCSS (which I’m using to style this whole website) pick up and generate the styling required by the web components. Because of this encapsulation (called the shadow DOM, by the way) this proved pretty hard - I probably would have needed to spend too much time looking into it (maybe next time?).

The Compromise

So, what did I do ? I simply rewrote the styles in plain css. I didn’t need that much styling to begin with, so why bother ?

Should you do this?

Honestly I don’t know; this “stack” fit my use case pretty well and is, by definition, portable (I could use these web components on basically any website). Could I have found another solution? Probably. Was this fun? Kind of. Was this interesting? Definitely.