Skip to content
duxt

What is read

Which headings become releases, which become groups, and what happens to the rest.

The parser reads a changelog as a flat run of headings and decides what each one is. Nothing is configured: the file's own shape is the input.

A release heading

A heading is a release when — with any link reduced to the text it shows — what is left is a version, optionally followed by a date in parentheses.

## [1.4.0](https://github.com/acme/app/compare/v1.3.0...v1.4.0) (2026-02-01)
### [1.4.1](https://github.com/acme/app/compare/v1.4.0...v1.4.1) (2026-02-03)
## 1.4.0 (2026-02-01)
## v2.0.0

All four read. The first two are what release-please writes — a minor release at ## and a patch at ### — and the last two are a hand-kept file. The level is not part of the test, which is what lets both forms through.

A link the heading opens on is kept as the release's compare URL — the diff against the release before it, which the page links beside its provenance. It is taken only when it is absolute and http(s): a relative href resolves against the site rendering the changelog rather than the repository the file came from.

The heading's level is read off the body rather than assumed, for the same reason: release-please writes a patch release at ### and its groups at ### as well, so "the shallowest heading in this release" is the only rule that reads both.

A group heading

Every heading below the release's own level is a group, and its name is the file's own — verbatim, untranslated, with no fixed taxonomy behind it.

### Features
### Bug Fixes

release-please writes those two, changesets writes its own set, Keep a Changelog writes another, and every one of them writes them in the language the project is kept in. A layer that mapped them onto a fixed list would fail silently on the first heading it did not know — so the filters on the overview are built from whatever came out of the file, and a changelog written in German filters in German with nothing configured.

The badge beside a group counts its top-level list items. A heading inside a fenced code block is text, not a heading, and a fence closes only on its own kind.

The title and the preamble

The first # in the file, if no release came before it, is the file's own title and is dropped — the page draws its heading from title, so keeping it would be a second <h1>.

Only that one, and only ahead of the first release. Dropping every # reads correctly on the file release-please writes, where there is exactly one, and is silent content loss on a hand-kept changelog that puts its releases at the top level.

Whatever else stands before the first release stays, as the overview's own introduction.

A heading that nearly is a release

The tolerance above has a silent edge: a heading the release test does not match is prose, so its lines fold into the release above it and the release it was meant to be never becomes a page. Nothing about the rendered site would say so.

So the build says it. A heading that opens on a dotted number but does not parse — ## 1.4 (Feb 2024), or the Keep a Changelog form ## [1.2.3] - 2024-01-01 — is reported in the build report, naming the line and the form that works.

It is a warning, not a failure, for the reason every content finding is one: the site renders, and a changelog this layer did not write is not the build's to reject. See What the build checks.

The URL a release is served at

The version, with a v in front of it where it starts with a digit.

/releases/v1.4.0

The v is not decoration. Content reads a file name made only of digits and dots as a version and stops refining it, which would leave the ordering prefix in the address — /releases/01.0.2.0. With the v the prefix is stripped as it is everywhere else, and the segment matches the tag the release was cut as.

Was this page helpful?