nicholai-work-2026/dev/contact-form-plan.md

9.5 KiB

Contact Form → n8n Webhook with Personalized MDX Response Plan

Overview

Wire the existing Astro contact form to an n8n webhook using PUBLIC_N8N_WEBHOOK_URL, enable n8n to return a personalized MDX/Markdown message, render it on the client, and implement automatic fallback to a standard toast notification when n8n is down or fails.


Implementation Steps

1. n8n Webhook + Environment Setup

Verify n8n Webhook Configuration:

  • In your n8n instance, create or verify a Webhook node configured for POST requests
  • Use a path like contact-form
  • Note the complete webhook URL (e.g., https://your-n8n-instance.com/webhook/contact-form)

Define Response Contract: The n8n workflow should return JSON in one of these formats:

  • Success: { success: true, format: 'mdx', message: '...markdown/mdx string...' }
  • Handled Error: { success: false, error: 'Human-friendly error message' }

Environment Variable:

  • Confirm PUBLIC_N8N_WEBHOOK_URL is set in .env with the webhook URL
  • Ensure the same variable is configured in your Cloudflare Pages environment settings
  • Optional: Update env.d.ts to type import.meta.env.PUBLIC_N8N_WEBHOOK_URL for TypeScript safety

2. Wire Astro Contact Form to n8n (with Robust Error Detection)

File to modify: src/pages/contact.astro

Form Markup Updates:

  • Add id="contact-form" to the form element
  • Remove action="#" and method="POST" attributes (JavaScript will handle submission)
  • Preserve all existing classes, labels, and the custom subject dropdown

Client-Side Submit Handler: Add a new script block (or extend the existing one) with:

  1. Form submission interception:

    • Attach a submit event listener that calls preventDefault()
    • Extract form data using FormData API
    • Build JSON payload including:
      • name, email, subject, message
      • Metadata: timestamp (ISO string), source: 'portfolio-website'
  2. Fetch with timeout wrapper:

    • Use fetch(import.meta.env.PUBLIC_N8N_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
    • Wrap with AbortController or Promise.race for 8-10 second timeout
  3. Failure detection conditions (any of these triggers fallback):

    • Network error or thrown exception
    • Timeout reached
    • Non-2xx HTTP response
    • 2xx response with success: false in JSON
  4. Success path:

    • Extract the message field from response
    • Pass to MDX/Markdown rendering logic (see Step 3)
    • Show brief success state on submit button
  5. Failure path:

    • Display standard toast notification with error message
    • Keep form data intact (don't reset)
    • Re-enable submit button

Button UX States:

  • Waiting: Disable button, change text to "Transmitting..."
  • Success: Briefly show "Message Sent!" then re-enable
  • Failure: Show "Transmission Failed" then revert to original text

3. Render Personalized MDX/Markdown Response

Add Markdown Renderer:

  • Install a lightweight markdown library via pnpm add marked (or markdown-it)
  • Import it in the client-side script section

Response Panel UI:

  • Create a dedicated container near the form submit area (e.g., bordered card)
  • Initially hidden (hidden class or display: none)
  • Becomes visible only when successful response is received
  • Style with existing design system classes for consistency

Rendering Logic: When response has success: true and format: 'mdx':

  1. Convert the message string to HTML using the markdown library
  2. Inject into response panel using innerHTML
  3. Apply typography classes (prose or custom) for proper formatting
  4. If markdown conversion throws, treat as failure and show fallback toast

Accessibility:

  • Add role="status" to the response panel
  • Ensure proper color contrast
  • Test with keyboard navigation and screen readers

Security:

  • Since content comes from your own n8n instance, it's trusted
  • Still avoid allowing script tags in the markdown content
  • Keep response panel visually constrained

4. n8n Workflow Processing & Templating

In your n8n workflow (after the Webhook node):

Template the Personalized Message:

  • Use Set or Function nodes to build a Markdown/MDX string
  • Use incoming fields like {{ $json.name }}, {{ $json.subject }}, {{ $json.message }}
  • Example structure:
    # Thanks, {{ name }}!
    
    I received your message about **{{ subject }}**.
    
    I'll review it and get back to you within 24-48 hours at {{ email }}.
    
    In the meantime, feel free to check out [my recent work](/work) or [blog posts](/blog).
    
    — Nicholai
    

Workflow Branches:

  • Validation node: Check for required fields (name, email, message)
    • If missing: Return { success: false, error: 'Please fill in all required fields' }
  • Email notification node: Send yourself a formatted email with the submission details
  • Optional logging node: Save to Google Sheets, database, or CRM

Webhook Response Node:

  • At the end of the workflow, add a "Respond to Webhook" node
  • Return JSON matching the contract:
    • Success: { success: true, format: 'mdx', message: '...' }
    • Error: { success: false, error: '...' }
  • For unexpected internal errors, either:
    • Let workflow fail (frontend timeout will catch it), or
    • Wrap in try/catch and still return { success: false }

5. Fallback Toast & Automatic Failure Detection UX

Toast Notification Implementation:

  • Create a reusable toast function (if not already present)
  • Should support both success and error styles
  • Position in top-right or bottom-right of viewport
  • Auto-dismiss after 5-7 seconds with smooth fade-out

Error Toast Content:

"We couldn't reach the messaging system. Please try again or email me directly at nicholai@nicholai.work"

Automatic Detection:

  • Trigger error toast for any failure condition from Step 2
  • Works even if n8n is completely unreachable (DNS/SSL issues, 500 errors, timeouts)

User Experience:

  • On failure: Do not clear the form (preserves user's work)
  • Optional: Add inline text under submit button: "Auto-response unavailable; message will still be delivered via email"
  • Ensure toast has role="alert" for accessibility

6. Testing & Validation

Happy Path Tests:

  • With n8n workflow active and webhook listening:
    1. Submit form with various subject/message combinations
    2. Verify n8n receives correct payload with all fields
    3. Confirm n8n builds expected personalized MDX string
    4. Check that frontend displays rendered response panel with proper formatting
    5. Verify email notification is sent
    6. Test that form resets appropriately

Failure Path Tests:

  1. n8n completely down:

    • Stop n8n instance or point env var to invalid URL
    • Submit form
    • Confirm: Timeout triggers, error toast appears, form data preserved, no response panel shown
  2. n8n returns error:

    • Modify workflow to return { success: false, error: 'Test error' }
    • Submit form
    • Confirm: Error toast shows n8n's error message, no response panel
  3. Network timeout:

    • Add artificial delay in n8n workflow (>10 seconds)
    • Confirm: Frontend timeout triggers fallback
  4. Invalid markdown:

    • Have n8n return malformed markdown that breaks the parser
    • Confirm: Rendering error is caught and fallback toast appears

Browser & Responsiveness:

  • Test on desktop (Chrome, Firefox, Safari)
  • Test on mobile viewport (iOS Safari, Chrome Android)
  • Verify response panel and toasts don't break layout
  • Check animations and transitions are smooth
  • Test with keyboard-only navigation
  • Test with screen reader (VoiceOver or NVDA)

Production Verification:

  • After deploying with env var configured:
    1. Submit real test message from live site
    2. Confirm end-to-end flow works
    3. Check browser console for CORS errors (adjust n8n/proxy if needed)
    4. Verify SSL/HTTPS works correctly
    5. Test from different networks (WiFi, mobile data)

7. Future-Proofing Options

Server-Side Proxy (Optional): If you want to hide the webhook URL and do MDX→HTML conversion server-side:

  1. Create an Astro API route (e.g., /api/contact.ts) or Cloudflare Worker
  2. Have it:
    • Accept form JSON from browser
    • Add server-side validation/rate limiting
    • Call n8n webhook
    • Convert returned MDX to HTML server-side
    • Return normalized { success, html } to client
  3. Frontend code changes minimally (just POST URL changes)

Benefits:

  • Webhook URL never exposed to client
  • Additional security layer
  • Server-side rate limiting
  • Can add spam protection (honeypot, CAPTCHA)

Richer MDX Components: If you later want actual MDX components (not just markdown):

  • Add runtime MDX renderer like @mdx-js/mdx on client
  • Or render MDX to React components server-side in the proxy route
  • Would allow n8n to return interactive components, not just static markdown

Critical Files

  • src/pages/contact.astro - Main file to modify (form markup + client script)
  • .env - Contains PUBLIC_N8N_WEBHOOK_URL
  • env.d.ts - Optional TypeScript environment variable typing
  • n8n workflow - Webhook node + processing nodes + response node

Success Criteria

Form submits to n8n webhook successfully n8n returns personalized MDX message Frontend renders markdown as HTML in response panel Timeout/error conditions trigger fallback toast Form data preserved on failure Works on desktop and mobile Accessible to keyboard and screen reader users No CORS issues in production Email notifications sent from n8n