Marketing Sites
Contact Forms

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.uk

Step 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

ServiceFree TierPricingProsCons
Resend100/day$20/mo for 50kModern API, good deliverabilityExternal dependency
Formspree50/month$10/moHandles spam filteringLimited free tier
EmailJS200/monthFreeFrontend-onlyLess secure
SendGrid100/day$15/moEstablished, reliableComplex 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