Skip to content

Instantly share code, notes, and snippets.

@Nooshu
Created March 13, 2026 19:55
Show Gist options
  • Select an option

  • Save Nooshu/f4655e77defa1cfa875c376e2f85de24 to your computer and use it in GitHub Desktop.

Select an option

Save Nooshu/f4655e77defa1cfa875c376e2f85de24 to your computer and use it in GitHub Desktop.
This is the Cloudflare Turnstile version of the contact.js file that sits in the functions/api directory.
import { escapeHtml } from '../../_helpers/escape-html.js';
export const onRequestPost = async ({ request, env }) => {
try {
// Accept standard form submissions
const contentType = request.headers.get('content-type') || '';
if (!contentType.includes('application/x-www-form-urlencoded') && !contentType.includes('multipart/form-data')) {
return new Response(JSON.stringify({ error: 'Unsupported content type' }), {
status: 415,
headers: { 'content-type': 'application/json' },
});
}
const form = await request.formData();
// Honeypot - reject if set
if (form.get('_akjhaskjdkjhakjshdadjknjnkdsa')) {
const hpUrl = new URL('/', request.url);
return Response.redirect(hpUrl.toString(), 303);
}
const name = (form.get('name') || '').toString().trim();
const email = (form.get('email') || '').toString().trim();
const message = (form.get('message') || '').toString().trim();
const redirectUrl = (form.get('_redirect') || '/contact/thanks/').toString();
if (!name || !email || !message) {
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
status: 400,
headers: { 'content-type': 'application/json' },
});
}
// Basic validation
if (name.length > 200 || email.length > 320 || message.length > 5000) {
return new Response(JSON.stringify({ error: 'Invalid field lengths' }), {
status: 400,
headers: { 'content-type': 'application/json' },
});
}
// Verify Cloudflare Turnstile token - REQUIRED for all submissions
const turnstileToken = form.get('cf-turnstile-response');
if (!turnstileToken) {
return new Response(JSON.stringify({ error: 'Turnstile token is required' }), {
status: 400,
headers: { 'content-type': 'application/json' },
});
}
const secret = env.TURNSTILE_SECRET_KEY;
if (!secret) {
return new Response(
JSON.stringify({
error: 'Server not configured for CAPTCHA (missing TURNSTILE_SECRET_KEY)',
}),
{ status: 500, headers: { 'content-type': 'application/json' } }
);
}
// Include remote IP for additional verification (best practice)
const remoteIp = request.headers.get('CF-Connecting-IP') || request.cf?.clientAddress || null;
const verifyBody = {
secret,
response: turnstileToken.toString(),
};
if (remoteIp) {
verifyBody.remoteip = remoteIp;
}
const verifyResp = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(verifyBody),
});
if (!verifyResp.ok) {
return new Response(JSON.stringify({ error: 'Failed to verify Turnstile' }), {
status: 502,
headers: { 'content-type': 'application/json' },
});
}
const verifyJson = await verifyResp.json();
// Check if verification was successful
if (!verifyJson.success) {
const errorCodes = verifyJson['error-codes'] || [];
return new Response(
JSON.stringify({
error: 'CAPTCHA verification failed',
details: errorCodes.length > 0 ? errorCodes.join(', ') : 'Unknown error',
}),
{ status: 400, headers: { 'content-type': 'application/json' } }
);
}
// Optional: Verify hostname matches (helps prevent token reuse from other domains)
const requestHost = new URL(request.url).hostname;
if (
verifyJson.hostname &&
verifyJson.hostname !== requestHost &&
!requestHost.endsWith(`.${verifyJson.hostname}`)
) {
console.warn(`Turnstile hostname mismatch: expected ${requestHost}, got ${verifyJson.hostname}`);
}
// Prepare email via Resend
const fromEmail = 'website@nooshu.com';
const subject = form.get('_email.subject')?.toString() || 'New Message from Nooshu.com';
const textBody = `New contact form submission on nooshu.com\n\nName: ${name}\nEmail: ${email}\n\nMessage:\n${message}`;
const htmlBody = `<h2>New contact form submission on nooshu.com</h2><p><strong>Name:</strong> ${escapeHtml(name)}<br/><strong>Email:</strong> ${escapeHtml(email)}</p><p><strong>Message:</strong></p><pre style="white-space:pre-wrap">${escapeHtml(message)}</pre>`;
const apiKey = env.RESEND_API_KEY;
if (!apiKey) {
return new Response(
JSON.stringify({
error: 'Server not configured for email (missing RESEND_API_KEY)',
}),
{ status: 500, headers: { 'content-type': 'application/json' } }
);
}
const resendResp = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
from: `Nooshu Contact <${fromEmail}>`,
to: ['test@example.com'],
reply_to: email,
subject,
text: textBody,
html: htmlBody,
}),
});
if (!resendResp.ok) {
const text = await resendResp.text();
return new Response(JSON.stringify({ error: 'Email send failed', details: text }), {
status: 502,
headers: { 'content-type': 'application/json' },
});
}
// Redirect to thank you page (absolute URL required)
const finalRedirect = new URL(redirectUrl, request.url);
return Response.redirect(finalRedirect.toString(), 303);
} catch (err) {
return new Response(JSON.stringify({ error: 'Server error', details: String(err) }), {
status: 500,
headers: { 'content-type': 'application/json' },
});
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment