Per-file com­mit logs with Eleventy

Using com­puted data and sim­ple-git to gen­er­ate file-spe­cific changel­ogs.

Background

Sometimes it’s a good idea to pub­licly doc­u­ment how a web­site changes over time. I’m think­ing of things like le­gal doc­u­ments, tech­ni­cal writ­ing, pub­lic pol­icy, or any other piece of con­tent you want to be ex­tra trans­par­ent about.

If you’re go­ing to do this, you prob­a­bly want to:

If your con­tent is un­der ver­sion con­trol you’re al­ready do­ing both of these things. Unless you go out of your way, you lit­er­ally can­not change a file with­out cre­at­ing a per­ma­nent record con­tain­ing the diff, your name, the date, and a mes­sage de­scrib­ing the change. And git has ex­tremely good built-in tools to query those records by file, date, au­thor, and other con­tex­tual pa­ra­me­ters.

Why not lever­age that and gen­er­ate changel­ogs for in­di­vid­ual pages di­rectly from the com­mit his­tory?

Eleventy + Simple Git

We’re go­ing to use Simple Git to read the com­mit his­tory and make it avail­able to tem­plates us­ing Eleventy’s com­puted data fea­ture.

Let’s as­sume we want to gen­er­ate changel­ogs for Markdown files in a col­lec­tion called posts. We start by cre­at­ing a data file at /posts/posts.11tydata.js. Note that the file­name must match the name of the col­lec­tion.

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

Creating our data file in­side the /posts di­rec­tory puts it at the end of Eleventy’s data cas­cade, al­low­ing us to read and write data for in­di­vid­ual posts.

We start by read­ing page.inputPath, an auto-gen­er­ated prop­erty that con­tains the path to the Markdown file be­ing processed. Then, we pass that in­for­ma­tion to git.log() to get that file’s com­mit his­tory, and write the re­sult into the post’s data ob­ject.

posts.11­ty­data.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 ob­ject for each post con­tains a list of com­mits to the un­der­ly­ing Markdown file in re­verse-chrono­log­i­cal or­der:

 {
   "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 what­ever tem­plat­ing en­gine we want to ren­der this data to the page. I hap­pen to use Liquid, so I’d write some­thing 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-gen­er­ated changelog for this post us­ing a slightly mod­i­fied ver­sion of the code above:

Notes