GitHub Packages · MIT · Node ≥ 18

font-pulse

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. 1 Point the @mrlouiswright scope at GitHub Packages in .npmrc

    @mrlouiswright:registry=https://npm.pkg.github.com
  2. 2 Install

    npm i -D @mrlouiswright/font-pulse
Installing from GitHub Packages needs a token

Published 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.

~/project — zsh
$ 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]
Sample output from the font-pulse README.

Why it exists

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.

Features

Loading

font-display missing or block/auto; Google Fonts URLs without display=swap; third-party font hosts without preconnect.

Preloads

self-hosted fonts with no preload, preloads without crossorigin (double download), preloads of files no @font-face uses, and too many font preloads.

Formats and size

@font-face without a WOFF2 source, font files over 100 kB (subset them), large fonts without unicode-range.

Waste

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).

Understands real CSS

the font shorthand, CSS custom properties, variable-font weight ranges, @import chains and inline styles.

Usage

# 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_SUMMARY

CI / GitHub Action

permissions:
  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 }

Options & flags

FlagDescription
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-urlsPages 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-rulesPrint rules and default severities
--no-fixesHide fix suggestions in text output
--no-colorDisable colours

Full reference, config file format and API in the README.

Support this project

Support open-source security tools

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

Portrait of Louis Wright

About the author

Louis Wright

Cyber security researcher and software engineer based in the UK.

More tools by Louis Wright

58 tools

Sentinels security scanners & CI gates · 20