← Files CloudflareARCHIVED FILE

skills/turnstile-spin/references/nextjs-pages.md

2.67 KB · Oct 1, 2026 · 00:02 UTC

↓ Download file

# Next.js (Pages Router)

For older Next.js projects using `pages/` rather than `app/`. The widget renders client-side; siteverify lives in the API route.

```tsx title="pages/signup.tsx"
import Script from "next/script";

export default function SignupPage() {
	return (
		<>
			<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" />
			<form action="/api/signup" method="POST">
				<input name="email" type="email" required />
				<div
					className="cf-turnstile"
					data-sitekey="YOUR_SITEKEY"
					data-action="signup"
				/>
				<button type="submit">Sign up</button>
			</form>
		</>
	);
}
```

This native form navigates to the API response, so it does not need client-side reset code.

API route (canonical siteverify):

```ts title="pages/api/signup.ts"
import type { NextApiRequest, NextApiResponse } from "next";

const expectedHostnames = new Set(
	(process.env.TURNSTILE_HOSTNAMES ?? "")
		.split(",")
		.map((h) => h.trim())
		.filter(Boolean),
);

export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse,
) {
	const token = req.body["cf-turnstile-response"] ?? req.body.token;
	if (expectedHostnames.size === 0) {
		return res.status(403).json({ error: "Verification failed" });
	}
	const remoteip =
		(req.headers["x-forwarded-for"] as string | undefined)?.split(",")[0] ??
		req.socket.remoteAddress;

	const verify = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
		method: "POST",
		headers: { "Content-Type": "application/x-www-form-urlencoded" },
		body: new URLSearchParams({
			secret: process.env.TURNSTILE_SECRET!,
			response: token,
			...(remoteip ? { remoteip } : {}),
		}),
	});
	const result = await verify.json();
	if (
		verify.ok !== true ||
		result.success !== true ||
		result.action !== "signup" ||
		!expectedHostnames.has(result.hostname)
	) {
		return res.status(403).json({ error: "Verification failed" });
	}
	// process signup
	return res.json({ ok: true });
}
```

`signup` is the stable action for this surface. Preserve an existing custom migration action and compare the returned action to the same value. Siteverify is mandatory for every widget mode, including pre-clearance. Set `TURNSTILE_HOSTNAMES` to the deployment-specific frontend hostnames; a production value must not include `localhost` or `127.0.0.1`.

## Substitutions

| Placeholder         | Replace with                                                         |
| ------------------- | -------------------------------------------------------------------- |
| `YOUR_SITEKEY`      | The widget site key from Step 8                                      |
| `TURNSTILE_SECRET`  | Env-var name. Value is the secret captured in Step 8, kept off disk. |

SHA-256: f6fa729155fa682f6c77b157cbb75492caf172f7451bcc889bc417441a3f02a5