Wires the site-builder Contact Form to the form-sender relay (companion PR: whp form-sender-relay).
Adds recipientEmail + thankYouUrl settings; toHtml emits a <!--WHP-FORM …--> marker (stripped at publish, so the recipient never ships), method=POST action=__WHP_FORM_ACTION__…__ placeholder, and a hidden _gotcha honeypot — only when a recipient is set.
Backward compatible: with no recipient, output is byte-identical to the old form (verified by test).
## ContactForm relay wiring
Wires the site-builder Contact Form to the form-sender relay (companion PR: whp `form-sender-relay`).
- Adds `recipientEmail` + `thankYouUrl` settings; `toHtml` emits a `<!--WHP-FORM …-->` marker (stripped at publish, so the recipient never ships), `method=POST action=__WHP_FORM_ACTION__…__` placeholder, and a hidden `_gotcha` honeypot — only when a recipient is set.
- Backward compatible: with no recipient, output is **byte-identical** to the old form (verified by test).
- vitest: `ContactForm.toHtml.test.ts`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fix non-relay form output to match pre-change byte-for-byte by conditionally omitting the honeypot and fields lines when empty. Add backward-compat regex assertion to catch extra blank lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Id consistency (ContactForm.tsx:439-446): fid is generated once per toHtml() call and reused for both the marker's id="..." and the __WHP_FORM_ACTION__<fid>__ placeholder — they can't drift apart within a single render.
Escaping: recipientEmail/thankYouUrl are passed through esc() (line 396) before being interpolated into the <!--WHP-FORM ...--> marker. Since esc() escapes " → "and> → >, a value containing " can't break out of the pseudo-attribute, and one containing --> can't prematurely close the HTML comment (the > is neutralized). Confirmed by hand-tracing esc() at line 396.
Honeypot: type="text" (not type="hidden", which spam bots often skip on purpose) + position:absolute;left:-9999px + tabindex="-1" + aria-hidden="true" + autocomplete="off", named _gotcha (matches the common Formspree-style convention). It's emitted before fieldsHtml, so it's the first child inside <form>, and being a real <input> it's submitted normally.
Backward compat (common case): with no recipientEmail, marker/honeypot are both '', and actionAttr falls back to the pre-existing esc(props.formAction || '#'). For the realistic case — fields non-empty (i.e. defaultFields, or any user-populated field list) — I byte-diffed the old (d0925d9) template against the new one and they produce identical output.
Math.random().toString(36).slice(2,8)-style id generation matches the existing project convention (same pattern used in Countdown.tsx, Tabs.tsx, Gallery.tsx, NumberCounter.tsx, Menu.tsx, ContentSlider.tsx), so no new pattern introduced.
Test file asserts marker regex, action="__WHP_FORM_ACTION__<id>__", _gotcha presence, id-match between marker and action, and marker/gotcha absence in the non-relay path. All pass.
Finding (non-blocking nit)
ContactForm.tsx:450 — the "byte-identical" guard doesn't actually hold for empty fields, and the new regression test only exercises that non-representative case.
Both ContactForm.toHtml.test.ts cases use fields: []. With empty fields and no recipientEmail, I confirmed by direct comparison that the new code's output differs from the true pre-PR (d0925d9) baseline: the old template unconditionally emitted ${fieldsHtml} producing a whitespace-only line (" ") between <form> and <button>; the new code's fieldsHtml ? ... : '' ternary drops that line entirely when fieldsHtml is falsy.
For the realistic case — non-empty fields (the default, and what every real published form uses) — I confirmed output is byte-identical old vs. new. So this doesn't affect any real site today (nobody publishes a contact form with zero fields), and arguably the new output is better (no stray whitespace-only line). But two things worth tightening:
The commit message/PR description claims general "byte-identical" backward compat; that's only true when fields is non-empty. Worth a one-line caveat, or just special-case empty-fields to match old output exactly if literal byte-identity is the goal.
The regression test itself only covers the fields: [] edge case (which is not byte-identical to pre-PR baseline) and never exercises the common defaultFields/non-empty-fields path, which is the one that actually needs the regression guard. Recommend adding a case with real fields (or defaultFields) asserting full-string equality against a fixed expected HTML string, to actually pin the byte-identity guarantee for production forms.
Neither point blocks merge — the relay wiring, escaping, id-matching, and honeypot behavior are all correct for the shipping scenarios.
## Review: ContactForm relay wiring (`contact-form-relay` → `main`)
**Verdict: Approve, with one non-blocking nit.**
Verified locally: `npx vitest run src/components/forms/ContactForm.toHtml.test.ts` → 2/2 pass; `tsc --noEmit` clean.
### What checks out
- **Id consistency** (`ContactForm.tsx:439-446`): `fid` is generated once per `toHtml()` call and reused for both the marker's `id="..."` and the `__WHP_FORM_ACTION__<fid>__` placeholder — they can't drift apart within a single render.
- **Escaping**: `recipientEmail`/`thankYouUrl` are passed through `esc()` (line 396) before being interpolated into the `<!--WHP-FORM ...-->` marker. Since `esc()` escapes `"` → `"` *and* `>` → `>`, a value containing `"` can't break out of the pseudo-attribute, and one containing `-->` can't prematurely close the HTML comment (the `>` is neutralized). Confirmed by hand-tracing `esc()` at line 396.
- **Honeypot**: `type="text"` (not `type="hidden"`, which spam bots often skip on purpose) + `position:absolute;left:-9999px` + `tabindex="-1"` + `aria-hidden="true"` + `autocomplete="off"`, named `_gotcha` (matches the common Formspree-style convention). It's emitted before `fieldsHtml`, so it's the first child inside `<form>`, and being a real `<input>` it's submitted normally.
- **Backward compat (common case)**: with no `recipientEmail`, `marker`/`honeypot` are both `''`, and `actionAttr` falls back to the pre-existing `esc(props.formAction || '#')`. For the realistic case — `fields` non-empty (i.e. `defaultFields`, or any user-populated field list) — I byte-diffed the old (`d0925d9`) template against the new one and they produce **identical output**.
- `Math.random().toString(36).slice(2,8)`-style id generation matches the existing project convention (same pattern used in `Countdown.tsx`, `Tabs.tsx`, `Gallery.tsx`, `NumberCounter.tsx`, `Menu.tsx`, `ContentSlider.tsx`), so no new pattern introduced.
- Test file asserts marker regex, `action="__WHP_FORM_ACTION__<id>__"`, `_gotcha` presence, id-match between marker and action, and marker/gotcha absence in the non-relay path. All pass.
### Finding (non-blocking nit)
**`ContactForm.tsx:450` — the "byte-identical" guard doesn't actually hold for empty `fields`, and the new regression test only exercises that non-representative case.**
Both `ContactForm.toHtml.test.ts` cases use `fields: []`. With empty fields and no `recipientEmail`, I confirmed by direct comparison that the new code's output *differs* from the true pre-PR (`d0925d9`) baseline: the old template unconditionally emitted ` ${fieldsHtml}` producing a whitespace-only line (`" "`) between `<form>` and `<button>`; the new code's `fieldsHtml ? ... : ''` ternary drops that line entirely when `fieldsHtml` is falsy.
```
OLD: "<form action=\"/legacy\" method=\"POST\">\n \n <button type=\"submit\">Send Message</button>\n</form>"
NEW: "<form action=\"/legacy\" method=\"POST\">\n <button type=\"submit\">Send Message</button>\n</form>"
IDENTICAL: false
```
For the realistic case — non-empty `fields` (the default, and what every real published form uses) — I confirmed output *is* byte-identical old vs. new. So this doesn't affect any real site today (nobody publishes a contact form with zero fields), and arguably the new output is *better* (no stray whitespace-only line). But two things worth tightening:
1. The commit message/PR description claims general "byte-identical" backward compat; that's only true when `fields` is non-empty. Worth a one-line caveat, or just special-case empty-fields to match old output exactly if literal byte-identity is the goal.
2. The regression test itself only covers the `fields: []` edge case (which is *not* byte-identical to pre-PR baseline) and never exercises the common `defaultFields`/non-empty-fields path, which is the one that actually needs the regression guard. Recommend adding a case with real fields (or `defaultFields`) asserting full-string equality against a fixed expected HTML string, to actually pin the byte-identity guarantee for production forms.
Neither point blocks merge — the relay wiring, escaping, id-matching, and honeypot behavior are all correct for the shipping scenarios.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
ContactForm relay wiring
Wires the site-builder Contact Form to the form-sender relay (companion PR: whp
form-sender-relay).recipientEmail+thankYouUrlsettings;toHtmlemits a<!--WHP-FORM …-->marker (stripped at publish, so the recipient never ships),method=POST action=__WHP_FORM_ACTION__…__placeholder, and a hidden_gotchahoneypot — only when a recipient is set.ContactForm.toHtml.test.ts.🤖 Generated with Claude Code
Review: ContactForm relay wiring (
contact-form-relay→main)Verdict: Approve, with one non-blocking nit.
Verified locally:
npx vitest run src/components/forms/ContactForm.toHtml.test.ts→ 2/2 pass;tsc --noEmitclean.What checks out
ContactForm.tsx:439-446):fidis generated once pertoHtml()call and reused for both the marker'sid="..."and the__WHP_FORM_ACTION__<fid>__placeholder — they can't drift apart within a single render.recipientEmail/thankYouUrlare passed throughesc()(line 396) before being interpolated into the<!--WHP-FORM ...-->marker. Sinceesc()escapes"→"and>→>, a value containing"can't break out of the pseudo-attribute, and one containing-->can't prematurely close the HTML comment (the>is neutralized). Confirmed by hand-tracingesc()at line 396.type="text"(nottype="hidden", which spam bots often skip on purpose) +position:absolute;left:-9999px+tabindex="-1"+aria-hidden="true"+autocomplete="off", named_gotcha(matches the common Formspree-style convention). It's emitted beforefieldsHtml, so it's the first child inside<form>, and being a real<input>it's submitted normally.recipientEmail,marker/honeypotare both'', andactionAttrfalls back to the pre-existingesc(props.formAction || '#'). For the realistic case —fieldsnon-empty (i.e.defaultFields, or any user-populated field list) — I byte-diffed the old (d0925d9) template against the new one and they produce identical output.Math.random().toString(36).slice(2,8)-style id generation matches the existing project convention (same pattern used inCountdown.tsx,Tabs.tsx,Gallery.tsx,NumberCounter.tsx,Menu.tsx,ContentSlider.tsx), so no new pattern introduced.action="__WHP_FORM_ACTION__<id>__",_gotchapresence, id-match between marker and action, and marker/gotcha absence in the non-relay path. All pass.Finding (non-blocking nit)
ContactForm.tsx:450— the "byte-identical" guard doesn't actually hold for emptyfields, and the new regression test only exercises that non-representative case.Both
ContactForm.toHtml.test.tscases usefields: []. With empty fields and norecipientEmail, I confirmed by direct comparison that the new code's output differs from the true pre-PR (d0925d9) baseline: the old template unconditionally emitted${fieldsHtml}producing a whitespace-only line (" ") between<form>and<button>; the new code'sfieldsHtml ? ... : ''ternary drops that line entirely whenfieldsHtmlis falsy.For the realistic case — non-empty
fields(the default, and what every real published form uses) — I confirmed output is byte-identical old vs. new. So this doesn't affect any real site today (nobody publishes a contact form with zero fields), and arguably the new output is better (no stray whitespace-only line). But two things worth tightening:fieldsis non-empty. Worth a one-line caveat, or just special-case empty-fields to match old output exactly if literal byte-identity is the goal.fields: []edge case (which is not byte-identical to pre-PR baseline) and never exercises the commondefaultFields/non-empty-fields path, which is the one that actually needs the regression guard. Recommend adding a case with real fields (ordefaultFields) asserting full-string equality against a fixed expected HTML string, to actually pin the byte-identity guarantee for production forms.Neither point blocks merge — the relay wiring, escaping, id-matching, and honeypot behavior are all correct for the shipping scenarios.