Interactive Features
The contact form interaction is managed through a client-side Astro page that collects user input and a server-side API endpoint that processes the submission. The client-side component handles initial validation, honeypot spam detection, and Cloudflare Turnstile CAPTCHA integration, while the server-side component performs rigorous validation, security token verification, and email delivery via Cloudflare Email Service.
Client-Side Form Handling
Section titled “Client-Side Form Handling”The contact form is rendered in src/pages/contact.astro and utilizes a standard HTML form structure with specific fields for name, email, subject, and message 1. The form includes a honeypot field (#honeypot) designed to be left empty by legitimate users to detect automated bots.
Client-side JavaScript attached to the form’s submit event performs the following actions:
- Honeypot Check: It immediately returns if the honeypot field contains data, indicating a bot submission.
- Turnstile Verification: It checks for the presence of a Cloudflare Turnstile token (
cf-turnstile-response) or a fallback flag (data-turnstile-failed). If neither is present, it displays an error message requiring the user to complete the security check. - Submission: If validation passes, it sends a
POSTrequest to/api/contactwith the form data serialized as JSON. - Feedback: It updates the UI with success or error messages in the
#form-messagediv and resets the form upon success.
The page also includes a fallback mechanism for Turnstile errors (e.g., due to VPNs or WARP), which allows form submission without a token if the security check fails to load.
Server-Side API Validation
Section titled “Server-Side API Validation”The server-side logic resides in src/pages/api/contact.ts and handles the POST request from the client 2. The endpoint performs several layers of validation before processing the email:
- Required Fields: It checks for the presence of
name,email,subject, andmessage, returning a 400 error if any are missing. - Turnstile Token Verification: If the
TURNSTILE_SECRET_KEYenvironment variable is set (production), it requires a valid Turnstile token. It calls theverifyTurnstilehelper function to validate the token against Cloudflare’s API using the user’s IP address. If verification fails or the token is missing, it returns a 403 error. - Email Format: It validates the email address format using a regular expression, returning a 400 error if invalid.
- Message Length: It ensures the message does not exceed 5000 characters, returning a 400 error if it does.
Email Processing and Delivery
Section titled “Email Processing and Delivery”Upon successful validation, the API processes the contact form data into an email message:
- Sanitization: All user inputs are sanitized using an
escapeHtmlhelper function to prevent XSS attacks in the email body. - Metadata: The API attaches technical metadata as email headers, including the client IP, user agent, timestamp, and country code.
- Message Construction: It creates a MIME message with both HTML and plain text versions using the
mimetextlibrary. The HTML version includes styled content for better readability.
If any step fails, the API returns a 500 error with an appropriate error message.
---
import Layout from '../layouts/Layout.astro';
const title = 'Contact | sddc.info';
const description = 'Get in touch with the SDDC.info team for questions about our automated Kubernetes infrastructure projects.';
const canonicalURL = '/contact';
const turnstileSitekey = '0x4AAAAAACsyIo1SFmB1QJcg';
---
<Layout title={title} description={description} canonicalURL={canonicalURL}>
<div class="glass-card p-6 mb-6">
<h1 class="text-4xl font-bold text-gradient mb-4">Get In Touch</h1>
<p class="text-lg text-gray-600 dark:text-gray-300">Have questions about our infrastructure automation projects? We'd love to hear from you.</p>
</div>
<div class="glass-card p-6">
<form id="contact-form" class="max-w-2xl space-y-6">
<div>
<label for="name" class="block text-sm font-medium mb-2">Name *</label>
<input type="text" id="name" name="name" required class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
</div>
<div>
<label for="email" class="block text-sm font-medium mb-2">Email *</label>
<input type="email" id="email" name="email" required class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
</div>
<div>
<label for="subject" class="block text-sm font-medium mb-2">Subject *</label>
<select id="subject" name="subject" required class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
<option value="">Select a topic</option>
<option value="general">General Inquiry</option>
<option value="kubernetes">Kubernetes Cluster</option>
<option value="provisioning">Provisioning Server</option>
<option value="hardware">Hardware Questions</option>
<option value="collaboration">Collaboration</option>
</select>
</div>
<div>
import { EmailMessage } from 'cloudflare:email';
import { env } from 'cloudflare:workers';
import { createMimeMessage, Mailbox } from 'mimetext';
// Helper to sanitize HTML content
function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// Helper to verify Turnstile token
async function verifyTurnstile(token: string, secretKey: string, ip: string): Promise<boolean> {
const formData = new FormData();
formData.append('secret', secretKey);
formData.append('response', token);
formData.append('remoteip', ip);
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const result = await fetch(url, {
body: formData,
method: 'POST',
});
const outcome = await result.json();
return outcome.success;
}
export async function POST({ request }) {
try {
const { name, email: senderEmail, subject, message, 'cf-turnstile-response': turnstileToken } = await request.json();
// Validate required fields
if (!name || !senderEmail || !subject || !message) {
return new Response(
JSON.stringify({ error: 'All fields are required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }