Skip to content

Instantly share code, notes, and snippets.

@OmerFarukOruc
Last active October 31, 2026 10:33
Show Gist options
  • Select an option

  • Save OmerFarukOruc/39e5e7f0f38e739f7ee77db101eb332e to your computer and use it in GitHub Desktop.

Select an option

Save OmerFarukOruc/39e5e7f0f38e739f7ee77db101eb332e to your computer and use it in GitHub Desktop.
Font subsetting prompt for Claude Opus 5.5: subset, split and prove with pixel diffs

Font subsetting prompt, rewritten for Claude Opus 5.5

font-subsetting-prompt.md asks a coding agent to make a site's web fonts load faster without changing how anything looks: measure the fonts, audit what the site uses, subset each font, split it into a preloaded core file and an extended file behind unicode-range, and prove the result with before and after pixel diffs.

It is a rewrite of a 25-sentence prompt that worked in an earlier project. Every original sentence is still in it (see the coverage map), restructured along Anthropic's prompting guides for Claude Opus 5.5.

How to use it

  1. Paste font-subsetting-prompt.md into Claude Code at the repository root.
  2. Fill in <project_notes> with the framework, where the fonts and @font-face rules live, and the build and production-like serve commands, or delete the block and let the agent find them.
  3. Run at effort medium, the Opus 5.5 default. The Opus 5.5 guide says to raise effort only where you measured a gain, so raise it if a run misses audit items.

Claim review

Each factual claim in the original prompt, checked against a primary source.

Sentence Claim Verdict
S15 Extended file downloads only when needed Verified
S16 unicode-range lists only contained characters Verified
S18 A fallback face between core and extended wins Verified
S19 OFL Reserved Font Name forces a rename Verified, conditional
S8 hyphens: auto draws U+2010 Verified in spec
S8, S12 "OpenType features turned on" Gap, fixed
S11 Fix an axis only if nothing overrides it Verified
S2, S12 Drop hinting and stay identical Tension, reported
S25 Embed core font in the stylesheet later Trade-off noted
  • S15: CSS Fonts 4 §4.5 example: for Latin-only text "only the font for Latin characters is downloaded".
  • S16: CSS Fonts 4 §4.5: a code point inside the declared range that the file lacks still triggers the download, then "the effective unicode-range used for font matching excludes this code point" and matching moves on. The page pays for the file and still draws a fallback glyph.
  • S18: CSS Fonts 4 §5: when the matched face lacks the glyph, "the next family name is selected". A fallback face with no unicode-range covers every character, so if it sits in the family list before the extended face, it draws those characters. The fix is one shared family name for core and extended, which CSS Fonts 4 §4.5.1 combines into one composite font.
  • S19: OFL FAQ 2.6 says removing parts of a font for web delivery is modification; FAQ 2.8 says such a font may not use Reserved Font Names; FAQ 3.8 says a rename is needed "Only if there are declared RFNs". Some fonts declare none, Geist among them (OFL.txt in vercel/geist-font), so the rewrite tells the agent to read the license first.
  • S8 hyphen: CSS Text 3 says hyphenation is "usually by inserting a hyphen, U+2010". That browsers fall back to U+002D when the font lacks U+2010 is not confirmed from source here.
  • S8, S12 features: CSS Fonts 4 §7.1 requires browsers to turn on rlig, liga, clig, calt, locl, ccmp, mark and mkmk with no CSS at all. An audit of CSS alone would call them unused and drop them, which changes ligatures and mark placement. The rewrite counts them as used.
  • S11: a fixed wght axis makes the browser synthesize bold for any other weight a rule asks for (font-synthesis-weight, CSS Fonts 4 §2.8.1), which is visible.
  • S2, S12 hinting: macOS ignores TrueType hints and Windows applies them (general knowledge, not checked here), so a pixel diff on Linux or macOS cannot prove that dropping hinting is invisible on Windows. The rewrite keeps the instruction and makes the agent report the gap with the bytes at stake.
  • S25: on a site that inlines its CSS into every HTML page, a data-URL font repeats in every page and needs font-src data: in a Content Security Policy. The rewrite keeps the idea as later work the agent only assesses.

Coverage map

Where each original sentence lives in the rewrite.

# Original sentence starts Now in
S1 Make this site's web fonts load faster <goal>
S2 Shrink font bytes as far as possible <goal>
S3 Treat any visual change as a regression <hard_constraints> 1
S4 If an optimization would change rendering <hard_constraints> 1, <report> 5
S5 Keep the current loading behavior <hard_constraints> 2
S6 Don't add font processing to the build <hard_constraints> 3, step 6
S7 First measure the current state step 1
S8 Then find out what the site actually uses step 2
S9 Cut each font down to that step 3
S10 Keep the used characters plus Latin-1 step 3
S11 Limit variable axes to the ranges in use step 3
S12 Drop unused OpenType features and hinting step 3, step 7
S13 Split each font into two files step 4
S14 A small core with ASCII step 4
S15 An extended file behind unicode-range step 4
S16 Each file's unicode-range should list step 4
S17 Watch out for a few things step 5 heading
S18 A generated fallback face step 5
S19 Fonts with a Reserved Font Name step 5
S20 Check the generated files step 5
S21 Before calling it done, build a baseline step 7
S22 Explain every visible difference step 7
S23 Re-measure bytes per page step 7
S24 When you're done, tell me <report>
S25 Later, it may be worth embedding <later>

What the rewrite adds

From the Opus 5.5, Opus 5 and general prompting guides on platform.claude.com:

  • Sections in XML tags (<goal>, <hard_constraints>, <steps>, <report>, <later>, <working_style>), so instructions, constraints and output shape stay apart.
  • A reason on each rule, such as why the fallback faces stay untouched or why a fixed axis is risky. The best-practices guide says Claude generalizes from the reason.
  • A "Done when" line on every step, and a fix-and-rediff loop when a diff shows a regression, so the agent can tell finished from unfinished.
  • Wanted stops named explicitly: the approval question about a rendering change, asked after all other work, and a blocker only the user can clear. The Opus 5.5 guide says the model responds to instructions that name which stops are wanted, and that a text-only message ends its turn.
  • A checklist file of the steps and held-back optimizations, from the Opus 5.5 guide on long runs with several parts.
  • Progress cadence: one sentence before the first tool call, then updates on important findings.
  • The Opus 5 scope paragraph ("Deliver what was asked, at the scope intended") and its subagent limit.
  • An example table for the pixel-diff results, because the guides say an example steers output shape better than a description.
  • Technical gaps closed: default-on OpenType features, @font-face descriptors matching the new axis ranges, a check when a limited axis range moves the default, Windows hinting, a temporary page when no page shows a required category (such as CJK), and a precise test for "sub-pixel rounding".

Left out on purpose: "double-check your work" lines. The Opus 5 guide says the model verifies its own work and such lines cause over-verification. The pixel diffs stay, because they are the deliverable, not a self-check.

<project_notes> Optional. Name the framework, where the font files and @font-face rules live, and the commands that build the site and serve it the way production does. Delete this block if the agent should find these itself. </project_notes>

Make this site's web fonts load faster without changing how anything looks. Shrink font bytes as far as possible, keep rendering identical, and prove it with before and after pixel diffs. Visitors should see exactly the page they see today, only sooner, so every claim in your report needs a measurement or a diff image behind it.

<hard_constraints>

  1. Treat any visual change as a regression unless I approve it. If an optimization would change rendering, leave it out of the result so the site keeps looking the same by default, and tell me what it saves in bytes and what it changes on screen, with the diff image. Finish all other work first; this approval question is one of the stops I want from you.
  2. Keep the current loading behavior. Leave font-display, the fallback font faces and their metric overrides (size-adjust, ascent-override and similar) exactly as they are. They were tuned to limit layout shift while the web font loads, and the pixel diffs only show the page after the fonts load, so a change there would slip past them.
  3. Keep font processing out of the build. Write a script I run by hand, commit the files it writes, and note when it needs rerunning (step 6). The build stays fast and deterministic, and the committed font files are what gets reviewed. </hard_constraints>
