05-Apr-2026 — release-notes: image detection, year headings, and path normalisation on index #219
release-notes: image detection, year headings, and path normalisation on index and preserve camera markers and never silently drop new notes (#219, #576, #213)
Three related bugs surfaced when auditing the release-notes-for-humans page
after the #219 camera-marker work. All three produced drift on the detail
index that accumulated silently with each build.
- Image detection missed everything except col.imageSource.
The one-off fixup script's rowHasImage only recognised column-level
imageSource plus nested col.rows. Release-notes pages author images in
many shapes — markdown
and
in contentText, row.carousel
with a linked album, row.albumIndex, col.icon. Anything outside the
narrow shape was marked image-less, so the humans page's camera-filter
dropped most entries. Moved detection into server/lib/release-notes/
image-detection.ts as a proper, tested module (23 new unit tests) and
made it strict enough to reject the CMS editor's default-initialised
"stub" carousel object (all-null fields attached to plain text rows)
that was the source of false positives on e.g. 2025-11-10-issue-33.
- Leading-slash paths on the detail index were invisible to round-trip.
Older entries were authored as ](https://ngx-ramblers.org.uk/how-to/committee/release-notes/…)
and newer ones as ](https://ngx-ramblers.org.uk/how-to/committee/release-notes/…). The fixup
scripts' regex required no leading slash, so 54 entries (including
every 2025 entry with images, e.g. 2025-05-07) were silently skipped.
parseIndexLine now strips a leading slash on parse, so every build
normalises legacy paths without losing the entries.
- The detail index had no year grouping except a single stray "## 2026".
Any year heading between entries was dropped by updateIndexPageContent
on round-trip (only the preamble survived). Refactored the generator
to emit "## " headings above each year's entries on every write,
sorted newest-first. Preamble sanitisation strips any leftover year
headings before re-emission. Also exported refreshIndexPageContent()
so one-off recovery scripts can re-apply current formatting rules to
the live page without merging a new entry.
Test suite goes from 519 to 545 passing:
- 23 new tests in image-detection.spec.ts covering stub carousel rejection,
markdown/HTML image detection, nested rows, and the specific 2025-11-10
false positive and 2025-01-17 true positive shapes
- 3 new tests in content-generator.spec.ts covering year heading emission,
leading-slash normalisation on round-trip, and refreshIndexPageContent
Two independent bugs in the automated release-notes workflow, fixed together.
- Camera 📸 markers stripped from the index on every rewrite.
The entry line regex in content-generator.ts did not allow any trailing
content after the markdown link's closing paren, so index entries with a
trailing 📸 marker (used to flag release notes with embedded images)
failed parseIndexLine, returned null, and were silently dropped from the
re-serialised list on every build.
- Extend entryLineRegex to capture an optional trailing " 📸"
- Add hasCamera to IndexEntry; parseIndexLine records it and
formatIndexLine re-emits it
- updateIndexPageContent preserves the existing hasCamera when
re-adding an entry at the same key
- New regression tests in content-generator.spec.ts
- New release notes silently dropped when the target page already exists.
When a page already existed at the resolved path, generateReleaseNote
would skip creation entirely (to preserve manual edits like embedded
images) and do nothing else — the new commits had no release note
anywhere, and the index was rewritten with an entry still pointing at
the old, pre-edit page. Build #576 for commit 4b7e274f hit exactly
this: three new #213 commits left undocumented.
- When a collision is detected, write the new note at a distinct
build-specific path suffix (-build-, or -commit-
when no build number is available)
- Collision-proof the suffix with a recursive counter fallback
- Always create — never silently skip
- Skip removeLegacyReleasePages when we wrote to a suffixed path