Conversation
Validate Siteverify action and hostname (including Vercel preview URLs), run rate limiting before Turnstile, fail closed on unknown client IPs, fix Resend re-subscribe behavior, and clear the email field after success.
Feat/newsletter capture
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
santy311
approved these changes
Jun 20, 2026
Contributor
Author
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
Newsletter signup form in the site footer with a five-layer server action pipeline: honeypot,Zod validation, Upstash rate limiting, Cloudflare Turnstile verification, and Resend subscription. The form uses React
useActionStatewith a client-generated submission ID for response correlation, and an extracteduseTurnstilehook for Turnstile widget lifecycle management.Architecture
flowchart TD A[Form submit] --> B{Honeypot filled?} B -->|yes| C[Fake success - no subscribe] B -->|no| D[Zod email validation] D -->|invalid| E[Email error] D -->|valid| F[Upstash rate limit - 5/min per IP] F -->|limited| G[Too many attempts] F -->|ok| H[Cloudflare Siteverify] H -->|fail| I[Generic verification error] H -->|ok| J[Resend subscribe] J -->|fail| K[Generic or config error] J -->|ok| L[Success message]Pipeline steps
companyfield)trim,lowercase,min(1),max(254),z.email()aria-invalidRate limiting
@upstash/ratelimitsliding window (5 req / 1 min) keyed by client IPx-vercel-forwarded-for>x-real-ip>x-forwarded-for>Forwardedheader
Turnstile integration
Client (
useTurnstilehook):render=explicit, handles remount and prior failed loadserrorstate; late callbacks (expired/timeout after reset) are no-opsServer (
verifyTurnstileToken):actionmatchesnewsletter(allowstestaction with test secret keys)hostnameagainst allowed list (TURNSTILE_ALLOWED_HOSTNAMES+VERCEL_URL)localhost/127.0.0.1in developmentTURNSTILE_SECRET_KEYis absentResend subscribe flow
flowchart TD A[Get existing contact] --> B{Contact exists?} B -->|not found| C[Create contact with segment] --> D[Done - new] B -->|found| E{Unsubscribed?} E -->|yes| F[Update unsubscribed=false] --> G[Check segment membership] E -->|no| G G --> H{In segment?} H -->|yes| I[Done - already subscribed] H -->|no| J[Add to segment with retry] --> K[Done]RESEND_API_KEY/RESEND_SEGMENT_ID) fail with specificmessage
Client state management
submissionId: client-generatedcrypto.randomUUID()sent as hidden field, echoed byserver in response state. All feedback (
showMessage,showEmailError,showConfirmedMessage) is gated onstateBelongsToActiveSubmissionto prevent staleresponses from prior submissions leaking through.
messageDismissed: settrueon email input change, resetfalseon submit and onsuccessful subscription. Prevents typing during a pending request from permanently hiding the
success message.
messageKey.Test coverage
42 tests across 4 test files (1,050 lines):
submit.test.ts(17 tests) -- full pipeline: honeypot bypass, validation errors, ratelimit gating, Turnstile rejection, Resend success/failure/already-subscribed, submission ID
echo
turnstile.test.ts(11 tests) -- Siteverify: missing config, missing token, actionmismatch, hostname mismatch, Vercel URL auto-allow, test secret key handling, network errors
resend.test.ts(8 tests) -- contact create, reactivate unsubscribed, segmentmembership check, segment add retry, configuration errors
rate-limit.test.ts(6 tests) -- IP parsing (x-vercel-forwarded-for, x-forwarded-for,Forwarded header), IPv6-mapped IPv4 normalization, loopback skip, null identifier fail-closed
bun test src/lib/integrations/resend.test.ts src/lib/integrations/turnstile.test.ts
src/lib/newsletter/rate-limit.test.ts src/lib/newsletter/submit.test.ts
42 pass, 0 fail, 87 expect() calls