# 06-Apr-2026 — release-notes: preserve paragraph breaks in generated release note markdown [#219](https://github.com/nbarrett/ngx-ramblers/issues/219)

## [build 579](https://github.com/nbarrett/ngx-ramblers/actions/runs/24048805738) — [commit b697940](https://github.com/nbarrett/ngx-ramblers/commit/b6979404e2696a772363eb20b2e0a498d73f9b33)

_____

### **release-notes**: preserve paragraph breaks in generated release note markdown, image detection, year headings, and path normalisation on index and preserve camera markers and never silently drop new notes ([#219](https://github.com/nbarrett/ngx-ramblers/issues/219), [#576](https://github.com/nbarrett/ngx-ramblers/issues/576), [#213](https://github.com/nbarrett/ngx-ramblers/issues/213))

formatCommitBody was collapsing all lines into a single block by splitting
on individual newlines, filtering empties, and rejoining. This destroyed
paragraph breaks, setext-style headings, and soft-wrapped lines within a
paragraph, producing a wall-of-text rendering on the CMS.

Refactored to split on paragraph boundaries (\n\s*\n) first, process each
paragraph's lines independently, then rejoin paragraphs with double
newlines. Soft wraps within a paragraph are preserved as single newlines.
Multiple consecutive blank lines are collapsed to a single paragraph
separator.

4 new tests covering:
- Paragraph breaks preserved between sections
- Multiline soft-wrapped paragraphs kept as single paragraphs
- Setext-style section headings intact with surrounding blank lines
- Runs of multiple blank lines collapsed to a single separator

Test suite: 549 passing (was 545).

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.

1. 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 ![](https://ngx-ramblers.org.uk/…) and <img> 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.

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

3. 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 "## <year>" 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.

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

2. 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-<buildNumber>, or -commit-<short>
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