From 66117d375ef6e1925daf0fcb7760548185aa116c Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 7 Jul 2026 08:24:56 -0700 Subject: [PATCH] site-builder: ContactForm relay wiring (recipient, thank-you, honeypot, marker) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../forms/ContactForm.toHtml.test.ts | 24 +++++++++++++ craft/src/components/forms/ContactForm.tsx | 34 ++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 craft/src/components/forms/ContactForm.toHtml.test.ts diff --git a/craft/src/components/forms/ContactForm.toHtml.test.ts b/craft/src/components/forms/ContactForm.toHtml.test.ts new file mode 100644 index 0000000..d79c413 --- /dev/null +++ b/craft/src/components/forms/ContactForm.toHtml.test.ts @@ -0,0 +1,24 @@ +import { describe, test, expect } from 'vitest'; +import { ContactForm } from './ContactForm'; + +const toHtml = (ContactForm as any).toHtml; + +describe('ContactForm.toHtml relay wiring', () => { + test('with recipientEmail: emits marker, placeholder action, honeypot', () => { + const { html } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx', fields: [] }, ''); + expect(html).toMatch(//); + expect(html).toMatch(/action="__WHP_FORM_ACTION__F[0-9a-z]+__"/); + expect(html).toContain('method="POST"'); + expect(html).toContain('name="_gotcha"'); + // marker id and action id match + const mid = html.match(/id="(F[0-9a-z]+)"/)![1]; + expect(html).toContain(`__WHP_FORM_ACTION__${mid}__`); + }); + + test('without recipientEmail: no marker, falls back to formAction', () => { + const { html } = toHtml({ formAction: '/legacy', fields: [] }, ''); + expect(html).not.toContain('WHP-FORM'); + expect(html).toContain('action="/legacy"'); + expect(html).not.toContain('_gotcha'); + }); +}); diff --git a/craft/src/components/forms/ContactForm.tsx b/craft/src/components/forms/ContactForm.tsx index a813e01..0955cd9 100644 --- a/craft/src/components/forms/ContactForm.tsx +++ b/craft/src/components/forms/ContactForm.tsx @@ -21,6 +21,8 @@ interface ContactFormProps { labelColor?: string; inputBg?: string; inputBorder?: string; + recipientEmail?: string; + thankYouUrl?: string; } const defaultFields: ContactFormField[] = [ @@ -187,6 +189,23 @@ const ContactFormSettings: React.FC = () => { /> + {/* Relay recipient */} +
+ + setProp((p: ContactFormProps) => { p.recipientEmail = e.target.value; })} + placeholder="you@example.com" style={{ ...inputStyle, padding: '4px 8px', fontSize: 12 }} /> +

+ Delivered via the site's contact-form relay. Requires the relay to be enabled on this server. +

+
+
+ + setProp((p: ContactFormProps) => { p.thankYouUrl = e.target.value; })} + placeholder="/thank-you (blank = hosted page)" style={{ ...inputStyle, padding: '4px 8px', fontSize: 12 }} /> +
+ {/* Success Message */}
@@ -358,6 +377,8 @@ ContactForm.craft = { labelColor: '#374151', inputBg: '#ffffff', inputBorder: '#d1d5db', + recipientEmail: '', + thankYouUrl: '', }, rules: { canDrag: () => true, @@ -414,8 +435,19 @@ ContactForm.craft = { alignSelf: 'flex-start', }); + const useRelay = !!props.recipientEmail; + const fid = 'F' + Math.random().toString(36).slice(2, 8); + const actionAttr = useRelay ? `__WHP_FORM_ACTION__${fid}__` : esc(props.formAction || '#'); + const honeypot = useRelay + ? `` + : ''; + const marker = useRelay + ? `` + : ''; + return { - html: `
+ html: `${marker} + ${honeypot} ${fieldsHtml}
`,