Skip to main content

static-site-audit

test

Find the bugs that make buttons silently do nothing in no-build HTML/JS sites — plain .html pages with classic <script> tags, no React, no bundler.

Linters for that world mostly don't exist, so these checks came out of real bugs in a production site with 80+ pages and ~2,000 buttons:

Check Catches Why it's hard to see
dead-buttons Buttons no script can reach With event delegation there's no listener on the button to look for
dup-ids Duplicate ids, and ids your scripts inject that already exist in the HTML getElementById returns the first one; the other never updates
global-collisions Two classic scripts on one page declaring the same top-level let/const/class The later script throws Identifier 'x' has already been declared and none of it runs — the page just looks empty
SSA.reachability (browser) Same as dead-buttons, for buttons rendered by JS They don't exist in the source
SSA.clicks (browser) Controls that throw, log errors, or change nothing when pressed — with writes blocked
SSA.occlusion (browser) Controls covered by a fixed overlay, so a tap hits the overlay instead el.click() skips hit-testing, so click tests pass

Install

pip install git+https://github.com/gallakorea/static-site-audit

Python 3.8+, no dependencies.

Static checks (CLI)

static-site-audit path/to/site
static-site-audit . --check dup-ids --skip 'admin|-test\.html'
static-site-audit . --json > report.json
[dead-buttons] index.html
  ✗ "Nobody listens" — handles orphan-btn not found in any script

[global-collisions] index.html
  ✗ `store` declared at top level in js/a.js, js/b.js — js/b.js will fail to run

2 error(s), 0 warning(s)
Option
--check NAME Run only this check (repeatable)
--skip REGEX Ignore pages/scripts whose path matches
--exclude-dir NAME Skip a directory (node_modules, vendor, dist, build are always skipped)
--one-scope global-collisions: treat all scripts as one document, for SPA shells that inject page scripts
--strict Exit 1 on warnings too
--json Machine-readable output

Exit code is 1 when there are errors, so it drops into CI as is.

GitHub Actions

- uses: actions/checkout@v4
- uses: gallakorea/static-site-audit@v0
  with:
    path: public          # site root, default "."
    args: --skip admin    # any CLI options

pre-commit

- repo: https://github.com/gallakorea/static-site-audit
  rev: v0
  hooks:
    - id: static-site-audit
      args: [--skip, admin]

How dead-buttons decides

Many sites wire buttons like this:

document.addEventListener("click", e => { if (e.target.closest(".buy")) buy(); });

so "does the button have a listener?" can't be answered. Instead, every handle the button carries — id, classes, data-* names and values — is searched for in all scripts, written the way a selector or lookup would write it ("#buy", '.buy', [data-buy]). If none is found, the four nearest ancestors get the same test (a parent may delegate). Buttons with onclick, type="submit"/"reset" and hidden layout placeholders are skipped.

It's a heuristic: a class name that appears in a script for an unrelated reason counts as "reachable". It errs on the side of fewer false alarms.

Runtime checks (browser)

Open your site, sign in if the pages need it, paste browser/static-site-audit.js into the console, then:

await SSA.reachability(["index", "search", "item.html?id=3"])
await SSA.clicks("settings")
await SSA.occlusion("checkout")

Each page is loaded in a hidden same-origin iframe. Options go last: { wait: 2000, width: 420, height: 900, skip: "nav,.tabbar" }.

SSA.clicks is safe to run signed in. Non-GET fetch/XHR requests never reach the network, confirm() always answers no, and the iframe can't navigate your tab. Because writes are faked, APIs that read through POST (RPC endpoints, GraphQL) get odd responses and may cause errors your real app never has — check before filing a bug. noEffect is a candidate list, not a verdict.

Contributing

Issues and PRs are welcome — especially false positives and false negatives from real sites. Please include a minimal HTML/JS snippet that reproduces it; it can go straight into tests/fixtures/. Run the tests with:

pip install -e . && python -m unittest discover -s tests

License

MIT

Metadata

Release files for static-site-audit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for static-site-audit 0.1.0
File Size Uploaded Provenance Actions
static_site_audit-0.1.0.tar.gz 11.6 kB With provenance Details for static_site_audit-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for static-site-audit 0.1.0
File Interpreter ABI Platform Actions
static_site_audit-0.1.0-py3-none-any.whl Python 3 none any Details for static_site_audit-0.1.0-py3-none-any.whl

Total release size: 22.4 kB

Release files / static_site_audit-0.1.0.tar.gz

Download URL static_site_audit-0.1.0.tar.gz
Size 11.6 kB
Tags Source
SHA-256 checksum
How to use checksums
5991cf6e6855f6a1652d81ab84fb85b49746fed095ebfe2131588cbfeab90801
BLAKE2b-256 checksum
How to use checksums
2257007220cbdc0a51c41b87465a10ac88eed0f719c495b5fca7e3dd4ece1df6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / static_site_audit-0.1.0-py3-none-any.whl

Download URL static_site_audit-0.1.0-py3-none-any.whl
Size 10.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b4b61734740da29f8518e4c0b4e3d401be9a37e3d3da8a62d14de92b1c854ad0
BLAKE2b-256 checksum
How to use checksums
f2ae155d4d009c3382318eea7ec0ff28f9342ef539b749365b93437200bd2671
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for static-site-audit 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for static_site_audit-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page