Bladeren bron

feat(ui): add cache busting for static icon assets

Embed content checksums in icon URLs (e.g. /icon/<checksum>/sprite.svg)
so browsers fetch updated assets on upgrade instead of serving stale
cached versions. This matches the existing pattern used for JS and CSS
bundles.

Closes #3728
Frédéric Guillot 3 weken geleden
bovenliggende
commit
d33544e26a

+ 11 - 2
internal/template/functions.go

@@ -21,6 +21,7 @@ import (
 	"miniflux.app/v2/internal/mediaproxy"
 	"miniflux.app/v2/internal/model"
 	"miniflux.app/v2/internal/timezone"
+	"miniflux.app/v2/internal/ui/static"
 	"miniflux.app/v2/internal/urllib"
 )
 
@@ -88,10 +89,11 @@ func (f *funcMap) Map() template.FuncMap {
 			return ts.Format("2006-01-02 15:04:05")
 		},
 		"theme_color": model.ThemeColor,
+		"iconPath":    f.iconPath,
 		"icon": func(iconName string) template.HTML {
 			return template.HTML(fmt.Sprintf(
-				`<svg class="icon" aria-hidden="true"><use href="%s/icon/sprite.svg#icon-%s"/></svg>`,
-				f.basePath,
+				`<svg class="icon" aria-hidden="true"><use href="%s#icon-%s"/></svg>`,
+				f.iconPath("sprite.svg"),
 				iconName,
 			))
 		},
@@ -159,6 +161,13 @@ func (f *funcMap) Map() template.FuncMap {
 	}
 }
 
