Changelog

2026-07-30 — Immutable asset caching

Frontend files are now served at version-addressed URLs (/__immutable/45/frontend/index.tsx) with Cache-Control: public, max-age=31536000, immutable, so repeat visits load the whole client graph from the browser cache — roughly 3× faster (~665ms → ~157ms measured). Publishing your val bumps its version, which changes every URL, so caches invalidate automatically. Old-version URLs return 404 after a publish (like Next.js build assets); a reload picks up the new version.

What changed in the template:

  • frontend/index.htmlfrontend/root.tsx: the HTML shell is now JSX and stamps asset URLs with immutableFileUrl()
  • index.ts: serves Root() at /, and one app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path)) route serves all frontend files

Upgrading an existing remix

Your copy of this template doesn't update automatically. Two options:

Minimal (most of the benefit): point both routes at serveImmutableFile — your existing bare URLs redirect into versioned space, the new route serves them, and the whole module graph gets cached (costs one redirect per page view):

import { serveImmutableFile } from "https://esm.town/v/std/utils/index.ts"; app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path)); app.get("/frontend/**/*", (c) => serveImmutableFile(c.req.path)); // was serveFile

Full upgrade (also removes the redirect): three self-contained steps — no other files needed. Adjust the <title>, favicon, and body of Root() to match your app's existing index.html, and stamp anything else your shell references directly (images, css) with immutableFileUrl().

  1. Create frontend/root.tsx:
/** @jsxImportSource npm:hono@4/jsx */ import { raw } from "npm:hono@4/html"; import { immutableFileUrl } from "https://esm.town/v/std/utils/index.ts"; // The HTML shell for the app. It is never cached, so immutableFileUrl can stamp // the current val version into each asset URL — the browser caches those // immutably, and publishing the val changes the URLs, refetching exactly once. export function Root() { return ( <> {raw("<!DOCTYPE html>")} <html lang="en"> <head> <meta charSet="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>React Hono Val Town Starter</title> <script src="https://cdn.twind.style" crossOrigin="" /> <link rel="icon" href={immutableFileUrl("/frontend/favicon.svg")} type="image/svg+xml" /> </head> <body className="font-sans text-gray-800 p-6"> <main> <div id="root" /> </main> <script src="https://esm.town/v/std/catch" /> <script src={immutableFileUrl("/frontend/index.tsx")} type="module" /> </body> </html> </> ); }
  1. In your Hono entrypoint, import Root and serveImmutableFile, and replace your / and frontend file routes with:
import { serveImmutableFile } from "https://esm.town/v/std/utils/index.ts"; import { Root } from "./frontend/root.tsx"; // Serve the (never-cached) HTML shell at the root / app.get("/", (c) => c.html(Root())); // Serve frontend files at the version-addressed URLs Root() stamps, cached immutably app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));
  1. Delete frontend/index.html (and any now-unused serveFile import).