{"id":"6a52c8267d2ac2e4ce4ced9b","title":"2026 07 11 Issue 313","path":"how-to/committee/release-notes/2026-07-11-issue-313","contentMarkdown":"# 11-Jul-2026 — Resolvable walk links, upload attribution, email image hosts, walk export improvements [#313](https://github.com/nbarrett/ngx-ramblers/issues/313)\n\n## [build 761](https://github.com/nbarrett/ngx-ramblers/actions/runs/29170684836) — [commit d865a56](https://github.com/nbarrett/ngx-ramblers/commit/d865a56d0ddb2b98caa3e5e0e85069150e2473b6)\n\n_____\n\n## Walk links no longer 404 and edit no longer opens an empty walk\n\nA walk with no stored `url` (an awaiting-leader slot, or a walk whose title had been cleared) still produced a title-or-date based slug for its links. That fabricated slug matched nothing on lookup, so clicking a walk title went to \"Page not found\", and the Edit button silently rendered a blank default walk dated today - closing which dropped the real walk from the in-memory programme until a refresh.\n\nThe slug logic was duplicated across six link builders (`walkSlug`, `groupEventLink`, `event-contact`, `email-composer`, `committee-query`, the walk-notification email footer), each fabricating slightly differently, so any single fix left the rest broken. Separately, clearing a title blanked the walk's `url`, and the copy-walk action never regenerated one.\n\n- New single reader `eventSlug(event)` in `functions/walks/event-slug.ts` = stored `url`, else `groupEvent.id`, else `id`. It never fabricates a title slug: the client `kebabCase` and the server `convertTitleToSlug` differ (stop-words), so a client-fabricated slug cannot be relied on to match a walk's own stored `url`. All six link builders now call it.\n- New single generator `WalksAndEventsService.urlFor(walk)` = titled walks resolve through the backend `urlFromTitle` service (stop-words stripped, uniqueness `-N` suffix); title-less slots get the dated slug. Copy-walk, walk title-change, group-event title-change, and a new save-time backfill all route through it, so a walk is never persisted without a resolvable `url`.\n- Title-change no longer blanks the `url` when the title is emptied.\n\nExisting walks with a missing `url` become reachable again by id and pick up a proper `url` the next time they are saved; no data migration was needed.\n\n## Ramblers walk upload reports who ran it\n\nThe Serenity walk-upload scenario hardcoded the actor to \"Walks Admin\", so the steps could not distinguish Nick from Kerry. The uploader's name already reached the metadata file but was dropped by the extractor and ignored by the scenario.\n\n- `ramblersUser` added to `WalkRequestParameters` and the metadata extractor.\n- The scenario resolves the actor name from the metadata, falling back to \"Walks Admin\".\n- The client sends the uploader's first name.\n- The Ramblers login username always comes from system config, so the display name cannot leak into the login.\n\n## Welcome / new-joiner email images now load when sent\n\nNotification image overrides are stored as relative paths (`api/aws/s3/site-content/...jpeg`). The batch send rewrote these to absolute URLs, but the generic transactional path - which sends the welcome email - passed the raw relative path to Brevo. In the in-app preview a relative `src` resolves against the browser page origin so it looked fine; a real email client has no base href, so the images failed (as seen in the new-joiner welcomes).\n\n- Image-host normalisation moved into `renderBrandedTemplate`, which every branded send and the preview go through, using the `APP_URL` already in params.\n- The now-redundant per-send normalisation was removed from the batch path.\n- Regression tests added (relative override becomes absolute; absolute override unchanged). Server suite green at 844 passing.\n\n## Quieter cookie handling on the walks upload\n\nThe cookie-banner dismissal ran on every navigation, cluttering the upload report even when there was nothing to clear. It now runs only when a banner is actually present (a `Check` performs the clearing in its branch), so its dismissal steps appear only when there is something to dismiss. Fully hiding the check's own line (the `IsHidden`/`IsSilent` reporting markers) lives in serenity-js itself, on the `feat/hidden-and-silent-activities` fork branch, rather than as a patch here.\n\n## A single headless switch\n\nEvery Chromium launch (walks upload, site migration, integration worker) now goes through one `resolveHeadless()` / `launchChromium()` in `server/lib/shared/playwright-browser.ts`, with the default in a clearly-named `DEFAULT_HEADLESS` constant and overridable by `PLAYWRIGHT_HEADLESS` via the shared `asBoolean()` helper. The CLI `--no-headless` flag feeds the same one place.\n\n## Angular 21\n\nDocumentation updated to reflect the Angular 21 upgrade: `AGENTS.md`, `README.md`, `codebase-evolution-stats.html` and `docs/contributing/agentic-development.md` moved from Angular 20 to Angular 21.","contentHtml":"<h1>11-Jul-2026 — Resolvable walk links, upload attribution, email image hosts, walk export improvements <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/313\">#313</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/29170684836\">build 761</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/d865a56d0ddb2b98caa3e5e0e85069150e2473b6\">commit d865a56</a></h2>\n<hr>\n<h2>Walk links no longer 404 and edit no longer opens an empty walk</h2>\n<p>A walk with no stored <code>url</code> (an awaiting-leader slot, or a walk whose title had been cleared) still produced a title-or-date based slug for its links. That fabricated slug matched nothing on lookup, so clicking a walk title went to &quot;Page not found&quot;, and the Edit button silently rendered a blank default walk dated today - closing which dropped the real walk from the in-memory programme until a refresh.</p>\n<p>The slug logic was duplicated across six link builders (<code>walkSlug</code>, <code>groupEventLink</code>, <code>event-contact</code>, <code>email-composer</code>, <code>committee-query</code>, the walk-notification email footer), each fabricating slightly differently, so any single fix left the rest broken. Separately, clearing a title blanked the walk&#39;s <code>url</code>, and the copy-walk action never regenerated one.</p>\n<ul>\n<li>New single reader <code>eventSlug(event)</code> in <code>functions/walks/event-slug.ts</code> = stored <code>url</code>, else <code>groupEvent.id</code>, else <code>id</code>. It never fabricates a title slug: the client <code>kebabCase</code> and the server <code>convertTitleToSlug</code> differ (stop-words), so a client-fabricated slug cannot be relied on to match a walk&#39;s own stored <code>url</code>. All six link builders now call it.</li>\n<li>New single generator <code>WalksAndEventsService.urlFor(walk)</code> = titled walks resolve through the backend <code>urlFromTitle</code> service (stop-words stripped, uniqueness <code>-N</code> suffix); title-less slots get the dated slug. Copy-walk, walk title-change, group-event title-change, and a new save-time backfill all route through it, so a walk is never persisted without a resolvable <code>url</code>.</li>\n<li>Title-change no longer blanks the <code>url</code> when the title is emptied.</li>\n</ul>\n<p>Existing walks with a missing <code>url</code> become reachable again by id and pick up a proper <code>url</code> the next time they are saved; no data migration was needed.</p>\n<h2>Ramblers walk upload reports who ran it</h2>\n<p>The Serenity walk-upload scenario hardcoded the actor to &quot;Walks Admin&quot;, so the steps could not distinguish Nick from Kerry. The uploader&#39;s name already reached the metadata file but was dropped by the extractor and ignored by the scenario.</p>\n<ul>\n<li><code>ramblersUser</code> added to <code>WalkRequestParameters</code> and the metadata extractor.</li>\n<li>The scenario resolves the actor name from the metadata, falling back to &quot;Walks Admin&quot;.</li>\n<li>The client sends the uploader&#39;s first name.</li>\n<li>The Ramblers login username always comes from system config, so the display name cannot leak into the login.</li>\n</ul>\n<h2>Welcome / new-joiner email images now load when sent</h2>\n<p>Notification image overrides are stored as relative paths (<code>api/aws/s3/site-content/...jpeg</code>). The batch send rewrote these to absolute URLs, but the generic transactional path - which sends the welcome email - passed the raw relative path to Brevo. In the in-app preview a relative <code>src</code> resolves against the browser page origin so it looked fine; a real email client has no base href, so the images failed (as seen in the new-joiner welcomes).</p>\n<ul>\n<li>Image-host normalisation moved into <code>renderBrandedTemplate</code>, which every branded send and the preview go through, using the <code>APP_URL</code> already in params.</li>\n<li>The now-redundant per-send normalisation was removed from the batch path.</li>\n<li>Regression tests added (relative override becomes absolute; absolute override unchanged). Server suite green at 844 passing.</li>\n</ul>\n<h2>Quieter cookie handling on the walks upload</h2>\n<p>The cookie-banner dismissal ran on every navigation, cluttering the upload report even when there was nothing to clear. It now runs only when a banner is actually present (a <code>Check</code> performs the clearing in its branch), so its dismissal steps appear only when there is something to dismiss. Fully hiding the check&#39;s own line (the <code>IsHidden</code>/<code>IsSilent</code> reporting markers) lives in serenity-js itself, on the <code>feat/hidden-and-silent-activities</code> fork branch, rather than as a patch here.</p>\n<h2>A single headless switch</h2>\n<p>Every Chromium launch (walks upload, site migration, integration worker) now goes through one <code>resolveHeadless()</code> / <code>launchChromium()</code> in <code>server/lib/shared/playwright-browser.ts</code>, with the default in a clearly-named <code>DEFAULT_HEADLESS</code> constant and overridable by <code>PLAYWRIGHT_HEADLESS</code> via the shared <code>asBoolean()</code> helper. The CLI <code>--no-headless</code> flag feeds the same one place.</p>\n<h2>Angular 21</h2>\n<p>Documentation updated to reflect the Angular 21 upgrade: <code>AGENTS.md</code>, <code>README.md</code>, <code>codebase-evolution-stats.html</code> and <code>docs/contributing/agentic-development.md</code> moved from Angular 20 to Angular 21.</p>\n"}