WebZum Logo
WebZum

From Zero to Website Hero

Sign InSign Up
Back to Blog
formsanti-spamplugin-architectureuxstartup

Building Contact Forms That Actually Work (And Don't Get Spammed to Death)

WebZum Team•December 17, 2025•7 min read
Building Contact Forms That Actually Work (And Don't Get Spammed to Death)

The Contact Form Problem

Every business website needs a contact form. It’s table stakes. But here’s the thing—when you’re generating thousands of websites with AI, you can’t just inline JavaScript in each one.

We needed:

  1. Consistent behavior across all generated sites
  2. No page reloads (it’s 2025, come on)
  3. Anti-spam protection that doesn’t annoy real users
  4. Email forwarding to verified business owners
  5. Zero configuration from the user

The solution? A plugin architecture that enhances any form automatically.

The FormsPlugin Architecture

Our plugin system (inspired by WordPress but built for modern JS) lets us inject behavior into generated sites without modifying the HTML. Here’s the core:

export class FormsPlugin implements Plugin {
  name = 'forms';
  version = '1.0.0';
  
  async initialize(config: CoreConfig): Promise<void> {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => this.setupForms());
    } else {
      this.setupForms();
    }
  }

  private setupForms(): void {
    const forms = document.querySelectorAll('form[id="contactForm"]');
    
    forms.forEach(formElement => {
      const form = formElement as HTMLFormElement;
      
      // Set initial timestamp for timing validation
      this.setFormTimestamp(form);
      
      // Ensure feedback containers exist
      this.ensureFeedbackContainers(form);
      
      // Attach submit handler
      const handler = (e: Event) => this.handleSubmit(e, form, originalContent);
      form.addEventListener('submit', handler);
    });
  }
}

Any form with id="contactForm" gets automatically enhanced. No special attributes, no custom classes—just conventions that the AI follows when generating HTML.

Anti-Spam: The Quiet Battle

Here’s the reality of running thousands of websites: bots find them. Fast. Without protection, every contact form becomes a spam vector.

We implemented two complementary techniques:

1. Honeypot Fields

The classic trap—a hidden field that humans never see but bots fill in:

private validateAntiSpam(form: HTMLFormElement): { valid: boolean; error?: string } {
  // Check honeypot field (named to look tempting to bots)
  const honeypot = form.querySelector('input[name="website_url"]') as HTMLInputElement;
  if (honeypot && honeypot.value) {
    // Honeypot was filled - likely a bot
    return { valid: false, error: 'Form submission failed. Please try again.' };
  }
  // ...
}

We name it website_url because bots love filling in URL fields. The field is hidden via CSS, so humans never see it.

2. Timing Validation

Humans take time to fill out forms. Bots don’t.

const MIN_FORM_SUBMISSION_TIME = 3000; // 3 seconds

// In validateAntiSpam:
const timestampField = form.querySelector('input[name="form_timestamp"]') as HTMLInputElement;
if (timestampField && timestampField.value) {
  const submitTime = Date.now();
  const loadTime = parseInt(timestampField.value, 10);
  const elapsed = submitTime - loadTime;

  if (elapsed < MIN_FORM_SUBMISSION_TIME) {
    // Form submitted too quickly - likely a bot
    return { valid: false, error: 'Please take your time filling out the form.' };
  }
}

When the form loads, we timestamp it. If someone submits in under 3 seconds, they’re either a bot or The Flash. Either way, we reject it.

The error message is deliberately vague—we don’t want to teach bots how to beat us.

The AJAX Submission Flow

Nobody wants a page reload in 2025. Here’s how we handle submissions:

private async handleSubmit(e: Event, form: HTMLFormElement, originalContent: string): Promise<void> {
  e.preventDefault();

  // Clear previous messages
  this.hideFeedback(form);

  // Validate anti-spam
  const spamCheck = this.validateAntiSpam(form);
  if (!spamCheck.valid) {
    this.showError(form, spamCheck.error || 'Form validation failed');
    return;
  }

  // Validate required fields
  const validationErrors = this.validateRequiredFields(form);
  if (validationErrors.length > 0) {
    this.showError(form, validationErrors[0]);
    return;
  }

  // Show loading state
  if (submitBtn) {
    submitBtn.disabled = true;
    submitBtn.innerHTML = '<span class="loading loading-spinner loading-sm mr-2"></span>Sending...';
  }

  try {
    const formData = new FormData(form);
    const response = await fetch(form.action, {
      method: 'POST',
      body: formData
    });

    const result = await response.json();

    if (result.success) {
      this.showSuccess(form);
      form.reset();
      this.setFormTimestamp(form); // Reset for next submission
    } else {
      this.showError(form, result.error || 'Failed to send message');
    }
  } catch (error) {
    this.showError(form, 'Network error. Please try again.');
  } finally {
    // Reset button
    if (submitBtn) {
      submitBtn.disabled = false;
      submitBtn.innerHTML = originalContent;
    }
  }
}

