Skip to content

Release: serve /f from a route handler (13 KB, one request) - #80

Merged
coreyhaines31 merged 2 commits into
mainfrom
development
Sep 5, 2026
Merged

coreyhaines31 merged 2 commits into
mainfrom
development

Conversation

@coreyhaines31

Copy link
Copy Markdown
Owner

Two commits. No migration required — this release adds no drizzle/*.sql, so it can merge directly.

What ships

/f/{formId} (#56) moves from an App Router page to a route handler.

before after
transferred 150.0 KB 13.0 KB
requests 9 1
external <script src> 8 0

The hosted form has no Client Component beneath it and submits with scripting switched off, but an App Router route ships react-dom plus the router runtime regardless, with no per-route opt-out. That was 134 KB of the 150 KB — on traffic the customer paid for, to render what is often four inputs.

The logic is unchanged and src/lib/render/* never moved, so the hosted form and the MCP tool still derive from one FormSchemaDocument.

The stylesheet is compiled at build time by scripts/build-form-css.mts from the same forms.css entry — not a scoped subset, so no class can go silently unstyled — and inlined, because a form on someone else's page is judged on first paint. A test recompiles and compares so the checked-in output cannot drift.

Verified before merge

npm run verify exits 0; CI green on #78 and #79. Against a real server on seeded data: both themes screenshotted, the 404 / no-schema / ?ef_embed=inline branches, the noindex and no-referrer headers, and a submission posted with curl — which cannot execute a script even if one existed — which stored and stamped unverified.

Risk

Lower than the last release: no schema change, no new secrets, one route's rendering path. The rollback is reverting #78.

thanks/page.tsx is untouched and still carries the runtime; it is seen once, after the lead is captured.

🤖 Generated with Claude Code

coreyhaines31 and others added 2 commits September 4, 2026 22:11
* Compile the hosted form's stylesheet into an inlinable module

A route handler cannot import a stylesheet — nothing processes it — so the same
forms.css entry the App Router used is compiled at build time and checked in.

The entry is unchanged, not a scoped subset: Tailwind v4 does not tree-shake per
entry, so narrowing the source globs would fork the utility surface for a
customer-facing page, and a class used only in the form would render unstyled.
64 KB minified, 11.5 KB over the wire, and no fork.

Checked-in build output drifts, so a test recompiles and compares. Its control
asserts the stylesheet is non-empty, because otherwise "matches" and "both
empty" are the same green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add the hosted form's document shell

<html>, <head> and the inlined stylesheet for a surface with no layout to
provide them. noindex is stated as a meta tag here and as a header in the route,
because the metadata API is no longer there to emit either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Serve /f from a route handler instead of an App Router page

Closes #56.

The logic is unchanged — loadForm, the Hindsight variant, the flash cookie, the
embed context, prefill, steps — and only the last step differs, where the tree
is rendered to a string rather than handed to the framework. The published
schema is still the only source, so the hosted form and the MCP tool keep
deriving from one FormSchemaDocument.

The reason is arithmetic. An App Router route ships react-dom plus the router
runtime whether or not the page uses them, and this page uses neither: no Client
Component beneath it, no useState, no event handler. That runtime was 134 KB of
the 150 KB the form transferred across 9 requests, on traffic the customer paid
for, to render what is often four inputs.

Measured after: 13 KB gzipped, one request, zero external scripts, one 275-byte
inline theme script. Verified that it still submits with no JavaScript by
posting with curl, which cannot execute a script even if one existed — the row
stored, stamped unverified.

react-dom/server is imported at call time because Next refuses a static import
of it under app/; the check keeps it out of client bundles and does not
distinguish a route handler, which has no client bundle at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Correct the forms layout comment: it no longer wraps the form

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
endpointforms Ready Ready Preview Sep 5, 2026 5:15am UTC

Request Review

@coreyhaines31
coreyhaines31 merged commit 8e7e32c into main Sep 5, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Preview — 8b15abaa Deployed Sep 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant