Fingerprinted assets
content-hashed file names and framework output folders (_next/static, _astro, _app/immutable, _nuxt, CRA static/js) need max-age=31536000, immutable; short, missing, no-cache and no-store policies are reported.
GitHub Packages · MIT · Node ≥ 18
Check that your static hosting config caches the right things: fingerprinted assets for a year, HTML revalidated, and nothing stale after a deploy.
Check static hosting caching for performance: long max-age + immutable on fingerprinted assets, revalidation on HTML, no long cache on unhashed files, Vary and Set-Cookie pitfalls, and precompressed .br/.gz files that match. Reads _headers, vercel.json, netlify.toml, firebase.json. SARIF, 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/cache-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 cache-pulse test/fixtures/badcache-pulse — /workspace/cache-pulse/test/fixtures/bad5 file(s) (2 fingerprinted), 4 header rule(s) HIGH assets/app.3f2a9c1b.js.gz assets/app.3f2a9c1b.js.gz does not decompress to the current assets/app.3f2a9c1b.js [cache-precompressed-stale]HIGH vercel.json:3 HTML /index.html is cached for 86400s (public, max-age=86400) [cache-html-long]HIGH vercel.json:4 Fingerprinted /assets/app.3f2a9c1b.js is cached only 3600s (public, max-age=3600) (+1 more file(s) with the same rule) [cache-hashed-short]HIGH vercel.json:5 Unfingerprinted /styles.css is cached immutable (public, max-age=31536000, immutable) [cache-unhashed-immutable]MEDIUM styles.css styles.css (3 KB) has no .br or .gz sibling [cache-precompressed-missing]MEDIUM vercel.json:4 Vary: * on /assets/app.3f2a9c1b.js (+1 more file(s) with the same rule) [cache-vary-star]
Caching is the cheapest performance win and the easiest to get backwards. Fingerprinted bundles (index-BdT9x2Qa.js) should be cached for a year with immutable; HTML and unfingerprinted files (/styles.css, /logo.png) must revalidate, or visitors keep yesterday's site. cache-pulse reads the header rules in your build output or repo (_headers, vercel.json, netlify.toml, firebase.json, serve.json), resolves the Cache-Control that applies to every file, and reports the mistakes, grouped by the header rule that causes them.
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.
content-hashed file names and framework output folders (_next/static, _astro, _app/immutable, _nuxt, CRA static/js) need max-age=31536000, immutable; short, missing, no-cache and no-store policies are reported.
cached without revalidation or immutable is high severity; s-maxage for the CDN with max-age=0 for browsers is accepted.
immutable or a max-age over a week on /styles.css or /app.js means users do not see deploys; missing policies are noted.
Vary: *, Set-Cookie on static files (no CDN caching), Content-Encoding on images and fonts.
when the build ships .br/.gz files, every text asset over 1.4 kB should have one, and each is decompressed and compared with the original to catch stale copies.
# Example
npx cache-pulse dist# Example
npx cache-pulse . --include '/assets/**'# Example
npx cache-pulse public --expect-precompressed --fail-on mediumpermissions:
contents: read
security-events: write # only needed for the SARIF upload
jobs:
cache-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/cache-pulse@v1
with:
path: dist
fail-on: high
sarif: cache-pulse.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: cache-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) |
| Caching | |
--expect-precompressed | Require .br/.gz siblings for text assets even if none exist yet |
| Common | |
-c, --config <file> | JSON config (default: .cache-pulse.json or "cachePulse" 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