H1
missing, more than one, hidden, very short, sharing no words with the <title>, and the same H1 on several pages.
GitHub Packages · MIT · Node ≥ 18
Audit the heading structure of your built static site: one visible H1 per page, a logical H1 → H2 → H3 outline, and headings that actually say something.
Audit heading structure in your built static site: exactly one visible H1, no skipped levels, no empty, generic, duplicate, image-only or overlong headings, H1 vs title agreement and site-wide duplicate H1s. Text/JSON/SARIF/Markdown, allowlist with expiry, 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/heading-beaconPublished 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 heading-beacon test/fixtures/badheading-beacon — /workspace/heading-beacon/test/fixtures/bad7 page(s); 18 heading(s) HIGH about.html /about.html has no <h1> [h1-missing] ↳ fix: Add one <h1> describing the page (e.g. "About Acme").HIGH index.html:1 Empty <h2> [heading-empty] ↳ fix: Add text to the heading, or remove it if it is only for styling.HIGH index.html:1 Empty <h2> (image without alt text) [heading-empty] ↳ fix: Add text to the heading, or remove it if it is only for styling.MEDIUM hidden.html:1 /hidden.html: the only <h1> ("Hidden page") is hidden [h1-hidden] ↳ fix: Make the main heading visible; hidden headings carry little weight and confuse screen-reader users.MEDIUM index.html:1 / has 2 <h1> elements: "Welcome", "Second main heading here" [h1-multiple]
Headings are the outline search engines, answer engines and screen readers use to understand a page. Templates quietly break that outline: a logo wrapped in an “<h1>” adds a second H1, a card component jumps from “<h2>” to “<h4>”, a CMS leaves an empty “<h2>” behind, or every section is called "Overview". heading-beacon reads every built HTML page and reports the file, line, severity and a concrete fix for each problem, before you deploy.
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.
missing, more than one, hidden, very short, sharing no words with the <title>, and the same H1 on several pages.
skipped levels (<h2> → <h4>), lower-level headings before the H1 in the main content (headings in <header>, <nav>, <aside> and <footer> are ignored), and long pages with no <h2>.
empty headings (including images without alt), image-only headings, generic or keyword-empty text ("Introduction", "Read more", "Section 3", lorem ipsum, plus your own list), duplicates at the same level, and overlong headings.
Page has no visible <h1> (default: high)
Page has more than one <h1> (default: medium)
Heading has no text (and no image alt text) (default: high)
Heading skips a level (e.g. <h2> followed by <h4>) (default: medium)
A lower-level heading appears before the <h1> (default: low)
# Example
npx heading-beacon dist# Example
npx heading-beacon dist --fail-on medium# Example
npx heading-beacon _site --max-length 90 --generic "Our services,Learn more"# Example
npx heading-beacon out --allow-multiple-h1 --format markdown > headings.mdpermissions:
contents: read
security-events: write # only needed for the SARIF upload
jobs:
heading-beacon:
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/heading-beacon@v1
with:
path: dist
fail-on: high
sarif: heading-beacon.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: heading-beacon.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) |
| Checks | |
--max-length <n> | Report headings longer than n characters (default 120) |
--long-page-words <n> | Pages with more words than this need an <h2> (default 600) |
--generic <words> | Extra generic heading texts to flag (comma-separated) |
--allow-multiple-h1 | Do not report pages with several <h1> |
| Common | |
-c, --config <file> | JSON config (default: .heading-beacon.json or "headingBeacon" 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