Add cache busting to local CSS and JS assets
Compute MD5 digests of static asset files at boot time and append
them as query parameters (e.g. /css/app.css?v=0f0be602c2). This
ensures browsers fetch fresh copies after deployments that change
these files.

https://claude.ai/code/session_01WV7M82TahkbYrL12MygZGE
change
commit e8aa458ef0b329a8fb896edd48ee1f198691c4d1
author Claude <noreply@anthropic.com>
date
parent zvzqxoum
diff --git a/lib/ketchup/views/layout.rb b/lib/ketchup/views/layout.rb
index 5ee369f..b9c521e 100644
--- a/lib/ketchup/views/layout.rb
+++ b/lib/ketchup/views/layout.rb
@@ -1,9 +1,17 @@
 # frozen_string_literal: true
 
+require "digest"
 require "phlex"
 
 module Views
   class Layout < Phlex::HTML
+    ASSET_VERSIONS = begin
+      root = File.expand_path("../../../public", __dir__)
+      %w[/css/reset.css /css/utopia.css /css/app.css /js/app.js].to_h do |path|
+        [path, Digest::MD5.file(File.join(root, path)).hexdigest[0, 10]]
+      end.freeze
+    end
+
     def initialize(current_user:, title: "Ketchup", active_view: nil)
       @current_user = current_user
       @title = title
@@ -18,9 +26,9 @@ module Views
           meta(name: "viewport", content: "width=device-width, initial-scale=1")
           title { @title }
           link(rel: "icon", href: "/favicon.svg", type: "image/svg+xml")
-          link(rel: "stylesheet", href: "/css/reset.css")
-          link(rel: "stylesheet", href: "/css/utopia.css")
-          link(rel: "stylesheet", href: "/css/app.css")
+          link(rel: "stylesheet", href: asset_path("/css/reset.css"))
+          link(rel: "stylesheet", href: asset_path("/css/utopia.css"))
+          link(rel: "stylesheet", href: asset_path("/css/app.css"))
           link(rel: "preconnect", href: "https://fonts.googleapis.com")
           link(rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: true)
           link(rel: "stylesheet",
@@ -28,7 +36,7 @@ module Views
           script(src: "https://unpkg.com/overtype@2.1.1/dist/overtype.min.js",
                  integrity: "sha384-zp8RL0j4VLfaFKgqehca9l8rfcE4Jh0Nt1CFoVyUBn+qa4velUokXJXsW2h0J5xT",
                  crossorigin: "anonymous")
-          script(src: "/js/app.js", defer: true)
+          script(src: asset_path("/js/app.js"), defer: true)
           script(src: "https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.15.0/dist/cdn.min.js",
                  integrity: "sha384-6WOLkykwLb3YWzXZ6lAq+GI0p3V+enUm9jY6yIXGpIriiAUOSF5dgNJLoSSNam4j",
                  crossorigin: "anonymous", defer: true)
@@ -55,6 +63,11 @@ module Views
 
     private
 
+    def asset_path(path)
+      version = ASSET_VERSIONS[path]
+      version ? "#{path}?v=#{version}" : path
+    end
+
     def render_footer
       config = CONFIG
       parts = []