From 90cfd06fa67b6bc258a35891e76e614b894b3de0 Mon Sep 17 00:00:00 2001 From: juprodh Date: Mon, 11 May 2026 14:53:02 +0800 Subject: [PATCH] Add cache-burster to CSS URL --- src/lib/assets.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++ src/views/layout.ts | 9 +++++---- 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 src/lib/assets.ts diff --git a/src/lib/assets.ts b/src/lib/assets.ts new file mode 100644 index 0000000..05344d8 --- /dev/null +++ b/src/lib/assets.ts @@ -0,0 +1,48 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Content-addressed URLs for our shipped static assets, so a deploy that + * changes a file invalidates CDN + browser caches automatically (no manual + * purge). External URLs (CDNs) and URLs with an existing query string pass + * through unchanged. + * + * Hashes are computed lazily on first request and cached for the lifetime of + * the process — server restart picks up new content. + */ + +const PUBLIC_DIR = join(import.meta.dir, "..", "..", "public"); +const hashCache = new Map(); + +function computeHash(localPath: string): string | null { + try { + const buf = readFileSync(join(PUBLIC_DIR, localPath)); + return createHash("sha256").update(buf).digest("hex").slice(0, 10); + } catch { + // Missing asset (e.g. in tests before build) — silently skip versioning. + return null; + } +} + +/** + * Returns a versioned URL for an asset path. Pass paths that begin with + * `/public/` or are external URLs; both are handled. + * + * Examples: + * assetUrl("/public/dist.css") → "/public/dist.css?v=abcd1234ef" + * assetUrl("https://cdn.example/foo.js") → "https://cdn.example/foo.js" (unchanged) + */ +export function assetUrl(path: string): string { + if (/^https?:\/\//.test(path)) return path; + if (path.includes("?")) return path; + if (!path.startsWith("/public/")) return path; + + const localPath = path.slice("/public/".length); + let hash = hashCache.get(localPath); + if (hash === undefined) { + hash = computeHash(localPath) ?? ""; + hashCache.set(localPath, hash); + } + return hash ? `${path}?v=${hash}` : path; +} diff --git a/src/views/layout.ts b/src/views/layout.ts index 776aea2..ce0de62 100644 --- a/src/views/layout.ts +++ b/src/views/layout.ts @@ -1,4 +1,5 @@ import { type AccessLevel, canEdit, canManage } from "../lib/access.ts"; +import { assetUrl } from "../lib/assets.ts"; import { escapeHtml } from "../lib/html.ts"; import { type Locale, SUPPORTED_LOCALES, t } from "../lib/i18n/index.ts"; import { @@ -198,11 +199,11 @@ export function layout( options?: LayoutOptions, ): string { const extraScripts = (options?.scripts ?? []) - .map((src) => ` `) + .map((src) => ` `) .join("\n"); const extraStyles = (options?.stylesheets ?? []) - .map((href) => ` `) + .map((href) => ` `) .join("\n"); const locale = options?.locale ?? "en"; @@ -278,10 +279,10 @@ export function layout( ${options?.noindex ? `` : ""} ${renderOpenGraph(options, title)} - + ${extraStyles} - + ${extraScripts} -- 2.51.2