Files
platform-mcpandClaude Opus 4.8 721d683eb9
E2E Test Suite / Form Interaction Tests (push) Failing after 10m53s
E2E Test Suite / Security Header Tests (push) Failing after 10m39s
E2E Test Suite / Mobile Device Tests (push) Failing after 11m31s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 12m23s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 13m16s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 14m8s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 15m0s
Ping Search Engines / Notify Search Engines (push) Failing after 14m47s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m30s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Failing after 14m58s
Deploy to Production / Build & Verify (push) Failing after 19m22s
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m59s
Deploy to Production / Deploy to Railway (push) Failing after 14m58s
Deploy to Production / Deploy to Render (push) Failing after 14m57s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m6s
Deploy to Production / Deploy to Fly.io (push) Failing after 14m5s
Deploy to Production / Notify on Failure (push) Failing after 14m58s
feat(contact): CRO improvements — align services dropdown, value-prop H1, response promise above form, tighter message field
Addresses contact-page audit points 22–27:
- Confirmed no '200+ companies' credibility claim; canonical stat set retained
- Service dropdown now matches real services, led by ERP/Enterprise + Government IT;
  removed Cloud Services and AI & ML, added UI/UX Design
- Backend allowlist + labels updated to accept 'ui-ux' and reflect real service names
- H1 paired with value prop: free 1-page proposal in 24 hours
- '< 24h response' promise badge moved above the form
- Clarified contradictory hours to 'We reply within 24 business hours, Mon–Fri'
- Reduced message textarea height (rows 5→4) and max length (5000→2000)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:33:37 +05:30

9.0 KiB
Raw Permalink Blame History

Form Backend Integration

Agent: backend-specialist Date: 2026-03-21 Status: Complete ✓


Overview

Two API endpoints handle form submissions for the WorkRoot site:

Endpoint File Purpose
POST /api/contact src/pages/api/contact.ts Contact form submissions
POST /api/newsletter src/pages/api/newsletter.ts Newsletter subscriptions

Both endpoints include server-side validation, rate limiting, spam protection, and graceful degradation when optional dependencies (SMTP, newsletter providers) are not configured.


Architecture

Contact Form (/api/contact)

Flow:

  1. Client submits form → POST /api/contact (JSON body)
  2. Rate limit check (5 requests / IP / hour, sliding window)
  3. Honeypot field check (silent success if bot detected)
  4. Server-side field validation
  5. Email delivery via SMTP (nodemailer) — logs to console if SMTP not configured
  6. JSON response returned

Fields validated:

  • name — required, 2100 characters
  • email — required, valid format, max 254 chars
  • phone — optional; if present, must match phone regex
  • subject — required, allowlist enum (erp-solutions, government-systems, web-development, mobile-development, ui-ux, consulting, other; legacy values cloud-services, ai-ml, support still accepted by the API but no longer offered in the contact-page dropdown)
  • message — required, 102000 characters
  • website — honeypot, must be empty

Rate limit: 5 requests per IP per hour


Newsletter (/api/newsletter)

Flow:

  1. Client submits email → POST /api/newsletter (JSON body)
  2. Rate limit check (3 requests / IP / hour)
  3. Email format validation
  4. Subscribe via configured provider (Mailchimp → ConvertKit → SMTP notification → log-only)
  5. JSON response returned

Rate limit: 3 requests per IP per hour


Configuration

Set these environment variables (see .env.example):

Email (SMTP) — for contact form delivery and newsletter fallback

SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@example.com
SMTP_PASS=your_app_password
CONTACT_EMAIL=hello@workroot.in

Gmail tip: Enable 2FA, then create an App Password at https://myaccount.google.com/apppasswords

Newsletter — pick ONE provider

Mailchimp (recommended):

MAILCHIMP_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us1
MAILCHIMP_LIST_ID=abc123def456
MAILCHIMP_DC=us1

Uses double opt-in (status: "pending"). Handles already-subscribed gracefully.

ConvertKit:

CONVERTKIT_API_KEY=your_api_key
CONVERTKIT_FORM_ID=1234567

If no provider is configured, submissions are logged to server console. Safe for development and staging.


Dependencies

nodemailer (installed)

The SMTP functionality requires nodemailer. It is installed as a production dependency:

npm install nodemailer
npm install --save-dev @types/nodemailer

Both are now in package.json. The dynamic import (await import('nodemailer')) in the API routes handles the case where the module is unavailable with graceful degradation.


Graceful Degradation Matrix

Config state Contact form Newsletter
SMTP not configured Logs submission to console, returns 200 Falls through to next provider
SMTP configured, nodemailer unavailable Warns and logs only, returns 200 Warns and logs only, returns 200
No newsletter provider Logs to console, returns 200
SMTP set, no newsletter provider Sends email Falls back to SMTP notification
All configured Sends HTML email Sends to Mailchimp/ConvertKit

This prevents the site from breaking in any environment, including when secrets aren't yet configured.


Security

