Dark mode switch icon Light mode switch icon

Eleventy: custom markup alongside item contents

ca. 1200 words

I’ve been working on something big that is scheduled to land on my blog soon and I encountered an interesting problem. I wanted to put a piece of HTML code in a selected place alongside the post content, preferably without client-side JavaScript. An hour later I arrived at a solution I deem good enough, but since it’s now living rent-free in my head and I’m a heartless landlord when it comes to my brain space, here goes.

Setup

A typical use case when this problem pops up involves putting intrusive ads between subsequent paragraphs of the article. In the simplest scenario this is achievable by embedding necessary HTML / JS code directly in the post content. Modern content management systems are usually smart enough to make it a non-issue.

But I run Eleventy, a static site generator, and I have no content management system other than Markdown files, Git repository and a bunch of JavaScript code to make it all look and work like a modern website.

My blog articles are rendered (or supposed to render) in various contexts: on the website, in RSS and JSON feeds, in EPUB / PDF files. Therefore, I didn’t want to insert any extra code between article paragraphs as it would have been useless outside of web browser.

When I asked about it in the fediverse, Robb suggested inserting the code into the content and using a filter to strip it out from contexts other the website.

That was a great call. But there was a better one just around the corner.

Couldn’t we do it the other way round?

Minimum viable solution

So here’s my take.

Inside the content, put a plain ol’ HTML comment with a very specific text inside, such as

<!-- MY_SECRET_TAG -->

Since it’s a regular HTML comment, it’s only visible in the page source. It will display everywhere HTML is rendered, but no fireworks or popcorn should be expected.

Now, we need an Eleventy filter to replace this comment with a our own piece of HTML code. To do so, let’s add the following code to .eleventy.js:

const OUR_MARKUP = `
 <div>Our embedded code</div>
`;

module.exports = function (eleventyConfig) {
  eleventyConfig.addFilter("mySecretTag", function(content) {
    return content.replace('<!-- MY_SECRET_TAG -->', OUR_MARKUP);
  });
}

Now, in the template file that corresponds to the context in which our embedded code is supposed to run, find the expression that renders the content of our item, like this:

{{ content | safe }}

Add the filter:

{{ content | mySecretTag | safe }}

Aaand we’re done! Our mySecretTag filter should replace <!-- MY_SECRET_TAG --> with HTML code we defined in OUR_MARKUP variable.

Of course I have enough integrity and I didn’t put this much effort to stuff my blog with ads. So let me present my real use case for all this magic.

Usage scenario: table of contents for article series

Imagine we want to publish a series of interconnected articles designed to be binge-read as a single entity, like a season of our favorite Netflix show. Each of the articles contains no less than a few thousand words and a fat footnote section. That means our imaginary reader can be expected to read all of the articles in the series, but they are likely to skip most of the footnote section.

As a publisher on my blog I want my readers to keep reading. But since my blog is designed to display navigation links at the very bottom, I don’t want to force people to scroll through kilometres of boring footnote sections just so that they can click ‘next article’.

Additionally, since that article series consists of a few articles, we want the reader to always know how much they have read so far and how many parts they have left.

How about a table of contents that shows an arrow and a text “you are here” next to the part we’re in?

Entirely doable with what we know so far.

Let’s pick up from the place we ended up in the previous section. We still have mySecretTag filter and a bunch of HTML code to be used.

The awesome thing about Eleventy filters is that they accept arguments that can be used for altering their behavior. We’re going to use that to highlight the active article in our table of contents.

Let’s write new code for our table of contents:

const OUR_MARKUP = `
  <div style="background: #ededed; padding: 16px">
    <h2>Sample blog post series</h2>
    <ul>
      <li>
        <a href="/series-episode-1/">
          Post series: Episode 1
        </a>
      </li>
      <li>
        <a href="/series-episode-2/">
          Post series: Episode 2
        </a>
      </li>
      <li>
        <a href="/series-episode-3/">
          Post series: Episode 3
        </a>
      </li>
    </ul>
  </div>
`;

Now, we need a way to know which of the three articles is being read. This can be done in a few steps.

First, let’s add a new property to front matter of each post of the series, like this:

---
title: "Post series: Episode 1"
permalink: /series-episode-1/
date: 2024-01-20 19:20:00
seriesPart: 1
---

Post content

The seriesPart property will be visible in our post template and we can pass it to our mySecretTag Eleventy filter. Let’s do this:

{{ content | mySecretTag(seriesPart) | safe }}

We still have two more things to do. First, let’s change OUR_MARKUP variable into a function that accepts selectedPart argument. This is where our seriesPart property will be passed. Based on it, we’re going to conditionally display the text '<-- you are here! next to the active link:

const generateMarkup = (selectedPart) => `
 <div style="background: #ededed; padding: 16px">
   <h2>Sample blog post series</h2>
   <ul>
    <li>
      <a href="/series-episode-1/">
    Post series: Episode 1
      </a>
      ${selectedPart === 1 ? '<-- you are here!' : ''}
    </li>
    <li>
      <a href="/series-episode-2/">
    Post series: Episode 2
      </a>
      ${selectedPart === 2 ? '<-- you are here!' : ''}
    </li>
    <li>
      <a href="'/series-episode-3/">
        Post series: Episode 3
      </a>
      ${selectedPart === 3 ? '<-- you are here!' : ''}
    </li>
  </ul>
 </div>
`;

And finally, let’s edit our Eleventy filter so that it accepts and passes through the number of active part of the series:

eleventyConfig.addFilter("mySecretTag", function(content, selectedPart) {
  return content.replace('<!-- MY_SECRET_TAG -->', generateMarkup(selectedPart));
});

That’s it! The table of contents should now be displayed alongside the content of each post in our series.

Okay, whatever, show me the code

View demo or check out sample GitHub repo.

This is a pretty naive and straightforward solution that relies on static code provided inside .eleventy.js. I’m not the biggest fan of this approach, as I’d love to do it in more dynamic way instead of hardcoding things. However, with my current time constraints this has to do.

You don’t want to know what time it is when I’m finishing this article. Yeah, that is definitely good enough.

Originally published on by Łukasz Wójcik