A changelog is a product surface, not a git log

Product

There are two kinds of changelog.

The first is generated from commit messages. It contains lines like fix: handle null in resolver and chore: bump deps. It is technically a complete record of what changed, and no user has ever read it twice.

The second is written for the person who uses your product. It says what is now possible that was not possible last week. It is short. People subscribe to it.

The difference is not tooling. It is that someone decided the changelog was part of the product.

Type your entries

The single highest-leverage change is giving every entry a type. Kookee uses seven:

feature · fix · improvement · breaking · security · deprecated · other

This is not decoration. It is what makes the changelog filterable, which is what makes it useful to more than one audience:

TypeScript
// What's new — for users
const features = await kookee.changelog.list({
  filter: { changelogType: 'feature' },
  limit: 10,
});

// What will break — for integrators
const breaking = await kookee.changelog.list({ filter: { changelogType: 'breaking' } });

An engineer integrating your API cares about breaking and deprecated and nothing else. A user cares about feature. A security team wants security on its own page. One typed list serves all three; one flat list serves none of them well.

The type is a structured field rather than a column on the entry, which matters when you render it. entry.type is the string 'changelog'; the release type comes out of the fields array:

TypeScript
import { fieldOptionKey, fieldStringValue } from '@kookee/sdk';

const releaseType = fieldOptionKey(entry.fields, 'changelogType'); // 'feature'
const version = fieldStringValue(entry.fields, 'version');         // '1.6.0'

Give every entry a version

A type answers "what kind of change is this". A version answers "do I have it yet", which is the question a user is actually asking when they arrive from a support thread.

Versions are free-text, so they hold whatever scheme you use — 1.6.0, 2024.11, v3-beta. Two habits make them worth having: set one on every entry, and use the same scheme for all of them. A changelog where half the entries have a version reads as an oversight, and it is one.

Filter by an exact version when someone asks what shipped in a specific release:

TypeScript
const release = await kookee.changelog.list({ filter: { version: '1.6.0' } });

Entries come back newest first. If you want strict version order instead — and you probably do for a release page, since dates lie about backported patches and releases that slip — sort them yourself, because a string sort puts 1.10.0 before 1.9.0:

TypeScript
const { data } = await kookee.changelog.list({ limit: 100 });

const byVersion = [...data].sort((a, b) =>
  (fieldStringValue(b.fields, 'version') ?? '').localeCompare(
    fieldStringValue(a.fields, 'version') ?? '',
    undefined,
    { numeric: true },
  ),
);

numeric: true is what makes 1.10.0 sort after 1.9.0.

Let people react

Every changelog entry supports emoji reactions:

TypeScript
await kookee.changelog.react(entryId, { reactionType: 'fire', action: 'add' });

This looks like a small social feature. It is really the cheapest product feedback loop you will ever build. Shipping ten things a month and seeing which two got all the reactions tells you where to spend the next month — and it costs the user one click instead of a survey.

Write the entry with the release

The practical failure is not writing bad entries, it is writing them a week late, from memory, in a batch. By then nobody remembers why the change mattered, so you get the commit-message changelog by default.

Kookee entries are drafted while the pull request is still open and published when the release ships — no deploy, no rebuild, no waiting on a static site generator. Draft them in the dashboard, or from a coding assistant connected over MCP, which can read the last few entries and write the next one in the same voice, typed and versioned, for you to review.

Then it is a fetch away, wherever you want it:

TypeScript
const { data } = await kookee.changelog.list({ limit: 5 });

Your marketing site, your in-app "What's new" panel, your release email. One source, written once, by the person who actually made the change.