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:
parent
03fb98afc4
commit
9783199691
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -7,8 +7,11 @@
|
||||
<meta name="description" content={assigns[:page_description] || @theme_settings.site_description || "Welcome to #{@theme_settings.site_name}"} />
|
||||
<.live_title><%= assigns[:page_title] || @theme_settings.site_name %></.live_title>
|
||||
<!-- Preload critical fonts for the current typography preset -->
|
||||
<%= for preload <- SimpleshopTheme.Theme.Fonts.preload_links(@theme_settings.typography) do %>
|
||||
<link rel="preload" href={preload.href} as={preload.as} type={preload.type} crossorigin />
|
||||
<%= for preload <- SimpleshopTheme.Theme.Fonts.preload_links(
|
||||
@theme_settings.typography,
|
||||
&SimpleshopThemeWeb.Endpoint.static_path/1
|
||||
) do %>
|
||||
<link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin />
|
||||
<% end %>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||
<script defer phx-track-static src={~p"/assets/js/app.js"}>
|
||||
|
||||
@ -20,11 +20,16 @@ defmodule SimpleshopThemeWeb.Endpoint do
|
||||
# When code reloading is disabled (e.g., in production),
|
||||
# the `gzip` option is enabled to serve compressed
|
||||
# static files generated by running `phx.digest`.
|
||||
#
|
||||
# Cache headers: 1 year for all static assets. Digested assets (with hash
|
||||
# in filename) use `immutable`. Fonts/mockups/images rarely change and
|
||||
# benefit from aggressive caching.
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
from: :simpleshop_theme,
|
||||
gzip: not code_reloading?,
|
||||
only: SimpleshopThemeWeb.static_paths()
|
||||
only: SimpleshopThemeWeb.static_paths(),
|
||||
cache_control_for_etags: "public, max-age=31536000, immutable"
|
||||
|
||||
if Code.ensure_loaded?(Tidewave) do
|
||||
plug Tidewave
|
||||
|
||||
Loading…
Reference in New Issue
Block a user