Two posts ago we changed our privacy policy and said the change was in the commit. That was true and nearly useless, because the repository is private and a reader cannot open a commit. This post makes the history part of the page. Every legal page on the site now ends with a list of every change to its text since it was first published: the date, a one line summary, and the exact lines that moved, which a reader can open and read without trusting anybody.
Policy as code
A privacy policy is a promise about behaviour, and behaviour changes. The usual way to handle that is a line at the top saying "last updated" followed by a date that someone is supposed to remember to change, and a sentence further down encouraging you to check back periodically. Our policy had both. Neither tells you what changed. A date tells you that something did, and you are left to diff it against your memory.
Our legal pages are markdown files in the same repository as the rest of the site, which means they have the same history as the code: every change is a commit with a date, an author, a summary and a diff. That history already existed. The only work was to bring it to the page.
The change
A short script runs before every build. For each legal page it asks git for the commits that touched the file, following the file through the one move it has made, and for each commit it keeps the date, the summary, and the diff of the text with the headers stripped:
git log --follow -p --unified=2 -M --format=<hash><date><subject> -- content/legal/privacy-policy.md
Commits that moved the file without changing a word are dropped, because a move is not a change a reader needs to see. The result is written to a JSON file the page reads. The page shows the newest date at the top as "last changed", and a section at the bottom with one entry per change, each a disclosure that opens to the diff, additions in green and removals in red.
The hand kept updated field came out of the frontmatter and out of the schema, because a date that is derived from the history cannot drift from it, and a date someone types can. The sitemap, which had been reading that field, now reads the same history file, so the date a crawler sees for a legal page is the date its text last changed. And the three sentences that promised to update a "last updated" date now say what actually happens: every change is listed at the bottom of the page.
What the history shows
The privacy policy has changed three times since it came into the repository in June. The first entry is its arrival. The second is the counter, twelve lines added and two removed, in the same commit as the script that needed them. The third is the inventory, forty nine lines added and three removed. Open the second one and the diff shows the sentence that said we used no analytics being replaced by the sentence that says what we count, in the same change that started counting. That is the promise from post seven, kept in a form a reader can check.
The terms of service show a change from the twenty second of August adding seven lines and removing none. That is the commit that fixed the bold labels, and it is in the list because it changed the file, which is the right answer: the history shows what changed, not what we think was important.
The part we could not know from a laptop
The script reads history from the checkout the build runs in. On a laptop that is the whole repository. On the host, a build starts from a fresh clone, and hosts often clone shallow, one commit deep, to save time. A shallow clone would give every page a history of one entry, today, and a changelog that lied by omission on its first day.
So the script checks whether the checkout is shallow, tries to deepen it, and if it cannot, says so in the file it writes, and the page says so too, in a sentence under the heading: the build that made this page could not read the full history. We wrote that sentence before we knew whether it would show.
It did not show. The first build on the host produced the full history, four entries on the privacy policy: the three changes above, and a fourth, the commit that added the changelog, which removed the hand kept date and rewrote the sentence about updates. The changelog lists the change that created it, at the top, with its own diff. What we cannot tell from the page is whether the host's clone was complete to begin with or our deepening step made it so, because the script does not record which path it took. That is a small gap in the evidence and we are noting it rather than guessing.
The number for today: four changes on the privacy policy, each one readable line by line, and no date anyone has to remember to update.

