static-site-audit
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.