Skip to content

v0.40.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 22:55
· 63 commits to main since this release
8e044e6

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/fonts with 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 with size-adjust match 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>.css and /_/assets/client.<lang>.<hash>.js cache 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 with no-store rather than 404. The document decodes to 11 KB, down from 170 KB.
  • Static assets carry cache headers. public/_headers caches the version-stamped htmx-2.0.4.min.js and 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 one h1, settings labels use for=, 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.ts logs 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.
  • normalizeUrl keeps a trailing ? or # that is URL content. It stripped the last character whenever it was ? or #, whether or not that character was the delimiter, so https://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() and setLanguage() toasted success and reloaded whether or not the PUT succeeded, so a transient 5xx or an expired session looked like a saved preference. Both check res.ok like 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 as Prod $$ key no longer renders as Prod $ 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 waitUntil and nothing stubbed it; a miniflare outbound stub answers those fetches, which removes a CI timeout and a cross-test foreign-key error.
  • compatibility_date advances to 2026-08-22, the newest date the vitest pool's bundled workerd accepts. yarn types generates worker-configuration.d.ts from wrangler.jsonc in place of the hand-written Env. e2e/perf.spec.ts pins page-weight budgets, layout shift and DOM accessibility invariants inside yarn e2e, and yarn perf runs it under Fast 3G with 4x CPU throttling.
  • OpenAPI paths and schemas are unchanged from 0.39.0; only info.version changes. 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.