Skip to content
Dave Klein

Changelog

Generates a public changelog and a version number from the git log on every push, with overrides, skip rules and name redaction. Changelog is one of the Claude Code skills I wrote for building this site, with the site-specific values turned into placeholders and examples so it works on any project. The skill is the SKILL.md below, exactly as it ships in the zip.

Install

unzip changelog.zip -d ~/.claude/skills

That installs it for every project. For one project, unzip into that project’s .claude/skills/ instead. Either way, Claude Code reads the folder on its next session and applies the skill when a request matches its description:

Generate a public changelog and a version number from the git log, with hand-written overrides for entries that should read better, skip rules for bookkeeping and unlisted routes, name redaction, and a separate unfiltered dataset for development statistics. Runs in the publish pipeline so a push is the only step. Use when a site or product wants a "what changed" page that stays current without anyone writing it by hand.

Inside the zip

  • —changelog/SKILL.md · 5 KB

The skill

A changelog nobody has to write stays current. The trick is treating the commit log as the source and the changelog as an editorial layer over it: most entries come straight from conventional commit subjects, a few are rewritten by hand, some are hidden on purpose, and the rules for all of that live in code next to the generator.

The pipeline

  1. Generate on push, commit the result. The publish pipeline (a pre-push hook or the push script) runs the generator and amends changelog.json into the commit being pushed. The site reads the committed JSON. On the host, a shallow clone would produce a near-empty log, so the generator exits early when it detects one (VERCEL, or git rev-parse --is-shallow-repository reporting true) and the committed file stands.
  2. One record per commit: git log --no-merges with a format that separates date, subject, and body using control characters, so multi-line bodies parse cleanly.
  3. Conventional commits map to entry types: feat to feature, fix to fix, style to design, perf to performance, content and docs to content, everything else to infrastructure. A subject that is not conventional keeps its text and counts as a feature.
  4. Normalize the title: replace em and en dashes with commas, sentence-case unless the first word is a camel-case brand (iPhone, macOS), end with a period.
  5. Parse the body: - lines become bullet items under the entry, any other prose becomes the entry’s paragraph. Co-author and session trailers are dropped.
  6. Overrides win. A JSON map keyed by the exact raw subject supplies friendly copy for entries that should read better than their commit message. Add a line whenever an entry reads like engineering shorthand.
  7. Skip what is not news. Two lists: regex patterns for whole categories (scheduled syncs, [skip ci], work-in-progress commits, version bumps, image optimization, a bookkeeping tally, anything scoped to an unlisted route), and exact subjects for editorial removals (a burst of iteration commits for something later pulled, and the curation commits themselves, which are not public copy either).
  8. Unlinked areas never appear. If the site has a lab or a draft area that is gated from production, an entry that still mentions it after overrides is dropped, and matching body lines are scrubbed from entries that stay. Use a word-boundary match so “label” survives.
  9. Redact names. Commit messages are engineering notes and may credit people; the changelog is public copy and names no third parties. A list of replacements, specific phrasings first, then catch-alls, runs over every title, item, and paragraph.
  10. Collapse same-day duplicates: an amend or a re-commit can leave two identical subjects minutes apart; keep the first per date and title.
  11. A second dataset for statistics. Write every human commit as {date, type} with no editorial filtering, excluding only automated commits (a machine’s scheduled syncs and backups), so a development dashboard counts real work rather than the curated list.

The version number

Bump package.json from the commits being pushed (origin/main..HEAD), highest level wins: a ! after the type or a BREAKING CHANGE footer is major, any feat is minor, everything else is patch. Rewrite only the version line and amend it into the last commit. Show the version in the footer next to a “last updated” link that opens the changelog.

Presenting it

Group entries by day under month headings. Render the current month open and collapse older months into one row each, because the list grows past a thousand entries within a year. Let a month open from the URL (?month=) with replaceState, so a link to a month works and the URL never names a collapsed one. Keep the page static; a changelog that renders per request is the slowest page on the site for no reason.

Habits that keep it honest

  • Write commit subjects as the changelog line you want to read; the override file is for exceptions, not the norm.
  • When you curate an entry away, expect the curation commit to need curating too.
  • A page gated from production cannot be announced; hide its publish commit until launch and let the launch commit speak.
  • Re-run the generator locally after editing any list and read the diff of the JSON before pushing.