Loading
font-display missing or block/auto; Google Fonts URLs without display=swap; third-party font hosts without preconnect.
GitHub Packages · MIT · Node ≥ 18
Audit how your built site loads web fonts: font-display, preloads, WOFF2, subsetting, unused weights and total font payload.
Audit web font loading in a built static site: font-display, preload with crossorigin, unused preloads, WOFF2, heavy or unsubset fonts, unicode-range, unused faces and weights, per-page font payload, Google Fonts display=swap and preconnect. SARIF, GitHub Action. Zero deps.
1 Point the @mrlouiswright scope at GitHub Packages in .npmrc
@mrlouiswright:registry=https://npm.pkg.github.com2 Install
npm i -D @mrlouiswright/font-pulsePublished to GitHub Packages, not npmjs.com. GitHub Packages needs a GitHub token with read:packages to install, even for public packages: add //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} to .npmrc or run npm login --registry=https://npm.pkg.github.com.
$ npx font-pulse test/fixtures/badfont-pulse — /workspace/font-pulse/test/fixtures/bad1 page(s), 1 with web fonts, 4 font file(s) MEDIUM css/fonts.css:2 @font-face "brand" has no font-display [font-display-missing]MEDIUM css/fonts.css:2 @font-face "brand" offers woff, ttf but no WOFF2 [font-no-woff2]MEDIUM css/fonts.css:2 fonts/brand.woff is 88 KB [font-heavy]MEDIUM css/fonts.css:3 @font-face "brand" uses font-display: block [font-display-missing]MEDIUM index.html / declares 3 font file(s) totalling 195 KB (budget 150 KB) [font-payload]MEDIUM index.html:2 /: <link rel="preload" href="/fonts/brand.woff"> has no crossorigin [font-preload-no-crossorigin]
Web fonts are a quiet source of slow text and layout shift: a missing font-display hides text for up to three seconds, a preload without crossorigin downloads the font twice, and a design system that ships nine weights when the pages use two costs hundreds of kilobytes. font-pulse reads the HTML and CSS in your static build output, follows linked stylesheets and @import, and checks every @font-face against how the site actually uses it.
CLI, library and GitHub Action. Node.js 18+, zero runtime dependencies. Works on any static build output (Astro, Next.js “output: 'export'”, Eleventy, Hugo, Jekyll, Docusaurus, VitePress, plain HTML) and runs offline: point it at the folder your build writes. By Louis Wright.
font-display missing or block/auto; Google Fonts URLs without display=swap; third-party font hosts without preconnect.
self-hosted fonts with no preload, preloads without crossorigin (double download), preloads of files no @font-face uses, and too many font preloads.
@font-face without a WOFF2 source, font files over 100 kB (subset them), large fonts without unicode-range.
families declared but never used in font-family, weights and italic faces never used (headings and <strong> count as bold), more than six font files per page, and a per-page payload budget (default 300 kB).
the font shorthand, CSS custom properties, variable-font weight ranges, @import chains and inline styles.
# Example
npx font-pulse dist# Example
npx font-pulse public --max-payload-kb 200 --fail-on medium# Example
npx font-pulse _site --format markdown >> $GITHUB_STEP_SUMMARYpermissions:
contents: read
security-events: write # only needed for the SARIF upload
jobs:
font-pulse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { persist-credentials: false }
- run: npm ci && npm run build # produce the static site first
- uses: MrLouisWright/font-pulse@v1
with:
path: dist
fail-on: high
sarif: font-pulse.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: font-pulse.sarif }
| Flag | Description |
|---|---|
| Site | |
--base-url <url> | Public URL of the site, e.g. https://example.com (default: inferred from canonical/og:url) |
--include <globs> | Only check these pages (file or URL path globs, comma-separated) |
--exclude <globs> | Skip these pages |
--clean-urls | Pages are served without .html (about.html -> /about) |
| Fonts | |
--max-payload-kb <n> | Font budget per page in KB (default 300) |
--max-file-kb <n> | Size above which a font file should be subset, in KB (default 100) |
| Common | |
-c, --config <file> | JSON config (default: .font-pulse.json or "fontPulse" in package.json) |
-f, --format <fmt> | text (default) | json | sarif | markdown |
--json <file> | Also write a JSON report |
--sarif <file> | Also write SARIF 2.1.0 |
--markdown <file> | Also write Markdown (e.g. for a job summary) |
--fail-on <sev> | critical | high | medium | low | info | none (default: high) |
--list-rules | Print rules and default severities |
--no-fixes | Hide fix suggestions in text output |
--no-color | Disable colours |
Full reference, config file format and API in the README.
Support this project
I build free, open-source tools that help developers ship safer websites. Every coffee helps keep them maintained, documented and free for everyone.
Buy me a coffee (opens Stripe in a new tab)Secure checkout by Stripe