I’ve been silent for a while because of a confluence of events: an onslaught of urgent tasks at work, several important tasks for A Viral World, a lot of time spent getting to know observability, a full evening of working my way through Go By Example, and, least enjoyable of all, the reoccurrence and brief worsening of a chronic health issue which made me quite miserable for a week. But that’s not to say I didn’t find time for a major refactoring.

The abortive attempt to properly harness the power of markdown-it

I’ve written previously about how I built my responsive images solution using PostHTML:

Problems aside, I’m happy to report I can now write this in my entries:

<image-link key="emacs-rust-syntax-highlighting.png" alt="An Emacs buffer showing a properly
highlighted Rust file." />

And have it automatically turned into this:

<a class="image-link" href="/assets/images/emacs-rust-syntax-highlighting.png"><img class="image-image" alt="An Emacs buffer showing a properly
highlighted Rust file." src="/assets/images/emacs-rust-syntax-highlighting.400w.png" srcset="/assets/images/emacs-rust-syntax-highlighting.400w.png 400w, /assets/images/emacs-rust-syntax-highlighting.800w.png 800w" loading="lazy" sizes="(min-width: 32em) 75vw, (min-width: 64em) 28em, 50vw"></a>

I later extended it to image groups and added sic and ellipsis tags in the same way. It worked, but it was delicate. I had to be very careful not to add any whitespace in the wrong places, I had to put the image tags in their own paragraph and surround them with <p></p>, and if I accidentally used self-closing tags like <sic/> instead of <sic></sic>, it would swallow the rest of that block. What’s more, because PostHTML’s API isn’t too flexible and doesn’t provide context, every single new tag was another full tree traversal after the Markdown stage.

I was using PostHTML to generate heading permalinks as well, so I was quite pleased a week and a half ago when someone on the Eleventy Discord linked a post about automatically generating them. I followed the link to Amber Wilson’s post on the subject of accessible anchor links. The two together naturally led to my replacing the PostHTML processing with markdown-it-anchor. I found myself wanting to customize it, so I started writing a rendering function. That was when I remembered I really didn’t want to try to make sense of the markdown-it API.

Rewrite! Rewrite! Rewrite!

In the middle of all this, I found remark. It seemed to offer a polished, composable API that was uniform across several kinds of markup languages. It had a directory of what also looked like polished, composable, and well-maintained plugins. It was supported by Eleventy. Best of all, there was a remark-directive plugin that provided a simple, consistent syntax and API for custom elements!

I removed markdown-it, installed remark and the plugins, and began rewriting my code. I almost gave up a few hours in because Eleventy doesn’t support ES Modules, which are the sole form of packaging for several core remark & rehype modules, but the esm shim module has a major outstanding bug. There’s even a longstanding pull request to fix it. Fortunately, Agoric had published the fixed code in their fork (which I felt more confident using than the branch in the aforementioned PR) and I was able to get everything to work.

remark is fairly simple to use in and of itself. I quickly converted my site. Writing the custom directives, in contrast, took a while, because the ‘Introduction to syntax trees’ document is ‘not yet written’.[1] I wasted hours trying to understand what data I needed to return where. I still don’t know where to use data.hProperties, data.hChildren, etc. and where to use properties, children, etc. It looked like a jumble to me, and all I could do was try first one then the other until everything worked. Perhaps the rule is that the when you add an HTML element to a tree under a regular element, that HTML element must have hProperties &c. but its children needn’t. Then again, even in those cases I sometimes needed hProperties and sometimes needed properties, and I always needed hName. There needs to be more guidance.

Nevertheless, as always, an hour or two of trial & error combined with the documentation and a few existing plugins resolved my problems. I was delighted to be able to use this much simpler syntax:

:::imagegroup
  ::image[Old homepage]{key="20210503/homepage-before.png"}

  ::image[New homepage]{key="20210503/homepage-after.png"}
:::

Instead of this mess:

<p><image-link key="20210503/homepage-before.png">Old homepage</image-link><image-link
key="20210503/homepage-after.png">New homepage</image-link></p>

I wouldn’t even need the :::imagegroup directive if there were an equivalent to markdown-it-attrs, but I don’t mind too much since it helps to, well, group the images.

The few other customizations I made were much easier. Now I can write this:

> :ellipsis{} and said STahp! :sic{}

::video{controls="controls" loop="loop" src="/assets/media/20210503/orphan-prevention.mp4" type="video/mp4"}

To produce this:

<blockquote>
  <p><ins class="editorial">[…]</ins> and said STahp! <ins class="editorial">[sic]</ins></p>
</blockquote>
<video controls="" loop="">
  <source src="/assets/media/20210503/orphan-prevention.mp4" type="video/mp4">Sorry, your browser doesn’t support embedded videos.
</video>

(The {} after the inline directives isn’t required but it helps disambiguate them.)

Nothing is perfect

When I first started using remark, I noticed it slowed my builds down from roughly 50ms per file to 250ms, or from 8 to 50 seconds overall. Through the process of elimination, I deduced the culprit was remark-prism. A little console.time magic in node_modules[2] showed this was because of re-creating the highlighter for each call. I submitted a pull request to allow it to be reused. This brought the time back down to normal.

Another minor nuisance is that remark-prism wraps the output in <div class="remark-highlight">. At first I thought this might let me put things like the language, a copy button, and other metadata next to the code, but the div is just a useless wrapper, so I wrote a small rehype-unwrap plugin to remove it.

I wanted to use remark-gfm for the tables and strikethrough, but it has an issue with blockquotes, so I’m using the micromark strikethrough plugin directly. Considering the remark-gfm author’s explanation, I’m better off not using the full module anyway.

Another problem I faced was that any raw HTML in my Markdown was being removed, so I couldn’t use things like <kbd> and <q>. Since I control the content, I want all the HTML to be reproduced verbatim. I switched to using remark-rehype directly with allowDangerousHtml, but that wasn’t enough—I also had to tell rehype-stringify the same thing.

Miscellany

After all my adventures in monitoring and Go, I have exciting ideas for implementing auto-archival and sending Webmentions (when I have a few moments).[3] I’ll reuse existing code for the underlying logic; the exciting part is that I’m going to do it in a complex way as an excuse to try new things.


  1. I see now that there’s a syntax-tree/unist repository with documentation.
  2. Easy temporary modifications to library code are my favourite thing about Node.
  3. Testing suggests Archive.org’s rate limiting will be a major bottleneck for auto-archival.

Next in series: (#14 in Colophon: Finding A Place For My Head)