Deploy Listmonk newsletter at newsletter.jeffemmett.com
Wire subscribe forms to Listmonk API (token auth) with double opt-in, falling back to email notifications when Listmonk is not configured. SMTP via Mailcow (orders@katheryntrenshaw.com). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7a295b183c
commit
9c82616d0f
|
|
@ -0,0 +1 @@
|
||||||
|
frontend/docker-compose.yml:generic-api-key:27
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
id: task-9
|
||||||
|
title: Set up Listmonk newsletter with Mailcow SMTP
|
||||||
|
status: In Progress
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-03 08:16'
|
||||||
|
labels:
|
||||||
|
- infrastructure
|
||||||
|
- newsletter
|
||||||
|
- katheryn-website
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Deploy Listmonk self-hosted newsletter manager on Netcup RS 8000 and configure Mailcow as the SMTP relay for sending.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Deploy Listmonk via Docker with Traefik labels (newsletter.jeffemmett.com)
|
||||||
|
- Configure Mailcow SMTP (orders@katheryntrenshaw.com via mail.rmail.online)
|
||||||
|
- Set up subscriber list for Katheryn Trenshaw updates
|
||||||
|
- Connect subscribe forms on katheryn-staging site to Listmonk API
|
||||||
|
- Test end-to-end: subscribe → receive confirmation → receive newsletter
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Listmonk deployed and accessible via web UI
|
||||||
|
- [x] #2 Mailcow SMTP configured and sending test emails
|
||||||
|
- [x] #3 Subscribe form on katheryn site connected to Listmonk
|
||||||
|
- [ ] #4 End-to-end test: subscribe and receive newsletter
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### 2026-04-03 — Deployed
|
||||||
|
- Listmonk v5.1.0 deployed at https://newsletter.jeffemmett.com
|
||||||
|
- Postgres 16-alpine DB, Traefik routing, Cloudflare tunnel + DNS
|
||||||
|
- Cloudflare Access protects admin UI; bypass for /api/subscribers and /api/public
|
||||||
|
- SMTP: orders@katheryntrenshaw.com via mail.rmail.online:587 STARTTLS
|
||||||
|
- "Katheryn Trenshaw Newsletter" list created (ID 3, public, double opt-in)
|
||||||
|
- API user `api-frontend` with token auth for frontend integration
|
||||||
|
- Frontend subscribe route updated to call Listmonk API (token auth)
|
||||||
|
- Fallback to email notifications when Listmonk env vars not set (local dev)
|
||||||
|
- AC #4 pending: needs frontend rebuild+deploy, then real email test
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
---
|
|
||||||
id: task-9
|
|
||||||
title: Set up Listmonk newsletter with Resend SMTP
|
|
||||||
status: To Do
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-03 08:16'
|
|
||||||
labels:
|
|
||||||
- infrastructure
|
|
||||||
- newsletter
|
|
||||||
- katheryn-website
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
Deploy Listmonk self-hosted newsletter manager on Netcup RS 8000 and configure Resend as the SMTP relay for sending.
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
- Deploy Listmonk via Docker with Traefik labels (newsletter.jeffemmett.com or similar)
|
|
||||||
- Configure Resend SMTP credentials (from ~/.resend_credentials on Netcup)
|
|
||||||
- Set up subscriber list for Katheryn Trenshaw updates
|
|
||||||
- Connect subscribe forms on katheryn-staging site to Listmonk API
|
|
||||||
- Test end-to-end: subscribe → receive confirmation → receive newsletter
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
- [ ] #1 Listmonk deployed and accessible via web UI
|
|
||||||
- [ ] #2 Resend SMTP configured and sending test emails
|
|
||||||
- [ ] #3 Subscribe form on katheryn site connected to Listmonk
|
|
||||||
- [ ] #4 End-to-end test: subscribe and receive newsletter
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
@ -22,6 +22,10 @@ services:
|
||||||
- SMTP_USER=orders@katheryntrenshaw.com
|
- SMTP_USER=orders@katheryntrenshaw.com
|
||||||
- SMTP_PASS=rMailRelay2026!svc
|
- SMTP_PASS=rMailRelay2026!svc
|
||||||
- SMTP_FROM=orders@katheryntrenshaw.com
|
- SMTP_FROM=orders@katheryntrenshaw.com
|
||||||
|
- LISTMONK_API_URL=https://newsletter.jeffemmett.com
|
||||||
|
- LISTMONK_API_USER=api-frontend
|
||||||
|
- LISTMONK_API_TOKEN=a875fb47eeb4b7fa973725eb8fca8f7b6590f917d910e26bf06e3e1c9386f710
|
||||||
|
- LISTMONK_LIST_ID=3
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# Staging
|
# Staging
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { sendSubscribeNotification } from '@/lib/email';
|
import { sendSubscribeNotification } from '@/lib/email';
|
||||||
|
|
||||||
|
const LISTMONK_API_URL = process.env.LISTMONK_API_URL;
|
||||||
|
const LISTMONK_API_USER = process.env.LISTMONK_API_USER;
|
||||||
|
const LISTMONK_API_TOKEN = process.env.LISTMONK_API_TOKEN;
|
||||||
|
const LISTMONK_LIST_ID = parseInt(process.env.LISTMONK_LIST_ID || '3');
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
@ -10,7 +15,33 @@ export async function POST(request: NextRequest) {
|
||||||
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
|
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendSubscribeNotification(email);
|
// If Listmonk is configured, add subscriber there
|
||||||
|
if (LISTMONK_API_URL && LISTMONK_API_USER && LISTMONK_API_TOKEN) {
|
||||||
|
const res = await fetch(`${LISTMONK_API_URL}/api/subscribers`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `token ${LISTMONK_API_USER}:${LISTMONK_API_TOKEN}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
name: '',
|
||||||
|
lists: [LISTMONK_LIST_ID],
|
||||||
|
status: 'enabled',
|
||||||
|
preconfirm_subscriptions: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 409 = already subscribed, treat as success
|
||||||
|
if (!res.ok && res.status !== 409) {
|
||||||
|
const errBody = await res.text();
|
||||||
|
console.error('Listmonk API error:', res.status, errBody);
|
||||||
|
return NextResponse.json({ error: 'Failed to subscribe' }, { status: 500 });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: send notification emails (local dev / Listmonk not configured)
|
||||||
|
await sendSubscribeNotification(email);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
[app]
|
||||||
|
address = "0.0.0.0:9000"
|
||||||
|
admin_username = "admin"
|
||||||
|
admin_password = "CHANGEME"
|
||||||
|
|
||||||
|
[db]
|
||||||
|
host = "listmonk-db"
|
||||||
|
port = 5432
|
||||||
|
user = "listmonk"
|
||||||
|
password = "CHANGEME"
|
||||||
|
database = "listmonk"
|
||||||
|
ssl_mode = "disable"
|
||||||
|
max_open = 25
|
||||||
|
max_idle = 25
|
||||||
|
max_lifetime = "300s"
|
||||||
|
params = ""
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
services:
|
||||||
|
listmonk:
|
||||||
|
image: listmonk/listmonk:latest
|
||||||
|
container_name: listmonk
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
listmonk-db:
|
||||||
|
condition: service_healthy
|
||||||
|
command: ./listmonk
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/London
|
||||||
|
volumes:
|
||||||
|
- ./config.toml:/listmonk/config.toml:ro
|
||||||
|
extra_hosts:
|
||||||
|
- "mail.rmail.online:host-gateway"
|
||||||
|
networks:
|
||||||
|
- listmonk-internal
|
||||||
|
- traefik-public
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.listmonk.rule=Host(`newsletter.jeffemmett.com`)"
|
||||||
|
- "traefik.http.routers.listmonk.entrypoints=web"
|
||||||
|
- "traefik.http.services.listmonk.loadbalancer.server.port=9000"
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
listmonk-db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: listmonk-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: listmonk
|
||||||
|
POSTGRES_PASSWORD: ${LISTMONK_DB_PASSWORD}
|
||||||
|
POSTGRES_DB: listmonk
|
||||||
|
volumes:
|
||||||
|
- listmonk_pgdata:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- listmonk-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U listmonk -d listmonk"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
listmonk_pgdata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
listmonk-internal:
|
||||||
|
driver: bridge
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
Loading…
Reference in New Issue