htmx-go-templates
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesehtmx on Go html/template
Go html/template 中的 htmx
The server owns state; a template only presents it; htmx moves HTML between the two. Most drift
in these apps comes from that split being decided ad hoc, per handler and per template: seven ways
to detect an htmx request, the same four attributes hand-copied across files, errors that vanish.
The rules below make each of those decisions once.
This skill holds the general rules. The repo's AGENTS.md holds what only the repo knows: its
swap-target ids, the names of its shared partials, and the htmx version it vendors. Read it first.
server 掌管 state;template 只负责呈现;htmx 在二者之间移动 HTML。此类应用中的大多数漂移,都源于这项拆分在每个 handler、每个 template 中被临时决定:检测 htmx request 的方式有七种,同一组四个属性被手工复制到多个文件,error 则会悄然消失。下面的规则让每项决策只做一次。
该 skill 包含通用规则。repo 的 AGENTS.md 记录只有该 repo 才知道的信息:swap-target id、shared partial 的名称,以及 repo 所使用的 htmx version。请先阅读它。
Where to look
查看位置
| If you're working on… | Load |
|---|---|
A handler: choosing page / fragment / redirect, htmx headers, | |
| A template: partials, repeated attribute bundles, forms, live regions, conditionals | |
Any client JS: whether you need it at all, islands, htmx glue, polling guards, | |
| 如果你正在处理…… | 加载 |
|---|---|
handler:选择 page / fragment / redirect、htmx headers、 | |
| template:partials、重复的 attribute bundle、forms、live regions、conditionals | |
任意 client JS:是否需要它、islands、htmx glue、polling guards、 | |
The rules
规则
- htmx headers go through one helper set, never hand-typed strings.
A typo in or a check for
"HX-Request"instead of""fails silently; one package of constants and predicates makes the wrong spelling unrepresentable. If the repo has no such set yet, introduce it the first time a handler you touch needs an htmx header —"true"included.Vary - Every route declares its response shape — page, fragment or redirect — in one place.
A handler that sniffs headers inline grows a new branch per feature. When one URL can return
either a page or a fragment, set , or a cache or the back button can serve the bare fragment as a page. A history-restore request (
Vary: HX-Request) always gets the full page.HX-History-Restore-Request: true - A non-htmx form POST answers 303 to a GET; an htmx one answers or a fragment. 303 stops a refresh from resubmitting. A 303 to an htmx request is followed by the XHR and the whole redirected page gets swapped into the target, which is never what you want.
HX-Location - Forms work without JavaScript. Real and
action, a real submit button; htmx attributes layer on top. The non-JS path is also the path the 303 rule serves.method - A repeated attribute bundle becomes a named partial. Four attributes copied to seventeen links drift the moment one copy changes. One partial, one place to change it. (
{{define}}has the vocabulary for why: one deep module beats many shallow copies.)codebase-design - Polling never fights the user. on every polling element, a guard that skips the poll while the user is focused or typing inside the target, and a target that never contains an input the user may be editing.
hx-sync - Errors reach the user. htmx 2.x does not swap 4xx/5xx bodies by default, so a refused
action looks like nothing happened. Fix it in two halves: a entry that swaps the status you use (typically 422), and
responseHandling/HX-Retargetfrom the server so the error lands in a known region. That region is a persistentHX-Reswapcontainer.aria-live - In-flight state is visible. on submit controls so a double-click cannot double-submit;
hx-disabled-eltorhx-indicatoron the region being replaced.aria-busy - Client JS is the exception, and it is never inline. Before writing any, ask whether the
server can own it (a preference, a re-rendered region) or HTML can (,
<details>,<dialog>). A widget with real client state becomes an island in the repo's frontend framework, mounted throughpopoverand unmounted on swap. The only other JS is a fixed set of htmx glue listeners. Nohtmx.onLoadblocks,<script>oronclickin templates: inline handlers are untestable and re-run unpredictably on swaps. htmx config goes inhx-onwith<meta name="htmx-config">, so htmx itself refuses"allowEval": falseandhx-onvalues.js: - Templates branch on presentation only. chains are domain logic in the wrong layer. Compute a view-model field in Go (
{{if eq .Status "failed"}},.Badge) and let the template read it..CanRetry
- htmx headers 统一通过一套 helper 处理,绝不手写字符串。
在 上出现拼写错误,或检查
"HX-Request"而不是"",都会悄无声息地失败;用一套包含 constants 和 predicates 的 package,可以让错误拼写根本无从表达。如果 repo 尚无这样一套工具,应在首次修改某个 handler、而该 handler 需要 htmx header 时引入它——包括"true"。Vary - 每条 route 都要在一个地方声明自己的 response shape——page、fragment 或 redirect。
inline 检测 headers 的 handler 会随着每个 feature 的加入而不断新增分支。当同一 URL 既可能返回 page,也可能返回 fragment 时,设置 ,否则 cache 或后退按钮可能会把裸 fragment 当作 page 提供。history-restore request(
Vary: HX-Request)始终应返回完整 page。HX-History-Restore-Request: true - 非 htmx form 的 POST 要以 303 响应并转到 GET;htmx form 则响应 或 fragment。 303 可以防止刷新时重复提交。对 htmx request 返回 303 后,XHR 会跟随该重定向,而整个被重定向的 page 会被 swap 到 target 中,这从来都不是你想要的。
HX-Location - Form 必须能在没有 JavaScript 的情况下工作。
使用真实的 和
action,以及真正的 submit button;htmx attributes 只是叠加在其上。无 JS 路径也正是 303 规则所服务的路径。method - 重复的 attribute bundle 应变成命名的 partial。 四个属性复制到十七个 link 中,只要其中一处发生变化就会产生漂移。只保留一个 partial,只修改一处。(
{{define}}提供了说明原因所需的术语:一个 deep module 胜过许多 shallow copies。)codebase-design - Polling 绝不能与用户操作冲突。
每个 polling element 都要设置 ;设置 guard,在用户聚焦于 target 或正在 target 内输入时跳过 poll;并且 target 内绝不能包含用户可能正在编辑的 input。
hx-sync - Error 必须传达给用户。
htmx 2.x 默认不会 swap 4xx/5xx body,因此被拒绝的 action 看起来就像什么都没发生。分两部分修复:添加 entry,swap 你使用的 status(通常是 422),同时由 server 返回
responseHandling/HX-Retarget,让 error 落到已知区域。该区域应是一个持久存在的HX-Reswapcontainer。aria-live - In-flight state 必须可见。
对 submit controls 使用 ,防止双击造成重复提交;对正在替换的 region 使用
hx-disabled-elt或hx-indicator。aria-busy - Client JS 是例外,而且绝不能 inline。
编写任何 JS 之前,先问能否由 server 负责(preference、重新渲染的 region),或由 HTML 负责(、
<details>、<dialog>)。具有真实 client state 的 widget 应成为 repo 的 frontend framework 中的 island,通过popover挂载,并在 swap 时卸载。其他 JS 仅限一组固定的 htmx glue listener。Template 中不得有htmx.onLoadblock、<script>或onclick:inline handler 无法测试,且在 swap 后会不可预测地再次执行。htmx config 放在hx-on中,并设置<meta name="htmx-config">,这样 htmx 本身就会拒绝"allowEval": false和hx-on值。js: - Template 只根据 presentation 分支。
chain 是放错层的 domain logic。先在 Go 中计算 view-model field(
{{if eq .Status "failed"}}、.Badge),再让 template 读取它。.CanRetry
Choosing a response shape
选择 response shape
request ──► HX-History-Restore-Request: true? ──yes──► page
│ no
▼
HX-Request: true? ──no──► POST? ──yes──► 303 to GET (or re-render page with 422 on invalid)
│ yes └─no──► page
▼
route declares: fragment ──► ExecuteTemplate(w, "<block>", vm)
page+select ──► page (template uses hx-select)
redirect ──► HX-Location (or HX-Redirect for a full reload)references/responses.mdrequest ──► HX-History-Restore-Request: true? ──yes──► page
│ no
▼
HX-Request: true? ──no──► POST? ──yes──► 303 to GET (or re-render page with 422 on invalid)
│ yes └─no──► page
▼
route declares: fragment ──► ExecuteTemplate(w, "<block>", vm)
page+select ──► page (template uses hx-select)
redirect ──► HX-Location (or HX-Redirect for a full reload)references/responses.mdhtmx version
htmx version
These rules target htmx 2.x. Check the version the repo vendors before relying on defaults.
If it is on 4.x, re-check rules 7 and 9 against the
4.x migration notes:
- error responses swap by default, with per-status control through ;
hx-status - attribute inheritance is explicit, so bundles that relied on inheriting break;
hx-target - event names changed, so listeners in the TS module need renaming.
这些规则针对 htmx 2.x。依赖默认值前,先检查 repo 使用的 version。如果 repo 使用 4.x,请根据 4.x migration notes重新检查规则 7 和 9:
- error responses 默认会被 swap,并可通过 按 status 控制;
hx-status - attribute inheritance 现在是显式的,因此依赖继承 的 bundle 会失效;
hx-target - event names 已更改,因此 TS module 中的 listener 需要重命名。
What not to do
不应采用的做法
- Don't reach for the extension to route errors. It moves a server decision into markup and adds a second mechanism next to
response-targets.HX-Retarget - Don't swap an region with
aria-live. Screen readers track the original node; a replaced live region announces nothing. Swap itsouterHTML.innerHTML - Don't render into the directly. Execute into a buffer first, so a template error becomes a 500 rather than a half-written 200.
ResponseWriter - Don't add to fix a navigation problem. Declare the nav-link partial instead; boost changes every link and form under it at once.
hx-boost - Don't restate the htmx attribute reference here or in the repo; link htmx.org/reference.
- 不要使用 extension 来路由 error。它会把 server 决策移入 markup,并在
response-targets旁边增加第二套机制。HX-Retarget - 不要用 swap
outerHTMLregion。Screen reader 会跟踪原节点;被替换的 live region 不会播报任何内容。应 swap 它的aria-live。innerHTML - 不要直接 render 到 。先 Execute 到 buffer 中,这样 template error 会变成 500,而不是半写入的 200。
ResponseWriter - 不要添加 来解决 navigation problem。应声明 nav-link partial;boost 会一次性改变其下的所有 link 和 form。
hx-boost - 不要在这里或 repo 中重述 htmx attribute reference;请链接 htmx.org/reference。