Changelog
All notable changes to ghtmx are documented in this file.
This file is assembled, not edited. Contributors add a fragment in
changelog.d/ (one file per PR, so two PRs can never conflict); the release
automation folds those into a new section here under the version it actually
assigns, and brings the fold back to main as a changelog/<tag> pull
request. Do not edit it by hand: CI rejects hand-written version headings,
and entries belong in fragments โ the next release's fold would collide with
anything written here directly.
Pre-1.0 stability posture: breaking changes to language syntax, generated-code
shape, and the runtime API are allowed between minor versions and are always
recorded here with a migration note. The discipline is enforced: every entry
in a Changed, Removed, or Breaking section must carry a Migration: note
("Migration: none required" is valid), and internal/installcheck fails the
build otherwise โ for the fragments in changelog.d/ too. Releases follow
RELEASING.md.
[0.2.1] - 2026-09-04
Added
auth.WithSafeMethodsreplaces a CSRF middleware's safe-method list, andauth.DefaultSafeMethodsreturns the default one (GET, HEAD, OPTIONS, QUERY). It is the way to decline the QUERY exemption v0.2.0 introduced, which applies to every consumer whatever htmx version the project pins โ the middleware is runtime code and cannot see the compile-timehtmxVersion. The option works identically forauth.CSRFand for the gin, echo, fiber, and fiber v3 glue, and the decision now runs throughauth.CSRFOptions.SafeMethod, so the safe-list cannot diverge between adapters.AUTH.mddocuments the change of surface and the opt-out (#45).MIGRATION.md: what breaks between ghtmx versions, the htmx 2 โ 4 path, and where each version's migration notes live.README.mdgained a Supported htmx versions section โ the support table, the fact that a project without aghtmx.jsonis on the2.0.10default, and a link to the migration guide (#47).
Fixed
.versiononmainis aX.Y.Z-devmarker rather than a stale release number, so a binary built from a source checkout no longer reportsv0.1.0or stamps it into every file it generates. Released binaries are unaffected: the release automation writes the real version into the tagged tree and verifies it against the tag (#48).
[0.2.0] - 2026-09-03
Added
- Three htmx 4 examples, each pinned to
4.0.0in its ownghtmx.jsonand served as live demos on ghtmx.dev:examples/htmx4-inheritance(:inheritedand:inherited:append),examples/htmx4-status(hx-status:422/5xxrouting,hx-disable,<hx-partial>), andexamples/htmx4-query(the boundQUERYverb withinnerMorph). ghtmx.IsHistoryRestoreRequest: reports an htmx back/forward refetch (HX-History-Restore-Request: true).
Changed
- The adapters' automatic render mode answers a history-restore request
with the full page instead of the fragment. htmx 4 selects the
[hx-history-elt]element out of that response and swaps nothing when it is missing, so a fragment blanked the region on back navigation; htmx 2 accepts the page as well. Migration: handlers or tests that expected the fragment on a request carryingHX-History-Restore-Requestnow get the page; passMode(ModeStandalone)to keep the old shape. - The documentation site runs on htmx 4.0.0, with explicit
:inheritedattributes on<body>and compiler-checkedhx-on::listeners, and the Getting Started guide builds an htmx 4 project (aghtmx.jsonstep pinning4.0.0). Migration: none required โ the default pin for projects without aghtmx.jsonstays2.0.10.
Fixed
ghtmx generate -path <dir>below the module root resolves template packages against the module, so a nested project with its ownghtmx.json(a differently pinned example) binds bare handler identifiers correctly instead of reportingGHTMX-E0101.
[0.1.23] - 2026-09-02
Added
- htmx 4.0.0 support, side by side with htmx 2.0.x: pin
htmxVersion: "4.0.0"inghtmx.json(or-htmx-version 4.0.0) and the compiler,ghtmx generate -check, the language server, andghtmxgen.HTMXScript()follow htmx 4 syntax โ the:inherited/:appendattribute-name modifiers,hx-status:<code>,hx-query,hx-action/hx-method,hx-config,hx-ignoreand the newhx-disable, the morph swap styles andshowTarget:/scrollTarget:modifiers, the htmx 4 trigger modifiers, colon-form event names (hx-on::after:swap), the attributes of every extension shipped with htmx 4, and<hx-partial>. The 4.0.0 script asset is pinned with its subresource-integrity hash. Projects without an explicit pin stay on 2.0.10. - Migration hints under an htmx 4 pin: htmx 2 leftovers (
hx-vars,hx-ext,hx-disabled-elt,hx-request,hx-target-404,hx-on-click,hx-on::after-swap,queue:triggers,show:#id:top,focus-scroll:,hx-include="inherit") reportGHTMX-E0501naming the replacement instead of a generic unknown-attribute error. GHTMX-W0202: under htmx 4 an inheritable attribute without:inheritedon an element that issues no request, while a descendant does, warns that the descendant no longer sees it (htmx 4 inheritance is explicit);hx-headersandhx-boostwrappers warn on their own, with a CSRF remark when the header looks like a token. Silence withGHTMX-W0202=off.- Typed swap API for htmx 4:
ghtmx.SwapInnerMorph,SwapOuterMorph,SwapOuterSync, theSwapBefore/SwapPrepend/SwapAppend/SwapAfteraliases, and theSwapScrollTarget,SwapShowTarget,SwapTarget,SwapStrip,SwapEmpty, andSwapFocusScrollV4modifiers. QUERYas a bindable route verb (hx-query={ handler },//ghtmx:route QUERY /search handlers.Search), andauth.MethodQuerynaming the method.- An opt-in test,
GHTMX_SRI_CHECK=1 go test -run PinnedIntegrity ., re-hashes every pinned htmx build from the CDN against the embedded subresource-integrity values. - Language-server completion for htmx 4: htmx events after
hx-on::,:inherited/:appendafter an inheritable attribute,hx-status:, morph styles and aliases forhx-swap, andhx-querybindings.
Changed
-
Under an htmx 4 pin the generated central package no longer emits the
Emit<Event>AfterSwapandEmit<Event>AfterSettlesymbols: htmx 4 removed theHX-Trigger-After-SwapandHX-Trigger-After-Settleresponse headers they set. Projects pinned to htmx 2 are unchanged.Migration: none required for htmx 2 projects. When moving a project to
htmxVersion: "4.0.0", replace calls to theAfterSwap/AfterSettleemitters with the plainEmit<Event>and listen for the event onhtmx:after:swapif the timing mattered. -
GHTMX-E0501now also covers constructs the pinned version removed or renamed (previously only constructs newer than the pin), and the unsupported-version error (GHTMX-E0502) lists every supported version family.Migration: none required.
-
auth.SafeMethod(and so the CSRF middleware of every adapter) treats QUERY as a safe method alongside GET, HEAD, and OPTIONS: anhx-queryrequest needs no CSRF token.Migration: none required unless a handler mutates state on QUERY โ it must not; move such work to POST/PUT/PATCH/DELETE.
-
ghtmx.CSRFHeader's documentation andAUTH.mdnow spell out that under htmx 4 the header on a common ancestor must be writtenhx-headers:inherited={ ghtmx.CSRFHeader(token) }.Migration: none required for htmx 2 projects; htmx 4 projects add
:inherited(the compiler reports the bare form asGHTMX-W0202).
[0.1.22] - 2026-08-21
Added
auth.WithOnReject: an observability hook on the CSRF middleware, called just before it answers 403 with the request's method, path, and an error wrappingauth.ErrCSRF. Applications can now count probes or debug a migration without wrapping the middleware. The option type is the core package's and every adapter'sCSRFaccepts it, so one hook value works behind gin, echo, fiber v2, and fiber v3 unchanged; read the identity from the same context withauth.IdentityFrom.auth.HasFormContentType: the rule deciding whether a request body may carry a CSRF token, exported so the fiber adapters โ which read the body natively through fasthttp โ apply the same gateauth.VerifyCSRFdoes instead of an equivalent one.
Changed
-
The fiber v2 and fiber v3 CSRF middleware now consult the request body for a
_csrffield only when the Content-Type names a form, matching the net/http middleware exactly rather than relying on fasthttp to parse nothing for other types.Migration: none required for well-formed requests. Urlencoded and multipart submissions are unaffected, including with a
charsetorboundaryparameter. The change is strictly fail-closed: a POST whose Content-Type is unparseable, or merely starts withapplication/x-www-form-urlencodedwithout being it, is now rejected instead of having its body searched for a token. A request carrying the token in theX-CSRF-Tokenheader is never gated.
Fixed
fiberv3auth's documentation no longer claims fiber v3 serializes cookies throughnet/http. It does not โ like v2 it writes the header with fasthttp, and builds anhttp.Cookieonly to validate it. The reasonMaxAge -1suffices in v3 where v2 needs a pastExpiresis the newer fasthttp release v3 requires. The code was always correct; the explanation would have misled anyone "fixing" it to match.
[0.1.21] - 2026-08-21
Security
- The
adapters/irismodule floors its indirectgithub.com/sirupsen/logrusdependency at 1.8.3. Iris pulls logrus in transitively โ no ghtmx code calls it โ and 1.8.3 is the release that fixes upstream'slogrus.Writer()denial of service on single-line payloads larger than 64KB, so an application that does reach logrus through Iris is not held back by the adapter's module graph.
[0.1.20] - 2026-08-10
Added
authpackage: secure cookie session authentication middleware for server-rendered apps. The application implements one interface โAuthenticate(ctx, token) (ID, error)โ and the library owns the request-side mechanics: opaque 256-bit session tokens with the store-the-hash pattern, always-HttpOnlyhost-only cookies (Secureby default,SameSiteStrict or Lax only, automatic__Host-prefix when site-wide), htmx-aware login redirects (303 for browser navigations,HX-Redirect+ 204 for htmx requests), always-on per-session CSRF protection (derived synchronizer tokens, header or hidden-field channel, constant-time compare), and a pre-session double-submit token for the login form itself. The middleware is net/http-shaped, soadapters/nethttpservers and chi routers use it directly.- Auth glue packages for the framework adapters whose contexts differ:
adapters/gin/ginauth,adapters/echo/echoauth,adapters/fiber/fiberauth, andadapters/fiberv3/fiberv3auth, each exporting the same seven-function surface (New,CSRF,IdentityFrom,SetSessionCookie,ClearSessionCookie,SetLoginCSRFCookie,ValidLoginCSRF), enforced by a parity gate. Each fiber glue owns its major version's correct cookie-deletion serialization (pastExpireson v2,Max-Age=0on v3). AUTH.md: the authentication reference โ configuration, per-framework quick starts, the login flow, and the security model โ also on the docs site under /docs/auth.
[0.1.19] - 2026-08-09
Added
- Four more first-party render adapters โ
adapters/beego(Beego v2),adapters/iris(Iris v12),adapters/revel(Revel), andadapters/martini(Martini) โ extending FR-035 automatic render-mode selection to every framework in common circulation alongside the existing chi, echo, gin, and fiber adapters. Each is a nested module released in lockstep asadapters/<name>/vX.Y.Zand delegates toadapters/nethttp, so mode selection, status, and header behavior are identical by construction. Revel's adapter isResult-based to match that framework's controller idiom; Martini is archived upstream and its adapter pins the framework's last published revision.
[0.1.18] - 2026-08-08
The entries below accumulated in this file's former [Unreleased]
section while releases v0.1.1 through v0.1.17 shipped without moving
them; they are folded here, where the fragment system first assigned
them a version.
Added
- Method values are discoverable as route handlers:
r.Get("/users", h.ListUsers)now enters the table asHandlers.ListUserswhenever the receiver's type is named in the same function โ a parameter, avarwith an explicit type, a composite literal, ornew(T). Giving handlers their dependencies through a struct receiver is the ordinary Go shape; requiring package-level funcs pushed projects into rebuilding dependency injection through request-scoped storage. The inference stays syntax-only and single-function, so nothing that resolved before changes: an import alias still wins over a same-named receiver, and a receiver whose type is not syntactically visible (a constructor call, a struct field, a call result) keeps reportingGHTMX-E0402. //ghtmx:routeannotations acceptType.Methodandpkg.Type.Method, so the escape hatch covers what discovery cannot. A method is not a symbol a template can name, so these routes bind through their generated central symbols, which fold the dot away:ghtmxgen.HandlersListUsersPathwithout parameters,ghtmxgen.HandlersGetUser(id)with. A folded name that collides with another handler's still reportsGHTMX-E0404, and the language server offers the generated symbol rather than the method.- The
routetablepackage andghtmx routes -check-against <file.json>, which turn annotation-versus-reality drift into a test.generate -checkverifies template-versus-generated-code drift; it cannot see whether the paths the toolchain believes in are the paths the framework serves, which is exactly what a project taking the//ghtmx:routeescape hatch outsiderouteScopegives up.Loadreturns the same tableghtmx routesprints,Normalizeconverts a router's own path syntax with the toolchain's normalizer rather than a copy that could drift, andDiffreports missing, unexpected, and handler-mismatched routes.routes -jsonnow emits the package's own type, so CLI output and the type it unmarshals into cannot diverge. //ghtmx:routeprefix /admin/user, a package-scoped directive declaring the mount prefix a sub-application's routes are served under. Routes registered inside a sub-app are discovered at their sub-app-relative path, and a mount site using a variable prefix and a cross-package router value cannot be recognised syntactically โ so the prefix is declared rather than inferred. Every route the package registers, discovered or annotated, composes under it; group prefixes still nest inside. The prefix must be static, and two files declaring different prefixes for one package isGHTMX-E0403. Annotated paths become relative to it: an annotation that already spells the mount point composes twice, so shorten those when adopting the directive.adapters/fiberv3, a first-party render adapter for fiber's v3 major version โ the same thin bridge overadapters/nethttpas the fiber v2 adapter, ported to v3'sfiber.Ctxinterface andContext()accessor. A nested module like the other adapters, released in lockstep asadapters/fiberv3/vX.Y.Z. The directory isfiberv3rather thanfiber/v3because Go reads a trailing/v3as a module major-version suffix and would demandv3.x.xtags.GHTMX-W0105: warns when one handler symbol is registered for the same verb at more than one path โ typically a route both discovered and declared by annotation โ naming every site and the path template bindings actually resolve to. PreviouslyLookuppicked a winner silently, so a binding could generate an unexpected URL with no diagnostic. Warning-class: projects that deliberately serve one handler at several URLs can setGHTMX-W0105=off.GHTMX-E0308: importing the ghtmx root package in a template file โ or aliasing any import asghtmxorghtmxruntimeโ is now reported at the import line the author wrote, with the alias escape hatch in the message. Previously the collision surfaced as a Go redeclaration error inside generated code, pointing at a line nobody can edit. The check runs in bothghtmx generateand the language server.- A trailing
navmarker on//ghtmx:routeannotations (//ghtmx:route GET /audit handlers.AuditLog nav) declares a navigation-only route โ reached by<a href>or a native form post โ exempting that route from theGHTMX-W0104unbound-route warning without silencing the check project-wide. Server-rendered portals previously had to turnGHTMX-W0104off globally, losing orphan detection everywhere. htmxScriptinghtmx.json(flag:-htmx-script): setfalseto omit theHTMXScript()helper from the central generated package, for projects that use ghtmx purely as a server-side template engine and load no htmx at all.htmxVersionstill drives attribute validation, but no longer needs a pinned script asset when the helper is off.ghtmx.HTMXScriptIntegrity(version): returns the pinned subresource-integrity hash for a supported htmx version, so a project serving htmx itself (WithScriptSrc) can assert in a unit test that its vendored file is the exact published build the tag pins, instead of scraping the hash out of rendered HTML.templateExtensioninghtmx.json(flag:-template-extension): the file extension templates are written with,.ghtmxby default and.htmxas the alternative. A project uses exactly one โ with.htmxconfigured, a.ghtmxfile is not a template and is neither generated from nor served by the language server. Any other value is rejected by name;.goin particular would make the generator walk its own output. The value salts the build cache key, so switching does not reuse artifacts built for the other extension. The-watch-patterndefault follows it.- Editor support for
.htmxalongside.ghtmxin the VS Code extension, the Neovim plugin, and the JetBrains TextMate bundle, plus a file icon for templates in light and dark variants. - Published coverage reporting. The
coverageCI job now uploads its profile to Codecov, so the README carries a live badge and every pull request gets a line-by-line coverage diff.codecov.ymlmirrors the exclusionsinternal/covergatealready enforces and marks Codecov's own statuses informational โ the 90% floor remains the only coverage check that can fail a build. scripts/install.sh, which installsghtmxandgoplsonto your PATH. Installing an editor extension never installed the binaries the extension needs, and the only route was twogo installcommands found in the README. The script downloads the release archive for your platform, verifies it againstchecksums.txtbefore writing anything, installsgoplswhen a Go toolchain is present, and prints the exactexport PATH=line when the install directory is unreachable โ it never edits startup files. Configured byGHTMX_VERSION,GHTMX_BIN_DIR,GOPLS_VERSION, andGHTMX_SKIP_GOPLS. It is a wrapper around the existing release-archive install path, not a third supported path: bash only, so Linux, macOS, and WSL.- The VS Code extension in that same one-liner. After the binaries are
in place the script looks for the
codeCLI and, when the ghtmx extension is not installed, asks whether to add it and installs the.vsixfrom the same release โ so a VS Code user goes from nothing to a working setup in one command. It only ever asks: any answer butyinstalls nothing, and a run with no terminal to ask on skips the question rather than blocking.--no-interactiveturns off every prompt (the extension passes it when it runs the script itself),GHTMX_INSTALL_VSCODEanswers yes in advance, andGHTMX_SKIP_VSCODEleaves the editor alone. - A way out of the VS Code extension's "cannot start the server" error.
It now names both binaries and offers to run the installer in a
terminal โ with the command typed but not executed, so nothing runs
without a keypress โ to open the
ghtmx.pathsetting, or to reload the window. The same flow is in the command palette as ghtmx: Install ghtmx and gopls. - A CI check that fails a pull request changing code a release will
ship without a changelog fragment, and rejects hand-written
CHANGELOG.mdversion sections, so the mandatory-changelog rule is enforced rather than remembered.internal/installchecklints the fragments themselves: known section headings only, and the pre-1.0Migration:discipline applies before entries ever reachCHANGELOG.md.
Changed
- Generated render functions acquire their buffer with one
GetBuffercall and one deferredghtmxruntime.ReleaseAcquiredBuffer, replacing the nine-statement acquire-and-conditionally-release block every one of them used to carry. Behaviour is identical โ the buffer is still released only by the outermost component, a flush error is still reported only when the render itself succeeded โ but generated statements land in a consuming project's coverage figures, and this was the largest block of code nobody wrote. Migration: runghtmx generateand commit the result;generate -checkreports the stale files otherwise. Hand-written code is unaffected, andGetBuffer/ReleaseBufferremain exported. - How changelog entries are written: add a fragment in
changelog.d/instead of editingCHANGELOG.md's[Unreleased]section. One file per PR means two open PRs can no longer conflict on the same lines, and nobody writes a version number โ the release automation folds the fragments intoCHANGELOG.mdunder the version it actually assigns, inside the release-prep commit the tag points at, and opens achangelog/<tag>pull request that brings the fold back to the protectedmain(and, through it, to the changelog page on the docs site). Migration: none required for users; contributors writechangelog.d/<branch>.mdfiles from now on โ seechangelog.d/README.md.
Fixed
- Dev-mode hot literal reload for projects using a non-default template
extension. The generator hashes a template's real path to name its
literals sidecar, while generated code recovered that path from its own
_ghtmx.goname by appending.ghtmxunconditionally, so the two sides addressed different files and the reader found nothing.
[0.1.0] - 2026-08-02
First tagged release. Before this, the module had no tags at all, so
go get github.com/go-monolith/ghtmx could only resolve to a pseudo-version
and the nested adapter modules did not resolve at all.
Added
- Live example demos on the documentation site: every
examples/*application is now an importable package with a thincmdmain, and the docs binary compiles the real routers in and serves them at their native paths (/todos,/items,/fragments,/events,/hello). Run examples standalone withgo run ./cmdfrom the example directory (previouslygo run .). - The official documentation site (
docs/official): a nested module that dogfoods the engine โ chi routes with symbol and constructor bindings, compile-time fragments behind the chi adapter'sWithPage, theDocViewedevent contract, and every reference document and example embedded viaembed.FS. Compiles natively and tojs/wasmfor Cloudflare Workers (syumai/workers); the embedded copies are drift-gated against their single-source documents in CI. - Initial hard fork of templ at commit
04abee5(seeTEMPL_SYNTAX_BASELINE.md): parser, generator, runtime, formatter, CLI (generate,fmt,lsp,info,version), live-reload dev proxy, and LSP proxy, renamed to theghtmxmodule with the.ghtmxfile extension and*_ghtmx.gogenerated files.
Changed (vs. upstream templ)
- Module path is
github.com/go-monolith/ghtmx; the runtime package isghtmxand generated code callsghtmxruntime(ghtmx/runtime). Migration: replacegithub.com/a-h/templimports withgithub.com/go-monolith/ghtmxand rename.templfiles to.ghtmx. - Generated files use the
ghtmx_7f3b9d1a_variable prefix and the// Code generated by ghtmx - DO NOT EDIT.header. Migration: delete old*_templ.gofiles and runghtmx generate. - Dev-mode environment variables are
GHTMX_DEV_MODE*; the live-reload reserved path is/_ghtmx/reload/. Migration: rename anyTEMPL_DEV_MODE*usage and update proxies that special-cased the old reload path. - The five
hx-*verb attributes are typed route bindings (FR-004 carve-out 1): string URLs areGHTMX-E0602, arbitrary expressionsGHTMX-E0601. Migration: bind a handler symbol (hx-post={ handlers.CreateUser }) or a generated constructor (hx-get={ ghtmxgen.GetUser(id) }); seeCONFORMANCE.md. - Rendering no longer flushes the underlying
http.Flusherafter every component (upstream forced chunked encoding and early header commit). Migration: none for typical apps; for deliberate streaming, render aghtmx.Flush()component where a flush is wanted.
Removed (vs. upstream templ)
- The runtime fragment API (
templ.Fragment,RenderFragments,WithFragments): replaced by compile-timefragmentdeclarations with dual generated entry points (upcoming milestone). Migration: declare afragmentin the template and call its generated...Fragmententry point. - The Prettier integration in
fmtand the test suite (-prettier-command,-prettier-requiredflags): ghtmx has no Node.js dependency anywhere in the toolchain. Migration: none required;ghtmx fmtoutput no longer reformats<script>/<style>bodies via Prettier. - The legacy
templ.WriteWatchModeStringAPI is not carried over. Migration: none required; watch mode uses the dev-mode text sidecars automatically. ghtmx.Class,ghtmx.SafeClass, andghtmx.ConstantCSSClassare carried over (the conformance corpus exercises them) but remain deprecated and are slated for removal before v1.0. Migration: use plain strings for CSS classes.