Repository navigation
Conversation
Covers deferring Turnstile until the reader engages, a daily digest of signups that fail verification, an end-of-post sign-off replacing the scroll modal, and tracking for the new sign-off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Adds a Vercel cron (0 13 * * *, authenticated with CRON_SECRET) that emails the previous Eastern day's blocked signups with each address's Resend subscription status, and notes when the 100-entry daily cap was reached. recordBlockedSignup now uses HSETNX so the first report for an email wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Lookups now run one at a time, 600ms apart, to stay under Resend's per-team rate limit. The route answers 502 when entries existed but the email was not sent, so Vercel Cron surfaces the failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
The floating button is now the only trigger. The newsletter index and archive forms get a source so their signups are attributable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Drop reports without plausible form timing or with a bad email, constrain path and reason, emit the reported event only for newly stored entries, keep the real Turnstile failure reason over token_wait_limit, pace the digest send, and label digest entries as unverified with a source column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved issues allow concurrent reports to exceed the daily cap, break local configuration through blank environment entries, and pollute article search results with repeated signup content.
3 open issues
What changed in this PR
This PR improves newsletter signup reliability and places the subscription prompt immediately after article content. It also adds a daily digest of blocked signup attempts for manual review, without automatically subscribing anyone.
Changes:
- Defer Turnstile verification until form engagement and report failed signup attempts.
- Store filtered reports in Redis and send a protected, scheduled email digest.
- Replace scroll-triggered prompts and bottom banners with an email-only sign-off, including source and visibility tracking.
| File | Description |
|---|---|
vercel.json |
Schedule the daily digest cron. |
src/utils/resend.ts |
Add subscription-status lookups. |
src/utils/env.ts |
Validate the cron authorization secret. |
src/utils/email/templates/BlockedSignupsDigest.tsx |
Render blocked reports with unverified-signup warnings. |
src/utils/email/tags.ts |
Add the digest email category. |
src/utils/email/sendBlockedSignupsDigest.tsx |
Perform paced lookups and send the digest. |
src/utils/email/sendBlockedSignupsDigest.test.ts |
Cover digest content, pacing, and failures. |
src/server/routers/mailingList.ts |
Validate and record blocked signup reports. |
src/server/routers/mailingList.test.ts |
Cover report filtering and storage outcomes. |
src/server/redis.ts |
Provide a shared Redis client. |
src/server/rateLimit.ts |
Reuse the shared Redis client. |
src/server/blockedSignups.ts |
Store deduplicated daily reports with retention and capacity limits. |
src/server/blockedSignups.test.ts |
Cover storage, deduplication, and daily limits. |
src/server/blocked-signups-digest-route.test.ts |
Cover cron authorization and responses. |
src/pages/posts/[slug].tsx |
Add the sign-off and reorder related content. |
src/pages/newsletter/page/[page].tsx |
Identify archive signup sources. |
src/pages/newsletter/index.tsx |
Identify newsletter-index signup sources. |
src/pages/newsletter/[slug].tsx |
Add the sign-off and remove the bottom banner. |
src/pages/api/cron/blocked-signups-digest.ts |
Authorize and send the previous day's digest. |
src/lib/newsletters.ts |
Select the latest published issue. |
src/hooks/useTurnstileGate.ts |
Defer verification and manage retries and interactive waits. |
src/hooks/useNewsletterModalTrigger.ts |
Restrict modal opening to the floating button. |
src/hooks/useNewsletterModalTrigger.test.ts |
Verify manual-only modal behavior. |
src/components/SubscriptionForm/TurnstileStatus.tsx |
Revise verification-failure guidance. |
src/components/SubscriptionForm/SubscriptionForm.tsx |
Report blocked attempts and support email-only forms. |
src/components/SubscriptionForm/SubscriptionForm.test.tsx |
Cover deferred verification and blocked reports. |
src/components/Post/Post.tsx |
Forward the sign-off to the full article. |
src/components/Post/FullPost.tsx |
Render the sign-off after article content. |
src/components/NewsletterSignup/PostSignoff.tsx |
Add the signup block and visibility tracking. |
src/components/NewsletterSignup/PostSignoff.test.tsx |
Cover sign-off content, links, and tracking. |
docs/superpowers/specs/2026-11-03-newsletter-signup-conversion-design.md |
Document goals, behavior, and constraints. |
docs/superpowers/plans/2026-11-03-newsletter-signup-conversion.md |
Outline implementation and verification steps. |
.env.example |
Add Turnstile and cron configuration examples. |
Review effort: Balanced
Comment on lines
+36
to
+40
| NEXT_PUBLIC_TURNSTILE_SITE_KEY= | ||
| TURNSTILE_SECRET_KEY= | ||
|
|
||
| # Authorizes Vercel cron requests to /api/cron routes | ||
| CRON_SECRET= |
Comment on lines
+37
to
+43
| if ((await redis.hlen(key)) >= MAX_BLOCKED_SIGNUPS_PER_DAY) { | ||
| if (!(await redis.hexists(key, field))) return 'full'; | ||
| } | ||
|
|
||
| const added = await redis.hsetnx(key, field, JSON.stringify(entry)); | ||
| await redis.expire(key, RETENTION_SECONDS); | ||
| return added ? 'stored' : 'duplicate'; |
- Enforce the daily blocked-signup cap atomically with a Lua script, so concurrent reports can't exceed it. - Lower the cap to 50 and maxDuration to 60s; Vercel Hobby rejects anything over 60s, which failed the preview deploy. - Keep the post sign-off out of the Pagefind index. - Comment out optional env entries in .env.example so blank values don't override the local test defaults. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz
This branch was successfully deployed
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.

