Backlinks in Eleventy

evergreen
Written on Updated on

Tagged with eleventy, code, guide, and pkm

Backlinks show the connections between notes by indicating which notes reference others, and are a fundamental feature of most personal knowledge management systems and digital gardens.

This guide details how to add backlinks to Eleventy. I've also written a guide on how to do this in Astro.

1. Get the notes

Let's begin by getting our notes. In your Eleventy configuration file, create a new collection. I found filtering by glob the most reliable method of getting my notes:

eleventyConfig.addCollection('backlinks', async (collection) => {
  const notes = collection.getFilteredByGlob('src/notes/*.md');
}

This glob returns a collection of Markdown files stored in the src/notes/ folder. Adjust this to match your collection path and whatever file format(s) you use.

2. Extract links from the note content

Next, we need to find and extract any links in our notes. Regex is useful here, as Markdown links follow a consistent format, [text](url).

We'll also create an empty object to hold the links our regex finds.

const linkRegex = /\[.*?\]\((.*?)\)/g;
const allLinks = {};

Now we need to iterate over each note, checking the body for links. We can get the note's body by using Eleventy's supplied page.rawInput data:

notes.map((note) => {
  const content = note.page.rawInput;
  const links = [...content.matchAll(linkRegex)];
});

Note: My previous iteration used the internal Eleventy read() function (discovered by following an error message into the Eleventy codebase). It worked, but on Mastodon, I was informed that using page.rawInput exists and frankly it's a much simpler way of accessing the content.

Each of these matched links comes with a few properties, but we want the URL of the link itself, which is the second property. Remember, this is zero-indexed, so we'll use link[1] to get it.

Within the notes.map() method, iterate across each note's links and extract the link URL:

if (links) {
  links.forEach((link) => {
    const linkUrl = link[1];
  }
}

We need to populate our allLinks object with each extracted link. First, we'll check the link doesn't already exist in the object, and initialise it with an array if not, then push some data into the array. We also want to add a guard to ensure duplicate links aren't being added to the array (which can happen if a note references another note multiple times). We can add this check with Array.some().

Within the links.forEach() loop, add the following:

if (linkUrl) {
  if (!allLinks[linkUrl]) {
    allLinks[linkUrl] = [];
  }

  const alreadyLinked = allLinks[linkUrl].some(item => item.url === note.url);

  if (!alreadyLinked) {
    allLinks[linkUrl].push({
      title: note.data.title,
      url: note.url
    });
  }
}

Here, we're extracting data from the note, and from the note's frontmatter with the note's data object. Adjust this to match your note frontmatter and the data you want to extract.

Finally, we just need to return the links. At the end of the backlinks collection function, add the following:

return allLinks;

3. Check for the current note

Now we have a collection containing all the links in every note, we need to cross-reference them with the current note's URL. We can get this by using Eleventy's supplied page.url data.

I created a new Nunjucks component at this stage, backlinks.njk. At the top, add a new variable,

{% set currentUrl = page.url | lower | replace(r/\/+$/, "") %}

This normalises the current note's URL by making it lowercase and removing any trailing slashes. Then, we can populate a new variable with any matching entries in our backlinks collection:

{% set links = collections.backlinks[currentUrl] %}

This gives us an array of backlinks for each note.

4. Generate HTML

With this at the top of the backlinks component, we can add some HTML, iterating over the links array to populate a list of backlinks for each note.

{% if links %}
  <ul>
    {% for link in links %}
      <li>
        <a href="{{ link.url }}">{{ link.title }}</a>
      </li>
    {% endfor %}
  </ul>
{% endif %}

5. Final code

Here's the complete code.

In eleventy.config.js:

eleventyConfig.addCollection('backlinks', async (collection) => {
    const notes = collection.getFilteredByGlob('src/notes/*.md');
    const linkRegex = /\[.*?\]\((.*?)\)/g;
    const allLinks = {};

    notes.map((note) => {
      const content = note.page.rawInput;
      const links = [...content.matchAll(linkRegex)];

      if (links) {
        links.forEach((link) => {
          const linkUrl = link[1];
          
          if (linkUrl) {
            if (!allLinks[linkUrl]) {
              allLinks[linkUrl] = [];
            }
            
            const alreadyLinked = allLinks[linkUrl].some(item => item.url === note.url);
            
            if (!alreadyLinked) {
              allLinks[linkUrl].push({
                title: note.data.title,
                url: note.url
              });
            }
          }
        });
      }
    });

    return allLinks;
  });

In backlinks.njk:

{% set currentUrl = page.url | lower | replace(r/\/+$/, "") %}
{% set links = collections.backlinks[currentUrl] %}

{% if links %}
  <ul>
    {% for link in links %}
      <li>
        <a href="{{ link.url }}">{{ link.title }}</a>
      </li>
    {% endfor %}
  </ul>
{% endif %}

By adding this component to our note layout file, backlinks will start appearing as we interlink our notes.

This is exactly how I've implemented backlinks into my own digital garden.