# Changelog

This file records the notable changes of each release in the format of
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The release
workflow publishes the section of a version as its GitHub release notes.
Releases up to v0.10.0 have their notes on
[GitHub Releases](https://github.com/romshark/datapages/releases) only.

## [Unreleased]

## [0.11.2] - 2026-11-01

Serve the documentation of a release under its feature version, as `/docs/v0.11/`, which patch releases keep. Links to `/docs/v0.11.1/` stop working. Fix the sidebar, the search results and the contents of the documentation pages. No code changes since v0.11.1.

## [0.11.1] - 2026-10-30

Rewrite the documentation introduction. No code changes since v0.11.0.

## [0.11.0] - 2026-10-30

An app upgraded from v0.10.1 does not compile until templ and Datapages regenerate its code. A `*_templ.go` file written by a templ before v0.3.1100 fails with errors such as `undefined: templ.ResolveAttributeValue`, and `datapages gen` fails on it with `parsing app package`. Code generated by v0.10.1 for an app that uses `datapages.SSE` fails with `undefined: dpsse.New`. Upgrade in this order:

1. Install the CLI with `go install github.com/romshark/datapages/cmd/datapages@v0.11.0`.
2. Run `go get github.com/romshark/datapages@v0.11.0` and `go mod tidy`.
3. Install templ with `go install github.com/a-h/templ/cmd/templ@v0.3.1100`.
4. Run `templ generate`, then `datapages gen`.
5. Redeploy.

### Added

#### `datapages lint` and `datapages gen`

- Accept `QUERYXXX` actions for the `QUERY` method of RFC 10008, which Datastar 1.0.4 sends with `@query`. Declare one on a page or on `App`, as in `// QUERYSearch is /search`, and build its expression with the `action` package, as in `action.PageIndex.Search.QUERY()`. Datastar sends the signals in the request body. A `QUERYXXX` action skips the CSRF check and must not change server state.
- Accept static and request-scoped `StreamConfig` methods on pages with SSE streams. The static form returns `datapages.StreamConfig` during `NewServer`. The request-scoped form returns `(datapages.StreamConfig, error)` per stream and may take the session, path and signals. Both override `datapages.WithSSECompression`. Invalid configurations fail `NewServer` or the stream request. A page or `App` configures the SSE response of an action the same way, with a method named `StreamConfig` plus the action's method name, such as `StreamConfigPOSTSave`.
- Serve files from generated handlers with `datapages.File`. Any action may return one, and a `GETXXX` action also gets a generated `href` builder. A `GETXXX` action skips the cross-origin and CSRF checks and must not change server state. Applications previously needed custom middleware or a separate `net/http` handler and had to maintain its route and URL by hand.
- Add a [Toxiproxy](https://github.com/Shopify/toxiproxy) proxy in front of the dev server of `datapages watch`. It applies toxics such as latency, client and server bandwidth limits, data caps, connection resets and HTTP errors on every endpoint or on 1 route pattern. Client bandwidth and data caps apply per browser or per request. A settings page applies and edits the presets of `watch.toxiproxy.presets` and saves them into `datapages.yaml`. Both listen on ports the OS picks unless `watch.toxiproxy.host` and `watch.toxiproxy.ui-host` set them. `datapages lint` reports an override of a route no app has. `watch.toxiproxy.disabled: true` turns the proxy off.
- Accept a `datapages.DocumentAttrs` return value that sets the attributes of `<html>`, as in `docAttrs datapages.DocumentAttrs`. It maps attribute names to values, as in `datapages.DocumentAttrs{"lang": "en", "data-theme": "dark"}`. A page's `GET` and an action that returns a body may return one. An invalid name or `data-nonce` fails the page with 500. Return the same attributes from every page through a shared function. An action that returns them without a body fails with `action returning datapages.DocumentAttrs must return body datapages.Component`.

#### Generated code

Run `datapages gen` to apply these.

- Add `datapages.WithSSECompression` to configure SSE compression and `datapages.WithNoSSECompression` to disable it. The presets `datapages.SSECompressionBalanced`, `datapages.SSECompressionLowMemory`, `datapages.SSECompressionLowBandwidth` and `datapages.SSECompressionLargeFragments` cover common trade-offs. A `Cache-Control: no-transform` set before a stream opens keeps it uncompressed.

#### Runtime and modules

- Reload open pages when a deployment changes the build. Set `datapages.WithBuildID` when replicas run different binaries of 1 release. A `Content-Security-Policy` must allow the inline build script.
- Apply the attributes of the live `<html>` element when a shim written with `PageCacheWriter.SetShim` hydrates. The classes are added to those the page has.

#### `datapages init`

- Write a `watch.toxiproxy` section with the presets Fast 4G, Slow 4G and 3G and no toxics into a new `datapages.yaml`. With the section, a busy proxy port fails `datapages watch` instead of turning the proxy off. `datapages watch` writes the same file into a module without one.
- Scaffold Web Components instead of requiring manual frontend setup. Choose Datastar Rocket, esbuild-bundled TypeScript, JavaScript modules or none interactively or with `--web-components rocket|typescript|javascript|none`; `-n` defaults to none. Each setup adds an attribute-and-event example. Rocket loads the matching CDN bundle. TypeScript requires Node.js; init installs and builds it, and `datapages watch` rebuilds changes.
- Ask whether to commit the generated code, `datapagesgen/` and `*_templ.go`, interactively or with `--generated-code committed|hidden`; `-n` defaults to committed. Hidden lists it in `.gitignore`, hides it in VS Code with `.vscode/settings.json` and makes `make build` and CI generate it. CI no longer compares it with the commit. A clone of a hidden project builds only after `templ generate` and `datapages gen`, and `datapages gen` fails there while the app uses the `href`, `action` or `assets` packages.
- Ask whether to write the GitHub Actions workflow `.github/workflows/ci.yml`, interactively or with `--ci github-actions|none`; `-n` defaults to `github-actions`.
- Ask whether to write the AI agent instructions and skills, interactively or with `--no-ai-skills`; `-n` writes them.

### Changed

#### `datapages lint` and `datapages gen`

- Reject actions, `OnXXX` handlers, `StreamOpen`, `StreamClose` and `State[T]` on a page whose route ends in `{$}`, as `// PageUser is /user/{name}/{$}`. A `{$}` at the end of a page route declares a page that serves only `GET`. An action below such a route got an error saying that it was not under its page, with a suggested route that net/http rejects. Remove `{$}` from the route of such a page: it keeps its URL.
- Read an exported method on `App` named `QUERY` followed by an uppercase letter, such as `QUERYStats`, as an action. Such a method is rejected when it's a helper rather than an action: rename it.
- Read an exported method on `App` named `GET` followed by an uppercase letter, such as `GETStats`, as a GET action. Such a method is rejected when it's a helper rather than an action: rename it.
- Read an exported method on `App` named `StreamConfig` followed by the method name of an `App` action, such as `StreamConfigPOSTSave`, as the SSE configuration of that action. On `App`, `StreamConfig` alone and every other method whose name starts with `StreamConfig` are rejected. Rename such a helper.
- Reject page and action routes below the static-files URL prefix. `net/http` treats them as more specific, so matching asset requests reach the handler instead of the file server. Move the route outside the prefix.
- Reject application code that imports a package under `github.com/romshark/datapages/runtime`, test files included. Those packages are for the generated code only, and any release may change them without notice. Use the `datapages` package or the generated packages instead.
- Skip type-checking the function bodies of the standard library and of downloaded modules when loading the app package. `datapages lint` on `example/classifieds` takes 277ms instead of 406ms, and `datapages watch` regenerates sooner after each change to a Go file.
- Cache what loading the app package reads from the standard library and the module cache in `datapages/load` under the user cache directory, such as `~/.cache/datapages/load` on Linux. A run with the same imports, `go.mod`, `go.sum`, environment, `go` binary and `datapages` binary as the previous one loads the app package from the cache without running the `go` command. `DATAPAGES_LOADCACHE=off` disables the cache, and an absolute path moves it. Nothing removes outdated entries. Delete the directory to free their space.
- Reject an `OnXXX` handler that is not named after its event, as `OnNewMessage(event EventMessageSent, sse datapages.SSE)`. A page's `OnB` taking `EventA` hid an embedded `OnB` taking `EventB`, and the generated code did not compile. Rename the handler to `On` and the event name after `Event`, as `OnMessageSent`.
- Require the app package directory in `datapages watch --app`, as `--app app/frontend` instead of `--app frontend`. With `web/app` and `admin/app`, `--app app` was ambiguous and ran one of them.
- Reject a query tag with a character other than ASCII letters, digits, `-`, `.`, `_` and `~`, as `query:"q&a"`. For such a tag the handler read an empty value, or the action expression failed to compile in the browser. Rename the tag, as `query:"q_a"`.

#### Generated code

Run `datapages gen` to apply these.

- Reload a page when the session of its SSE stream closes or expires. A page with only user-addressed events also reloads when its stream reconnects after the session ended. The page previously kept showing the old session's content without updates.
- Render `PageError404` with status 404 for a page load whose handler returns `datapages.ErrNotFound`, as for a URL that no route matches. For such a page load, `PageError404.GET` receives the request's query and signals and a zero path. An app without `PageError500` previously answered with the status text.
- Answer 406 Not Acceptable to a request without `Datastar-Request: true`, such as a plain form submission, on an action declared on `App` that takes `datapages.PageCacheWriter`, as on a page. Such an action that redirects answered a plain form with an HTTP redirect and dropped the queued writes, such as `ClearAll`. Submit those forms through a Datastar action.
- Replace [datastar-go](https://github.com/starfederation/datastar-go) with the Datapages SSE writer. Common events allocate nothing. Measured against v0.10.1, they take 9% to 51% of the previous time on uncompressed and zstd streams, 43% to 77% on gzip streams and 67% to 83% on brotli streams. `datastar-go`, `github.com/CAFxX/httpcompression` and `github.com/valyala/bytebufferpool` leave the application module after `go mod tidy` unless it imports them directly.
- Stop copying strings while rendering an entry written through `datapages.PageCacheWriter`. A page load that writes 1 entry drops from 43 to 38 allocations.

#### Runtime and modules

- Require `github.com/romshark/morpheus` v0.3.1, which the settings page of `datapages watch` uses. `go get` of this release moves an application on Morpheus v0.1.0, v0.2.0 or v0.3.0 to v0.3.1. v0.2.0 replaces `neo.Condition` with `neo.IfPlatform`. v0.3.0 removes `--neo-sidebar-overlay-breakpoint` and fires `neo-toast-close` on every close of a toast. v0.3.1 requires templ v0.3.1100, as its [changelog](https://github.com/romshark/morpheus/blob/v0.3.1/CHANGELOG.md) lists. Load the v0.3.1 stylesheets and bundle to match.
- Require Go 1.27.3 instead of 1.27.1. `go get` raises the `go` line of the application's `go.mod` to 1.27.3. Where `GOTOOLCHAIN=local` is set, install Go 1.27.3 or later first.
- Require templ v0.3.1100. `*_templ.go` files written by an earlier templ fail to compile. Install templ v0.3.1100 and run `templ generate`.
- Label requests with the `QUERY` method as `QUERY` in the `method` label of the HTTP metrics. They were counted as `<other>`.
- Refuse a URL passed to `datapages.WithDatastarJS` that is invalid according to RFC 3986, such as one with `|` or a non-ASCII character. `datapages.NewServer` returns the error. Percent-encode the character it reports, as `%7C` for `|`.
- Answer 404 to an asset path spelled differently from its file name, such as `/static/App.js` for `app.js`, on every system and in dev mode. Windows and macOS served such paths, unlike Linux and an `embed.FS`, and `datapages.WithAssetsCache` kept an `ETag` per spelling until exit. Spell asset links exactly like their files.
- Require a custom `messaging.Broker` to deliver messages in the order they were published, whatever their subjects. A broker that delivers each subject on a goroutine of its own breaks this.
- Require a custom `messaging.Broker` to close a subscription's channel when the subscription can no longer receive messages, such as when its connection closes. Otherwise the page stops receiving events.
- Require a custom `csrf.TokenWriter` to write a different token on every `WriteToken` call, such as by masking it with fresh random bytes as `csrf.Tokens` does. Page and action responses are compressed by default now, and when such a response also reflects attacker input, its size can reveal a token that stays the same, an attack known as BREACH.
- Publish through the `inmem` broker in time linear in the open streams of a page that handles an event with a `datapages.Subject` field without a `signal` tag. Such streams subscribe with a wildcard, and a publish to them took quadratic time: 12.8ms with 10,000 streams, where it takes 86us now. A stream that opened or closed meanwhile waited for it.
- Stop copying strings written to a response. A `GET /` of `example/counter` drops from 27 to 19 allocations.
- Stop copying strings written through `offline.Middleware`. A page of 22 strings drops from 35 to 14 allocations.
- Let a file served through `offline.Middleware` use sendfile when it is an `*os.File`, as a file of an `http.Dir` or a `datapages.File` opened with `os.Open` is. Embedded files cannot.
- Serve the embedded files of `datapages.WithAssets`, and the files of a `datapages.WithAssetsFS` file system other than `http.Dir`, without allocating a copy buffer of up to 32 KB per response. In a benchmark over HTTP/1.1, a request for a 30 KB file takes 18% less time.
- Compress page, action and error bodies of at least 256 bytes by default with the first accepted coding among zstd, brotli and gzip. Applications previously needed custom middleware to compress these responses. An 8 KB page of cards shrinks to 19% in 14 us. `datapages.File` and dev-mode responses remain uncompressed. A `HEAD` response gets the headers of its `GET` response. `datapages.WithResponseCompression` configures compression. `datapages.WithNoResponseCompression` disables it for loopback-only clients or body-filtering proxies. Compressing proxies pass compressed bodies through. Send pages that combine secrets with attacker-controlled input with `Cache-Control: private, no-transform` to prevent compression side channels.
- Compress static assets of at least 256 bytes by default when their type is text, JSON, JavaScript, XML, YAML, TOML, SVG, WebAssembly, a TrueType or OpenType font, an icon or a BMP image. Prefer the first accepted coding among brotli and gzip. Compress each asset/coding copy on its first request, send it from memory and write it to a configured store in the background. Copies preserve `Content-Length`, support 1 range and get distinct weak `ETag` values. Multi-range requests and dev mode use the original asset. Configure codings, levels, minimum size and storage with `assetcompress.New` and `datapages.WithAssetsCompression`. `Precompress` creates copies eagerly. `modules/assetcompress/disk` persists them across restarts and removes copies unused for 7 days by default. `datapages.WithNoAssetsCompression` disables compression, and an `AssetsCacheConfig.CacheControl` with `no-transform` keeps assets uncompressed. Keep assets immutable until server exit. A changed file of `datapages.WithAssetsFS` keeps its old `ETag`, and its compressed responses can keep its old content, until a restart. Serve such files through a GET action returning `datapages.File`, or add `datapages.WithNoAssetsCompression` and set `AssetsCacheConfig.DisableETag`.
- Let the offline worker reuse a cached same-origin asset without a request until its `max-age` passes. It asked the server on every use, also for `immutable` assets.
- Send `Cache-Control: public, max-age=0` with static assets when `datapages.WithAssetsCache` is absent or disabled. A browser could reuse a `datapages.WithAssetsFS` file after a deploy without asking, because the file had a modification time and no `Cache-Control`. A CDN now revalidates such assets too, unless `WithAssetsCache` sets a `MaxAge`.
- Send an `ETag` with static assets when `datapages.WithAssetsCache` is absent or disabled. Browsers downloaded embedded assets in full on every page load. Set `AssetsCacheConfig.DisableETag` for `datapages.WithAssetsFS` files that change.
- Keep the app running in `datapages watch` when `datapages.yaml` changes, as on a save of the Toxiproxy settings page. A change reran `datapages gen` and restarted the app, which lost its in-memory state, although the app does not read the file and watch reads it only when it starts. A custom watcher of `watch.custom-watchers` still sees the change.

#### `datapages init`

- Teach generated AI instructions to use isolated Web Components for browser libraries or client logic too complex for Datastar expressions. The earlier instructions did not say where such JavaScript belongs. Run `datapages init` in an existing project to update them.
- Generate a new project's templates with templ v0.3.1100 and install that version in its `.github/workflows/ci.yml`. A project scaffolded earlier keeps v0.3.1020 in its workflow: change the version in the `go install github.com/a-h/templ/cmd/templ@` step to match the templ version in its `go.mod`.
- Run `templ generate` after `datapages gen` in a new project's `make gen`. It left `*_templ.go` stale after a `.templ` change. In a project scaffolded earlier, add `go run github.com/a-h/templ/cmd/templ@v0.3.1100 generate` after `datapages gen` in the `gen` target of `Makefile`.
- Ask whether to create a subdirectory for the project, or take its name from `--name`; `-n` uses the current directory and no longer requires `--name`. Ask whether to create a git repository instead of failing without one, or take the answer from `--no-git`; `-n` creates one. Ask for the module path directly; an empty answer takes the suggested one.

### Removed

#### `datapages lint` and `datapages gen`

- Disallow `GET` and actions on stream embeds, formerly abstract page types. They may still share `OnXXX`, `StreamOpen` and `StreamClose`. Move `GET` to each page, using a shared helper for common code. Move actions to one page or to `*App` when several pages use them.

### Fixed

#### `datapages lint` and `datapages gen`

- Reject a page that reaches the same stream embed through 2 embedded types at the same depth. The generated code for such a page did not compile: Go reported an ambiguous selector. Declare the handler on the page, which shadows the embedded ones.
- Reject a page or stream embed that embeds a type of another package, such as `sync.Mutex`. Declare handlers in the app package and move other fields to `App`.
- Reject a file of the app package with a `//go:build` line or a name such as `probe_windows.go`, except a test file and a file under `//go:build ignore`. `datapages gen` left out its pages, actions and events without an error on a platform that excludes the file. Move the file to another package, or remove its constraint.
- Report a missing app package directory, such as `./app` in a module without a `datapages.NewServer` call. The package of the parent directory was read as the app instead, and `datapages gen` wrote stubs into the missing directory.
- Refuse a `watch.app-host` without an `http` or `https` scheme and a host, such as `localhost:8080`, in `datapages watch`. It was read as a URL without a host, which the dev server could not send requests to. Write it as `http://localhost:8080`.
- Apply `watch.log.level` in `datapages watch`. `erronly`, the default, logged info like `verbose`. It now logs errors only.
- Reject an `OnXXX` handler that takes a second `datapages.SSE`, session or `datapages.StreamID` parameter, as every other handler does. A field with 2 names, such as `sse, sse2 datapages.SSE`, declares 2 parameters: they were read as one, and the generated call did not compile. 2 such parameters in separate fields were accepted before and received the same value. Remove the second one.
- Reject an `OnXXX` handler that takes its event by pointer, as in `event *EventPing`. The generated code for it did not compile. Take the event by value.
- Reject a route containing `"` or `\`. For such a route, `datapages gen` failed with a Go syntax error, or the generated server matched a different path when the backslash formed a Go escape such as `\u00e9`. Write the characters as `%22` and `%5C`, which `net/http` decodes before matching the route.
- Reject a route containing a backtick. On a page with a stream or a query field that reflects a signal, `datapages gen` failed with a Go syntax error. Write it as `%60`, which `net/http` decodes before matching the route.
- Reject an unexported defined type as the type argument of `datapages.Path`, `datapages.Query` or `datapages.Signals`, including one behind an exported alias. The generated code for such a handler did not compile. Export the type.
- Reject a route containing `?`, `#` or a `%` without 2 hex digits after it. The generated links, action expressions and stream URL carried the route as written, where the browser read `?` as the start of the query and `#` as the fragment, and `net/http` answered 400 to the `%`: none of them reached the page. Write the characters as `%3F`, `%23` and `%25`, which `net/http` decodes before matching the route.
- Reject an event with more than 1 `datapages.SubjectUser` field, such as `To, Cc datapages.SubjectUser`. Only the first field routed the event: a user named in a later field never received it, and the user in the first field received it whatever the others held. Keep 1 `SubjectUser` field and dispatch the event once per user.
- Reject an error-prone event field that holds a subject type, such as `Recipients []datapages.SubjectUser`. It looks like a list of recipients, but it was a payload field, and the event went to every stream of the pages handling it. Declare 1 `datapages.SubjectUser` field and dispatch once per user.
- Reject a page other than `PageIndex` at `/`, as in `// PageHome is /`. The router sent every request for `/` to that page, and `PageIndex` never rendered. Give the page another route, or move its handlers to `PageIndex`.
- Report an action of a page whose route ends in a `{name...}` wildcard, as in `// PageFiles is /files/{rest...}`, with an error saying that such a page cannot have actions. The report was a route conflict about a pattern the app does not declare, or a missing path comment with a suggested route that failed the same way. Remove the wildcard from the page route to add actions.
- Report the same conflicts on every run when handlers take `datapages.Session` with different data types. The data type of whichever page was read first won, which changed from run to run which handlers were reported. The first page by name now declares the type.
- Report the errors of a struct type that several events share, such as a field without a `json` tag, in the same order on every run: by event name.
- Accept a handler parameter typed with an alias of `datapages.Path`, `datapages.Query`, `datapages.Signals` or `datapages.State`, as in `type FormSignals = datapages.Signals[struct{...}]`. The check read the `Values` field of the wrapper instead of its type argument and reported errors located in `datapages.go`, and refused an alias of `datapages.State` as not a struct.
- Report an action route that ends in a `{name...}` wildcard, as in `// POSTUpload is /upload/{path...}`, with an error saying that an action route cannot end in a wildcard. The error was a route conflict on `POST /upload/{path...}/{$}`, a pattern the app does not declare. Pass the value in a query parameter or a signal instead.
- Reject an assets URL prefix that contains `%`, as in `// StaticFS is /my%20files/`. The server answered 404 to every request under such a prefix. Write the prefix without percent-encoding, such as `/my-files/`.
- Accept in an assets URL prefix the characters its error message lists, ASCII letters, digits, `-`, `.`, `_`, `~` and `/`, and no others. Quotes and other punctuation the message did not list were accepted, and a segment starting with a dot, such as `/.well-known/`, was refused. Replace any other character in the prefix, as `/static-files/` for `/static+files/`.
- Reject a `//go:embed` pattern on the assets variable that is a glob or names a single file, as in `//go:embed static/*.css` or `//go:embed static/app.css`. The server used the pattern as the directory to serve and answered 404 for every asset. Embed the directory, as in `//go:embed static` or `//go:embed static/*`.
- Stop reporting a valid `href={ LoginURL }` as relative on some runs when a function also declares a local `LoginURL` constant.
- Report each mismatch between the route and the `datapages.Path` struct of an action on `App` as its own error, at the path parameter or the field, as for a page. They were joined into 1 error at the method name.
- Accept a `datapages.NewServer` call with its fifth type argument written out, as in `datapages.NewServer[app.App, datapages.DisableSessions, datapages.DisablePrometheus, gen.Server, *gen.Server]`. It was refused with "datapages.NewServer needs four type arguments, got 5".
- Accept an app package whose name differs from its directory, as `package app` in `go-app/`, when the file calling `datapages.NewServer` imports it without an alias. The app type was refused with "must live in its own package".
- Read the `datapages.NewServer` call in a file whose `//go:build` line negates a tag, as `//go:build !windows`. `datapages gen` skipped the file and generated code as if the module had no call.
- Report a problem in a `datapages.NewServer` call with a path relative to the module root, as every other error is. A wrong number of type arguments, a wrong `Metrics` type argument and a file that does not parse were reported with an absolute path.
- Check the attributes inside a conditional attribute of a `.templ` file, as in `<a if ok { href="/login" }>`, in both branches. A hardcoded `href`, a form `action`, a hardcoded Datastar action URL or an action of another page went unreported there. Change each reported attribute as its `fix:` line says, such as `href="/login"` to `href={ href.PageLogin() }`.
- Include the HTTP method in the action names that route errors show, as in `PageUser.DELETEFollow`. Before, they showed `PageUser.Follow`, which could also mean `PageUser.POSTFollow`.
- Show the line number of a Go build error that comes without a column. Before, it showed line 0, and on Windows the drive letter as the file name.
- Check the `.templ` files of the packages that the app package imports from its module, such as `app/template`. Before, only the app package's own templates were checked. A wrong `href`, a form `action`, a hardcoded action URL or another page's action in those templates went unreported. Change each reported attribute as its `fix:` line says, such as a hardcoded `href` to a function of the generated `href` package.
- Reject an action that takes `datapages.SSE` and returns a body. Its HTML went into the open event stream as broken events, and with compression the browser could not read the stream at all. Send the body with `sse.PatchElement` instead.
- Reject a page whose events need the same signal both as a value and as an object, such as an event field tagged `signal:"chat"` and another tagged `signal:"chat.room"`. `$chat.room` exists only when `$chat` is an object, and the stream of such a page never opened. Bind one of the fields to another signal.
- Name the event whose subject must change in the fix for overlapping event subjects. When the event with subject fields sorted second, the fix named the other event.
- Suggest the valid subject types, including `datapages.SubjectStateID`, for a payload field named like a subject field, such as `SubjectUser []string`. The fix named the nonexistent `datapages.Subjects` and `datapages.SubjectUsers` types.
- Spell `datapages.SubjectUser` correctly in the fix for a `signal` tag on that field. It was written as `datapages.SubjectUser(s)`.
- State the accepted syntax in the fix for an invalid `signal` tag: each period-separated segment must match `[A-Za-z_][A-Za-z0-9_]*` and contain no `__`. The fix incorrectly required a lowercase first letter.
- Base the suggested route for an action with an invalid route comment on its page route. For a page at `/user/{name}`, the fix suggested `/user/follow` instead of `/user/{name}/follow`.
- Base the suggested route for an action without a route comment on the route suggested for its page when the page also has no route comment. For `PageUser.POSTFollow`, the fix suggested `/follow` instead of `/user/follow`.
- Report a subject that 2 applications of a module claim in `datapages lint` when 1 of them has other errors, mirroring `datapages gen` behavior.
- Reject a `reflectsignal` query field that can't be written as text, also when the `GET` takes no `Signals`. Its value grew in the URL on every reload. Give the field a string, bool, integer or float type or a type that implements `encoding.TextMarshaler`, or remove its `reflectsignal` tag.
- Accept an event or signals field of a struct type from another module, such as `url.URL` or `sql.NullString`. Its fields were held to the `json` tag rules, with errors pointing into the standard library.
- Keep the generated code when `datapages gen` rejects the `SessionData` type argument of `datapages.NewServer`. It was replaced and did not compile with the call.
- Keep the generated code of 2 applications whose events claim the same subject. It was replaced before the error was reported.
- Replace generated files only once all are written. A full disk or a killed `datapages gen` could leave a package that did not compile.
- Rerun `datapages gen` in `datapages watch` when the package calling `datapages.NewServer` changes. A new `Metrics` type argument took effect only after the next change to the app package.
- Accept a `reflectsignal` query field whose signal is a pointer, such as an `int` query field reflected into a `*int` signal. It was refused with "incompatible JSON kinds", although `encoding/json` decodes the value into it.
- Accept a string query field reflected into a signal that implements `encoding.TextUnmarshaler`, such as an `int64` type with an `UnmarshalText` method. It was refused with "incompatible JSON kinds", although `encoding/json` passes the string to `UnmarshalText`.
- Reject a number or bool query field reflected into a signal that implements `encoding.TextUnmarshaler` and not `json.Unmarshaler`. `encoding/json` decodes only strings into such a type, and an action that took the signal answered 400. Reflect a string field or a field that implements `encoding.TextMarshaler`.
- Show the update notice in `datapages watch` when the running version is a prerelease of the latest release, such as `0.11.0-rc.1` of `v0.11.0`. The check ignored the prerelease suffix and found both versions equal.

#### Generated code

Run `datapages gen` to apply these.

- Reconnect a page's SSE stream whenever it ends, without a retry limit. Some endings, such as a graceful shutdown, previously left the page without live updates.
- Write the CSRF script into every request-specific page and action response whose actions carry a session cookie, including documents whose handlers do not accept a session. This prevents actions submitted from those documents from returning 403. `PageOffline` and page-cache entries omit the script because visitors share them.
- Generate code that compiles when `App.Head` takes the session before the `*http.Request` in an app that uses `datapages.PageCacheWriter`.
- Generate code that compiles when the type argument of `datapages.Path`, `datapages.Query` or `datapages.Signals` is an alias of a defined type, as in `type Filter = SearchQuery`.
- Generate code that compiles when the type argument of `datapages.Path`, `datapages.Query` or `datapages.Signals` is a struct type literal with an embedded field, such as `time.Time`.
- Generate code that compiles when the session data type or a field type of `datapages.Path`, `datapages.Query` or `datapages.Signals` is an alias declared in a package with the same name as one the generated code imports, such as `stream`.
- Generate code that compiles when `PageError404.GET` takes a path, query, signals or dispatcher parameter. For a URL that no route matches, the handler receives that URL's query and signals and a zero path.
- Generate code that compiles when a path, query or signals field type comes from a package named `path`, `query`, `signals` or after another variable the generated handlers declare.
- Answer a page load whose handler returns `datapages.ErrBadRequest`, `datapages.ErrForbidden`, `datapages.ErrNotFound` or `datapages.ErrConflict` with that status in an app that defines `PageError500`, as every app created by `datapages init` does. Such a page load got `PageError500` with status 500, and no page could answer 404. `PageError500` now renders for status 500 only. Other statuses get their status text, except 404 in an app that defines `PageError404`.
- Keep the SSE streams of a page with both public and user-addressed events open while the tab is hidden when its `GET` returns `enableBackgroundStreaming` set to `true`, for signed-in visitors and guests alike. Such a page dropped its reload on visibility but still closed its stream, which left it without the events published while the tab was hidden.
- Escape a `'` in a route, as in `// PageAuthor is /o'reilly`, where the generated code writes the route into JavaScript strings: the page's stream URL, its action expressions and the URL a `reflectsignal` query field rewrites. The quote ended the string, which left the stream and the actions of such a page failing with a syntax error in the browser.
- Recover a panic in an `OnXXX` handler on the stream that a page with both public and user-addressed events serves to signed-out visitors, as the stream of signed-in visitors does. The panic dropped the connection, skipped `StreamClose` and never reached `RecoverError`, which leaked per-tab state that `StreamOpen` registered for every such guest.
- Send `Cache-Control: private` with a page rendered for a signed-in visitor, the 404 and error pages included, unless the response already has a `Cache-Control` header. Such a page carries a CSRF token derived from the session. A shared cache that stored the page served it to other visitors, and their actions failed with 403.
- Set the session cookie of an action that takes `datapages.PageCacheWriter` and returns `newSession` or `closeSession` but neither a redirect nor a body. The response went out without the cookie: a sign-in left the visitor signed out, and a sign-out left them signed in.
- Answer with the error status when an action that takes `datapages.PageCacheWriter` and returns neither a redirect nor a body fails or panics. The response was 200, with the error status written as text into the event stream.
- Read a subject field bound to a nested signal, as `signal:"chat.room"`, from the object Datastar sends, `{"chat":{"room":"lobby"}}`. The stream of a page handling such an event answered 400 on every connect.
- Preserve `json.RawMessage` content after a standalone carriage return in `SSE.PatchSignals` and `SSE.PatchSignalsIfMissing`.
- Reduce memory held by compressed SSE streams. A zstd stream holds about 1.5 MiB instead of brotli's 2 to 11 MiB, and encoders are pooled instead of allocating 6.3 MiB before each response. The default order is zstd, brotli and gzip. `datapages.WithSSECompression` configures the order, levels and windows. Compressed responses include `Vary: Accept-Encoding` and end their compressed data. For a 99 KB list re-render with 1 changed item, zstd sends 325 B instead of 22 B. Prefer brotli or enlarge `ZstdWindow` to send fewer bytes.
- Skip `RecoverError` after an HTML response body starts. Its SSE events previously corrupted a partial HTML response.
- Recover and log a panic in `RecoverError` once. It previously invoked `RecoverError` again and could write 2 compressed streams in 1 response.
- Send `Vary: X-Datapages-Offline-Version` before opening an SSE response from an action that takes `datapages.PageCacheWriter`.
- Log errors from bodies passed to `PageCacheWriter.Set` or `SetShim` when rendering an action's event stream or redirect script.
- Log the stack of a panic in `StreamOpen`, as for a panic in `GET`, an action or `OnXXX`. The log previously held only the panic value.
- Stop sending a page cache entry again on every load when its version is above 2^53, such as a 64-bit hash.
- Use the app package's own name in the error `datapages.NewServer` returns for an unexpected `WithSessionManager`, and in the example of `Init`. Both said `dpapp`, an alias internal to the generated code.
- Stop reloading a page with only user-addressed events for signed-out visitors on every tab switch. Each reload discarded client-side state.
- Give signed-out visitors of a stateful page with only user-addressed events a stream at `_$/anon/`. Their state actions answered 409. `StreamOpen` and `StreamClose` run on it with a guest session.
- Answer a request whose session, path, query or signals read panics, as in an `UnmarshalText` or `UnmarshalJSON` method of the app or in a `sessions.Manager`, the way a panic in its handler is answered, with `PageError500`, `RecoverError` or status 500. The server closed the connection without a response, also on the SSE stream of a page whose `StreamOpen` takes signals.
- Allocate a pointer path or query field, such as `*big.Int` or `*time.Time`, before its `UnmarshalText` runs. Every request that carried such a parameter panicked on the nil pointer, and the connection closed without a response. A pointer query field is nil when its parameter is absent.
- Count the errors of page loads, file actions and apps without `RecoverError` in `datapages_internal_errors_not_recovered_total`, in an app generated with `datapages.EnablePrometheus`. A panic after the response started counts too, and so do sentinels, such as `datapages.ErrNotFound` from a page's `GET`. The counter rose only when `RecoverError` returned an error or panicked.
- Close the `data-init` attribute of a page whose stream carries only user-addressed events and whose route has a path variable, such as `/post/{slug}`. A signed-out visitor got the start of the attribute without its end, which broke the `<body>` element of the page.

#### Runtime and modules

- Leave static assets unedited in `offline.Middleware`. An HTML asset got the connectivity and registration scripts, appended at its end when it had no `</head>` or `</body>`, as a fragment loaded with `fetch` has. The scripts also changed the bytes behind the asset's `ETag`.
- Percent-encode the characters a URL path cannot carry, such as `#`, `?` and `%`, in the file names that `assets.Path` and `href.Asset` turn into URLs. `assets.Path("a#b.css")` returned `/static/a#b.css`, which a browser requests as `/static/a`, and a `%` in a file name made the server answer 400.
- Label the HTTP metrics with the route when a middleware added with `datapages.WithMiddleware` passes on a new request, as `r.WithContext` returns when storing a CSP nonce. The request counter and the latency histogram labelled every request through such a middleware `<unmatched>`.
- Accept `&` and `'` in the URL passed to `datapages.WithDatastarJS`, as in `https://cdn.example.com/datastar.js?v=1&min=1`. RFC 3986 allows both, and the page writes the URL escaped. Such a URL was refused.
- Deliver events through the `natscore` broker in the order they were dispatched, as `inmem` does. 2 events of different types dispatched one after the other often arrived in reverse order.
- End the SSE streams subscribed through the `natscore` broker when its NATS connection closes, which nats.go does after the last failed reconnect attempt: 60 attempts 2s apart with the defaults of `nats.Connect`. Open pages kept their streams and received no more events, without an error. They now reconnect, and the server logs each reconnect that fails on the closed connection.
- Return from `natskv.SessionManager.UserSessions` and `CloseAllUserSessions` the token the session's cookie carries, as `inmem` does. In v0.10.0 and v0.10.1, each call returned a new token for the same session, and an application that compares a listed token with `Session.Token()` to find the current session never found it. A session created before the upgrade, or under one of the `PreviousEncryptionKeys`, keeps a cookie that differs from its listed token until the user signs in again.
- Return from `natskv.SessionManager.UserSessions` an iterator that yields the sessions on every range, as `inmem` does. A second range yielded nothing, and an iterator never ranged kept a KV watcher subscribed until the context passed to `UserSessions` ended.
- Return an error from `natskv.SessionManager.UserSessions` and `DeleteExpired` when their context ends, NATS stalls for 5s or the connection closes during the call. `UserSessions` returned the sessions read by then as the complete list, and `DeleteExpired` returned nil with expired sessions left in the bucket.
- Report a token that can't be decrypted as `sessions.ErrSessionNotFound` in `natskv.SessionManager.Session`, as `inmem` does. This includes forged tokens and cookies encrypted with a key that is no longer in `PreviousEncryptionKeys`. Before, `errors.Is(err, sessions.ErrSessionNotFound)` was false for them.
- Send what net/http logs, such as a panic in a handler or a failed TLS handshake, to the logger set with `datapages.WithLogger`, at error level. It went to stderr as JSON at info level, and the metrics server of `datapages.WithPrometheus` wrote it as plain text. A server passed to `datapages.WithHTTPServer` keeps its own `ErrorLog` if it sets one.
- Register the offline service worker for the whole origin when `offline.Config.ScriptURL` is in a subdirectory, such as `/static/sw.js`. The browser limited such a worker to the pages under that directory. Other pages were not available offline, and their `datapages.PageCacheWriter` writes never reached the worker.
- Refuse an `offline.Config.ScriptURL` that is not a plain path, such as `sw.js` or `/sw.js?v=2`. The worker never loaded from those. `offline.WithServiceWorker` and `WithOffline` return an error, and `offline.Middleware` panics. Use a path that starts with `/` and holds only `/` and the unreserved characters of RFC 3986.
- Stop logging a panic, `response writer failed to flush`, with a stack trace when a visitor leaves a page while its SSE stream opens. A stream on the `natscore` broker waits for a round trip to NATS before it opens, which makes this more likely.
- Keep a response marked `Cache-Control: no-store` out of the offline worker's cache, and fetch a stored response marked `no-cache` from the network first, using the stored copy only offline. The worker stored every same-origin response that an element or `fetch` loaded, such as an image, whatever its `Cache-Control`, and served the stored copy first. Increase `offline.Config.WorkerVersion`, which deletes what the previous worker stored.
- Remove a response from the offline worker's cache when the network answers 401, 403, 404 or 410. The worker kept serving the stored copy of a file after the server stopped authorizing it or deleted it, until `offline.Config.WorkerVersion` changed. Increase `offline.Config.WorkerVersion`, which deletes what the previous worker stored.
- Stop the offline worker from copying a range response with status 206, such as a video that a `video` element streams, for its cache, which refuses it. Nothing read the copy, which can keep every byte the page has read in memory.
- End the response of a static asset when its client accepts no more of the file for 1 minute. A client that stopped reading held the request and the open file for as long as it kept the connection. `datapages.WithFileSendTimeout` changes the time, which also applies to `datapages.File`.
- Abort SSE writes blocked on clients during graceful shutdown. They previously held shutdown until its timeout, skipped `StreamClose`, and could race with net/http cleanup.
- Stop `offline.Middleware` from logging `http: superfluous response.WriteHeader call` for each SSE response.
- Stop `offline.Middleware` from adding its scripts to a range response, status 206, of an HTML file, such as a static asset that a download manager resumes. The body was longer than its `Content-Range` said, and the saved file was corrupt.
- Stop rounding an `offline.Config.WorkerVersion` above 2^53 in the browser. A version rounded down added the registration script to every page.
- Treat `/tickets` and `/tickets/` as the same page in the offline cache, as the server does. A visit to `/tickets` missed the copy cached for `/tickets/`, so the page didn't load offline, and the server sent the copy again on every online visit.
- Count in `suppressed` a record that `datapages.WithLogSampling` holds back when it is logged at the same moment as the first one of its kind. Such a record was dropped uncounted.
- Stop appending the 500 page to a page that fails halfway through rendering behind `WithOffline` or `offline.Middleware`. The visitor got the half page and a second document after it.
- Refuse in `datapages.NewServer` a `ServerConfig` value that a custom `ServerOption` sets and the field's `With` option refuses. A session cookie name with a space, for example, passed and kept every visitor signed out.
- Apply the defaults of `datapages.WithPrometheus` to a `datapages.PrometheusConfig` that a custom `ServerOption` writes into `ServerConfig.Prometheus`. Without a `Registerer`, `NewServer` panicked, without a `Gatherer`, every scrape of `/metrics` panicked, and no HTTP request was counted.
- Serve font assets with their `font/*` type on every host. Some mime tables, such as shared-mime-info on Linux, typed `.otf` and `.otc` fonts as OpenDocument templates, which also kept them uncompressed.
- Send `action.WithRetryMaxWaitMs` as `retryMaxWait`, which Datastar 1.0.0 and later read. From v0.9.2 to v0.10.1 it sent `retryMaxWaitMs`, which Datastar ignores, so the wait between retries stayed capped at the 30s default.

#### `datapages init`

- Pass the app's `embed.FS` assets variable to `datapages.WithAssets` when creating a missing `cmd/server/main.go`. Generated asset URLs previously returned 404. An existing `main.go` is not rewritten; add `datapages.WithAssets(app.StaticFS, false)` manually.
- Shut down gracefully on SIGTERM in the `cmd/server/main.go` it writes. Docker, Kubernetes and systemd stop a process with SIGTERM, which previously ended the server without waiting for requests, SSE streams and `StreamClose` hooks. It does not rewrite an existing `main.go`: add `syscall.SIGTERM` to its `signal.NotifyContext` call.
- Check out the repository before installing Go in `.github/workflows/ci.yml`, which lets `actions/setup-go` restore its cache. In a project scaffolded earlier, move the `Checkout repository` step above `Install Go`.
- Use the enclosing Go module when run in a subdirectory, as `datapages gen` does, instead of creating a nested module there.
- Run in place in a project that has a `go.mod` and a `datapages.yaml` but no git repository. `datapages gen` recommends `datapages init -n` to update the agent instructions, which failed there with "--name is required", and an interactive run created a new project inside the existing one.
- Correct the templ pitfall in the generated `datapages-templates` skill. It told agents that wrapping text in an element keeps templ from parsing a leading `if`, `for` or `switch` as Go, but `<p>if you agree</p>` fails as well. It now says to put the text in a string expression, as `<p>{ "if you agree" }</p>`, and no longer warns about `else` and `case`, which templ reads as text. Run `datapages init` in an existing project to update it.

### Security

#### Generated code

Run `datapages gen` to apply these.

- Prevent carriage returns in `SSE.PatchElement`, `SSE.PatchElementAt` and `SSE.ExecuteScript` content from injecting SSE fields and JavaScript. An attacker who controls text rendered in a patched component could run JavaScript in each recipient's session. Applications using these methods are affected in v0.10.0 and v0.10.1. Applications calling the equivalent methods of datastar-go are affected in v0.1.0 through v0.9.4. An application that calls datastar-go directly stays affected on every Datapages version, because the fix is in the Datapages SSE writer. Until the upgrade, and wherever the application calls datastar-go, remove `\r` from untrusted text before rendering it.
- Render `PageOffline` with a zero session, as the specification states. A `GET` of `PageOffline` or an `App.Head` that takes the session rendered the page for the visitor whose browser installed the service worker, and the worker shows that copy to everyone who uses the browser while offline. Anyone who uses the same browser after a signed-in user, such as on a shared computer, could read what the page and the head rendered for that user. Applications that pass `WithOffline` and declare `PageOffline` are affected in v0.10.0 and v0.10.1 when its `GET` or `App.Head` takes the session. After the upgrade, increase `offline.Config.WorkerVersion`, which makes browsers fetch the page again. Until then, take no session in the `GET` of `PageOffline`, and have `App.Head` render nothing that depends on the session for the route of `PageOffline`.

#### Runtime and modules

- Prevent `natskv.SessionManager.CloseAllUserSessions` from returning nil while sessions of the user stay signed in. When its context ended during the call, as a request context does when the client disconnects, it closed only the sessions read by then. Whoever held a cookie of the others, such as an attacker the user meant to sign out, stayed signed in. A NATS stall of 5s during the call and a closed connection did the same. The end of the context no longer stops the call, and the other 2 return an error. Applications calling `CloseAllUserSessions` are affected in v0.1.0 through v0.10.1. Until the upgrade, pass it `context.WithoutCancel(r.Context())`, which covers the client that disconnects.
- Stop sending a signed-in user's CSRF token to other origins. A `@post`, `@put`, `@patch` or `@delete` action to another origin sent it the token if its CORS policy allowed the `X-CSRF-Token` header. This affects v0.1.0 through v0.10.1. Until the upgrade, call other origins without a Datastar action.

#### `datapages init`

- Write `.env` with mode 600 instead of 644. Any account on the machine could read its `SESSION_ENCRYPTION_KEY` and, with read access to the `SESSIONS` bucket, build a session cookie for any user. This affects projects created by v0.1.0 through v0.10.1. `init` leaves an existing `.env` alone. Run `chmod 600 .env` there, and replace the key if another account could read it.

## [0.10.1] - 2026-09-26

### Added

- Count streams that end at their session's `ExpiresAt` under `reason="expired"` in `datapages_sse_disconnects_total`.

### Changed

- Preserve an existing `AGENTS.md` without a datapages stamp when running `datapages init`. To replace the unstamped `AGENTS.md` generated by v0.10.0, delete it before running `datapages init`.

### Fixed

- Prevent `offline.Middleware` from corrupting or dropping a response when text before `</head>` contains invalid UTF-8 or a character whose lowercase form has a different byte length, such as `İ`. This affects `offline.WithServiceWorker`, `offline.Middleware` and the generated `WithOffline` in v0.10.0.
- Stop `datapages init` from backing up unedited generated skills and `AGENTS.md`. Generated instruction files include a content hash so later runs can detect user edits. Unedited skills from v0.10.0 are replaced without a backup too.

### Security

- Prevent signed-in users from receiving private events addressed to another user when their streams share the event's signal value. This affects v0.10.0. It also affects v0.7.0 through v0.9.4 when the page handles a public signal-scoped event. Upgrade to v0.10.1 and run `datapages gen`.
- Prevent attacker-controlled values in an `SSE.Prefetch` URL from running JavaScript (XSS) in browsers that receive the prefetch. Applications are affected when they build the URL from untrusted data without percent-encoding it. This affects v0.10.0. Upgrade to v0.10.1 and redeploy. Until then, use the `href` builders or escape each value with `url.PathEscape` or `url.QueryEscape`.
- Prevent `natskv.SessionManager.SaveSession` from re-creating a session deleted by `CloseSession`, `CloseAllUserSessions` or `DeleteExpired`. A stale save could let a signed-out user or an attacker with a revoked cookie authenticate again. Applications using `SaveSession` are affected in v0.1.0 through v0.10.0. Upgrade to v0.10.1 and redeploy. There is no workaround.
- End a user's private-event streams at the session's `ExpiresAt`. In v0.10.0, a client that opened a stream before expiry, including one using a stolen cookie, could keep reading afterward until the session was deleted. Applications setting a non-zero `NewSession.ExpiresAt` are affected. Upgrade to v0.10.1, run `datapages gen` and redeploy. Until then, call `DeleteExpired` frequently; the inmem and natskv stores end streams when they delete sessions.

[Unreleased]: https://github.com/romshark/datapages/compare/v0.11.2...HEAD
[0.11.2]: https://github.com/romshark/datapages/compare/v0.11.1...v0.11.2
[0.11.1]: https://github.com/romshark/datapages/compare/v0.11.0...v0.11.1
[0.11.0]: https://github.com/romshark/datapages/compare/v0.10.1...v0.11.0
[0.10.1]: https://github.com/romshark/datapages/compare/v0.10.0...v0.10.1
