From 978319969198ba769573a4649820ad97257f460b Mon Sep 17 00:00:00 2001 From: Jamey Greenwood Date: Sun, 25 Jan 2026 09:32:06 +0000 Subject: [PATCH] perf: use digested font paths in CSS and preloads Add path_resolver parameter to font generation functions so both font preloads and CSS @font-face declarations use the same digested paths in production. This prevents duplicate font downloads when preloads were using digested paths but CSS used non-digested paths. - Add path_resolver parameter to Fonts.generate_font_faces/2, Fonts.preload_links/2, and Fonts.generate_all_font_faces/1 - Update CSSGenerator.generate/2 to accept path_resolver - Update CSSCache.warm/0 to use Endpoint.static_path/1 - Move CSSCache to start after Endpoint in supervision tree - Add 1-year cache headers for static assets Co-Authored-By: Claude Opus 4.5 --- lib/simpleshop_theme/application.ex | 8 +- lib/simpleshop_theme/theme/css_cache.ex | 6 +- lib/simpleshop_theme/theme/css_generator.ex | 7 +- lib/simpleshop_theme/theme/fonts.ex | 88 ++++++++++++------- .../components/layouts/shop_root.html.heex | 7 +- lib/simpleshop_theme_web/endpoint.ex | 7 +- 6 files changed, 81 insertions(+), 42 deletions(-) diff --git a/lib/simpleshop_theme/application.ex b/lib/simpleshop_theme/application.ex index 2098f80..ca5b640 100644 --- a/lib/simpleshop_theme/application.ex +++ b/lib/simpleshop_theme/application.ex @@ -18,10 +18,10 @@ defmodule SimpleshopTheme.Application do {Oban, Application.fetch_env!(:simpleshop_theme, Oban)}, # Image variant cache - ensures all variants exist on startup SimpleshopTheme.Images.VariantCache, - # Theme CSS cache - SimpleshopTheme.Theme.CSSCache, - # Start to serve requests, typically the last entry - SimpleshopThemeWeb.Endpoint + # Start to serve requests + SimpleshopThemeWeb.Endpoint, + # Theme CSS cache - must start after Endpoint for static_path/1 to work + SimpleshopTheme.Theme.CSSCache ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/simpleshop_theme/theme/css_cache.ex b/lib/simpleshop_theme/theme/css_cache.ex index e274f84..0fa9dca 100644 --- a/lib/simpleshop_theme/theme/css_cache.ex +++ b/lib/simpleshop_theme/theme/css_cache.ex @@ -82,7 +82,11 @@ defmodule SimpleshopTheme.Theme.CSSCache do alias SimpleshopTheme.Theme.CSSGenerator settings = Settings.get_theme_settings() - css = CSSGenerator.generate(settings) + + # Use endpoint's static_path for digested URLs in production + path_resolver = &SimpleshopThemeWeb.Endpoint.static_path/1 + + css = CSSGenerator.generate(settings, path_resolver) put(css) :ok end diff --git a/lib/simpleshop_theme/theme/css_generator.ex b/lib/simpleshop_theme/theme/css_generator.ex index a334cb4..4c5b552 100644 --- a/lib/simpleshop_theme/theme/css_generator.ex +++ b/lib/simpleshop_theme/theme/css_generator.ex @@ -21,11 +21,14 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do pages don't need the attribute-based CSS selectors. Also includes @font-face declarations for the fonts used by the typography preset. + + Accepts an optional path_resolver function for digested font paths. + In production, pass `&SimpleshopThemeWeb.Endpoint.static_path/1`. """ - def generate(%ThemeSettings{} = settings) do + def generate(%ThemeSettings{} = settings, path_resolver \\ fn path -> path end) do """ /* Font faces for #{settings.typography} typography */ - #{Fonts.generate_font_faces(settings.typography)} + #{Fonts.generate_font_faces(settings.typography, path_resolver)} /* Theme Tokens - Layer 2 (dynamically generated) */ .themed { diff --git a/lib/simpleshop_theme/theme/fonts.ex b/lib/simpleshop_theme/theme/fonts.ex index 93b189a..e4680a6 100644 --- a/lib/simpleshop_theme/theme/fonts.ex +++ b/lib/simpleshop_theme/theme/fonts.ex @@ -114,8 +114,11 @@ defmodule SimpleshopTheme.Theme.Fonts do Generates @font-face CSS for a specific typography preset. Only includes the fonts needed for that preset. + + Accepts an optional path_resolver function to transform font URLs. + In production, pass `&SimpleshopThemeWeb.Endpoint.static_path/1` for digested paths. """ - def generate_font_faces(typography) do + def generate_font_faces(typography, path_resolver \\ &default_path_resolver/1) do %{heading: heading_key, body: body_key} = fonts_for_typography(typography) font_keys = @@ -126,7 +129,7 @@ defmodule SimpleshopTheme.Theme.Fonts do end font_keys - |> Enum.map(&generate_font_face_for_font/1) + |> Enum.map(&generate_font_face_for_font(&1, path_resolver)) |> Enum.join("\n") end @@ -134,20 +137,22 @@ defmodule SimpleshopTheme.Theme.Fonts do Generates @font-face CSS for ALL fonts. Used in the theme editor where users can switch between typography presets. + + Accepts an optional path_resolver function for digested paths. """ - def generate_all_font_faces do + def generate_all_font_faces(path_resolver \\ &default_path_resolver/1) do @fonts |> Map.keys() - |> Enum.map(&generate_font_face_for_font/1) + |> Enum.map(&generate_font_face_for_font(&1, path_resolver)) |> Enum.join("\n") end @doc """ - Generates preload link tags for a specific typography preset. + Returns the font file paths (without /fonts prefix) for a typography preset. - Returns a list of maps with href, as, type, and crossorigin attributes. + Used by templates to generate preload links with digested paths via ~p sigil. """ - def preload_links(typography) do + def preload_font_paths(typography) do %{heading: heading_key, body: body_key} = fonts_for_typography(typography) # Preload the most commonly used weights @@ -158,21 +163,60 @@ defmodule SimpleshopTheme.Theme.Fonts do body: [400, 600] } - heading_links = generate_preload_links_for_font(heading_key, preload_weights.heading) - body_links = generate_preload_links_for_font(body_key, preload_weights.body) + heading_paths = font_paths_for_weights(heading_key, preload_weights.heading) + body_paths = font_paths_for_weights(body_key, preload_weights.body) # Deduplicate in case heading and body use the same font - (heading_links ++ body_links) |> Enum.uniq_by(& &1.href) + (heading_paths ++ body_paths) |> Enum.uniq() + end + + defp font_paths_for_weights(key, weights) do + case get_font(key) do + %{file_prefix: prefix, weights: available_weights} -> + weights + |> Enum.filter(&(&1 in available_weights)) + |> Enum.map(fn weight -> + weight_suffix = if weight == 400, do: "regular", else: to_string(weight) + "#{prefix}-#{weight_suffix}.woff2" + end) + + nil -> + [] + end + end + + @doc """ + Generates preload link tags for a specific typography preset. + + Returns a list of maps with href, as, type, and crossorigin attributes. + + Accepts an optional path_resolver function for digested paths. + In production, pass `&SimpleshopThemeWeb.Endpoint.static_path/1`. + """ + def preload_links(typography, path_resolver \\ &default_path_resolver/1) do + typography + |> preload_font_paths() + |> Enum.map(fn filename -> + %{ + href: path_resolver.("/fonts/#{filename}"), + as: "font", + type: "font/woff2", + crossorigin: true + } + end) end # Private functions - defp generate_font_face_for_font(key) do + defp default_path_resolver(path), do: path + + defp generate_font_face_for_font(key, path_resolver) do case get_font(key) do %{family: family, file_prefix: prefix, weights: weights} -> weights |> Enum.map(fn weight -> weight_suffix = if weight == 400, do: "regular", else: to_string(weight) + font_path = path_resolver.("/fonts/#{prefix}-#{weight_suffix}.woff2") """ @font-face { @@ -180,7 +224,7 @@ defmodule SimpleshopTheme.Theme.Fonts do font-style: normal; font-weight: #{weight}; font-display: swap; - src: url('/fonts/#{prefix}-#{weight_suffix}.woff2') format('woff2'); + src: url('#{font_path}') format('woff2'); } """ end) @@ -191,24 +235,4 @@ defmodule SimpleshopTheme.Theme.Fonts do end end - defp generate_preload_links_for_font(key, weights) do - case get_font(key) do - %{file_prefix: prefix, weights: available_weights} -> - weights - |> Enum.filter(&(&1 in available_weights)) - |> Enum.map(fn weight -> - weight_suffix = if weight == 400, do: "regular", else: to_string(weight) - - %{ - href: "/fonts/#{prefix}-#{weight_suffix}.woff2", - as: "font", - type: "font/woff2", - crossorigin: true - } - end) - - nil -> - [] - end - end end diff --git a/lib/simpleshop_theme_web/components/layouts/shop_root.html.heex b/lib/simpleshop_theme_web/components/layouts/shop_root.html.heex index ba2185c..9b8ecd4 100644 --- a/lib/simpleshop_theme_web/components/layouts/shop_root.html.heex +++ b/lib/simpleshop_theme_web/components/layouts/shop_root.html.heex @@ -7,8 +7,11 @@ <.live_title><%= assigns[:page_title] || @theme_settings.site_name %> - <%= for preload <- SimpleshopTheme.Theme.Fonts.preload_links(@theme_settings.typography) do %> - + <%= for preload <- SimpleshopTheme.Theme.Fonts.preload_links( + @theme_settings.typography, + &SimpleshopThemeWeb.Endpoint.static_path/1 + ) do %> + <% end %>