{"id":"69d415119f23152e64341dd8","title":"2026 04 06 Issue 219","path":"how-to/committee/release-notes/2026-04-06-issue-219","contentMarkdown":"# 06-Apr-2026 — release-notes: preserve paragraph breaks in generated release note markdown [#219](https://github.com/nbarrett/ngx-ramblers/issues/219)\n\n## [build 579](https://github.com/nbarrett/ngx-ramblers/actions/runs/24048805738) — [commit b697940](https://github.com/nbarrett/ngx-ramblers/commit/b6979404e2696a772363eb20b2e0a498d73f9b33)\n\n_____\n\n### **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))\n\nformatCommitBody was collapsing all lines into a single block by splitting\non individual newlines, filtering empties, and rejoining. This destroyed\nparagraph breaks, setext-style headings, and soft-wrapped lines within a\nparagraph, producing a wall-of-text rendering on the CMS.\n\nRefactored to split on paragraph boundaries (\\n\\s*\\n) first, process each\nparagraph's lines independently, then rejoin paragraphs with double\nnewlines. Soft wraps within a paragraph are preserved as single newlines.\nMultiple consecutive blank lines are collapsed to a single paragraph\nseparator.\n\n4 new tests covering:\n- Paragraph breaks preserved between sections\n- Multiline soft-wrapped paragraphs kept as single paragraphs\n- Setext-style section headings intact with surrounding blank lines\n- Runs of multiple blank lines collapsed to a single separator\n\nTest suite: 549 passing (was 545).\n\nThree related bugs surfaced when auditing the release-notes-for-humans page\nafter the #219 camera-marker work. All three produced drift on the detail\nindex that accumulated silently with each build.\n\n1. Image detection missed everything except col.imageSource.\nThe one-off fixup script's rowHasImage only recognised column-level\nimageSource plus nested col.rows. Release-notes pages author images in\nmany shapes — markdown ![](https://ngx-ramblers.org.uk/…) and <img> in contentText, row.carousel\nwith a linked album, row.albumIndex, col.icon. Anything outside the\nnarrow shape was marked image-less, so the humans page's camera-filter\ndropped most entries. Moved detection into server/lib/release-notes/\nimage-detection.ts as a proper, tested module (23 new unit tests) and\nmade it strict enough to reject the CMS editor's default-initialised\n\"stub\" carousel object (all-null fields attached to plain text rows)\nthat was the source of false positives on e.g. 2025-11-10-issue-33.\n\n2. Leading-slash paths on the detail index were invisible to round-trip.\nOlder entries were authored as ](https://ngx-ramblers.org.uk/how-to/committee/release-notes/…)\nand newer ones as ](https://ngx-ramblers.org.uk/how-to/committee/release-notes/…). The fixup\nscripts' regex required no leading slash, so 54 entries (including\nevery 2025 entry with images, e.g. 2025-05-07) were silently skipped.\nparseIndexLine now strips a leading slash on parse, so every build\nnormalises legacy paths without losing the entries.\n\n3. The detail index had no year grouping except a single stray \"## 2026\".\nAny year heading between entries was dropped by updateIndexPageContent\non round-trip (only the preamble survived). Refactored the generator\nto emit \"## <year>\" headings above each year's entries on every write,\nsorted newest-first. Preamble sanitisation strips any leftover year\nheadings before re-emission. Also exported refreshIndexPageContent()\nso one-off recovery scripts can re-apply current formatting rules to\nthe live page without merging a new entry.\n\nTest suite goes from 519 to 545 passing:\n- 23 new tests in image-detection.spec.ts covering stub carousel rejection,\nmarkdown/HTML image detection, nested rows, and the specific 2025-11-10\nfalse positive and 2025-01-17 true positive shapes\n- 3 new tests in content-generator.spec.ts covering year heading emission,\nleading-slash normalisation on round-trip, and refreshIndexPageContent\n\nTwo independent bugs in the automated release-notes workflow, fixed together.\n\n1. Camera 📸 markers stripped from the index on every rewrite.\nThe entry line regex in content-generator.ts did not allow any trailing\ncontent after the markdown link's closing paren, so index entries with a\ntrailing 📸 marker (used to flag release notes with embedded images)\nfailed parseIndexLine, returned null, and were silently dropped from the\nre-serialised list on every build.\n- Extend entryLineRegex to capture an optional trailing \" 📸\"\n- Add hasCamera to IndexEntry; parseIndexLine records it and\nformatIndexLine re-emits it\n- updateIndexPageContent preserves the existing hasCamera when\nre-adding an entry at the same key\n- New regression tests in content-generator.spec.ts\n\n2. New release notes silently dropped when the target page already exists.\nWhen a page already existed at the resolved path, generateReleaseNote\nwould skip creation entirely (to preserve manual edits like embedded\nimages) and do nothing else — the new commits had no release note\nanywhere, and the index was rewritten with an entry still pointing at\nthe old, pre-edit page. Build #576 for commit 4b7e274f hit exactly\nthis: three new #213 commits left undocumented.\n- When a collision is detected, write the new note at a distinct\nbuild-specific path suffix (-build-<buildNumber>, or -commit-<short>\nwhen no build number is available)\n- Collision-proof the suffix with a recursive counter fallback\n- Always create — never silently skip\n- Skip removeLegacyReleasePages when we wrote to a suffixed path","contentHtml":"<h1>06-Apr-2026 — release-notes: preserve paragraph breaks in generated release note markdown <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/219\">#219</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/24048805738\">build 579</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/b6979404e2696a772363eb20b2e0a498d73f9b33\">commit b697940</a></h2>\n<hr>\n<h3><strong>release-notes</strong>: 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 (<a href=\"https://github.com/nbarrett/ngx-ramblers/issues/219\">#219</a>, <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/576\">#576</a>, <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/213\">#213</a>)</h3>\n<p>formatCommitBody was collapsing all lines into a single block by splitting\non individual newlines, filtering empties, and rejoining. This destroyed\nparagraph breaks, setext-style headings, and soft-wrapped lines within a\nparagraph, producing a wall-of-text rendering on the CMS.</p>\n<p>Refactored to split on paragraph boundaries (\\n\\s*\\n) first, process each\nparagraph&#39;s lines independently, then rejoin paragraphs with double\nnewlines. Soft wraps within a paragraph are preserved as single newlines.\nMultiple consecutive blank lines are collapsed to a single paragraph\nseparator.</p>\n<p>4 new tests covering:</p>\n<ul>\n<li>Paragraph breaks preserved between sections</li>\n<li>Multiline soft-wrapped paragraphs kept as single paragraphs</li>\n<li>Setext-style section headings intact with surrounding blank lines</li>\n<li>Runs of multiple blank lines collapsed to a single separator</li>\n</ul>\n<p>Test suite: 549 passing (was 545).</p>\n<p>Three related bugs surfaced when auditing the release-notes-for-humans page\nafter the #219 camera-marker work. All three produced drift on the detail\nindex that accumulated silently with each build.</p>\n<ol>\n<li><p>Image detection missed everything except col.imageSource.\nThe one-off fixup script&#39;s rowHasImage only recognised column-level\nimageSource plus nested col.rows. Release-notes pages author images in\nmany shapes — markdown <img src=\"https://ngx-ramblers.org.uk/%E2%80%A6\" alt=\"\"> and <img> in contentText, row.carousel\nwith a linked album, row.albumIndex, col.icon. Anything outside the\nnarrow shape was marked image-less, so the humans page&#39;s camera-filter\ndropped most entries. Moved detection into server/lib/release-notes/\nimage-detection.ts as a proper, tested module (23 new unit tests) and\nmade it strict enough to reject the CMS editor&#39;s default-initialised\n&quot;stub&quot; carousel object (all-null fields attached to plain text rows)\nthat was the source of false positives on e.g. 2025-11-10-issue-33.</p>\n</li>\n<li><p>Leading-slash paths on the detail index were invisible to round-trip.\nOlder entries were authored as ](<a href=\"https://ngx-ramblers.org.uk/how-to/committee/release-notes/%E2%80%A6\">https://ngx-ramblers.org.uk/how-to/committee/release-notes/…</a>)\nand newer ones as ](<a href=\"https://ngx-ramblers.org.uk/how-to/committee/release-notes/%E2%80%A6\">https://ngx-ramblers.org.uk/how-to/committee/release-notes/…</a>). The fixup\nscripts&#39; regex required no leading slash, so 54 entries (including\nevery 2025 entry with images, e.g. 2025-05-07) were silently skipped.\nparseIndexLine now strips a leading slash on parse, so every build\nnormalises legacy paths without losing the entries.</p>\n</li>\n<li><p>The detail index had no year grouping except a single stray &quot;## 2026&quot;.\nAny year heading between entries was dropped by updateIndexPageContent\non round-trip (only the preamble survived). Refactored the generator\nto emit &quot;## <year>&quot; headings above each year&#39;s entries on every write,\nsorted newest-first. Preamble sanitisation strips any leftover year\nheadings before re-emission. Also exported refreshIndexPageContent()\nso one-off recovery scripts can re-apply current formatting rules to\nthe live page without merging a new entry.</p>\n</li>\n</ol>\n<p>Test suite goes from 519 to 545 passing:</p>\n<ul>\n<li>23 new tests in image-detection.spec.ts covering stub carousel rejection,\nmarkdown/HTML image detection, nested rows, and the specific 2025-11-10\nfalse positive and 2025-01-17 true positive shapes</li>\n<li>3 new tests in content-generator.spec.ts covering year heading emission,\nleading-slash normalisation on round-trip, and refreshIndexPageContent</li>\n</ul>\n<p>Two independent bugs in the automated release-notes workflow, fixed together.</p>\n<ol>\n<li>Camera 📸 markers stripped from the index on every rewrite.\nThe entry line regex in content-generator.ts did not allow any trailing\ncontent after the markdown link&#39;s closing paren, so index entries with a\ntrailing 📸 marker (used to flag release notes with embedded images)\nfailed parseIndexLine, returned null, and were silently dropped from the\nre-serialised list on every build.</li>\n</ol>\n<ul>\n<li>Extend entryLineRegex to capture an optional trailing &quot; 📸&quot;</li>\n<li>Add hasCamera to IndexEntry; parseIndexLine records it and\nformatIndexLine re-emits it</li>\n<li>updateIndexPageContent preserves the existing hasCamera when\nre-adding an entry at the same key</li>\n<li>New regression tests in content-generator.spec.ts</li>\n</ul>\n<ol start=\"2\">\n<li>New release notes silently dropped when the target page already exists.\nWhen a page already existed at the resolved path, generateReleaseNote\nwould skip creation entirely (to preserve manual edits like embedded\nimages) and do nothing else — the new commits had no release note\nanywhere, and the index was rewritten with an entry still pointing at\nthe old, pre-edit page. Build #576 for commit 4b7e274f hit exactly\nthis: three new #213 commits left undocumented.</li>\n</ol>\n<ul>\n<li>When a collision is detected, write the new note at a distinct\nbuild-specific path suffix (-build-<buildNumber>, or -commit-<short>\nwhen no build number is available)</li>\n<li>Collision-proof the suffix with a recursive counter fallback</li>\n<li>Always create — never silently skip</li>\n<li>Skip removeLegacyReleasePages when we wrote to a suffixed path</li>\n</ul>\n"}