Why
Posts turn about 0.4% of visitors into newsletter subscribers. PostHog and Cloudflare data from the last 30 days point to two causes:
Design:
docs/superpowers/specs/2026-11-03-newsletter-signup-conversion-design.mdPlan:
docs/superpowers/plans/2026-11-03-newsletter-signup-conversion.mdWhat changed
Turnstile timing. The check runs in
execution: 'execute'mode and starts when the reader focuses the form or submits. Errors show only after a submit. The 8s wait pauses while Cloudflare shows a click challenge. A failure before submit gets one silent re-run on submit, andturnstile_erroris logged once per phase.Failed-signup digest. When a submit still fails Turnstile, the form reports it to
mailingList.reportBlockedSignup, which checks:emailIsBadReports that pass are stored in Upstash Redis, one hash per US-Eastern day, with HSETNX, a 100/day cap and a 7-day expiry. A Vercel cron (
0 13 * * *, BearerCRON_SECRET, 120s max duration) emails Mike the previous day's entries, labeled as unverified. Resend lookups are spaced out, and the route returns 502 if the send fails. Nothing is added to the list automatically. The subscribe endpoint itself is unchanged.End-of-post sign-off.
PostSignoffsits right after the post body on posts and newsletter issues. It has Mike's headshot, two lines of copy, an email-only form, and a link to the latest issue. "Keep reading" now follows the article directly. The bottom banner is removed, andnewsletter/signoff_viewedfires when the sign-off is half visible.No scroll modal. The modal opens only from the 💌 button. The newsletter index and archive forms now pass
source.Before merging
CRON_SECRETto Vercel Production. It's required there, so without it the production build fails./api/cron/blocked-signups-digestonce by hand with the Bearer secret, against a day with 2 or more entries, to see the digest render and confirm the Resend pacing.Testing
pnpm vitest run: 345 tests pass. Six files fail to load on missing env vars (src/lib/markdown-exportand the/mdroute), and they fail the same way without this branch.pnpm tsc --noEmitis clean.pnpm buildsucceeded, though it ran before the final fix commits, which only touched form, server and email code.pnpm lintreports only an existing parse error in.claude/skills/newsletter-image/scripts/render.mjs.Follow-ups (not in this PR)
getLatestNewsletter()during the build.🤖 Generated with Claude Code
https://claude.ai/code/session_012JgqHSAa6XeuSC32XJuqLz