Font League

Method v1.2. Everything below is mechanical and the code is the spec. Read this before citing a number.

1. The panel

A site is in the panel because a named source admitted it, never because we chose it. The offer is narrow on purpose: a source qualifies only if it publishes an API or feed for machine use. We do not scrape galleries or award sites, however good their taste – a directory's curation is its product, and reading it without an invitation is at best a grey area. So the panel is sites that launched in public in the last 12 weeks:

SourceWhat we takePanel
Show HNShow HN posts with 20+ points, through the Algolia HN API (Hacker News data is public by design) – the linked URL267
YC directoryCompanies in the three most recent batches, through the yc-oss open-data mirror on GitHub (MIT) – a community mirror of YC's public directory, not YC itself – if it stops, the source is retired244

URLs are reduced to a registrable domain, one entry per domain, first source to name it wins. We drop hosts that are not a product site (GitHub, YouTube, app stores, social, link shorteners, hosted sub-domains like *.vercel.app), and anything with adult or gambling hints in the name. A site stays in while a source has named it within the last 12 weeks, then ages out. There are no hand edits to the panel. The one hand-maintained file is the opt-out list: if you own a site in the panel and want it removed, get in touch and it is added to exclude.json, published with the archive.

What this scope means: the panel is developer- and startup-led. Agency showpieces and award-site work are under-represented, and the table says so rather than pretending otherwise. If you run a gallery or directory and want your sites counted, publish a feed or an API and tell us – that is the whole admission test.

How we fetch: source APIs are read once a week under the user agent FontLeague/1.2 (+https://fontleague.com/method), one request at a time with a 1.5-second pause, honouring robots.txt. Panel sites are loaded once a week as a normal Chrome visitor would load them (a browser user agent, because many sites serve different CSS to bots) – their robots.txt is checked first and a disallowed site is listed under “could not be read”. We never log in, bypass a wall, or store page content – only domain, face and character counts.

Known blind spots: bot walls, cookie walls, WebGL-only pages and sites that render no text at 1280px all fail and are listed as “could not be read” – 29 this week.

2. Rendering

Each site is loaded in headless Chrome at 1280×900 with a desktop user agent, waited for load plus up to six seconds of network idle, scrolled to 6,000px in 700px steps, waited for document.fonts.ready and one more second. Reading CSS is not enough: a face that is loaded is not necessarily used, and the body rule is not necessarily what is on screen. We look at what draws.

3. Counting

Every text node on the page is visited. Nodes that are hidden, zero-size, transparent, or inside script, style, svg are skipped. For each node we take the computed font-family stack and walk it until we find a family that actually draws letters: the family is rendered on a canvas with two different fallbacks and if the two widths match, the face has the glyphs and is loaded. Digit-only faces and unloaded faces get no credit. The node's character count goes to that family. Characters, not rules, so a code block does not out-vote a paragraph.

4. Roles

Each text node is assigned exactly one role, first match wins:

RoleRule
Displayinside h1, or font-size ≥ 40px
Headinginside h2h4, or 20–39px
UIinside nav, button, label, input, footer, figcaption, small, time, code, pre, kbd, or < 14px
Bodyeverything else – p, li, blockquote, 14–19px running text

Within a role, a site's vote goes to the family with the most characters. Mono faces land in whichever league they earn – mono is a kind of face, not a role, because a mono league would give a site two votes. Display is the noisiest league – a hero line is about twenty characters and one word can flip a vote – which is why the evidence shows shares, and why you should expect 54/46 splits there.

5. Names

Raw font-family strings are merged into families by a canon file (canon.json): weights, Display/Text cuts, build hashes like __Inter_e798ec and vendor prefixes like Replit Diatype all fold into one family. A superfamily is not a face – Source Sans, Source Serif and Source Code Pro are three families, as are IBM Plex Sans, Mono and Serif. Strings the canon does not know go to a language model once a week with the canon as context – its additions land as a reviewed diff, never as an edit to a row. Each face page lists the raw strings it was merged from, so a bad merge is visible.

6. The table

One site, one vote per league. A face appears once two sites set it (“two-sites rule”). Ties are broken alphabetically. Arrows compare this week's rank with the previous archived week – “new” means the face was not in the table last week. Specimens are set at a 15px cap height so faces compare at equal size. Faces we cannot embed are shown in JetBrains Mono with a red dot (licensed) or grey dot (open or system, not hosted where we can reach it).

7. Archive

Every week's census.json, prov.json and panel.json are kept forever and never overwritten – movement is a diff of two archives. This week's data: prov.json, census.json, panel.json.

Changelog

Made by Stephen Kelman. Questions, corrections, withdrawals: get in touch.