Turnstile with Workers
Inject Turnstile implicitly into HTML elements using the HTMLRewriter runtime API.
export default {  async fetch(request, env) {    const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret)    const TURNSTILE_ATTR_NAME = "your_id_to_replace"; // The id of the element to put a Turnstile widget in    let res = await fetch(request);
    // Instantiate the API to run on specific elements, for example, `head`, `div`    let newRes = new HTMLRewriter()
      // `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API      .on("head", {        element(element) {          // In this case, you are using `append` to add a new script to the `head` element          element.append(            `<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`,            { html: true },          );        },      })      .on("div", {        element(element) {          // Add a turnstile widget element into if an element with the id of TURNSTILE_ATTR_NAME is found          if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) {            element.append(              `<div class="cf-turnstile" data-sitekey="${SITE_KEY}"></div>`,              { html: true },            );          }        },      })      .transform(res);    return newRes;  },};export default {  async fetch(request, env): Promise<Response> {    const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret)    const TURNSTILE_ATTR_NAME = "your_id_to_replace"; // The id of the element to put a Turnstile widget in
    let res = await fetch(request);
    // Instantiate the API to run on specific elements, for example, `head`, `div`    let newRes = new HTMLRewriter()
      // `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API      .on("head", {        element(element) {          // In this case, you are using `append` to add a new script to the `head` element          element.append(            `<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`,            { html: true },          );        },      })      .on("div", {        element(element) {          // Add a turnstile widget element into if an element with the id of TURNSTILE_ATTR_NAME is found          if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) {            element.append(              `<div class="cf-turnstile" data-sitekey="${SITE_KEY}" data-theme="light"></div>`,              { html: true },            );          }        },      })      .transform(res);    return newRes;  },} satisfies ExportedHandler<Env>;import { Hono } from "hono";
interface Env {  SITE_KEY: string;  SECRET_KEY: string;  TURNSTILE_ATTR_NAME?: string;}
const app = new Hono<{ Bindings: Env }>();
// Middleware to inject Turnstile widgetapp.use("*", async (c, next) => {  const SITE_KEY = c.env.SITE_KEY; // The Turnstile Sitekey from environment  const TURNSTILE_ATTR_NAME = c.env.TURNSTILE_ATTR_NAME || "your_id_to_replace"; // The target element ID
  // Process the request through the original endpoint  await next();
  // Only process HTML responses  const contentType = c.res.headers.get("content-type");  if (!contentType || !contentType.includes("text/html")) {    return;  }
  // Clone the response to make it modifiable  const originalResponse = c.res;  const responseBody = await originalResponse.text();
  // Create an HTMLRewriter instance to modify the HTML  const rewriter = new HTMLRewriter()    // Add the Turnstile script to the head    .on("head", {      element(element) {        element.append(          `<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`,          { html: true },        );      },    })    // Add the Turnstile widget to the target div    .on("div", {      element(element) {        if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) {          element.append(            `<div class="cf-turnstile" data-sitekey="${SITE_KEY}" data-theme="light"></div>`,            { html: true },          );        }      },    });
  // Create a new response with the same properties as the original  const modifiedResponse = new Response(responseBody, {    status: originalResponse.status,    statusText: originalResponse.statusText,    headers: originalResponse.headers,  });
  // Transform the response using HTMLRewriter  c.res = rewriter.transform(modifiedResponse);});
// Handle POST requests for form submission with Turnstile validationapp.post("*", async (c) => {  const formData = await c.req.formData();  const token = formData.get("cf-turnstile-response");  const ip = c.req.header("CF-Connecting-IP");
  // If no token, return an error  if (!token) {    return c.text("Missing Turnstile token", 400);  }
  // Prepare verification data  const verifyFormData = new FormData();  verifyFormData.append("secret", c.env.SECRET_KEY || "");  verifyFormData.append("response", token.toString());  if (ip) verifyFormData.append("remoteip", ip);
  // Verify the token with Turnstile API  const verifyResult = await fetch(    "https://challenges.cloudflare.com/turnstile/v0/siteverify",    {      method: "POST",      body: verifyFormData,    },  );
  const outcome = await verifyResult.json<{ success: boolean }>;
  // If verification fails, return an error  if (!outcome.success) {    return c.text("The provided Turnstile token was not valid!", 401);  }
  // If verification succeeds, proceed with the original request  // You would typically handle the form submission logic here
  // For this example, we'll just send a success response  return c.text("Form submission successful!");});
// Default handler for GET requestsapp.get("*", async (c) => {  // Fetch the original content (you'd replace this with your actual content source)  return await fetch(c.req.raw);});
export default app;from pyodide.ffi import create_proxyfrom js import HTMLRewriter, fetch
async def on_fetch(request, env):    site_key = env.SITE_KEY  attr_name = env.TURNSTILE_ATTR_NAME    res = await fetch(request)
    class Append:        def element(self, element):            s = '<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>'            element.append(s, {"html": True})
    class AppendOnID:        def __init__(self, name):            self.name = name        def element(self, element):            # You are using the `getAttribute` method here to retrieve the `id` or `class` of an element            if element.getAttribute("id") == self.name:                div = f'<div class="cf-turnstile" data-sitekey="{site_key}" data-theme="light"></div>'                element.append(div, { "html": True })
    # Instantiate the API to run on specific elements, for example, `head`, `div`    head = create_proxy(Append())    div = create_proxy(AppendOnID(attr_name))    new_res = HTMLRewriter.new().on("head", head).on("div", div).transform(res)
    return new_resasync function handlePost(request, env) {    const body = await request.formData();    // Turnstile injects a token in `cf-turnstile-response`.    const token = body.get('cf-turnstile-response');    const ip = request.headers.get('CF-Connecting-IP');
    // Validate the token by calling the `/siteverify` API.    let formData = new FormData();
    // `secret_key` here is the Turnstile Secret key, which should be set using Wrangler secrets    formData.append('secret', env.SECRET_KEY);    formData.append('response', token);    formData.append('remoteip', ip); //This is optional.
    const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';    const result = await fetch(url, {        body: formData,        method: 'POST',    });
    const outcome = await result.json();
    if (!outcome.success) {        return new Response('The provided Turnstile token was not valid!', { status: 401 });    }    // The Turnstile token was successfully validated. Proceed with your application logic.    // Validate login, redirect user, etc.
  // Clone the original request with a new body    const newRequest = new Request(request, {        body: request.body, // Reuse the body        method: request.method,        headers: request.headers    });
    return await fetch(newRequest);}
export default {  async fetch(request, env) {    const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret)    const TURNSTILE_ATTR_NAME = 'your_id_to_replace'; // The id of the element to put a Turnstile widget in
    let res = await fetch(request)
    if (request.method === 'POST') {      return handlePost(request, env)    }
    // Instantiate the API to run on specific elements, for example, `head`, `div`    let newRes = new HTMLRewriter()      // `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API      .on('head', {        element(element) {
          // In this case, you are using `append` to add a new script to the `head` element          element.append(`<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`, { html: true });        },      })      .on('div', {        element(element) {          // You are using the `getAttribute` method here to retrieve the `id` or `class` of an element          if (element.getAttribute('id') === <NAME_OF_ATTRIBUTE>) {            element.append(`<div class="cf-turnstile" data-sitekey="${SITE_KEY}" data-theme="light"></div>`, { html: true });          }        },      })      .transform(res);    return newRes  }}Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark