Split out of #1846 because it is not a reachability problem — no client composable fixes it.
Measured in loghq (production, SSR, 33 routes)
Every form in the app:
| form | action | method | preventDefault | hand-rolled fetch |
|---|---|---|---|---|
login.stx | — | — | ✅ | ✅ |
register.stx | — | — | ✅ | ✅ |
forgot-password.stx | — | — | ✅ | ✅ |
reset-password.stx | — | — | ✅ | ✅ |
projects/new.stx | — | — | ✅ | ✅ |
Five of five carry no action and no method. Verified in a browser with script execution disabled: the pages render (SSR works) and the forms are inert. Sign-in, sign-up, password reset and project creation are unavailable if a script fails to load, is blocked, or errors before hydration.
That is not a choice the app made — it is the only shape available. <form @submit="handler"> binds a client handler; there is no server-side counterpart, so the server never sees a form POST from a page it rendered.
What the ecosystem provides
- Laravel — the form posts to a route; the controller validates;
@errorandold()repopulate on the redirect back. Works with JS off, enhanced with it on. - Remix / SvelteKit —
actionon the route;<Form>progressively enhances the same handler. - Next — Server Actions, same contract.
- Rails / Django — the original version of this.
In all of them the same handler serves both paths. stx has the server half (a page already runs <script server> per request) and the client half (@submit), with nothing joining them.
Cost in this app, concretely
Each form re-implements the same four states by hand. login.stx alone: 6 references to a loading signal, 10 to error, plus a :disabled binding and a :text label swap. Multiply by five forms. A server form action plus validation would delete most of it and make the pages work without JS.
Ask
A route/page-level form action: a way for a page's server block to receive its own form POST, return field errors, and repopulate — with @submit enhancing it rather than replacing it. If that is a large piece of work, an interim action/method passthrough that at least lets an app point a form at an existing routes/*.ts handler would restore the no-JS path.
Related: #1846 (the composables that would remove the client-side duplication exist but are unreachable), #1843.