Skip to content

Newsletter sign-up in the footer (double opt-in, topics) - #51

Merged
mykolamelnykml merged 2 commits into
mainfrom
newsletter-signup
Oct 8, 2026
Merged

mykolamelnykml merged 2 commits into
mainfrom
newsletter-signup

Conversation

@mykolamelnykml

Copy link
Copy Markdown
Contributor

Readers can now subscribe from the footer to any of three topics: Reviews, New tools and Benchmarks. Addresses are stored in a new apps/newsletter/ app. Sending the actual updates is not built yet; this adds the list, the consent trail and the unsubscribe path.

Flow

  1. The footer form posts to POST /api/v1/newsletter/subscriptions. The address is stored as pending and gets a confirmation link, valid for 7 days.
  2. /newsletter/confirm has a Confirm button that calls /confirmations. The row becomes active and a welcome email goes out with an unsubscribe link and a List-Unsubscribe header.
  3. /newsletter/unsubscribe has an Unsubscribe button that calls /unsubscriptions. The row becomes unsubscribed.

Notes for review

  • Subscribing returns 204 whatever state the address is in, so the form can't be used to find out who is on the list. An active address keeps its topics until its owner confirms new ones, because the topics are carried in the token.
  • Links are django.core.signing tokens, so nothing secret is stored. Rotating SECRET_KEY invalidates every outstanding link.
  • Email tasks receive the row and the topics, never a link, because django-q stores task arguments in the database.
  • Both landing pages act only on a button press, never on load: mail scanners open links before people do.
  • NEWSLETTER_SUBSCRIBE_RATE (default 10/hour per IP) limits sign-ups.
  • Staff can see the list in the admin.
  • send_templated_email gains an optional headers= argument.
  • The footer's top row is now two columns: brand and links on the left, sign-up on the right.
  • Separate commit: the About page contact address is now info@redaction-tools.com. The JSON-LD operator entity keeps info@stabrise.com so it still matches pdf-redaction.com.

Testing

  • make backend-test: 744 passed. make frontend-test: 927 passed. make lint is clean.
  • New backend/tests/test_newsletter.py (15 tests); frontend tests for the form and both landing pages.
  • Checked by hand in the local stack: subscribe from the footer, then the confirmation email, confirm, the welcome email and unsubscribe. The database row ended unsubscribed with both timestamps set.

The structured data keeps info@stabrise.com: it describes StabRise as the
operator and has to match what pdf-redaction.com publishes.
…ooter

A footer form takes an email and any of three topics. The list lives in a new
`apps/newsletter/` app; sending the actual updates is not built yet.

- Double opt-in: `POST /newsletter/subscriptions` stores the address as
  `pending` and mails a confirmation link (7 days). Only the click, on
  `/newsletter/confirm`, makes it `active` and sends a welcome email with an
  unsubscribe link and a `List-Unsubscribe` header.
- Subscribing answers 204 whatever the address's state, so the form cannot
  reveal who is on the list. An active address keeps its topics until its
  owner confirms new ones: the topics ride in the token, not the row.
- Confirm and unsubscribe links are `django.core.signing` tokens, so nothing
  secret is stored. Email tasks take the row and topics, never a link,
  because django-q keeps task arguments in the database.
- Both landing pages act on a button, never on load, since mail scanners
  open links before people do.
- `NEWSLETTER_SUBSCRIBE_RATE` (10/hour per IP) limits sign-ups; staff see the
  list and its consent trail in the admin.
- `send_templated_email` takes optional headers. The footer's top row is now
  two columns so the form sits beside the links.
- DEVELOPMENT.md and CLAUDE.md describe the flow.
@mykolamelnykml
mykolamelnykml merged commit 4148ac6 into main Oct 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant