Files

109 lines
6.0 KiB
Markdown

---
name: optimize-fonts
description: Optimize a project's web fonts — subset the files, prefer variable fonts, add unicode-range and metric-override fallbacks to cut payload and remove layout shift (CLS). Use when the user asks to set up, optimize, subset or fix fonts, @font-face rules or font-family stacks. Fonts may come from Google Fonts or be local files.
license: MIT
metadata:
author: Valentin Silyutin
version: "1.0.0"
---
# Web Font Optimization Instructions
Replace full-coverage font files with subset files, prefer a variable font when one exists, wire `unicode-range`, and add a metric-override fallback so the switch from the fallback font to the web font on load does not shift layout (CLS).
## Inputs
- **Family**: a font family name such as `Montserrat`. If the user does not give one, detect it from the font files in the project.
- **Subsets**: always `cyrillic` and `latin`. All projects in this workspace are Russian + Latin.
- **Paths**: the font folder and the stylesheet that declares `@font-face` / `font-family`. Read them from the project's `AGENTS.md`; ask the user only if `AGENTS.md` does not say.
## Workflow
### 1. Locate the fonts
Read `AGENTS.md` and find (a) the folder that holds the font files and (b) the file(s) where fonts are connected. Detect the family name, the weights the project ships, and which weights the styles actually use (grep for `font-weight`).
### 2. Try Google Fonts
Request the family's CSS **with a desktop browser User-Agent** (without it Google returns TTF instead of WOFF2):
```bash
curl -s -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
"https://fonts.googleapis.com/css2?family=<Family>:wght@<min>..<max>&display=swap"
```
- If the response declares `font-weight: <min> <max>`, it is a **variable** font — use it.
- Otherwise request the individual weights the project uses (`wght@400;500;700`).
- Take both the WOFF2 URLs **and** the `unicode-range` values for `cyrillic` and `latin` from the response. They are authoritative — do not hand-write them.
- If the family is **not** on Google Fonts (a custom or local font), keep the project's own file(s) and subset them; derive `unicode-range` from the font `cmap` (see the reference section).
### 3. Place subset files
Download the `cyrillic` and `latin` WOFF2 files into the project's font folder, named `<Family>-cyrillic.woff2` / `<Family>-latin.woff2`. For static fonts include the weight: `<Family>-<weight>-latin.woff2`. Ask the user before deleting the old files (WOFF fallbacks, full-coverage files) — they stop being referenced.
### 4. Rewrite the `@font-face` rules
Write one rule per subset, keeping the project's existing path style to the font folder:
```css
@font-face {
font-display: swap;
font-family: <Family>;
font-style: normal;
font-weight: <min> <max>; /* single value for static fonts */
src: url('./fonts/<Family>-<subset>.woff2') format('woff2');
unicode-range: <from Google Fonts CSS or cmap>;
}
```
### 5. Add the metric-override fallback
Run the bundled script on the web font file; it prints ready `@font-face` blocks.
```bash
python3 scripts/font-metrics.py <path-to-webfont.woff2> --name <Family> --weights "<min> <max>"
```
Append the printed blocks, then put the fallback families into every `font-family` stack, right after the web font:
```css
font-family: <Family>, '<Family> Fallback', '<Family> Fallback Android', system-ui;
```
Grep the whole project for `<Family>` in `font-family` declarations and update each one.
### 6. Drop unused weights
Grep `font-weight` and fix any weight the project does not ship (for example a lone `300` with no 300 file) to an existing weight.
## Font metrics script
`scripts/font-metrics.py` reads the metrics from the font file and prints the fallback `@font-face` blocks. It needs Python 3 with `fonttools` and `brotli` installed (`pip install fonttools brotli`).
- For a variable font it measures the instance at `--weight` (default `500`, the dominant body weight).
- Fallback metrics for Arial and Roboto are built in; it emits one block per fallback (Arial for Windows/macOS, Roboto for Android, where Arial is absent).
The numbers come from the font metrics:
```text
size-adjust = web.avg / fallback.avg · 100%
ascent-override = web.ascent / size-adjust · 100%
descent-override = web.descent / size-adjust · 100%
line-gap-override = web.lineGap / size-adjust · 100%
```
where `avg` is the mean advance width over `a-z A-Z 0-9 space`, normalised to `unitsPerEm`; `ascent` / `descent` / `lineGap` come from the font `hhea` table. `size-adjust` scales every metric, so the overrides are divided by it to land on the target line height.
## Verification
- Run the project's own linters and formatters on the changed files (see the project's `AGENTS.md`). If templates changed, rebuild and validate them the way the project does.
- Build the project and confirm the new font files land in the build output and the new `@font-face` rules are present in the built CSS.
## Reference
- **`unicode-range`** lists the code points actually present in that subset file. The browser downloads a file only when a character in its range is used, so subsets absent from the page are never fetched. The default (when omitted) is `U+0-10FFFF` — the whole of Unicode — which defeats subsetting.
- The Google `latin` subset is **not just letters**: it includes shared punctuation and symbols (dashes, quotes, `€`, `™`, digits). That is why its range is long.
- To derive a range from a font yourself: read the `cmap` table (`fonttools`), sort the code points and collapse consecutive runs into `U+XXXX-YYYY`.
- `size-adjust` is supported by all current browsers; `ascent-override` / `descent-override` / `line-gap-override` are ignored by Safari (a known, harmless gap — it still applies `size-adjust`).
- `font-display: swap` draws text immediately in the fallback font and swaps when the web font loads; the metric-override fallback is what keeps that swap from shifting layout.