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**.

```md
## [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](/reference/changelog/components). 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.

```md
### 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](/concepts/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.