GitHub Packages · MIT · Node ≥ 18

duplicate-beacon

Detect duplicate and near-duplicate titles, meta descriptions and page bodies across your built static site.

  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/duplicate-beacon
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 duplicate-beacon test/fixtures/badduplicate-beacon — /workspace/duplicate-beacon/test/fixtures/bad11 page(s); 11 compared (noindex and canonicalised pages excluded) HIGH     b.html 2 pages share the title "Widgets | Acme": /a.html, /b.html [duplicate-title]         ↳ fix: Give each page a unique, descriptive <title>.HIGH     b.html 2 pages have identical main content: /a.html, /b.html [duplicate-body]         ↳ fix: Consolidate them into one page, or point duplicates at the original with rel=canonical (or noindex them).MEDIUM   b.html 2 pages share the meta description "Buy widgets online from Acme today at low prices.": /a.html, /b.html [duplicate-description]         ↳ fix: Write a unique description for each page, or remove it and let search engines generate one.MEDIUM   c.html /a.html and /c.html are 94% similar [near-duplicate-body]
Sample output from the duplicate-beacon README.

Why it exists

Duplicate content splits ranking signals and makes search engines pick a URL for you. It usually comes from templates: every product page titled "Widgets | Acme", one meta description pasted into twenty pages, a print or AMP copy of every article, tag pages that are 95% the same. duplicate-beacon compares every built page with every other one, skipping pages that are noindex or already canonicalised elsewhere, and reports each group with the files, severity and a concrete fix.

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

Titles

exact duplicates, and titles that only differ by the site name once the common | Brand suffix or prefix is removed.

Meta descriptions

exact duplicates, near-duplicates (template text with a word swapped), and descriptions that just repeat the title.

Bodies

identical main content, and near-duplicates using 5-word shingles with MinHash/LSH candidate search and exact Jaccard similarity (--threshold, default 0.9). Navigation, header, footer and aside are left out of the main-content comparison.

Boilerplate

pages whose text is mostly shared with many other pages (info).

Usage

# Example

 npx duplicate-beacon dist

# Example

 npx duplicate-beacon dist --threshold 0.85 --fail-on medium

# Example

 npx duplicate-beacon public --ignore "/tags/**,/page/*" --min-words 80

# Example

 npx duplicate-beacon out --format json > duplicates.json

CI / GitHub Action

permissions:
  contents: read
  security-events: write   # only needed for the SARIF upload
jobs:
  duplicate-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/duplicate-beacon@v1
        with:
          path: dist
          fail-on: high
          sarif: duplicate-beacon.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: duplicate-beacon.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)
Checks
--threshold <0-1>Similarity for near-duplicate bodies (default 0.9)
--min-words <n>Ignore bodies shorter than n words (default 50)
--ignore <globs>Pages to leave out of the comparison
--no-template-checkDo not report boilerplate-heavy pages
Common
-c, --config <file>JSON config (default: .duplicate-beacon.json or "duplicateBeacon" 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