Skip to content

CI: anchor-integrity guard for in-page #fragment links - #73

Merged
rivassec merged 1 commit into
mainfrom
anchor-integrity-guard
Sep 12, 2026
Merged

CI: anchor-integrity guard for in-page #fragment links#73
rivassec merged 1 commit into
mainfrom
anchor-integrity-guard

Conversation

@rivassec

Copy link
Copy Markdown
Owner

What

Adds a CI guard that catches broken in-page anchor links (#fragment targets) in the built site - a class lychee (link-rot.yml) does not check. lychee verifies a linked page 200s; it does not verify the URL fragment lands on a real element. When a heading is renamed the anchor silently rots: the link still resolves, it just jumps to the top of the page.

How

  • scripts/check_anchors.py: for every built HTML page in output/, resolves every internal link carrying a #fragment and asserts a matching id (or legacy name=) exists on the destination doc.
    • same-page href="#id"
    • cross-page href="post.html#id" (document- and root-relative)
    • SITEURL-absolute href="https://rivassec.com/post.html#id"
    • Ignores: external hosts, href="#" alone, query-only links, and fragments to pages not built in output/ (missing pages are link rot, lychee's lane).
    • Emits GitHub ::error file=...:: annotations, exits 1 on any violation, one-line summary to stderr. Builds the site first (pelican content -o output -s publishconf.py, module-form fallback), matching the house style of check_canonical_noindex.py / check_link_graph.py.
  • tests/test_check_anchors.py: stdlib unittest, python3 -m unittest discover tests-compatible, fixture HTML trees covering valid/broken same-page and cross-page fragments plus the ignore rules.
  • .github/workflows/anchor-check.yml: push-to-main + pull_request + workflow_dispatch, mirroring link-graph-check.yml.

Pre-existing broken anchors

None. Ran against a real build of the current site: 117 pages, 457 fragment links checked, 0 broken. The guard passes clean on main content.

Tests

python3 -m unittest discover tests - all 38 tests pass (26 existing + 12 new).

Add scripts/check_anchors.py: for every built HTML page in output/, resolve
every internal link carrying a #fragment (same-page href="#id", cross-page
href="post.html#id", and SITEURL-absolute href="https://rivassec.com/post.html#id")
to its destination doc and assert an element with that id (or legacy name=)
exists. Reports each unresolved fragment as a GitHub ::error annotation, exits 1
on any violation. This is a class lychee does not check: lychee verifies the
linked page 200s, not that the fragment lands on a real element.

External hosts, href="#" alone, query-only links, and fragments to pages not
built in output/ (link rot, lychee's job) are ignored.

Add tests/test_check_anchors.py (stdlib unittest, discover-compatible) and
.github/workflows/anchor-check.yml (mirrors link-graph-check.yml).
@rivassec
rivassec merged commit 90701d9 into main Sep 12, 2026
18 checks passed
@rivassec
rivassec deleted the anchor-integrity-guard branch September 12, 2026 02:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant