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.