Repository navigation
feat(subscribe): subscriptions, post and digest emails, templates on /admin - #44
Merged
Merged
Conversation
… templates on /admin
…ned preference links
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.
Phase 5, part 2: subscriptions and email. Plan:
.claude/.Plans/as2026-11-07-graphite-hub-phase-5-subscribe.md.What changed
Subscribing
/subscribe: choose everything, a project or a member; each post or a weekly digest; a live sentence says what you will get. Reached from the footer form on every page ("choose what you get"), the blog header, and rail panels on/blog, every post, every project page and every member page (prefilled scope)./subscribe/confirm|manage|unsubscribe/:id/:hmac. The manage page saves scope and cadence, unsubscribes and resubscribes. The unsubscribe link renders one button on GET and acts on POST; the same POST servesList-Unsubscribe-Post: List-Unsubscribe=One-Clickfrom mail clients (no Origin, no CSRF; the HMAC is the credential). A GET never changes state.Sending
sendsrow (queued/sent/failed/bounced, attempts, message id). Oneemailjob in the existingjobstable drains what is due, at mostemail.hourlyan hour (default 20, so under Gmail's daily limit), and re-arms itself at the next due row or in five minutes when the budget is spent. Post and digest sends are spaced3600 / email.hourlyseconds apart when queued.onPublishedhook oncreatePosts. A partial unique index makes re-publishing idempotent.digestjob runs weekly atemail.digest.day/email.digest.hourUTC, re-arms itself and on settings change, is skipped when nothing was published since the last digest that went out, and sends each weekly subscriber only the posts their scope covers.RCPT TO(5xxEENVELOPE, or listed inrejected) is recorded asbouncedand the address is suppressed; other 5xx fail for good; 4xx and connection errors retry with backoff up to five attempts. Subscribing a suppressed address again clears the suppression and sends a fresh confirmation."<email.from.name>" <ALERTS_FROM>through the existingSMTP_URLtransport, withReply-Tofromemail.reply_to,List-Unsubscribe+List-Unsubscribe-Post+Precedence: bulkon post and digest emails.Emails
server/emails.tsrenders confirmation, manage, post and digest emails as HTML (600 px table layout, inline styles, the site's dark palette, the author's accent) and text. Subjects, intros and the footer line are settings with{{placeholders}}./admingains anemailarea: sender name, reply-to, emails an hour, digest day (newchoicesetting kind) and hour, the templates; a live preview (iframe to the owner-only/admin/email?kind=, reloaded when an email setting changes) and "send a test" to a typed address (defaults toALERTS_EMAIL). Asubscribersband shows counts, scopes, queued/sent today, next digest, recent sends and bounces. The owner menu links to it.Housekeeping
jobs.addno longer deletes the row of a job that is running when the handler re-arms its own kind (the digest and email jobs do), so the next run is never marked done by mistake.sendsafter 90./privacygets a Subscribers entry./subscribein the sitemap andllms.txt; manage pages arenoindex.Migration 007
Additive only. No dependency added (
nodemailerwas already present).Production
ALERTS_FROMis now also the subscription sender (already set);SMTP_URLas today.ALERTS_EMAILonly prefills the "send a test" address.Checks
npm run typecheck,npx eslint .,npx prettier --check .,npm run build,npm run size(largest page 146.7 KB, unchanged;/subscribe136.8 KB,/admin139.5 KB).server/subscribers.test.ts,server/sends.test.ts,server/digest.test.ts,server/jobs.test.ts,server/posts.test.ts,server/settings.test.ts,server/messages.test.ts(40 passing)./subscribeforms; confirmation email (FromProject Graphite <projectgraphite.org@gmail.com>,multipart/alternative); confirm link lands on the manage page; publishing a post from the editor sent the post email withList-Unsubscribe,List-Unsubscribe-Post: List-Unsubscribe=One-Clickand both parts; the digest job (forced due) sent "This week on Project Graphite: 1 new post" to the weekly subscriber only; manage page saved a new scope; unsubscribe page button and an RFC 8058 style POST without Origin both unsubscribed; resubscribe worked; a forged signature is a 404;/adminpreview and "send a test" delivered; a stub SMTP server answering550 5.1.1atRCPT TOproduced abouncedsend, suppressed the address and showed under bounces on/admin.Out of scope, noticed
Every post page logs a hydration error from the "about" rail panel (
<a>inside<a>in the mini window), from part 1.