06-Apr-2026 — release-notes: preserve paragraph breaks in generated release note markdown #219

build 579commit b697940


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, #576, #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:

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

  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 "## " 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:

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