Work through these in order. Each step ends on its "Done when" line.
  1. Measure the current state. Record for each font file: size in bytes, glyph count, variable axes with min, default and max, the OpenType features in GSUB and GPOS, and its hinting (TrueType instructions in fpgm, prep, cvt or the glyphs, or CFF hints). Then record per page the font bytes the browser downloads and what each page preloads. Measure the production build, served the way production serves it, in a real browser with an empty cache, because the dev server and a warm cache report other files. Done when every font file and every page template has a row in a table.

  2. Find out what the site actually uses. Read the real sources before you conclude anything: global stylesheets, inline component styles and <style> blocks, inline style attributes, and the CSS the framework writes into the build output. Find, per font:

    • Characters: every code point that appears in the content of each built page, including CSS content: strings, so text from templates, data files and generated markup all counts. Keep a per-page count for step 4.
    • Axis values and weights: every font-weight, font-style, font-stretch and font-variation-settings value that reaches text in that font, including one-off overrides in a single component, the browser's default bold on <strong>, <b> and headings, relative bolder and lighter, and bold inside italics, where a bold rule and an italic rule meet on one element.
    • OpenType features that are turned on: the ones browsers apply with no CSS (CSS Fonts 4 §7.1: rlig, liga, clig, calt, locl, ccmp, mark, mkmk, plus kern, and rvrn in variable fonts) whenever the font defines them, and every feature CSS turns on through font-feature-settings or font-variant-*, such as tnum from tabular-nums.
    • CSS side effects that draw characters the content does not contain: hyphens: auto draws U+2010 HYPHEN at each break, text-overflow: ellipsis draws U+2026, and quotes, list markers and text-transform can draw others.
    • Which characters each font draws today and which fall through to system fonts, such as CJK. Done when each font has its code points, axis ranges and features, each traced to the file and selector that needs it.
  3. Cut each font down to that.

    • Keep the used characters plus Latin-1 Supplement (U+0080 to U+00FF) and Latin Extended-A (U+0100 to U+017F), as far as the original font has them, so new copy with accents rarely needs a rerun. Leave CJK to system fonts if that's how it renders today, and add no glyph the original font lacks.
    • Limit variable axes to the ranges in use. Fix an axis at one value only if nothing overrides it, because a fixed axis makes the browser synthesize a fake bold or oblique wherever a rule asks for another value. Make each @font-face font-weight, font-style and font-stretch descriptor match the new ranges. If a limited range no longer contains the axis default, check that the new default draws the same glyphs. Limiting an axis makes the font tool recompute the variation data, and its rounding can move outlines by a fraction of a font unit at values you kept (fontTools 4.67 moved Geist's outlines up to 0.85 units when cut to 400 to 700). After a cut, compare each glyph's outline and advance width with the original at every axis value in use, and keep the full axis when any of them moved.
    • Drop unused OpenType features and hinting. Done when each subset's character set and feature list equal what step 2 found plus the Latin blocks above, and its axis ranges equal what step 2 found or, where a cut moved outlines, the original ranges.
  4. Split each font into two files.

    • Core file: ASCII (U+0020 to U+007E), Latin-1 Supplement and the punctuation most pages use, such as curly quotes, en and em dashes and the ellipsis, chosen from the per-page counts in step 2. Preload it on the pages that render text in that font.
    • Browsers shape each file as its own font, so no kerning pair or contextual form joins a character in the core file to one in the extended file: with é in the extended file, "café" came out 4 px wider at 100 px. Put the letters that share words with ASCII in the core file, at least Latin-1 and the letters of each language the site publishes. The latin subset Google Fonts serves (ASCII, Latin-1, ı, Œ, œ and common punctuation) is a good starting set.
    • Extended file: everything else from step 3, behind unicode-range, so it only downloads when a page needs one of its characters.
    • Write each file's unicode-range from that file's own cmap after subsetting, listing only characters it actually contains. A range that claims a missing character makes the browser download the file, find no glyph and move on (CSS Fonts 4 §4.5), so the page pays for the file and still draws a fallback glyph.
    • Give the core and extended faces the same font-family name and the same descriptors, so the browser combines them into one font (CSS Fonts 4 §4.5.1). Done when every kept character sits in exactly one of the two files and every page that renders only core characters requests no extended file.
  5. Watch out for a few things.

    • A generated fallback face between the core and extended files will draw the extended characters before the real font does. The browser takes each character from the first family in the font-family list that has it, and a fallback face without unicode-range has everything. One shared family name for core and extended (step 4) keeps them ahead of the fallback, which stays unchanged (constraint 2).
    • Fonts with a Reserved Font Name under the OFL must be renamed internally once subset, because subsetting makes a Modified Version (OFL FAQ 2.6 and 2.8). Read each font's license: rename the name table entries only when it declares a Reserved Font Name (FAQ 3.8).
    • Check the generated files, since font tools have quirks. Read every output back with a font library and confirm that its cmap equals the intended set, its axis ranges and default match step 3, STAT and the @font-face descriptors agree with its axes, each glyph's outline and advance width equal the original's at every axis value in use, GSUB and GPOS hold exactly the intended features, the hinting is gone, and the names are right. Then load it in the browser and confirm document.fonts reports it loaded with no console error. Done when every check above passes for every output file.
  6. Write the script. One script reads the original font files from where the project keeps them, writes the core and extended files and the @font-face rules or the data they come from, and gives byte-identical output for the same input. Pin the font tool versions it uses. Note when it needs rerunning, at the top of the script and in the doc that owns the font setup: new content uses a character outside the kept set, CSS uses a new weight, style, axis value or OpenType feature, a page starts to use hyphens: auto, or the original fonts are upgraded. Done when a fresh run of the script reproduces the committed files byte for byte.

  7. Prove it before calling it done.

    • Build a baseline from the same content: the same commit with the original fonts, so any difference comes from the fonts alone. Render baseline and change with the same browser, OS, viewport and device scale factor, at a phone width and a desktop width, with animations off, after document.fonts.ready.
    • Pixel-diff before and after on pages with code, italics, accented characters, words that mix ASCII with accented letters (such as "café" and "Straße"), components with their own font settings, and CJK. If no page shows one of these, add a temporary page that shows it to both builds and delete it afterwards.
    • Explain every visible difference as a content change, a regression to fix, or harmless sub-pixel rounding, and save its diff image. Count it as sub-pixel rounding only when the diff shows antialiasing alone, with no change to a glyph's shape, a position or a line break. A content change means the baseline is wrong; rebuild it from the same content.
    • Name the platform the diffs ran on. Hinting changes rasterization where the renderer applies TrueType hints, such as Windows, while macOS ignores them. If your diffs could not cover Windows, report hinting removal as unverified there, with the bytes it saves.
    • Re-measure bytes per page with the step 1 method, and confirm from the network log that the extended files only load where needed. When a diff shows a regression, find the cut that caused it, keep what that text needs, rerun the script and diff again. Done when every page category has a diff at both widths, every nonzero diff has a verdict, and the byte tables have before and after columns.
When you're done, start with one sentence on the outcome, then give: 1. Bytes before and after per font, for the original file and for its core and extended files, with glyph counts. 2. Font bytes before and after per page, and what each page preloads. 3. The pixel-diff results. 4. Anywhere you deviated from what I asked, and what it cost in bytes or rendering. 5. Optimizations you held back because they change rendering: what each saves and what it changes, for my approval. 6. How to rerun the script: the command, its inputs and the rerun triggers from step 6. 7. Your view on the later work below.

Use this shape for the pixel-diff results:

Page Width Differing pixels Verdict Diff image
/blog/some-post 390 0 identical -
/about 1440 112 sub-pixel rounding diffs/about-1440.png
Later, it may be worth embedding the core font in the global stylesheet and precaching the extended files after idle. Leave both out of this change. In the report, say in a few lines what each would gain and cost on this site, such as bytes repeated on every page, cache reuse across pages, and any Content Security Policy change like `font-src data:`.

<working_style>

  • Deliver what was asked, at the scope intended. If a request here seems mistaken or a better approach exists, say so in a sentence and continue with the task as asked.
  • Keep the seven steps and every held-back optimization in a checklist file and tick an item only after its own check ran.
  • A message with no tool call ends your turn. Put status notes in the same message as your next tool call and keep going. Stop only to ask for approval of a rendering change, after everything else is done, or when something only I can clear blocks you.
  • Before your first tool call, say in one sentence what you are about to do. After that, give a short update when you find something important or change direction.
  • Use a subagent only for a wide, independent sweep, such as reading every page on a large site for step 2. Do the rest yourself. </working_style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment