Email digest scheduling (weekly/monthly) - #250
Merged
Merged
Conversation
Subscribers can choose how often they hear about new posts: every new post (default), a weekly digest, a monthly digest, or no post emails. - Subscriber::EmailPreferences adds the email_frequency enum and a last_digest_at delivery cursor, moved on frequency changes so switching never resends or drops posts - SendDigestsJob (weekly Mondays 08:00, monthly on the 1st) emails a branded DigestMailer digest of posts published since each subscriber's last digest, skipping subscribers with nothing new; idempotent on retry - SendPostNotificationsJob now only emails immediate subscribers - /email-preferences page, reached via a signed link in post email footers, the unsubscribe page, or when signed in - Restructure config/recurring.yml with a shared anchor: Solid Queue loads only the current environment's block, so the top-level tasks (scheduled post publishing, scheduled newsletters, Stripe sync) were never loaded in production - Fix malformed /posts/<slug>.<slug> links in new-post notification emails Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Summary
Subscribers can now choose how often they hear about new posts: every new post (default, current behavior), a weekly digest, a monthly digest, or no post emails. Digests are recurring Solid Queue jobs that gather every post published since the subscriber's last digest into one branded email, and are skipped when nothing new was published.
Changes
Subscriber::EmailPreferences: newemail_frequencyenum (immediate/weekly/monthly/none, defaultimmediate) and alast_digest_atdelivery cursor. The cursor moves when the frequency changes. Switching from per-post emails to a digest starts from now, so posts already emailed aren't repeated. Switching between weekly and monthly keeps the cursor, so no posts are skipped.SendDigestsJob: scheduled inconfig/recurring.yml(weekly: Mondays 08:00; monthly: the 1st at 08:00, server time zone/UTC). Lists up to 20 posts plus a "See N more posts" link, and moves each recipient's cursor to the run time. A retried or duplicate run finds nothing new and sends nothing.DigestMailer#digest: uses the existing branded mailer layout. Each post shows its featured image (non-WebP variant for email clients), title, byline/date, excerpt and a "Read more" link. Nothing is sent if the subscriber unsubscribed or every post was unpublished after the digest was queued.SendPostNotificationsJobnow emails onlyimmediatesubscribers./email-preferencespage (EmailPreferencesController): reached through a signed link in post email footers (new "Email preferences" link next to Unsubscribe), a "switch to a digest instead" link on the unsubscribe page, or directly by a signed-in subscriber.email-preferencesis now a reserved page slug.Two existing bugs fixed along the way
config/recurring.ymltasks never ran in production. Solid Queue usesconfig[Rails.env]whenever that key exists, and the file had aproduction:block. So in production onlyclear_solid_queue_finished_jobsloaded:publish_scheduled_posts,send_scheduled_newslettersandsync_stripe_subscriptionswere silently skipped (and the new digest jobs would have been too). The file now defines the shared tasks under adefault: &defaultanchor that every environment merges in. I checked the task list each environment resolves to withSolidQueue::Configuration./posts/<slug>.<slug>.post_url(@post, slug: @post.slug)puts the record in the:formatslot. Fixed in the notification templates, with a test that fails on the old code. The same pattern still exists in the RSS feed, sitemap and comment-reply email; I've left those for a separate change so this PR stays focused.Assumptions (the issue left these open)
nonestops post emails only. Newsletters still go to every confirmed subscriber, because unsubscribing already covers those.Post#seo_description, which is already public in the page's meta tags, so members-only/paid posts show nothing beyond their normal teaser.Documentation
config/recurring.ymlcron schedules in UTC.recurring.ymlenvironment-block gotcha under Background Jobs, and the new concern in the model list.Testing
Subscriber.first.email_preferences_tokenin the console and visit/email-preferences?token=…. Pick "Weekly digest" and save.SendDigestsJob.perform_now("weekly")in the console. The digest appears in the mail log / letter opener, and running it again sends nothing.bin/rails test: 1498 runs, 0 failures. RuboCop: no offenses. Brakeman, bundler-audit, importmap audit: clean.Closes #42
🤖 Generated with Claude Code