From feceaffd32ea5ac26d32e7562b0c7959b6318119 Mon Sep 17 00:00:00 2001 From: whereareiam Date: Sat, 3 Oct 2026 12:31:30 +0200 Subject: [PATCH] Templates: Version asset links so updates bypass the browser cache Forgejo serves custom assets with a six-hour max-age and the templates linked them by a fixed path, so after a theme update browsers kept running the previous build's scripts against the new markup. The source tree then failed with "Could not load the file tree" because the cached script still requested the removed data-api-url. The assemble step now appends each linked stylesheet and script's content hash as a version query, and fails when a template links an asset the build does not ship. That check surfaced three stale stylesheet links in the navbar, which duplicated component_assets and are removed. Co-Authored-By: Claude Opus 5.5 --- scripts/assembleForgejo.ts | 24 +++++++++++++++++++++++- src/build-output.test.ts | 20 ++++++++++++++++++++ templates/base/head_navbar.tmpl | 3 --- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/scripts/assembleForgejo.ts b/scripts/assembleForgejo.ts index e0306d23..415fc158 100644 --- a/scripts/assembleForgejo.ts +++ b/scripts/assembleForgejo.ts @@ -1,4 +1,5 @@ -import { cpSync, mkdirSync, readdirSync, rmSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const root = process.cwd(); @@ -55,3 +56,24 @@ cpSync(join(root, "dist", "assets", "fonts"), join(output, "public", "assets", " for (const file of readdirSync(join(root, "dist"))) { if (file.endsWith(".css")) cpSync(join(root, "dist", file), join(css, file)); } + +// Forgejo serves custom assets with a long max-age and the templates link them by a fixed path, so a browser +// keeps running the previous build's scripts against the new markup. Stamp each link with its content hash. +const assets = join(output, "public", "assets"); +const stampAssetLinks = (directory: string): void => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) stampAssetLinks(path); + else if (entry.name.endsWith(".tmpl")) { + const source = readFileSync(path, "utf-8"); + const stamped = source.replace(/\{\{AssetUrlPrefix\}\}\/((?:css|js)\/[^"'?\s]+\.(?:css|js))/g, (link, asset) => { + const hash = createHash("sha256") + .update(readFileSync(join(assets, asset))) + .digest("hex"); + return `${link}?v=${hash.slice(0, 10)}`; + }); + if (stamped !== source) writeFileSync(path, stamped); + } + } +}; +stampAssetLinks(templates); diff --git a/src/build-output.test.ts b/src/build-output.test.ts index adcb2a08..7b180b86 100644 --- a/src/build-output.test.ts +++ b/src/build-output.test.ts @@ -148,6 +148,26 @@ it("ships component stylesheets unchanged with valid CSS and a template referenc } }); +it("stamps every template asset link with the hash of the shipped file", async () => { + const { createHash } = await import("node:crypto"); + const output = path.join(DIST_DIR, "forgejo"); + const links = fs + .readdirSync(path.join(output, "templates"), { recursive: true }) + .filter(file => String(file).endsWith(".tmpl")) + .flatMap(file => [ + ...fs + .readFileSync(path.join(output, "templates", String(file)), "utf-8") + .matchAll(/\{\{AssetUrlPrefix\}\}\/((?:css|js)\/[^"?]+)(\?v=[0-9a-f]+)?"/g), + ]); + expect(links.length).toBeGreaterThan(0); + for (const [, asset, version] of links) { + const file = path.join(output, "public", "assets", asset); + expect(fs.existsSync(file), `template links a missing asset: ${asset}`).toBe(true); + const hash = createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + expect(version, `unversioned asset link: ${asset}`).toBe(`?v=${hash.slice(0, 10)}`); + } +}); + describe("非 auto 主题 CSS 内容验证", () => { for (const fileName of SOLID_THEME_FILES) { describe(fileName, () => { diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index fd978fa0..d5b44583 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -2,9 +2,6 @@ {{$isUserProfile := and (not .Repository) .ContextUser .ContextUser.IsIndividual (or .PageIsUserProfile .PageIsViewProjects .IsPackagesPage .IsCodePage)}} {{$isOrganization := and (not .Repository) .Org}} {{if or $isUserProfile $isOrganization}} - - -{{if $isOrganization}}{{end}}
{{end}}