Skip to content

Commit 71b5a1b

Browse files
committed
feat(typography): auto-detect bundled fonts and skip Google Fonts CDN
Add a second resolution tier to the typography system: before falling back to Google Fonts, check assets/dist/font/ for a matching local file. When found, emit @font-face with local() fallback instead of a CDN <link>. Variable fonts detected from ".var." filename convention. Bundle JetBrains Mono alongside Inter so the default "modern" pack loads entirely from local files — zero external font requests.
1 parent aa27289 commit 71b5a1b

File tree

3 files changed

+82
-8
lines changed

3 files changed

+82
-8
lines changed
185 KB
Binary file not shown.
-244 KB
Binary file not shown.

modules/blox/layouts/_partials/functions/typography.html

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
{{/*
22
HugoBlox Theme Engine: Typography Rendering
33

4-
Resolves font families, emits Google Fonts <link> tags,
5-
and generates CSS custom properties for typography.
4+
Resolves font families, emits @font-face for bundled fonts,
5+
Google Fonts <link> tags for remote fonts, and CSS custom properties.
6+
7+
Font resolution order (per family):
8+
1. system-sans / system-serif / system-mono → native OS font stack
9+
2. Bundled file in assets/dist/font/<Name>* → local @font-face
10+
3. Fallback → Google Fonts CDN <link>
611

712
Input: Theme config dict from get_theme_config:
8-
- typography: { families, weights, sizes, leading, tracking }
13+
- typography: { families, weights, sizes, leading, tracking, variable }
914

10-
Output: <link> (Google Fonts) + <style> (CSS custom properties)
15+
Output: @font-face (bundled) + <link> (Google Fonts) + <style> (CSS custom properties)
1116
*/}}
1217

1318
{{ $typo := .typography }}
@@ -24,7 +29,7 @@
2429
{{ $system_serif := "ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif" }}
2530
{{ $system_mono := "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace" }}
2631

27-
{{/* Map font names to CSS values and collect Google Font families */}}
32+
{{/* Map font names to CSS values and collect Google Font / bundled font families */}}
2833
{{ $resolve_family := dict }}
2934
{{ $roles := slice "heading" "body" "code" "nav" }}
3035
{{ range $role := $roles }}
@@ -34,24 +39,93 @@
3439
{{ end }}
3540
{{ $css := "" }}
3641
{{ $is_google := false }}
42+
{{ $bundled_res := false }}
3743
{{ if eq $name "system-sans" }}
3844
{{ $css = $system_sans }}
3945
{{ else if eq $name "system-serif" }}
4046
{{ $css = $system_serif }}
4147
{{ else if eq $name "system-mono" }}
4248
{{ $css = $system_mono }}
4349
{{ else }}
44-
{{/* Google Font — pick the right generic fallback based on pack style and role */}}
50+
{{/* Check for a locally bundled font file in assets/dist/font/ */}}
51+
{{ $normalized := replace $name " " "" }}
52+
{{ $bundled_res = resources.GetMatch (printf "dist/font/%s*" $normalized) }}
53+
{{/* Build CSS font-family value with appropriate generic fallback */}}
4554
{{ if eq $role "code" }}
4655
{{ $css = printf "'%s', %s" $name $system_mono }}
4756
{{ else if eq $pack_style "serif" }}
4857
{{ $css = printf "'%s', %s" $name $system_serif }}
4958
{{ else }}
5059
{{ $css = printf "'%s', %s" $name $system_sans }}
5160
{{ end }}
52-
{{ $is_google = true }}
61+
{{/* Only request from Google Fonts if no local bundle exists */}}
62+
{{ if not $bundled_res }}
63+
{{ $is_google = true }}
64+
{{ end }}
65+
{{ end }}
66+
{{ $resolve_family = merge $resolve_family (dict $role (dict "css" $css "google" $is_google "name" $name "bundled" $bundled_res)) }}
67+
{{ end }}
68+
69+
{{/* Emit @font-face declarations for locally bundled fonts */}}
70+
{{ $bundled_seen := dict }}
71+
{{ range $role := $roles }}
72+
{{ with index $resolve_family $role }}
73+
{{ if .bundled }}
74+
{{ $name := .name }}
75+
{{ if not (index $bundled_seen $name) }}
76+
{{ $bundled_seen = merge $bundled_seen (dict $name true) }}
77+
{{ $res := .bundled }}
78+
{{/* Publish the font file so Hugo copies it to the output directory */}}
79+
{{ $res.Publish }}
80+
{{/* Determine the CSS font format from the file extension */}}
81+
{{ $ext := path.Ext $res.Name }}
82+
{{ $format := "" }}
83+
{{ if eq $ext ".woff2" }}
84+
{{ $format = "woff2" }}
85+
{{ else if eq $ext ".ttf" }}
86+
{{ $format = "truetype" }}
87+
{{ else if eq $ext ".woff" }}
88+
{{ $format = "woff" }}
89+
{{ else }}
90+
{{ warnf "TYPOGRAPHY: Bundled font '%s' has unrecognized extension '%s'. Expected .woff2 or .ttf." $name $ext }}
91+
{{ end }}
92+
{{/* Determine weight range: variable font files get full range, static get discrete weights.
93+
Detect variable fonts from the ".var." convention in the filename (e.g. Inter.var.woff2),
94+
which is more reliable than the pack's variable flag (used for Google Fonts URL construction). */}}
95+
{{ $is_var_file := strings.Contains $res.Name ".var." }}
96+
{{ $weight_val := "400" }}
97+
{{ if $is_var_file }}
98+
{{ $weight_val = "100 900" }}
99+
{{ else }}
100+
{{/* Collect all discrete weights across roles using this font */}}
101+
{{ $all_weights := slice }}
102+
{{ range $r := $roles }}
103+
{{ $entry := index $resolve_family $r }}
104+
{{ if and $entry (eq (index $entry "name") $name) }}
105+
{{ with index $weights $r }}
106+
{{ range $w := . }}
107+
{{ $all_weights = $all_weights | append $w }}
108+
{{ end }}
109+
{{ end }}
110+
{{ end }}
111+
{{ end }}
112+
{{ if gt (len $all_weights) 0 }}
113+
{{ $weight_val = delimit ($all_weights | uniq | sort) " " }}
114+
{{ end }}
115+
{{ end }}
116+
{{ if $format }}
117+
<style>
118+
@font-face {
119+
font-family: '{{ $name }}';
120+
src: local('{{ $name }}'), url('{{ $res.RelPermalink }}') format('{{ $format }}');
121+
font-weight: {{ $weight_val }};
122+
font-display: swap;
123+
}
124+
</style>
125+
{{ end }}
126+
{{ end }}
127+
{{ end }}
53128
{{ end }}
54-
{{ $resolve_family = merge $resolve_family (dict $role (dict "css" $css "google" $is_google "name" $name)) }}
55129
{{ end }}
56130

57131
{{/* Build Google Fonts URL */}}

0 commit comments

Comments
 (0)