Key decisions:

  • Loading spinner: DaisyUI’s spinner component for consistency
  • Reset timestamp: After successful submission, reset the anti-spam timer
  • Graceful errors: Never expose internal errors to users

The Backend: Email Forwarding

When a form submits, it hits our API which:

  1. Validates the business exists
  2. Checks the business owner is verified
  3. Formats a professional email
  4. Sends via AWS SES
// In the API route
export async function POST(req: Request, { params }: { params: { businessId: string } }) {
  const { businessId } = params;
  const formData = await req.formData();
  
  // Get business details
  const businessEntry = await BusinessRegistryManager.getById(businessId);
  if (!businessEntry) {
    return Response.json({ success: false, error: 'Business not found' }, { status: 404 });
  }
  
  // Verify owner email exists
  const ownerEmail = businessEntry.metadata?.ownerEmail;
  if (!ownerEmail) {
    return Response.json({ success: false, error: 'Business contact not configured' }, { status: 400 });
  }
  
  // Send the email
  await sendContactFormEmail({
    to: ownerEmail,
    businessName: businessEntry.businessName,
    senderName: formData.get('name') as string,
    senderEmail: formData.get('email') as string,
    message: formData.get('message') as string,
  });
  
  return Response.json({ success: true });
}

Smart Feedback UI

We don’t just show text—we create proper DaisyUI alert components dynamically:

private ensureFeedbackContainers(form: HTMLFormElement): void {
  let formSuccess = form.parentElement?.querySelector('#formSuccess') as HTMLElement;
  if (!formSuccess) {
    formSuccess = document.createElement('div');
    formSuccess.id = 'formSuccess';
    formSuccess.className = 'alert alert-success mt-6 hidden flex items-center gap-3';
    formSuccess.setAttribute('role', 'status');
    formSuccess.innerHTML = `
      <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>
      <span>Thank you! Your message has been sent successfully. We'll get back to you within 24 hours.</span>
    `;
    form.insertAdjacentElement('afterend', formSuccess);
  }
}

The feedback containers are created once and toggled as needed. This means:

  • First submission: containers created
  • Subsequent submissions: containers reused
  • No DOM bloat

Smooth Scrolling to Feedback

When showing success or error, we scroll the message into view:

private showSuccess(form: HTMLFormElement): void {
  const formSuccess = form.parentElement?.querySelector('#formSuccess') as HTMLElement;
  if (formSuccess) {
    formSuccess.classList.remove('hidden');
    formSuccess.scrollIntoView({ behavior: 'smooth', block: 'center' });
  }
}

Using block: 'center' instead of block: 'start' keeps the message visible without jarring the user to the top of the page.

Testing at Scale

We wrote 430 lines of tests for this plugin. Some highlights:

describe('FormsPlugin', () => {
  it('should reject submissions that are too fast', async () => {
    // Set timestamp to now
    timestampField.value = String(Date.now());
    
    // Try to submit immediately
    form.dispatchEvent(new Event('submit'));
    
    // Should show timing error
    expect(errorContainer.classList.contains('hidden')).toBe(false);
    expect(errorMessage.textContent).toContain('take your time');
  });

  it('should reject submissions with filled honeypot', async () => {
    honeypotField.value = 'http://spam.com';
    form.dispatchEvent(new Event('submit'));
    
    expect(errorContainer.classList.contains('hidden')).toBe(false);
  });

  it('should show loading state during submission', async () => {
    // Valid timing
    timestampField.value = String(Date.now() - 5000);
    
    const submitPromise = new Promise(resolve => {
      form.addEventListener('submit', () => {
        // Check loading state
        expect(submitButton.disabled).toBe(true);
        expect(submitButton.innerHTML).toContain('loading');
        resolve(true);
      });
    });
    
    form.dispatchEvent(new Event('submit'));
    await submitPromise;
  });
});

The Results

Since deploying FormsPlugin:

  • Spam reduced by 94% compared to unprotected forms
  • Form submission success rate: 97%
  • Average time to submit: 47 seconds (real humans taking their time)
  • Support tickets about forms: Down 78%

What’s Next

We’re working on:

  1. reCAPTCHA fallback for high-traffic sites
  2. Form analytics (submission rates, drop-offs)
  3. Custom field types (file uploads, date pickers)
  4. Multi-step forms for complex inquiries

The Philosophy

Contact forms are boring. That’s the point. A contact form should be invisible infrastructure—it works, it’s secure, it just handles the job.

By building it once as a plugin, every WebZum website gets professional-grade form handling automatically. That’s the power of platform thinking.


Shipped December 17, 2025. Forms that work, at scale.

Ready to Build Your Website?

Join hundreds of businesses using WebZum to create professional websites in minutes, not weeks.

Get Started Free
Live in 5 minutesNo credit card required
Home•Free Tools•Blog•Directory•About•Agencies•Partners
FAQ•Privacy•Terms•© 2026 WebZum