cloudflare-turnstile-nextjs
Skill0 starsChanged 3 months agoMITView on GitHub
Add Cloudflare Turnstile (privacy-friendly CAPTCHA) to a Next.js App Router form: client widget with explicit render and a React double-render guard, server-side token verification via siteverify, and a graceful fallback so the form keeps working until keys are configured. Use when adding bot protection, CAPTCHA, human verification, or anti-spam to a form or API route.
- Reads credentials
- Sends data out
In this repository23 other files it gives its agents.
- client-document-exportSkill
- ga4-analytics-nextjsSkill
- git-repo-privacy-auditSkill
- hardened-contact-form-nextjsSkill
- html-semantic-markupSkill
- multi-step-wizard-flowSkill
- nextjs-api-routesSkill
- nextjs-app-routerSkill
- nextjs-seo-metadataSkill
- nextjs-ui-foundationSkill
- next-themes-dark-modeSkill
- playwright-e2e-testingSkill
- react-components-patternsSkill
- react-hooks-patternsSkill
- recharts-dashboardSkill
- resend-email-nextjsSkill
- server-pdf-playwrightSkill
- shadcn-ui-setupSkill
- specialized-form-inputsSkill
- tailwind-css-v4Skill
- vitest-unit-testingSkill
- web-accessibility-wcagSkill
- zustand-persist-stateSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 23
Design principle: graceful fallback: When the keys are absent the widget is not rendered and the server skips verification, so the form …
- Very specific: line 30
Use explicit rendering (not the implicit class="cf-turnstile" scan) — it is far more reliable in React.
- Distinctive: line 107
Verify: widget appears; submitting without solving is blocked.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Cloudflare Turnstile in Next.js
Stops automated form submissions. Free, no user puzzle in most cases, and pairs well with a form that already has validation + honeypot + rate limiting.
Env vars
NEXT_PUBLIC_TURNSTILE_SITE_KEY= # public, safe in the client bundle
TURNSTILE_SECRET_KEY= # server-only, never NEXT_PUBLICDesign principle: graceful fallback. When the keys are absent the widget is
not rendered and the server skips verification, so the form behaves exactly as
before. It only enforces once both keys exist. This lets you ship the code first
and turn it on later. (NEXT_PUBLIC_* bakes in at build → redeploy after setting.)
Client (inside a "use client" form component)
Use explicit rendering (not the implicit class="cf-turnstile" scan) — it is
far more reliable in React. Guard against React's double-mount.
import Script from "next/script";
import { useEffect, useRef, useState } from "react";
const SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
interface TurnstileApi {
render: (el: HTMLElement, o: { sitekey: string; theme?: "light" | "dark" | "auto";
callback?: (t: string) => void; "error-callback"?: () => void; "expired-callback"?: () => void;
}) => string;
reset: (id?: string) => void;
remove: (id?: string) => void;
}
declare global { interface Window { turnstile?: TurnstileApi } }
// inside the component:
const [scriptReady, setScriptReady] = useState(false);
const [token, setToken] = useState<string | null>(null);
const boxRef = useRef<HTMLDivElement>(null);
const idRef = useRef<string | null>(null);
useEffect(() => {
if (!SITE_KEY || !scriptReady || !window.turnstile) return;
if (!boxRef.current || idRef.current) return; // guard: render once
idRef.current = window.turnstile.render(boxRef.current, {
sitekey: SITE_KEY, theme: "dark",
callback: setToken,
"expired-callback": () => setToken(null),
"error-callback": () => setToken(null),
});
return () => { if (idRef.current) { window.turnstile?.remove(idRef.current); idRef.current = null; } };
}, [scriptReady]);
// on submit: block if (SITE_KEY && !token); include token in the payload;
// after the response, window.turnstile?.reset(idRef.current) + setToken(null) — tokens are single-use.
// in JSX (only when SITE_KEY is set):
// <Script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
// strategy="afterInteractive" onLoad={() => setScriptReady(true)} />
// <div ref={boxRef} />Server (verify before doing any work)
async function verifyTurnstile(token: string | undefined, ip: string): Promise<boolean> {
const secret = process.env.TURNSTILE_SECRET_KEY;
if (!secret) return true; // not configured → skip (graceful)
if (!token) return false;
try {
const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ secret, response: token, remoteip: ip }),
});
const data = (await res.json()) as { success?: boolean };
return data.success === true;
} catch { return false; }
}
// in the route, AFTER parsing/honeypot and BEFORE sending anything:
// if (!(await verifyTurnstile(body.turnstileToken, ip)))
// return NextResponse.json({ error: "Verification failed." }, { status: 403 });Accept turnstileToken: z.string().max(4096).optional() in your request schema.
Cloudflare + Vercel setup
- Cloudflare dashboard → Turnstile → Add widget → name it, add your
production hostname (e.g.
example.com;localhostis allowed automatically), mode Managed. You get a Site Key (public) + Secret Key (private). - Vercel → project env:
NEXT_PUBLIC_TURNSTILE_SITE_KEY(site key) andTURNSTILE_SECRET_KEY(secret key). Redeploy (site key is build-time). - Verify: widget appears; submitting without solving is blocked; server 403s an invalid token.
Gotchas
- Implicit render (
class="cf-turnstile") is flaky in React SPAs — use explicit. - React 18 strict mode double-mounts effects — the
idRefguard prevents two widgets. - Turnstile tokens are single-use —
reset()after every submit attempt. TURNSTILE_SECRET_KEYmust be server-only; never prefix itNEXT_PUBLIC_.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

