|
1 | 1 | {{/* |
2 | 2 | HugoBlox Theme Engine: Typography Rendering |
3 | 3 |
|
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> |
6 | 11 |
|
7 | 12 | Input: Theme config dict from get_theme_config: |
8 | | - - typography: { families, weights, sizes, leading, tracking } |
| 13 | + - typography: { families, weights, sizes, leading, tracking, variable } |
9 | 14 |
|
10 | | - Output: <link> (Google Fonts) + <style> (CSS custom properties) |
| 15 | + Output: @font-face (bundled) + <link> (Google Fonts) + <style> (CSS custom properties) |
11 | 16 | */}} |
12 | 17 |
|
13 | 18 | {{ $typo := .typography }} |
|
24 | 29 | {{ $system_serif := "ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif" }} |
25 | 30 | {{ $system_mono := "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace" }} |
26 | 31 |
|
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 */}} |
28 | 33 | {{ $resolve_family := dict }} |
29 | 34 | {{ $roles := slice "heading" "body" "code" "nav" }} |
30 | 35 | {{ range $role := $roles }} |
|
34 | 39 | {{ end }} |
35 | 40 | {{ $css := "" }} |
36 | 41 | {{ $is_google := false }} |
| 42 | + {{ $bundled_res := false }} |
37 | 43 | {{ if eq $name "system-sans" }} |
38 | 44 | {{ $css = $system_sans }} |
39 | 45 | {{ else if eq $name "system-serif" }} |
40 | 46 | {{ $css = $system_serif }} |
41 | 47 | {{ else if eq $name "system-mono" }} |
42 | 48 | {{ $css = $system_mono }} |
43 | 49 | {{ 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 */}} |
45 | 54 | {{ if eq $role "code" }} |
46 | 55 | {{ $css = printf "'%s', %s" $name $system_mono }} |
47 | 56 | {{ else if eq $pack_style "serif" }} |
48 | 57 | {{ $css = printf "'%s', %s" $name $system_serif }} |
49 | 58 | {{ else }} |
50 | 59 | {{ $css = printf "'%s', %s" $name $system_sans }} |
51 | 60 | {{ 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 }} |
53 | 128 | {{ end }} |
54 | | - {{ $resolve_family = merge $resolve_family (dict $role (dict "css" $css "google" $is_google "name" $name)) }} |
55 | 129 | {{ end }} |
56 | 130 |
|
57 | 131 | {{/* Build Google Fonts URL */}} |
|
0 commit comments