Patch notes — newsletter follow-ups + native CMS (Hugo → live), Phase 1 (2026-06-15)
Two threads on Ethica.no: finishing the newsletter (deliverability + lifecycle
fixes after the platform shipped) and the first phase of moving the marketing
content off the Hugo static build into a live, DB-driven CMS rendered by
server.py — so content edits are instant, no rebuild. All items are on main.
Companion bug-fix write-up: Bugfix/bugfix-report-2026-06-15.md.
1 — Newsletter: layout unsubscribe slot, opt-in over the right SMTP, distinct unsubscribed state ✅
{{unsubscribe_url}}layout slot. Templates can now style their own unsubscribe link with the raw URL ({{unsubscribe_url}}) in addition to the ready-made{{unsubscribe}}link — needed for a dark, branded footer where the default link colour is unreadable. A layout that provides either slot no longer gets the default footer link appended (which was producing a second, bare link); the fallback still appends when a layout uses neither, so every email stays unsubscribable.- Opt-in confirmation sends over the newsletter SMTP. The homepage double-opt-in
confirmation was going through the contact-form mailer; it now sends over the
dedicated newsletter SMTP (same
From/identity as campaigns, withDate/Message-ID), falling back to the general mailer only if the newsletter SMTP isn’t configured. - Unsubscribe is a distinct, kept state. Unsubscribing set
confirmed=0, which the admin list rendered identically to a never-confirmed “pending” signup. A newunsubscribed_atcolumn (idempotent migration) marks it: the list shows a separate “unsubscribed” badge + stat, the row is excluded from sends, and re-confirming clears the flag. Manual Remove still hard-deletes. - Verified 10/10 on mail-tester end-to-end (signup → confirmation → confirm → campaign → one-click unsubscribe) over the self-hosted mail server.
2 — Native CMS: Hugo → live migration, Phase 1 ✅
The marketing blog posts and standalone pages can now be authored in the admin and
served live from Postgres — no image rebuild to publish. Built so it’s
non-breaking and incremental (full plan: ethica-no/migration/hugomigration.md).
- Content model. A
contenttable (kind = post | page, slug, body HTML, tags, status,published_at) + query helpers. Slugs reuse the docs reader’s Hugo-matching slugify, so migrated posts keep their/posts/<slug>/URLs. - Editor. A Tiptap (ProseMirror, MIT) rich-text editor, vendored as a single
self-contained bundle (built once with esbuild — the repo stays buildless), mounted
over a
body_htmlfield with a</> Sourcefallback. New admin Content screen (Website group): list, create/edit, publish/unpublish/delete. - Live public render.
server.pyrenders the blog index, posts, and pages class-for-class with the Hugo templates (same fonts +theme.css/components.css/editorial.css+ masthead + footer), so the look is unchanged.editorial.cssandsidebar-hydrate.jsmoved tostatic/(stable paths; Hugo still builds identically). - Non-breaking routing.
/posts(/),/posts/<slug>/, and a top-level/<slug>/(pages) render live only when a published DB row exists, otherwise they fall through to the static Hugo page. So with an emptycontenttable the public site is byte-identical to before — content can be migrated one item at a time.
3 — CMS editor extras ✅
Four editor/render improvements:
- Resizable body editor — drag the editor (and Source view) taller/shorter.
- Image upload — pick a file → client-side resize → upload → the editor inserts a
clean
/uploads/<id>URL. Images are stored base64 in a newcontent_imagestable (airgapped, survive container recreation) and served decoded + long-cacheable, so the post HTML isn’t bloated with base64. - Public URL in the editor — shows the item’s live URL (
/posts/<slug>/or/<slug>/) with a Copy button, so a page can be linked elsewhere. - Pages default to the watermark layout — a
pagerenders with the faint Σ-E backdrop + prose (the Privacy/Impressum look) unless the body HTML overrides it.
4 — Documentation ✅
- Newsletter docs (
ethica-no/newsletter/): a self-hosted setup & operations runbook and a 10/10 deliverability engineering post. - Migration plans (
ethica-no/migration/): the Hugo → live CMS plan and a server.py break-out (monolith →ethica/package) plan. - Spec-sheet updated: the newsletter is a shipped native platform (not the
EthicaMail fork hosted); EthicaMail clarified as the renamed fork of upstream
MailCarrier (MIT), ported into
newsletter.py.
Deployment status
All Ethica.no changes are on main and deploy via the standard CI build → compose
pull. The CMS is dormant on the public site until content is published (live routes
shadow Hugo only where a DB row exists), so the merge carried no visible change.
Verification
py_compile clean · newsletter unit tests 7/7 · docs_reader tests 10 / 4 / 13 ·
Docker smoke 91 / 0 (incl. the live blog index/post render, editorial chrome +
static assets, the static fallback for unknown slugs, image-upload round-trip, the
watermark page, and the public-URL hint).