+func (f *funcMap) iconPath(filename string) string {
+	if bundle, ok := static.BinaryBundles[filename]; ok {
+		return fmt.Sprintf("%s/icon/%s/%s", f.basePath, bundle.Checksum, filename)
+	}
+	return fmt.Sprintf("%s/icon/_/%s", f.basePath, filename)
+}
+
 func csp(user *model.User, nonce string) string {
 	policies := map[string]string{
 		"default-src":               "'none'",

+ 8 - 8
internal/template/templates/common/layout.html

@@ -16,14 +16,14 @@
 
     <link rel="manifest" href="{{ routePath "/manifest.json" }}" crossorigin="use-credentials">
 
-    <link rel="icon" type="image/png" sizes="16x16" href="{{ routePath "/icon/%s" "icon-16.png" }}">
-    <link rel="icon" type="image/png" sizes="32x32" href="{{ routePath "/icon/%s" "icon-32.png" }}">
-    <link rel="icon" type="image/png" sizes="128x128" href="{{ routePath "/icon/%s" "icon-128.png" }}">
-    <link rel="icon" type="image/png" sizes="192x192" href="{{ routePath "/icon/%s" "icon-192.png" }}">
-    <link rel="apple-touch-icon" sizes="120x120" href="{{ routePath "/icon/%s" "icon-120.png" }}">
-    <link rel="apple-touch-icon" sizes="152x152" href="{{ routePath "/icon/%s" "icon-152.png" }}">
-    <link rel="apple-touch-icon" sizes="167x167" href="{{ routePath "/icon/%s" "icon-167.png" }}">
-    <link rel="apple-touch-icon" sizes="180x180" href="{{ routePath "/icon/%s" "icon-180.png" }}">
+    <link rel="icon" type="image/png" sizes="16x16" href="{{ iconPath "icon-16.png" }}">
+    <link rel="icon" type="image/png" sizes="32x32" href="{{ iconPath "icon-32.png" }}">
+    <link rel="icon" type="image/png" sizes="128x128" href="{{ iconPath "icon-128.png" }}">
+    <link rel="icon" type="image/png" sizes="192x192" href="{{ iconPath "icon-192.png" }}">
+    <link rel="apple-touch-icon" sizes="120x120" href="{{ iconPath "icon-120.png" }}">
+    <link rel="apple-touch-icon" sizes="152x152" href="{{ iconPath "icon-152.png" }}">
+    <link rel="apple-touch-icon" sizes="167x167" href="{{ iconPath "icon-167.png" }}">
+    <link rel="apple-touch-icon" sizes="180x180" href="{{ iconPath "icon-180.png" }}">
 
     {{ $cspNonce := nonce }}
     {{ csp .user $cspNonce | safeHTML }}

+ 5 - 0
internal/ui/handler.go

@@ -8,6 +8,7 @@ import (
 
 	"miniflux.app/v2/internal/storage"
 	"miniflux.app/v2/internal/template"
+	"miniflux.app/v2/internal/ui/static"
 	"miniflux.app/v2/internal/worker"
 )
 
@@ -24,3 +25,7 @@ func (h *handler) routePath(format string, args ...any) string {
 	}
 	return h.basePath + format
 }
+
+func (h *handler) iconPath(filename string) string {
+	return h.basePath + fmt.Sprintf("/icon/%s/%s", static.BinaryBundles[filename].Checksum, filename)
+}

+ 14 - 14
internal/ui/static_manifest.go

@@ -85,12 +85,12 @@ func (h *handler) showWebManifest(w http.ResponseWriter, r *http.Request) {
 		StartURL:        h.routePath("/"),
 		BackgroundColor: themeColor,
 		Icons: []webManifestIcon{
-			{Source: h.routePath("/icon/%s", "icon-120.png"), Sizes: "120x120", Type: "image/png", Purpose: "any"},
-			{Source: h.routePath("/icon/%s", "icon-192.png"), Sizes: "192x192", Type: "image/png", Purpose: "any"},
-			{Source: h.routePath("/icon/%s", "icon-512.png"), Sizes: "512x512", Type: "image/png", Purpose: "any"},
-			{Source: h.routePath("/icon/%s", "maskable-icon-120.png"), Sizes: "120x120", Type: "image/png", Purpose: "maskable"},
-			{Source: h.routePath("/icon/%s", "maskable-icon-192.png"), Sizes: "192x192", Type: "image/png", Purpose: "maskable"},
-			{Source: h.routePath("/icon/%s", "maskable-icon-512.png"), Sizes: "512x512", Type: "image/png", Purpose: "maskable"},
+			{Source: h.iconPath("icon-120.png"), Sizes: "120x120", Type: "image/png", Purpose: "any"},
+			{Source: h.iconPath("icon-192.png"), Sizes: "192x192", Type: "image/png", Purpose: "any"},
+			{Source: h.iconPath("icon-512.png"), Sizes: "512x512", Type: "image/png", Purpose: "any"},
+			{Source: h.iconPath("maskable-icon-120.png"), Sizes: "120x120", Type: "image/png", Purpose: "maskable"},
+			{Source: h.iconPath("maskable-icon-192.png"), Sizes: "192x192", Type: "image/png", Purpose: "maskable"},
+			{Source: h.iconPath("maskable-icon-512.png"), Sizes: "512x512", Type: "image/png", Purpose: "maskable"},
 		},
 		ShareTarget: webManifestShareTarget{
 			Action:  h.routePath("/bookmarklet"),
@@ -99,14 +99,14 @@ func (h *handler) showWebManifest(w http.ResponseWriter, r *http.Request) {
 			Params:  webManifestShareTargetParams{URL: "uri", Text: "text"},
 		},
 		Shortcuts: []webManifestShortcut{
-			{Name: labelNewFeed, URL: h.routePath("/subscribe"), Icons: []webManifestIcon{{Source: h.routePath("/icon/%s", "add-feed-icon.png"), Sizes: "240x240", Type: "image/png"}}},
-			{Name: labelUnreadMenu, URL: h.routePath("/unread"), Icons: []webManifestIcon{{Source: h.routePath("/icon/%s", "unread-icon.png"), Sizes: "240x240", Type: "image/png"}}},
-			{Name: labelStarredMenu, URL: h.routePath("/starred"), Icons: []webManifestIcon{{Source: h.routePath("/icon/%s", "starred-icon.png"), Sizes: "240x240", Type: "image/png"}}},
-			{Name: labelHistoryMenu, URL: h.routePath("/history"), Icons: []webManifestIcon{{Source: h.routePath("/icon/%s", "history-icon.png"), Sizes: "240x240", Type: "image/png"}}},
-			{Name: labelFeedsMenu, URL: h.routePath("/feeds"), Icons: []webManifestIcon{{Source: h.routePath("/icon/%s", "feeds-icon.png"), Sizes: "240x240", Type: "image/png"}}},
-			{Name: labelCategoriesMenu, URL: h.routePath("/categories"), Icons: []webManifestIcon{{Source: h.routePath("/icon/%s", "categories-icon.png"), Sizes: "240x240", Type: "image/png"}}},
-			{Name: labelSearchMenu, URL: h.routePath("/search"), Icons: []webManifestIcon{{Source: h.routePath("/icon/%s", "search-icon.png"), Sizes: "240x240", Type: "image/png"}}},
-			{Name: labelSettingsMenu, URL: h.routePath("/settings"), Icons: []webManifestIcon{{Source: h.routePath("/icon/%s", "settings-icon.png"), Sizes: "240x240", Type: "image/png"}}},
+			{Name: labelNewFeed, URL: h.routePath("/subscribe"), Icons: []webManifestIcon{{Source: h.iconPath("add-feed-icon.png"), Sizes: "240x240", Type: "image/png"}}},
+			{Name: labelUnreadMenu, URL: h.routePath("/unread"), Icons: []webManifestIcon{{Source: h.iconPath("unread-icon.png"), Sizes: "240x240", Type: "image/png"}}},
+			{Name: labelStarredMenu, URL: h.routePath("/starred"), Icons: []webManifestIcon{{Source: h.iconPath("starred-icon.png"), Sizes: "240x240", Type: "image/png"}}},
+			{Name: labelHistoryMenu, URL: h.routePath("/history"), Icons: []webManifestIcon{{Source: h.iconPath("history-icon.png"), Sizes: "240x240", Type: "image/png"}}},
+			{Name: labelFeedsMenu, URL: h.routePath("/feeds"), Icons: []webManifestIcon{{Source: h.iconPath("feeds-icon.png"), Sizes: "240x240", Type: "image/png"}}},
+			{Name: labelCategoriesMenu, URL: h.routePath("/categories"), Icons: []webManifestIcon{{Source: h.iconPath("categories-icon.png"), Sizes: "240x240", Type: "image/png"}}},
+			{Name: labelSearchMenu, URL: h.routePath("/search"), Icons: []webManifestIcon{{Source: h.iconPath("search-icon.png"), Sizes: "240x240", Type: "image/png"}}},
+			{Name: labelSettingsMenu, URL: h.routePath("/settings"), Icons: []webManifestIcon{{Source: h.iconPath("settings-icon.png"), Sizes: "240x240", Type: "image/png"}}},
 		},
 	}
 

+ 1 - 1
internal/ui/ui.go

@@ -29,7 +29,7 @@ func Serve(store *storage.Storage, pool *worker.Pool) http.Handler {
 	mux.HandleFunc("GET /stylesheets/{filename}", handler.showStylesheet)
 	mux.HandleFunc("GET /{filename}", handler.showJavascript)
 	mux.HandleFunc("GET /favicon.ico", handler.showFavicon)
-	mux.HandleFunc("GET /icon/{filename}", handler.showAppIcon)
+	mux.HandleFunc("GET /icon/{checksum}/{filename}", handler.showAppIcon)
 	mux.HandleFunc("GET /manifest.json", handler.showWebManifest)
 
 	// New subscription pages.