Problem
The scaffold gallery and the optimistic-ui skill reference both teach <form method="post" action="">, and every app webjs create generates carries the shape.
It is not conformant. The HTML spec requires action to be a valid non-empty URL when the attribute is present, so action="" is a conformance error. Nothing breaks in practice, every browser treats it as "submit to the current URL", which is exactly the intent, and that is why it has gone unnoticed. But <form method="post"> with no action at all does the same thing, is conformant, and is one fewer token to explain.
The reason it matters now is that it puts two of our own surfaces in contradiction. #1167 (the #1154 form-action leak guard) documented "omit action rather than writing action="", which the spec treats as a conformance error", which is true, and which a reader would then immediately see violated by the gallery their own webjs create just generated. The claim was softened in that PR rather than shipping a doc that contradicts generated code, so the honest version of the advice is blocked on this cleanup.
Design / approach
Drop the empty attribute wherever it is taught. <form method="post"> posts to the page's own URL identically, so this is a pure deletion with no behaviour change and no migration concern.
Then restore the stronger wording in the docs that #1167 softened, so the guidance states the conformance point instead of quietly preferring one form over another.
One judgement call worth stating: do NOT go looking for action="" in test fixtures and delete it there. packages/core/test/routing/router-client.test.js:2535 uses formaction="" deliberately, as a fixture pinning how the client router resolves an empty formaction. That is testing the behaviour, not teaching the idiom, and it should stay.
Implementation notes (for the implementing agent)
Where to edit. Five teaching sites, found by grep -rn 'action=""' --include=*.ts --include=*.js --include=*.md . | grep -v node_modules:
packages/cli/templates/gallery/app/features/forms/page.ts:52 (the forms gallery card's own form)
packages/cli/templates/gallery/modules/todo/components/todo-app.ts:100 (the add-todo form)
packages/cli/templates/gallery/modules/todo/components/todo-app.ts:112 (the per-item form)
.agents/skills/webjs/references/optimistic-ui.md:72 (prose: "Wrap the mutation in a REAL <form method="post" action="">")
.agents/skills/webjs/references/optimistic-ui.md:77 (the code sample directly below it)
Then, once the scaffold no longer contradicts it, restore the conformance wording in the two surfaces #1167 softened:
.agents/skills/webjs/references/muscle-memory-gotchas.md, the "A function in <form action=${fn}> is refused" entry, RIGHT comment block
website/app/docs/troubleshooting/page.ts, the "a function was interpolated into action=" entry, Fix paragraph
Landmines.
- The agent skill is single-sourced.
.agents/skills/webjs/ at the repo root is canonical; packages/cli/lib/create.js around L648 copies the templates/.agents/skills/webjs bundle when it exists (produced at prepack by scripts/sync-scaffold-skill.mjs) and otherwise falls back to the repo-root copy, which is gitignored in the monorepo. So edit the repo-root file ONLY. Do not hand-create a templates/.agents/ copy to "fix" it in both places; that would fork a file the prepack script owns.
create.js also runs generated markdown through bunifyProse for --runtime bun (same region, ~L660). It rewrites npm commands, not HTML, so this change is unaffected, but do not be surprised to see the skill files transformed on copy.
- The gallery files are TypeScript templates emitted as source. They are real files here, not string-generated, so an escaping bug is not a risk for this one; still verify by generating, since that is the only thing that proves what an app actually receives.
Invariants to respect. No behaviour change is intended: a no-JS submit must still post to the page's own URL and reach the page's action export, and the optimistic path in todo-app.ts still depends on @submit + preventDefault(). AGENTS.md invariant 11 applies to the prose edits (the docs-site page is HTML in a template literal, so mind the escaping around ${).
Tests + docs surfaces.
- Scaffold:
test/scaffolds/ covers generation. Verify with a real webjs create app, then webjs check and boot it, per the scaffold-sync requirement; the forms gallery card and the todo example are both directly exercised.
- Smoke:
test/examples/*/smoke/* if the generated app's form routes are covered there.
- Add an assertion that no scaffold template ships
action="", so it cannot creep back. A grep-based test over packages/cli/templates/ is enough and is the counterfactual (it fails today).
- Docs: the two surfaces listed above, plus
.agents/skills/webjs/references/optimistic-ui.md itself.
Process. Invoke webjs-scaffold-sync before editing; it holds the authoritative map of scaffold surfaces and the generate + boot + webjs check verification this needs.
Acceptance criteria
Problem
The scaffold gallery and the optimistic-ui skill reference both teach
<form method="post" action="">, and every appwebjs creategenerates carries the shape.It is not conformant. The HTML spec requires
actionto be a valid non-empty URL when the attribute is present, soaction=""is a conformance error. Nothing breaks in practice, every browser treats it as "submit to the current URL", which is exactly the intent, and that is why it has gone unnoticed. But<form method="post">with noactionat all does the same thing, is conformant, and is one fewer token to explain.The reason it matters now is that it puts two of our own surfaces in contradiction. #1167 (the #1154 form-action leak guard) documented "omit
actionrather than writingaction="", which the spec treats as a conformance error", which is true, and which a reader would then immediately see violated by the gallery their ownwebjs createjust generated. The claim was softened in that PR rather than shipping a doc that contradicts generated code, so the honest version of the advice is blocked on this cleanup.Design / approach
Drop the empty attribute wherever it is taught.
<form method="post">posts to the page's own URL identically, so this is a pure deletion with no behaviour change and no migration concern.Then restore the stronger wording in the docs that #1167 softened, so the guidance states the conformance point instead of quietly preferring one form over another.
One judgement call worth stating: do NOT go looking for
action=""in test fixtures and delete it there.packages/core/test/routing/router-client.test.js:2535usesformaction=""deliberately, as a fixture pinning how the client router resolves an emptyformaction. That is testing the behaviour, not teaching the idiom, and it should stay.Implementation notes (for the implementing agent)
Where to edit. Five teaching sites, found by
grep -rn 'action=""' --include=*.ts --include=*.js --include=*.md . | grep -v node_modules:packages/cli/templates/gallery/app/features/forms/page.ts:52(the forms gallery card's own form)packages/cli/templates/gallery/modules/todo/components/todo-app.ts:100(the add-todo form)packages/cli/templates/gallery/modules/todo/components/todo-app.ts:112(the per-item form).agents/skills/webjs/references/optimistic-ui.md:72(prose: "Wrap the mutation in a REAL<form method="post" action="">").agents/skills/webjs/references/optimistic-ui.md:77(the code sample directly below it)Then, once the scaffold no longer contradicts it, restore the conformance wording in the two surfaces #1167 softened:
.agents/skills/webjs/references/muscle-memory-gotchas.md, the "A function in<form action=${fn}>is refused" entry, RIGHT comment blockwebsite/app/docs/troubleshooting/page.ts, the "a function was interpolated intoaction=" entry, Fix paragraphLandmines.
.agents/skills/webjs/at the repo root is canonical;packages/cli/lib/create.jsaround L648 copies thetemplates/.agents/skills/webjsbundle when it exists (produced at prepack byscripts/sync-scaffold-skill.mjs) and otherwise falls back to the repo-root copy, which is gitignored in the monorepo. So edit the repo-root file ONLY. Do not hand-create atemplates/.agents/copy to "fix" it in both places; that would fork a file the prepack script owns.create.jsalso runs generated markdown throughbunifyProsefor--runtime bun(same region, ~L660). It rewrites npm commands, not HTML, so this change is unaffected, but do not be surprised to see the skill files transformed on copy.Invariants to respect. No behaviour change is intended: a no-JS submit must still post to the page's own URL and reach the page's
actionexport, and the optimistic path intodo-app.tsstill depends on@submit+preventDefault(). AGENTS.md invariant 11 applies to the prose edits (the docs-site page is HTML in a template literal, so mind the escaping around${).Tests + docs surfaces.
test/scaffolds/covers generation. Verify with a realwebjs createapp, thenwebjs checkand boot it, per the scaffold-sync requirement; the forms gallery card and the todo example are both directly exercised.test/examples/*/smoke/*if the generated app's form routes are covered there.action="", so it cannot creep back. A grep-based test overpackages/cli/templates/is enough and is the counterfactual (it fails today)..agents/skills/webjs/references/optimistic-ui.mditself.Process. Invoke
webjs-scaffold-syncbefore editing; it holds the authoritative map of scaffold surfaces and the generate + boot +webjs checkverification this needs.Acceptance criteria
action=""remains inpackages/cli/templates/or in.agents/skills/webjs/references/webjs create) contains noaction="", passeswebjs check, and its forms still submit with JS disabledaction=""(counterfactual: it fails against today's tree)muscle-memory-gotchas.mdandwebsite/app/docs/troubleshooting/page.tspackages/core/test/routing/router-client.test.js:2535is left alone (it pins behaviour, it does not teach the idiom)