fix(auth): stop Turnstile SSR/client hydration mismatch on /login - #37
Open
tasklyappai wants to merge 2 commits into
Open
fix(auth): stop Turnstile SSR/client hydration mismatch on /login#37tasklyappai wants to merge 2 commits into
tasklyappai wants to merge 2 commits into
Conversation
isLocalhost was derived synchronously from window.location.hostname, so the server (no window) computed false and rendered the widget container while the client on localhost computed true and returned null — a hydration mismatch that regenerated the login form tree. Surfaced once a NEXT_PUBLIC_TURNSTILE_SITE_KEY was present in the dev env. Make it SSR-safe: isLocalhost is now useState(false) so SSR and the first client paint render the identical container; a post-mount effect flips it (and fires the localhost-dev-bypass token). The real-challenge skip inside the effects reads window directly via isLocal(), so localhost dev still never kicks off the doomed 110200 Cloudflare challenge. Verified: typecheck, 62 Turnstile tests, and a real-browser /login load with no hydration error in the dev log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Next 16 has no `clean` subcommand, so `next clean` read "clean" as a
project directory and errored ("no such directory: .../clean") — the
documented cache-clear command never worked. Point it at `rm -rf .next`,
which is what CLAUDE.md says it does. Handy right here: the app/on →
app/[province] route rename leaves a stale .next/types until cleared.
Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Problem
/loginthrew a React hydration mismatch that regenerated the entire login form tree on the client. Root cause is incomponents/security/Turnstile.tsx:window) →isLocalhost = false→ renders the widget container<div class="flex flex-col items-center gap-2">.isLocalhost = true→ returnsnull.Server rendered the div, client rendered nothing → mismatch. It only surfaced once a
NEXT_PUBLIC_TURNSTILE_SITE_KEYwas present in the dev env. This is exactly the "first client paint must match SSR" rule inCLAUDE.md.Fix
isLocalhostis nowuseState(false), so SSR and the first client paint render the identical container. A post-mount effect flips it totrueon localhost (and fires thelocalhost-dev-bypasstoken) — a normal post-hydration update, not a mismatch.windowdirectly via anisLocal()helper, so localhost dev still never kicks off the doomed Cloudflare 110200 challenge.Production behaviour is unchanged (non-localhost renders the widget on both server and client, as before).
Verification
npm run typecheck— cleanturnstile.test.ts+turnstile-widget.test.tsx)/login→ no hydration error in the dev-server log (vs. multiple[report-error] [client] Hydration failedlines before the fix)🤖 Generated with Claude Code