Spam Protection

  • Honeypot field: website input hidden from users via CSS (position: absolute; left: -9999px). Bots that fill all fields are detected server-side and receive a silent 200 response.
  • Rate limiting: In-memory sliding window per IP address. Configurable constants at the top of each API file:
    • Contact: RATE_LIMIT_MAX_REQUESTS = 5 per hour
    • Newsletter: RATE_LIMIT_MAX_REQUESTS = 3 per hour

Input Validation

  • All input validated server-side regardless of client-side checks
  • String lengths enforced to prevent DoS via large payloads
  • Subject field uses an allowlist (enum) to prevent injection
  • HTML in email templates uses escapeHtml() to prevent XSS in email clients

CORS

  • Production: Access-Control-Allow-Origin: https://workroot.in
  • Development: Access-Control-Allow-Origin: *
  • Preflight OPTIONS handlers implemented on both endpoints

Rate Limit Response Headers

Both endpoints return standard rate limit headers on every response:

X-RateLimit-Limit: 5
X-RateLimit-Remaining: 4
X-RateLimit-Reset: 1742000000

Response Format

All responses follow a consistent structure:

Success (200):

{ "success": true, "message": "Human-readable success message" }

Validation error (422):

{ "success": false, "error": "Specific validation error" }

Rate limited (429):

{ "success": false, "error": "Too many requests. Please try again later." }

Server error (500):

{ "success": false, "error": "Failed to send message. Please try again or email us directly." }

Client Integration

Contact form (src/pages/contact.astro)

Posts JSON to /api/contact. Handles all response states:

  • Success → toast notification + form reset
  • Validation error → toast with specific message
  • Rate limit (429) → error toast
  • Network error → error toast

Newsletter form (src/components/Footer.astro)

Posts JSON to /api/newsletter. Handles all response states:

  • Success → inline green message + form reset
  • Error → inline red message
  • Network error → inline red message
  • Message auto-hides after 5 seconds

Logging & Monitoring

Both endpoints use src/utils/logger.ts for structured logging:

  • Development: colored console output
  • Production: JSON output for log aggregators (Logtail, Datadog, etc.)

Errors are captured via src/utils/sentry.ts (optional — only active when SENTRY_DSN env var is set).

Each API request is logged with:

  • Method, path, status code
  • IP address
  • Duration in milliseconds

Testing

Test the endpoints manually with curl:

# Contact form - success case
curl -X POST http://localhost:4321/api/contact \
  -H "Content-Type: application/json" \
  -d '{"name":"Test User","email":"test@example.com","subject":"consulting","message":"Hello, I need help with a project."}'

# Newsletter - success case
curl -X POST http://localhost:4321/api/newsletter \
  -H "Content-Type: application/json" \
  -d '{"email":"subscriber@example.com"}'

# Test honeypot (should return 200 silently)
curl -X POST http://localhost:4321/api/contact \
  -H "Content-Type: application/json" \
  -d '{"name":"Bot","email":"bot@spam.com","subject":"other","message":"Buy cheap stuff now!","website":"http://spam.com"}'

# Test rate limiting (run 4+ times to trigger 429 on newsletter)
for i in 1 2 3 4; do
  curl -s -o /dev/null -w "%{http_code}\n" \
    -X POST http://localhost:4321/api/newsletter \
    -H "Content-Type: application/json" \
    -d '{"email":"test@example.com"}'
done

Playwright tests are located at:

  • tests/contact-form.spec.ts — contact form UI and API
  • tests/newsletter-subscription.spec.ts — newsletter UI and API
  • tests/api-integration.spec.ts — API endpoint integration tests
  • tests/e2e-form-interactions.spec.ts — form interaction flows

Run all form tests:

npm run test:forms

In-Memory Rate Limiter Notes

The current rate limiter is in-memory (per process). This means:

  • Works correctly for single-instance deployments (VPS, single container)
  • ⚠️ Resets on server restart
  • ⚠️ Not shared across multiple instances (e.g., PM2 cluster mode)

For multi-instance deployments, upgrade to Redis-backed rate limiting (see Future Improvements).


Alternative: FormSpree (No-code option)

If you prefer a managed third-party service without SMTP configuration:

  1. Sign up at https://formspree.io
  2. Create a form and get your form ID (e.g. xpwzgkab)
  3. Replace the fetch URL in contact.astro:
    const response = await fetch('https://formspree.io/f/xpwzgkab', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
      body: JSON.stringify(formData),
    });
    

The existing custom API endpoint can remain for future migration back to self-hosted.


Future Improvements

  • Persist rate limit state in Redis for multi-instance PM2 cluster deployments
  • Add CAPTCHA (hCaptcha or Cloudflare Turnstile) as additional spam layer
  • Store contact submissions in a database for CRM integration
  • Add confirmation/welcome email for newsletter signups (double opt-in flow)
  • Webhook notifications to Slack/Discord on new contact submissions
  • Admin dashboard to view contact form submissions