Per-file commit logs with Eleventy

Using computed data and simple-git to generate file-specific changelogs.

Background

Sometimes it's a good idea to publicly document how a website changes over time. I'm thinking of things like legal documents, technical writing, public policy, or any other piece of content you want to be extra transparent about.

If you're going to do this, you probably want to:

If your content is under version control you're already doing both of these things. Unless you go out of your way, you literally cannot change a file without creating a permanent record containing the diff, your name, the date, and a message describing the change. And git has extremely good built-in tools to query those records by file, date, author, and other contextual parameters.

Why not leverage that and generate changelogs for individual pages directly from the commit history?

Eleventy + Simple Git

We're going to use Simple Git to read the commit history and make it available to templates using Eleventy's computed data feature.

Let's assume we want to generate changelogs for Markdown files in a collection called posts. We start by creating a data file at /posts/posts.11tydata.js. Note that the filename must match the name of the collection.

 package.json
 .eleventy.js
 _includes/
 posts/
   one.md
   two.md
   three.md
+  posts.11tydata.js

Creating our data file inside the /posts directory puts it at the end of Eleventy's data cascade, allowing us to read and write data for individual posts.

We start by reading page.inputPath, an auto-generated property that contains the path to the Markdown file being processed. Then, we pass that information to git.log() to get that file's commit history, and write the result into the post's data object.

posts.11tydata.js

const git = require('simple-git')();

async function getChanges(data) {

  const options = {
    file: data.page.inputPath,
  }

  try {
    const history = await git.log(options);
    return history.all
  } catch (e) {
    return null;
  }

}

module.exports = {
  eleventyComputed: {
    changes: async data => await getChanges(data)
  }
}

When we run Eleventy now, the data object for each post contains a list of commits to the underlying Markdown file in reverse-chronological order:

 {
   "title": "My Page Title",
+  "changes": [
+    {
+      "hash": "0cd158fc81a4d3aefd52e6f416542d3549ef4b4e",
+      "date": "2022-03-19T22:46:53+01:00",
+      "message": "This is the latest commit",
+      "refs": "ori­gin/​mas­ter, ori­gin/​HEAD",
+      "body": "",
+      "au­thor_­name": "Max Kohler",
+      "author_email": "hello@maxkohler.com"
+    },
+    {
+      "hash": "0cd158fc81a4d3aefd52e6f416542d3549ef4b4e",
+      "date": "2022-03-19T22:46:53+01:00",
+      "message": "This is another commit",
+      "refs": "ori­gin/​mas­ter, ori­gin/​HEAD",
+      "body": "This one has an extended description",
+      "au­thor_­name":"Max Kohler",
+      "author_email":"hello@maxkohler.com"
+    }
+  ]
 }

We can now use whatever templating engine we want to render this data to the page. I happen to use Liquid, so I'd write something like:

_includes/post.liquid

{% if changes %}
<ul class="changes">
  {% for c in changes %}
  <li class="change">
    <time class="change__time">{{ c.date }}</time>
    <h3 class="change__title">{{ c.message }}</h3>
    <span class="change__hash">{{ c.hash }}</span>
  </li>
  {% endfor %}
</ul>
{% endif %}

Demo

Here's the real, auto-generated changelog for this post using a slightly modified version of the code above:

Notes