Repository navigation
v0.40.0
·
63 commits
to main
since this release
Performance, accessibility and defect release (PRs #59, #60 and #64, plus the post-merge review of #64). A cold admin load drops from 4.1 MB to 419 KB, a warm navigation moves 3 KB, and layout shift is zero. The release also closes a URL-normalization corruption on the redirect path, an SSRF pattern in the title fetcher, and unscannable QR codes.
- The icon font is 322 KB instead of 3.98 MB. The layout requested Material Symbols as a variable font with four axis ranges, 97% of the cold page weight, while the stylesheet only ever renders one weight and no fill. The static instance keeps every glyph, which matters because bundle icons are free text.
- Fonts are self-hosted with metric-matched fallbacks, and nothing shifts when they arrive. Manrope, Space Grotesk and Material Symbols live under
public/fontswith the upstream version in the file name, licenses beside them and immutable caching; the two render-blocking Google Fonts stylesheets and their preconnects are gone. Icon spans hold a fixed 1em box, and local fallback faces withsize-adjustmatch the web fonts' metrics, so CLS on the link detail page goes from 0.106 to 0. Preloads were measured and rejected: on Fast 3G they compete with the stylesheet and delay first paint. - The admin stylesheet and client script are content-hashed assets. Every admin document inlined 75 KB of CSS and 90 KB of JS and shipped no-cache.
/_/assets/admin.<hash>.cssand/_/assets/client.<lang>.<hash>.jscache as immutable and sit outside the admin auth, since they carry nothing account-specific. During a deploy window a document from one build can request its assets from an isolate on another; the route recognises the asset the path names and serves its own copy withno-storerather than 404. The document decodes to 11 KB, down from 170 KB. - Static assets carry cache headers.
public/_headerscaches the version-stampedhtmx-2.0.4.min.jsand the fonts as immutable, and brand assets for a day with stale-while-revalidate. - Screen readers get a usable admin DOM. Every icon ligature is
aria-hidden, each page has oneh1, settings labels usefor=, the quick URL box, the link detail editors, the back arrow and the inline save and cancel buttons carry names, and the links table declares column headers. - The settings page states whether MCP OAuth is configured. The status only reached the document by riding inside the inline client script's translation table. The MCP card now renders it.
- Unhandled exceptions log one JSON object and answer a body the client can parse. Hono's default printed unstructured text.
src/unhandled.tslogs method, path, error and stack as one Workers Logs entry. The route groups that answer JSON (/_/api,/_/admin/api,/_/admin/w, the MCP transport) declare it where they mount; everything else answers plain text unless the client accepts JSON. - JWKS verification no longer awaits another request's fetch. A module-level jose resolver shared its in-flight fetch between concurrent first verifications in a fresh isolate, which Workers reject with "Cannot perform I/O on behalf of a different request". The resolver is per request over a shared key cache, so certificates still download once per isolate.
normalizeUrlkeeps a trailing?or#that is URL content. It stripped the last character whenever it was?or#, whether or not that character was the delimiter, sohttps://a.com/?x=y?and an embedded redirect target ending in/?were corrupted before storage and on every 301.- The title fetcher only fetches public hosts.
autoLabelLink()fetched any user-supplied URL server-side and followed redirects blind.isPublicHttpUrl()rejects loopback, RFC 1918, link-local, cloud-metadata and every reserved range, including IPv4-mapped and NAT64 literals and trailing-dot hosts. Redirects are followed by hand with each hop vetted, and the whole chain shares one 5 s deadline instead of one per hop. - QR codes for versions 6 to 10 scan. Payloads over 106 bytes produced symbols no reader decoded: the encoder ran one Reed-Solomon pass where the spec requires split and interleaved blocks, never wrote the version-information blocks versions 7 and up require, and let alignment patterns overlap the finders. Verified with a real decoder. Payloads encode as UTF-8 and size the version by byte length, and the GF(256) tables are built once.
- Settings setters report failure.
setTheme()andsetLanguage()toasted success and reloaded whether or not the PUT succeeded, so a transient 5xx or an expired session looked like a saved preference. Both checkres.oklike their siblings. - The add-slug modal stays open when the slug is rejected, and the client-side
t()treats interpolated values as literal text like the server does, so a key title such asProd $$ keyno longer renders asProd $ key. SlugRepository.disable()throws on a non-custom slug instead of returning null and leaving the link without a primary. The unreachable New Link modal and the dead code behind it are removed.- The vitest suite runs off the network. Every created link scheduled a real title fetch in
waitUntiland nothing stubbed it; a miniflare outbound stub answers those fetches, which removes a CI timeout and a cross-test foreign-key error. compatibility_dateadvances to 2026-08-22, the newest date the vitest pool's bundled workerd accepts.yarn typesgeneratesworker-configuration.d.tsfromwrangler.jsoncin place of the hand-writtenEnv.e2e/perf.spec.tspins page-weight budgets, layout shift and DOM accessibility invariants insideyarn e2e, andyarn perfruns it under Fast 3G with 4x CPU throttling.- OpenAPI paths and schemas are unchanged from 0.39.0; only
info.versionchanges. The bump refreshes the recorded spec hash in all three SDKs. TypeScript ships 1.2.1, Python 1.2.1 and Dart 2.2.1 alongside it, each tightening the transport's response-shape checks. Full suite: 93 files, 1421 tests.