Changelog
This file records the notable changes of each release in the format of Keep a Changelog. 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 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:
- Install the CLI with
go install github.com/romshark/datapages/cmd/datapages@v0.11.0. - Run
go get github.com/romshark/datapages@v0.11.0andgo mod tidy. - Install templ with
go install github.com/a-h/templ/cmd/templ@v0.3.1100. - Run
templ generate, thendatapages gen. - Redeploy.
Added
datapages lint and datapages gen
- Accept
QUERYXXXactions for theQUERYmethod of RFC 10008, which Datastar 1.0.4 sends with@query. Declare one on a page or onApp, as in// QUERYSearch is /search, and build its expression with theactionpackage, as inaction.PageIndex.Search.QUERY(). Datastar sends the signals in the request body. AQUERYXXXaction skips the CSRF check and must not change server state. - Accept static and request-scoped
StreamConfigmethods on pages with SSE streams. The static form returnsdatapages.StreamConfigduringNewServer. The request-scoped form returns(datapages.StreamConfig, error)per stream and may take the session, path and signals. Both overridedatapages.WithSSECompression. Invalid configurations failNewServeror the stream request. A page orAppconfigures the SSE response of an action the same way, with a method namedStreamConfigplus the action's method name, such asStreamConfigPOSTSave. - Serve files from generated handlers with
datapages.File. Any action may return one, and aGETXXXaction also gets a generatedhrefbuilder. AGETXXXaction skips the cross-origin and CSRF checks and must not change server state. Applications previously needed custom middleware or a separatenet/httphandler and had to maintain its route and URL by hand. - Add a 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 ofwatch.toxiproxy.presetsand saves them intodatapages.yaml. Both listen on ports the OS picks unlesswatch.toxiproxy.hostandwatch.toxiproxy.ui-hostset them.datapages lintreports an override of a route no app has.watch.toxiproxy.disabled: trueturns the proxy off. - Accept a
datapages.DocumentAttrsreturn value that sets the attributes of<html>, as indocAttrs datapages.DocumentAttrs. It maps attribute names to values, as indatapages.DocumentAttrs{"lang": "en", "data-theme": "dark"}. A page'sGETand an action that returns a body may return one. An invalid name ordata-noncefails the page with 500. Return the same attributes from every page through a shared function. An action that returns them without a body fails withaction returning datapages.DocumentAttrs must return body datapages.Component.
Generated code
Run datapages gen to apply these.
- Add
datapages.WithSSECompressionto configure SSE compression anddatapages.WithNoSSECompressionto disable it. The presetsdatapages.SSECompressionBalanced,datapages.SSECompressionLowMemory,datapages.SSECompressionLowBandwidthanddatapages.SSECompressionLargeFragmentscover common trade-offs. ACache-Control: no-transformset before a stream opens keeps it uncompressed.
Runtime and modules
- Reload open pages when a deployment changes the build. Set
datapages.WithBuildIDwhen replicas run different binaries of 1 release. AContent-Security-Policymust allow the inline build script. - Apply the attributes of the live
<html>element when a shim written withPageCacheWriter.SetShimhydrates. The classes are added to those the page has.
datapages init
- Write a
watch.toxiproxysection with the presets Fast 4G, Slow 4G and 3G and no toxics into a newdatapages.yaml. With the section, a busy proxy port failsdatapages watchinstead of turning the proxy off.datapages watchwrites 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;-ndefaults 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, anddatapages watchrebuilds changes. - Ask whether to commit the generated code,
datapagesgen/and*_templ.go, interactively or with--generated-code committed|hidden;-ndefaults to committed. Hidden lists it in.gitignore, hides it in VS Code with.vscode/settings.jsonand makesmake buildand CI generate it. CI no longer compares it with the commit. A clone of a hidden project builds only aftertempl generateanddatapages gen, anddatapages genfails there while the app uses thehref,actionorassetspackages. - Ask whether to write the GitHub Actions workflow
.github/workflows/ci.yml, interactively or with--ci github-actions|none;-ndefaults togithub-actions. - Ask whether to write the AI agent instructions and skills, interactively or with
--no-ai-skills;-nwrites them.
Changed
datapages lint and datapages gen
- Reject actions,
OnXXXhandlers,StreamOpen,StreamCloseandState[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 onlyGET. 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
AppnamedQUERYfollowed by an uppercase letter, such asQUERYStats, as an action. Such a method is rejected when it's a helper rather than an action: rename it. - Read an exported method on
AppnamedGETfollowed by an uppercase letter, such asGETStats, 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
AppnamedStreamConfigfollowed by the method name of anAppaction, such asStreamConfigPOSTSave, as the SSE configuration of that action. OnApp,StreamConfigalone and every other method whose name starts withStreamConfigare rejected. Rename such a helper. - Reject page and action routes below the static-files URL prefix.
net/httptreats 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 thedatapagespackage 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 lintonexample/classifiedstakes 277ms instead of 406ms, anddatapages watchregenerates 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/loadunder the user cache directory, such as~/.cache/datapages/loadon Linux. A run with the same imports,go.mod,go.sum, environment,gobinary anddatapagesbinary as the previous one loads the app package from the cache without running thegocommand.DATAPAGES_LOADCACHE=offdisables the cache, and an absolute path moves it. Nothing removes outdated entries. Delete the directory to free their space. - Reject an
OnXXXhandler that is not named after its event, asOnNewMessage(event EventMessageSent, sse datapages.SSE). A page'sOnBtakingEventAhid an embeddedOnBtakingEventB, and the generated code did not compile. Rename the handler toOnand the event name afterEvent, asOnMessageSent. - Require the app package directory in
datapages watch --app, as--app app/frontendinstead of--app frontend. Withweb/appandadmin/app,--app appwas ambiguous and ran one of them. - Reject a query tag with a character other than ASCII letters, digits,
-,.,_and~, asquery:"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, asquery:"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
PageError404with status 404 for a page load whose handler returnsdatapages.ErrNotFound, as for a URL that no route matches. For such a page load,PageError404.GETreceives the request's query and signals and a zero path. An app withoutPageError500previously 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 onAppthat takesdatapages.PageCacheWriter, as on a page. Such an action that redirects answered a plain form with an HTTP redirect and dropped the queued writes, such asClearAll. Submit those forms through a Datastar action. - Replace 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/httpcompressionandgithub.com/valyala/bytebufferpoolleave the application module aftergo mod tidyunless 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/morpheusv0.3.1, which the settings page ofdatapages watchuses.go getof this release moves an application on Morpheus v0.1.0, v0.2.0 or v0.3.0 to v0.3.1. v0.2.0 replacesneo.Conditionwithneo.IfPlatform. v0.3.0 removes--neo-sidebar-overlay-breakpointand firesneo-toast-closeon every close of a toast. v0.3.1 requires templ v0.3.1100, as its changelog lists. Load the v0.3.1 stylesheets and bundle to match. - Require Go 1.27.3 instead of 1.27.1.
go getraises thegoline of the application'sgo.modto 1.27.3. WhereGOTOOLCHAIN=localis set, install Go 1.27.3 or later first. - Require templ v0.3.1100.
*_templ.gofiles written by an earlier templ fail to compile. Install templ v0.3.1100 and runtempl generate. - Label requests with the
QUERYmethod asQUERYin themethodlabel of the HTTP metrics. They were counted as<other>. - Refuse a URL passed to
datapages.WithDatastarJSthat is invalid according to RFC 3986, such as one with|or a non-ASCII character.datapages.NewServerreturns the error. Percent-encode the character it reports, as%7Cfor|. - Answer 404 to an asset path spelled differently from its file name, such as
/static/App.jsforapp.js, on every system and in dev mode. Windows and macOS served such paths, unlike Linux and anembed.FS, anddatapages.WithAssetsCachekept anETagper spelling until exit. Spell asset links exactly like their files. - Require a custom
messaging.Brokerto 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.Brokerto 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.TokenWriterto write a different token on everyWriteTokencall, such as by masking it with fresh random bytes ascsrf.Tokensdoes. 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
inmembroker in time linear in the open streams of a page that handles an event with adatapages.Subjectfield without asignaltag. 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 /ofexample/counterdrops 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.Middlewareuse sendfile when it is an*os.File, as a file of anhttp.Diror adatapages.Fileopened withos.Openis. Embedded files cannot. - Serve the embedded files of
datapages.WithAssets, and the files of adatapages.WithAssetsFSfile system other thanhttp.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.Fileand dev-mode responses remain uncompressed. AHEADresponse gets the headers of itsGETresponse.datapages.WithResponseCompressionconfigures compression.datapages.WithNoResponseCompressiondisables it for loopback-only clients or body-filtering proxies. Compressing proxies pass compressed bodies through. Send pages that combine secrets with attacker-controlled input withCache-Control: private, no-transformto 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 weakETagvalues. Multi-range requests and dev mode use the original asset. Configure codings, levels, minimum size and storage withassetcompress.Newanddatapages.WithAssetsCompression.Precompresscreates copies eagerly.modules/assetcompress/diskpersists them across restarts and removes copies unused for 7 days by default.datapages.WithNoAssetsCompressiondisables compression, and anAssetsCacheConfig.CacheControlwithno-transformkeeps assets uncompressed. Keep assets immutable until server exit. A changed file ofdatapages.WithAssetsFSkeeps its oldETag, and its compressed responses can keep its old content, until a restart. Serve such files through a GET action returningdatapages.File, or adddatapages.WithNoAssetsCompressionand setAssetsCacheConfig.DisableETag. - Let the offline worker reuse a cached same-origin asset without a request until its
max-agepasses. It asked the server on every use, also forimmutableassets. - Send
Cache-Control: public, max-age=0with static assets whendatapages.WithAssetsCacheis absent or disabled. A browser could reuse adatapages.WithAssetsFSfile after a deploy without asking, because the file had a modification time and noCache-Control. A CDN now revalidates such assets too, unlessWithAssetsCachesets aMaxAge. - Send an
ETagwith static assets whendatapages.WithAssetsCacheis absent or disabled. Browsers downloaded embedded assets in full on every page load. SetAssetsCacheConfig.DisableETagfordatapages.WithAssetsFSfiles that change. - Keep the app running in
datapages watchwhendatapages.yamlchanges, as on a save of the Toxiproxy settings page. A change rerandatapages genand 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 ofwatch.custom-watchersstill 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 initin 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 thego install github.com/a-h/templ/cmd/templ@step to match the templ version in itsgo.mod. - Run
templ generateafterdatapages genin a new project'smake gen. It left*_templ.gostale after a.templchange. In a project scaffolded earlier, addgo run github.com/a-h/templ/cmd/templ@v0.3.1100 generateafterdatapages genin thegentarget ofMakefile. - Ask whether to create a subdirectory for the project, or take its name from
--name;-nuses 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;-ncreates one. Ask for the module path directly; an empty answer takes the suggested one.
Removed
datapages lint and datapages gen
- Disallow
GETand actions on stream embeds, formerly abstract page types. They may still shareOnXXX,StreamOpenandStreamClose. MoveGETto each page, using a shared helper for common code. Move actions to one page or to*Appwhen 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 toApp. - Reject a file of the app package with a
//go:buildline or a name such asprobe_windows.go, except a test file and a file under//go:build ignore.datapages genleft 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
./appin a module without adatapages.NewServercall. The package of the parent directory was read as the app instead, anddatapages genwrote stubs into the missing directory. - Refuse a
watch.app-hostwithout anhttporhttpsscheme and a host, such aslocalhost:8080, indatapages watch. It was read as a URL without a host, which the dev server could not send requests to. Write it ashttp://localhost:8080. - Apply
watch.log.levelindatapages watch.erronly, the default, logged info likeverbose. It now logs errors only. - Reject an
OnXXXhandler that takes a seconddatapages.SSE, session ordatapages.StreamIDparameter, as every other handler does. A field with 2 names, such assse, 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
OnXXXhandler that takes its event by pointer, as inevent *EventPing. The generated code for it did not compile. Take the event by value. - Reject a route containing
"or\. For such a route,datapages genfailed 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%22and%5C, whichnet/httpdecodes 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 genfailed with a Go syntax error. Write it as%60, whichnet/httpdecodes before matching the route. - Reject an unexported defined type as the type argument of
datapages.Path,datapages.Queryordatapages.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, andnet/httpanswered 400 to the%: none of them reached the page. Write the characters as%3F,%23and%25, whichnet/httpdecodes before matching the route. - Reject an event with more than 1
datapages.SubjectUserfield, such asTo, 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 1SubjectUserfield 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 1datapages.SubjectUserfield and dispatch once per user. - Reject a page other than
PageIndexat/, as in// PageHome is /. The router sent every request for/to that page, andPageIndexnever rendered. Give the page another route, or move its handlers toPageIndex. - 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.Sessionwith 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
jsontag, in the same order on every run: by event name. - Accept a handler parameter typed with an alias of
datapages.Path,datapages.Query,datapages.Signalsordatapages.State, as intype FormSignals = datapages.Signals[struct{...}]. The check read theValuesfield of the wrapper instead of its type argument and reported errors located indatapages.go, and refused an alias ofdatapages.Stateas 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 onPOST /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:embedpattern on the assets variable that is a glob or names a single file, as in//go:embed static/*.cssor//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 staticor//go:embed static/*. - Stop reporting a valid
href={ LoginURL }as relative on some runs when a function also declares a localLoginURLconstant. - Report each mismatch between the route and the
datapages.Pathstruct of an action onAppas 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.NewServercall with its fifth type argument written out, as indatapages.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 appingo-app/, when the file callingdatapages.NewServerimports it without an alias. The app type was refused with "must live in its own package". - Read the
datapages.NewServercall in a file whose//go:buildline negates a tag, as//go:build !windows.datapages genskipped the file and generated code as if the module had no call. - Report a problem in a
datapages.NewServercall with a path relative to the module root, as every other error is. A wrong number of type arguments, a wrongMetricstype argument and a file that does not parse were reported with an absolute path. - Check the attributes inside a conditional attribute of a
.templfile, as in<a if ok { href="/login" }>, in both branches. A hardcodedhref, a formaction, a hardcoded Datastar action URL or an action of another page went unreported there. Change each reported attribute as itsfix:line says, such ashref="/login"tohref={ href.PageLogin() }. - Include the HTTP method in the action names that route errors show, as in
PageUser.DELETEFollow. Before, they showedPageUser.Follow, which could also meanPageUser.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
.templfiles of the packages that the app package imports from its module, such asapp/template. Before, only the app package's own templates were checked. A wronghref, a formaction, a hardcoded action URL or another page's action in those templates went unreported. Change each reported attribute as itsfix:line says, such as a hardcodedhrefto a function of the generatedhrefpackage. - Reject an action that takes
datapages.SSEand 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 withsse.PatchElementinstead. - 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 taggedsignal:"chat.room".$chat.roomexists only when$chatis 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 asSubjectUser []string. The fix named the nonexistentdatapages.Subjectsanddatapages.SubjectUserstypes. - Spell
datapages.SubjectUsercorrectly in the fix for asignaltag on that field. It was written asdatapages.SubjectUser(s). - State the accepted syntax in the fix for an invalid
signaltag: 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/followinstead 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/followinstead of/user/follow. - Report a subject that 2 applications of a module claim in
datapages lintwhen 1 of them has other errors, mirroringdatapages genbehavior. - Reject a
reflectsignalquery field that can't be written as text, also when theGETtakes noSignals. Its value grew in the URL on every reload. Give the field a string, bool, integer or float type or a type that implementsencoding.TextMarshaler, or remove itsreflectsignaltag. - Accept an event or signals field of a struct type from another module, such as
url.URLorsql.NullString. Its fields were held to thejsontag rules, with errors pointing into the standard library. - Keep the generated code when
datapages genrejects theSessionDatatype argument ofdatapages.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 gencould leave a package that did not compile. - Rerun
datapages genindatapages watchwhen the package callingdatapages.NewServerchanges. A newMetricstype argument took effect only after the next change to the app package. - Accept a
reflectsignalquery field whose signal is a pointer, such as anintquery field reflected into a*intsignal. It was refused with "incompatible JSON kinds", althoughencoding/jsondecodes the value into it. - Accept a string query field reflected into a signal that implements
encoding.TextUnmarshaler, such as anint64type with anUnmarshalTextmethod. It was refused with "incompatible JSON kinds", althoughencoding/jsonpasses the string toUnmarshalText. - Reject a number or bool query field reflected into a signal that implements
encoding.TextUnmarshalerand notjson.Unmarshaler.encoding/jsondecodes only strings into such a type, and an action that took the signal answered 400. Reflect a string field or a field that implementsencoding.TextMarshaler. - Show the update notice in
datapages watchwhen the running version is a prerelease of the latest release, such as0.11.0-rc.1ofv0.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.
PageOfflineand page-cache entries omit the script because visitors share them. - Generate code that compiles when
App.Headtakes the session before the*http.Requestin an app that usesdatapages.PageCacheWriter. - Generate code that compiles when the type argument of
datapages.Path,datapages.Queryordatapages.Signalsis an alias of a defined type, as intype Filter = SearchQuery. - Generate code that compiles when the type argument of
datapages.Path,datapages.Queryordatapages.Signalsis a struct type literal with an embedded field, such astime.Time. - Generate code that compiles when the session data type or a field type of
datapages.Path,datapages.Queryordatapages.Signalsis an alias declared in a package with the same name as one the generated code imports, such asstream. - Generate code that compiles when
PageError404.GETtakes 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,signalsor after another variable the generated handlers declare. - Answer a page load whose handler returns
datapages.ErrBadRequest,datapages.ErrForbidden,datapages.ErrNotFoundordatapages.ErrConflictwith that status in an app that definesPageError500, as every app created bydatapages initdoes. Such a page load gotPageError500with status 500, and no page could answer 404.PageError500now renders for status 500 only. Other statuses get their status text, except 404 in an app that definesPageError404. - Keep the SSE streams of a page with both public and user-addressed events open while the tab is hidden when its
GETreturnsenableBackgroundStreamingset totrue, 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 areflectsignalquery 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
OnXXXhandler 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, skippedStreamCloseand never reachedRecoverError, which leaked per-tab state thatStreamOpenregistered for every such guest. - Send
Cache-Control: privatewith a page rendered for a signed-in visitor, the 404 and error pages included, unless the response already has aCache-Controlheader. 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.PageCacheWriterand returnsnewSessionorcloseSessionbut 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.PageCacheWriterand 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.RawMessagecontent after a standalone carriage return inSSE.PatchSignalsandSSE.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.WithSSECompressionconfigures the order, levels and windows. Compressed responses includeVary: Accept-Encodingand 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 enlargeZstdWindowto send fewer bytes. - Skip
RecoverErrorafter an HTML response body starts. Its SSE events previously corrupted a partial HTML response. - Recover and log a panic in
RecoverErroronce. It previously invokedRecoverErroragain and could write 2 compressed streams in 1 response. - Send
Vary: X-Datapages-Offline-Versionbefore opening an SSE response from an action that takesdatapages.PageCacheWriter. - Log errors from bodies passed to
PageCacheWriter.SetorSetShimwhen rendering an action's event stream or redirect script. - Log the stack of a panic in
StreamOpen, as for a panic inGET, an action orOnXXX. 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.NewServerreturns for an unexpectedWithSessionManager, and in the example ofInit. Both saiddpapp, 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.StreamOpenandStreamCloserun on it with a guest session. - Answer a request whose session, path, query or signals read panics, as in an
UnmarshalTextorUnmarshalJSONmethod of the app or in asessions.Manager, the way a panic in its handler is answered, withPageError500,RecoverErroror status 500. The server closed the connection without a response, also on the SSE stream of a page whoseStreamOpentakes signals. - Allocate a pointer path or query field, such as
*big.Intor*time.Time, before itsUnmarshalTextruns. 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
RecoverErrorindatapages_internal_errors_not_recovered_total, in an app generated withdatapages.EnablePrometheus. A panic after the response started counts too, and so do sentinels, such asdatapages.ErrNotFoundfrom a page'sGET. The counter rose only whenRecoverErrorreturned an error or panicked. - Close the
data-initattribute 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 withfetchhas. The scripts also changed the bytes behind the asset'sETag. - Percent-encode the characters a URL path cannot carry, such as
#,?and%, in the file names thatassets.Pathandhref.Assetturn 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.WithMiddlewarepasses on a new request, asr.WithContextreturns 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 todatapages.WithDatastarJS, as inhttps://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
natscorebroker in the order they were dispatched, asinmemdoes. 2 events of different types dispatched one after the other often arrived in reverse order. - End the SSE streams subscribed through the
natscorebroker when its NATS connection closes, which nats.go does after the last failed reconnect attempt: 60 attempts 2s apart with the defaults ofnats.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.UserSessionsandCloseAllUserSessionsthe token the session's cookie carries, asinmemdoes. 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 withSession.Token()to find the current session never found it. A session created before the upgrade, or under one of thePreviousEncryptionKeys, keeps a cookie that differs from its listed token until the user signs in again. - Return from
natskv.SessionManager.UserSessionsan iterator that yields the sessions on every range, asinmemdoes. A second range yielded nothing, and an iterator never ranged kept a KV watcher subscribed until the context passed toUserSessionsended. - Return an error from
natskv.SessionManager.UserSessionsandDeleteExpiredwhen their context ends, NATS stalls for 5s or the connection closes during the call.UserSessionsreturned the sessions read by then as the complete list, andDeleteExpiredreturned nil with expired sessions left in the bucket. - Report a token that can't be decrypted as
sessions.ErrSessionNotFoundinnatskv.SessionManager.Session, asinmemdoes. This includes forged tokens and cookies encrypted with a key that is no longer inPreviousEncryptionKeys. 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 ofdatapages.WithPrometheuswrote it as plain text. A server passed todatapages.WithHTTPServerkeeps its ownErrorLogif it sets one. - Register the offline service worker for the whole origin when
offline.Config.ScriptURLis 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 theirdatapages.PageCacheWriterwrites never reached the worker. - Refuse an
offline.Config.ScriptURLthat is not a plain path, such assw.jsor/sw.js?v=2. The worker never loaded from those.offline.WithServiceWorkerandWithOfflinereturn an error, andoffline.Middlewarepanics. 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 thenatscorebroker waits for a round trip to NATS before it opens, which makes this more likely. - Keep a response marked
Cache-Control: no-storeout of the offline worker's cache, and fetch a stored response markedno-cachefrom the network first, using the stored copy only offline. The worker stored every same-origin response that an element orfetchloaded, such as an image, whatever itsCache-Control, and served the stored copy first. Increaseoffline.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.WorkerVersionchanged. Increaseoffline.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
videoelement 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.WithFileSendTimeoutchanges the time, which also applies todatapages.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.Middlewarefrom logginghttp: superfluous response.WriteHeader callfor each SSE response. - Stop
offline.Middlewarefrom 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 itsContent-Rangesaid, and the saved file was corrupt. - Stop rounding an
offline.Config.WorkerVersionabove 2^53 in the browser. A version rounded down added the registration script to every page. - Treat
/ticketsand/tickets/as the same page in the offline cache, as the server does. A visit to/ticketsmissed 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
suppresseda record thatdatapages.WithLogSamplingholds 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
WithOfflineoroffline.Middleware. The visitor got the half page and a second document after it. - Refuse in
datapages.NewServeraServerConfigvalue that a customServerOptionsets and the field'sWithoption refuses. A session cookie name with a space, for example, passed and kept every visitor signed out. - Apply the defaults of
datapages.WithPrometheusto adatapages.PrometheusConfigthat a customServerOptionwrites intoServerConfig.Prometheus. Without aRegisterer,NewServerpanicked, without aGatherer, every scrape of/metricspanicked, 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.otfand.otcfonts as OpenDocument templates, which also kept them uncompressed. - Send
action.WithRetryMaxWaitMsasretryMaxWait, which Datastar 1.0.0 and later read. From v0.9.2 to v0.10.1 it sentretryMaxWaitMs, which Datastar ignores, so the wait between retries stayed capped at the 30s default.
datapages init
- Pass the app's
embed.FSassets variable todatapages.WithAssetswhen creating a missingcmd/server/main.go. Generated asset URLs previously returned 404. An existingmain.gois not rewritten; adddatapages.WithAssets(app.StaticFS, false)manually. - Shut down gracefully on SIGTERM in the
cmd/server/main.goit writes. Docker, Kubernetes and systemd stop a process with SIGTERM, which previously ended the server without waiting for requests, SSE streams andStreamClosehooks. It does not rewrite an existingmain.go: addsyscall.SIGTERMto itssignal.NotifyContextcall. - Check out the repository before installing Go in
.github/workflows/ci.yml, which letsactions/setup-gorestore its cache. In a project scaffolded earlier, move theCheckout repositorystep aboveInstall Go. - Use the enclosing Go module when run in a subdirectory, as
datapages gendoes, instead of creating a nested module there. - Run in place in a project that has a
go.modand adatapages.yamlbut no git repository.datapages genrecommendsdatapages init -nto 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-templatesskill. It told agents that wrapping text in an element keeps templ from parsing a leadingif,fororswitchas 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 aboutelseandcase, which templ reads as text. Rundatapages initin an existing project to update it.
Security
Generated code
Run datapages gen to apply these.
- Prevent carriage returns in
SSE.PatchElement,SSE.PatchElementAtandSSE.ExecuteScriptcontent 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\rfrom untrusted text before rendering it. - Render
PageOfflinewith a zero session, as the specification states. AGETofPageOfflineor anApp.Headthat 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 passWithOfflineand declarePageOfflineare affected in v0.10.0 and v0.10.1 when itsGETorApp.Headtakes the session. After the upgrade, increaseoffline.Config.WorkerVersion, which makes browsers fetch the page again. Until then, take no session in theGETofPageOffline, and haveApp.Headrender nothing that depends on the session for the route ofPageOffline.
Runtime and modules
- Prevent
natskv.SessionManager.CloseAllUserSessionsfrom 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 callingCloseAllUserSessionsare affected in v0.1.0 through v0.10.1. Until the upgrade, pass itcontext.WithoutCancel(r.Context()), which covers the client that disconnects. - Stop sending a signed-in user's CSRF token to other origins. A
@post,@put,@patchor@deleteaction to another origin sent it the token if its CORS policy allowed theX-CSRF-Tokenheader. This affects v0.1.0 through v0.10.1. Until the upgrade, call other origins without a Datastar action.
datapages init
- Write
.envwith mode 600 instead of 644. Any account on the machine could read itsSESSION_ENCRYPTION_KEYand, with read access to theSESSIONSbucket, build a session cookie for any user. This affects projects created by v0.1.0 through v0.10.1.initleaves an existing.envalone. Runchmod 600 .envthere, 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
ExpiresAtunderreason="expired"indatapages_sse_disconnects_total.
Changed
- Preserve an existing
AGENTS.mdwithout a datapages stamp when runningdatapages init. To replace the unstampedAGENTS.mdgenerated by v0.10.0, delete it before runningdatapages init.
Fixed
- Prevent
offline.Middlewarefrom 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 affectsoffline.WithServiceWorker,offline.Middlewareand the generatedWithOfflinein v0.10.0. - Stop
datapages initfrom backing up unedited generated skills andAGENTS.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.PrefetchURL 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 thehrefbuilders or escape each value withurl.PathEscapeorurl.QueryEscape. - Prevent
natskv.SessionManager.SaveSessionfrom re-creating a session deleted byCloseSession,CloseAllUserSessionsorDeleteExpired. A stale save could let a signed-out user or an attacker with a revoked cookie authenticate again. Applications usingSaveSessionare 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-zeroNewSession.ExpiresAtare affected. Upgrade to v0.10.1, rundatapages genand redeploy. Until then, callDeleteExpiredfrequently; the inmem and natskv stores end streams when they delete sessions.