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 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 09:32:06 +00:00
parent 03fb98afc4
commit 9783199691
6 changed files with 81 additions and 42 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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