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
POSTrequests - 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_URLis set in.envwith the webhook URL - Ensure the same variable is configured in your Cloudflare Pages environment settings
- Optional: Update
env.d.tsto typeimport.meta.env.PUBLIC_N8N_WEBHOOK_URLfor 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="#"andmethod="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:
-
Form submission interception:
- Attach a
submitevent listener that callspreventDefault() - Extract form data using
FormDataAPI - Build JSON payload including:
name,email,subject,message- Metadata:
timestamp(ISO string),source: 'portfolio-website'
- Attach a
-
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
AbortControllerorPromise.racefor 8-10 second timeout
- Use
-
Failure detection conditions (any of these triggers fallback):
- Network error or thrown exception
- Timeout reached
- Non-2xx HTTP response
- 2xx response with
success: falsein JSON
-
Success path:
- Extract the
messagefield from response - Pass to MDX/Markdown rendering logic (see Step 3)
- Show brief success state on submit button
- Extract the
-
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(ormarkdown-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 (
hiddenclass ordisplay: 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':
- Convert the
messagestring to HTML using the markdown library - Inject into response panel using
innerHTML - Apply typography classes (
proseor custom) for proper formatting - 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' }
- If missing: Return
- 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: '...' }
- Success:
- 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:
- Submit form with various subject/message combinations
- Verify n8n receives correct payload with all fields
- Confirm n8n builds expected personalized MDX string
- Check that frontend displays rendered response panel with proper formatting
- Verify email notification is sent
- Test that form resets appropriately
Failure Path Tests:
-
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
-
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
- Modify workflow to return
-
Network timeout:
- Add artificial delay in n8n workflow (>10 seconds)
- Confirm: Frontend timeout triggers fallback
-
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:
- Submit real test message from live site
- Confirm end-to-end flow works
- Check browser console for CORS errors (adjust n8n/proxy if needed)
- Verify SSL/HTTPS works correctly
- 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:
- Create an Astro API route (e.g.,
/api/contact.ts) or Cloudflare Worker - 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
- 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/mdxon 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- ContainsPUBLIC_N8N_WEBHOOK_URLenv.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