Contact Forms
Overview
Unified contact form system across all marketing sites with:
- Shared, reusable component
- Server-side validation
- Spam protection
- Email notifications
- Site-specific handling
Architecture
Recommended Stack
- Component: Shared Astro component in
packages/shared/ - Backend: Vercel Edge Functions (per site)
- Email Service: Resend.com
- Spam Protection: Cloudflare Turnstile
Why This Stack?
- ✅ Full control over validation and processing
- ✅ Server-side security (no exposed API keys)
- ✅ Can integrate with CRM later
- ✅ Site-specific routing (university vs student inquiries)
- ✅ Built into existing infrastructure
Implementation
Step 1: Shared Component
Located at packages/shared/components/ContactForm.astro:
---
interface Props {
site: 'university' | 'student' | 'workplace';
title?: string;
description?: string;
}
const { site, title = 'Get in Touch', description } = Astro.props;
---
<section class="contact-form">
<h2>{title}</h2>
{description && <p>{description}</p>}
<form id="contact-form" data-site={site}>
<div class="form-group">
<label for="name">Name *</label>
<input type="text" id="name" name="name" required />
</div>
<div class="form-group">
<label for="email">Email *</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="organization">Organization</label>
<input type="text" id="organization" name="organization" />
</div>
<div class="form-group">
<label for="message">Message *</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit">Send Message</button>
</form>
<div id="form-status" class="hidden"></div>
</section>
<script>
const form = document.getElementById('contact-form') as HTMLFormElement;
const status = document.getElementById('form-status') as HTMLElement;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (response.ok) {
status.textContent = 'Thank you! We will be in touch soon.';
status.className = 'success';
form.reset();
} else {
throw new Error('Failed to send message');
}
} catch (error) {
status.textContent = 'Sorry, something went wrong. Please try again.';
status.className = 'error';
}
status.classList.remove('hidden');
});
</script>Step 2: API Endpoint (Per Site)
Each site needs its own API endpoint at src/pages/api/contact.ts:
import type { APIRoute } from 'astro';
import { Resend } from 'resend';
const resend = new Resend(import.meta.env.RESEND_API_KEY);
export const POST: APIRoute = async ({ request }) => {
try {
const { name, email, organization, message, site } = await request.json();
// Validation
if (!name || !email || !message) {
return new Response(
JSON.stringify({ error: 'Missing required fields' }),
{ status: 400 }
);
}
// Send email
await resend.emails.send({
from: 'Mind Measure <noreply@mindmeasure.co.uk>',
to: 'hello@mindmeasure.co.uk',
subject: `New ${site} inquiry from ${name}`,
html: `
<h2>New Contact Form Submission</h2>
<p><strong>From:</strong> ${name} (${email})</p>
${organization ? `<p><strong>Organization:</strong> ${organization}</p>` : ''}
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
});
return new Response(
JSON.stringify({ success: true }),
{ status: 200 }
);
} catch (error) {
console.error('Contact form error:', error);
return new Response(
JSON.stringify({ error: 'Failed to send message' }),
{ status: 500 }
);
}
};Step 3: Environment Variables
Add to each Vercel project:
RESEND_API_KEY=re_xxxxxxxxxxxxx
CONTACT_EMAIL=hello@mindmeasure.co.ukStep 4: Usage in Pages
---
// sites/university/src/pages/contact.astro
import Layout from '@/layouts/Layout.astro';
import ContactForm from '@mindmeasure/shared/components/ContactForm.astro';
---
<Layout title="Contact Us">
<main>
<h1>Get in Touch</h1>
<p>We would love to hear from you about implementing Mind Measure at your institution.</p>
<ContactForm
site="university"
title="Contact Our Team"
description="Fill out the form below and we will get back to you within 24 hours."
/>
</main>
</Layout>Spam Protection
Option 1: Cloudflare Turnstile (Recommended)
- Free and privacy-friendly
- Better UX than reCAPTCHA
- Easy integration
Option 2: Simple Honeypot
- Add hidden field to form
- Reject submissions if filled (bots fill all fields)
- No external dependency
Option 3: Rate Limiting
- Limit submissions per IP address
- Implement in Edge Function
- Prevents abuse
Email Service Comparison
| Service | Free Tier | Pricing | Pros | Cons |
|---|---|---|---|---|
| Resend | 100/day | $20/mo for 50k | Modern API, good deliverability | External dependency |
| Formspree | 50/month | $10/mo | Handles spam filtering | Limited free tier |
| EmailJS | 200/month | Free | Frontend-only | Less secure |
| SendGrid | 100/day | $15/mo | Established, reliable | Complex API |
Recommendation: Resend for its modern API and developer experience.
Testing Checklist
- Form validation works client-side
- Form validation works server-side
- Email delivery works
- Error handling displays properly
- Success message displays
- Spam protection works
- Mobile responsive
- Accessibility (ARIA labels, keyboard nav)
Future Enhancements
- CRM integration (HubSpot, Salesforce)
- Auto-responder emails
- File upload support
- Multi-step forms
- Calendly integration for booking demos
- Live chat fallback
Status: Planned
Implementation Date: TBD