Per-file commit logs with Eleventy

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

We're going to use simple-git to read the commit history and make it available to templates using eleventy's computed data feature.

To generate changelogs for markdown files in the posts collection, we start by creating a data file at /posts/posts.11tydata.js (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 here 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 back to the post's data object.

posts/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 any templating engine to render this data to the page. In Liquid you could 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:

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, so it's a natural solution to the problem.

Notes