Maintaining working local links in Eleventy



This is the second post detailing the different customizations I've implemented in Eleventy for this site. In the first post I briefly described setting up code syntax highlighting, which primarily involved installing and configuring an existing plugin. Another tweak I really enjoy is the way links are handled, and for that I had to write some code off my own! I've actually customized both internal and external links, each in different ways, but let's focus on the internal ones first.

Internal links

As I wrote in my earlier introduction to Eleventy, I very much appreciate the clean separation of content and presentation that Eleventy and many other static site generators provide. I write all content for this site in Markdown files, and ideally I would like to keep the contet free of any "knowledge" about the site. I've even considered managing it completely separately from the site's codebase, perhaps in Obsidian? This is mostly out of principle, but it also ensures that the content can easily be transferred to a different tool in the future, should the need arise. Having page metadata in the content front matter is very convenient however, and for convenience I'm willing to bend my principles slightly. I guess I'm more pragmatic than idealistic in the end.

As I was getting to know Eleventy, the only thing I was slightly disappointed by was the fact that local links between Markdown files aren't automatically transformed into their "online" format during page rendering. For example, a post called /posts/frogs.md will be rendered into a HTML page in the file /posts/frogs/index.html by default, and should be linked to as /posts/frogs/, but any Markdown links to this file remains unchanged as /posts/frogs.md. Consequently, for the links to function correctly on the rendered site, they must use the online format in the source files. This introduces precisely the kind of website-specific knowledge into the content that I want to avoid. It also means I have to make a choice between having functional links on the live site or within the Markdown files. It's an easy choice of course, but ideally such a compromize shouldn't have to be made. Additionally, having proper local links in the source files means text editors can help with autocompletion while writing them, which can be very helpful for long paths.

I decided to try to do something about this before filling up the site with too much content. First I reverted all the local links back to their original form, i.e. /posts/frogs.md in the example above. I then implemented an Eleventy transform to convert these into the appropriate online format. In Eleventy, a transform is a function that gets called after each page is rendered, allowing for post-processing modifications. They are typically configured in .eleventy.js:

const LinkTransformer = require("./_scripts/linktransformer");

module.exports = function (eleventyConfig) {
    eleventyConfig.addTransform("updateLocalLinks", function (content, outputPath) {
        return new LinkTransformer().transformLinks(content, outputPath);

    // Further configuration ...

    return eleventyConfig;

LinkTransformer is a small class I wrote for this task. It utilizes Cheerio to traverse and modify the rendered HTML content, and replaces all local links ending in .md with the ones ending in /:

const cheerio = require("cheerio");

class LinkTransformer {
    transformLinks(content, outputPath) {
        // Only transform HTML pages
        if (!outputPath?.endsWith(".html")) {
            return content;

        const $ = cheerio.load(content);

        // Iterate over all <a> tags
        $("a").each((_, link) => {
            const href = $(link).attr("href");

            // Replace '.md' with '/' at the end of the href
            $(link).attr("href", href.replace(/\.md$/, "/"));

        return $.html();


Being relatively new to Eleventy, I'm not entirely sure if this approach is the most straightforward, but no one would be happier than me if it turns out I've simply overlooked an obvious, built-in solution. In any case, I'm quite pleased with the results. Now I can follow links between my posts both locally in my text editor and online on the published site. It would be interesting to hear what the author of Eleventy, Zach Leatherman, would think about this modification. From a design principle standpoint, I believe maintaining functional local links is a better approach.

Zach replied to me on Mastodon pointing out that this is an old issue in Eleventy and that he'll get it resolved in the upcoming 3.0.0 release 🤩

Next up I'll describe how I modified the tiny LinkTransformer class to also deal with external links, so stay tuned for that. I've also set up a page with all my Eleventy posts for those who want an easy overview of my Eleventy journey.

Another update
Zach has now implemented the solution to the aforementioned issue, and as of Eleventy 3.0.0-alpha5 you get this functionality as part of the package. I've written a follow-up post on how to use it.


Out of curiosity I tried opening my posts directory in Obsidian, and to my surprise it presents the front matter in a very pleasant way. I just played around with it very briefly, so I can't be a hundred percent sure, but unfortunately it seems Obsidian only wants to autocomplete local links while creating special Obsidian links, so perhaps I'll stick to VS Code for now. Still, it made me even more curious about working with content fully separated from the code in the future.

This post being edited in Obsidian