Newsletter: Listmonk integration, double opt-in signup, two-way unsubscribe sync - #170
Merged
Conversation
…y unsubscribe sync MySpeedPuzzling is the source of truth, Listmonk mirrors it: - NewsletterSubscriber entity for guests from the new footer signup form (double opt-in, stateless CSRF so anonymous pages stay session-free, rate-limited per address and IP) - Per-recipient signed unsubscribe links land on a two-option page (one-click unsubscribe / manage notification settings); tokens are stateless HMAC claims with no expiry that die on e-mail change - myspeedpuzzling:sync-newsletter-subscribers (cron */15) reconciles both directions: pulls Listmonk one-click-header unsubscribes into MSP, pushes creates (bulk CSV import for the initial ~10k), updates, unsubscribes and removals of deleted players; never re-confirms a Listmonk unsubscribe - only explicit user actions do - Immediate async pushes from profile toggle, opt-in confirm, unsubscribe page and DeletePlayerHandler (GDPR removal from Listmonk) - 6 per-locale lists auto-created by name; branded campaign template with localized footer versioned in docs/features/newsletter/ - All UI and e-mail copy in all six locales Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVCsN9ELLwdwNPEXpGUcnt
…cing texts Footer pitch, opt-in confirmation e-mail and the profile help text now say the newsletter is only about what's new on the platform - all six locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVCsN9ELLwdwNPEXpGUcnt
Links list keeps the left column; the signup block (capped at 430px, right-aligned on desktop, centered on mobile) sits above the social icons, which now carry title tooltips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVCsN9ELLwdwNPEXpGUcnt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVCsN9ELLwdwNPEXpGUcnt
… to top Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVCsN9ELLwdwNPEXpGUcnt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Everything needed to send the first MySpeedPuzzling newsletter via Listmonk. MySpeedPuzzling is the source of truth for who is subscribed; Listmonk is a mirror + send engine. Full docs:
docs/features/newsletter/README.md.Signup (guests) — footer form on every page
NewsletterSubscriberentity + migration, double opt-in (branded confirmation e-mail, 48h token)config/packages/csrf.php) so the footer form does NOT start sessions on anonymous pages — shared caching stays intact (guarded by existing tests)Unsubscribe — works from anywhere, syncs everywhere
NewsletterTokenSigner), no expiry, die on e-mail changeList-Unsubscribeone-click headers (Gmail/Yahoo) land in Listmonk and are pulled into MSP by the cron within 15 minSync —
myspeedpuzzling:sync-newsletter-subscribers(cron */15)Newsletter EN/CS/DE/ES/FR/JA), auto-created by name, subscriber attribs carry locale + unsubscribe/manage URLsnewsletterEnabled=false/ guest unsubscribedDeletePlayerHandler)NewsletterSyncPlanner) with 13 unit tests; blocklisted (bounced) subscribers and foreign-list memberships are never touchedVerified end-to-end in dev
Footer signup → confirmation e-mail → confirm → appears in Listmonk; unsubscribe page → Listmonk suppression; Listmonk-side unsub → pulled into MSP; deleted row → removed from Listmonk; sync idempotent on 8.3k subscribers (second run touches nothing).
Production (already prepared, activates on merge)
LISTMONK_API_URLenv + cron entry pushed (installs on this deploy)dummykernel secret (Infisical never set it). A strong secret is now in Infisical and lands with this deploy. Impact was limited to e-mail-verification link signing (login links + password resets are DB-backed random tokens). In-flight verification links (<24h) break once at deploy.All 6 locales
UI pages, footer form, confirmation e-mail and the Listmonk template footer are localized in en/cs/de/es/fr/ja.
Quality gates
PHPStan max ✅ · PHPCS ✅ · 1418 tests ✅ (incl. 20 new) · schema validate ✅ · cache warmup ✅ · translations check 0 missing ✅
🤖 Generated with Claude Code
https://claude.ai/code/session_01GVCsN9ELLwdwNPEXpGUcnt