After having moved from Jekyll to Eleventy, I realized I could extend Liquid in fancy ways to make some things a little easier (or down right possible). In this article, I’d like to share how I built a tiny footnotes plugin with Liquid. If you are not interested in how the sausage is made and just want to use the code, check eleventy-plugin-footnotes for usage instructions.
I have recently blogged about accessible footnotes again and if you haven’t read the article yet, I recommend you do so you fully grasp what comes next. To put things simply, we need 2 things: a way to register a footnote reference within the text, and a way to display the footnotes for a given page at the bottom of a post. Let’s start with the first one.
Registering footnotes
To author a footnote within text content, we use a footnoteref Liquid tag which takes the footnote identifier and the footnote content as arguments (in that order). It looks like this:
Something about {% footnoteref "css-counters" "CSS counters are, in essence,
variables maintained by CSS whose values may be incremented by CSS rules to
track how many times they’re used." %} CSS counters{% endfootnoteref %} that
deserves a footnote explaining what they are.
The Eleventy configuration would be authored like this:
const FOOTNOTE_MAP = [];
config.addPairedShortcode(
"footnoteref",
function footnoteref(content, id, description) {
const key = this.page.inputPath;
const footnote = { id, description };
FOOTNOTE_MAP[key] = FOOTNOTE_MAP[key] || {};
FOOTNOTE_MAP[key][id] = footnote;
return </span><span class="token string"><a href="#</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">-note" id="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">-ref" aria-describedby="footnotes-label" role="doc-noteref" class="Footnotes__ref"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>content<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"></a></span><span class="token template-punctuation string">;
},
);
Here is how it works: when rendering the footnoteref Liquid tag, we retrieve the registered footnotes for the current page (if any) from the FOOTNOTE_MAP map. We add the newly registered footnote to it, and we render an anchor link to the footnote.
Rendering footnotes
For that I created a footnotes.liquid partial which I render at the bottom of the post layout (passing it the current page object), like so:
<article>
{{ content }}
{% include "components/footnotes.liquid", page: page %}
</article>
Now, we need a way to retrieve the footnotes from the page. That’s actually not too easy in Liquid unfortunately since there is no way to inject a global variable or simply assign a function call to a variable. Liquid’s utilities mostly aim at rendering HTML (as shown above) so it’s not too straightforward to return an array.
I played around a few solutions, and eventually landed with a wacky filter. Basically I expose a footnotes filter which expects the page as argument, and returns the footnotes for that page.
{% assign footnotes = '' | footnotes: page %}
This is pretty ugly. We need a value to be able to apply a filter, even though that value can be anything since the filter will just replace it with an array of footnotes.
Here is how it’s defined:
config.addFilter(
"footnotes",
// The first argument is the value the filter is applied to,
// which is irrelevant here.
(_, page) => Object.values(FOOTNOTES_MAP[page.inputPath] || {}),
);
From there, we can render the necessary markup to output the footnotes using a for loop to iterate over each of them.
{% assign footnotes = '' | footnotes: page %}
{% assign count = footnotes | size %}
{% if count > 0 %}
<footer role="doc-endnotes">
<h2 id="footnotes-label">Footnotes</h2>
<ol>
{% for footnote in footnotes %}
<li id="{{ footnote.id }}-note">
{{ footnote.description }}
<a
href="#{{ footnote.id }}-ref"
aria-label="Back to reference {{ forloop.index }}"
role="doc-backlink"
>↩</a
>
</li>
{% endfor %}
</ol>
</footer>
{% endif %}
Wrapping up
So to sum up:
- We have a
footnoterefLiquid tag to wrap footnote references in the text. It takes an id and the footnote description as arguments, and renders an anchor to the correct footnote. - We have a
footnotesLiquid filter which is basically a hacky way to get the footnotes for a given page so it can be assigned onto a variable. This hack is solved by using the plugin. - We have a
footnotes.htmlLiquid partial which get the footnotes for the current page and render them within the appropriate DOM structure. The plugin exposes afootnotesshortcode does that.
That’s about it. Pretty cool, huh? ✨
If you are interested in using these footnotes in Eleventy, check out eleventy-plugin-footnotes on GitHub. There are install instructions, guidelines and examples.