feat(audit): validate link anchors, not just link paths - #1073
Conversation
audit-phase2 checked that a link's path resolves in dist/ but never that its `#fragment` exists on the target page, and it skipped same-page `#frag` links entirely. That blind spot is why #1046 shipped dead anchors and why 73 links had quietly rotted by the time anyone looked — every heading rename since the Jekyll days broke its inbound links silently. New `BROKEN LINK ANCHORS` section covers three cases: - **missing-anchor** — the fragment has no matching id on the target page, whether the link is cross-page or same-page. - **anchor-on-redirect-stub** — the target is a `redirect_from` meta-refresh stub, which has no headings at all, so the fragment is dead however plausible it looks. Three of the 73 hid exactly here (`/transformations/python/`, `/transformations/r/`, `/extractors/other/aws-s3/`). Implementation notes: - `resolves()` kept its exact semantics (a bare directory still counts) and the candidate list moved into `candidates()`; the new `resolveFile()` returns only *file* candidates, since a directory resolves but cannot be read. - ids come from the `<body>` slice, like the existing link scan, and are cached per file — pages are visited once per inbound link. - `_top` and `starlight__*` ids are scaffolding, not authored anchors, so they are ignored; fragments are compared after `decodeURIComponent`. Verified in a production build: **BROKEN LINK ANCHORS: 0** on this branch, with every other category byte-identical (45 broken internal links / 0 missing images / 101 old-docs smells / 3 multi-h1 / 0 unclosed fences / 0 malformed tables), and the count agrees with the standalone checker used to fix the 73. Proved it fires by planting one of each case — a bad cross-page fragment, a bad same-page fragment, and a link into a redirect stub — all three reported, then reverted. Not wired into CI: the gate would also see the 45 pre-existing broken internal links and fail every PR. Separate decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…verride treatments
Owner decision 2026-08-05 (Jordan's option on the 07-30 call): Variables lives under
Flows. `/transformations/variables/` becomes `/flows/variables/`, with the API pages as
its children; the old URLs keep working via `redirect_from`.
The override of a configuration variable from a flow was documented in FOUR places:
`transformations/variables` "Flow Usage", `flows/index.md` "## Variables",
`flows/index.md` "Control Task Execution" (the only one with current UI), and the API
page's "Orchestrator Integration". They collapse into one section on the new page.
Corrected while merging them, against the live flow schemas:
- Conditional flows are `keboola.flow`; `keboola.orchestrator` is legacy. The API page's
"Orchestrator Integration" documented the legacy component as if it were current. It is
now "Driving Variables from a Flow", split into Conditional Flows (variable tasks,
merge-by-name, `variableOverrides` tri-state) and Legacy Flows (the existing task
payload, labelled legacy with a migration-guide link).
- The evaluation-sequence rules and the surrounding prose said "orchestration" throughout.
Shared Code is a different feature — it substitutes code, not values, and only for
transformations — so it moves out to `/transformations/shared-code/`, UI plus the API
section lifted off the Variables API page. Its 17 images move with it.
Also: the Jekyll-era escape `{{ "{{ multiplier " }}}}` rendered literally on the page;
`flows/index.md` drops from 391 to 227 lines; `workspace/sql-editor` linked variables
through an absolute help.keboola.com URL.
Verified: build clean, 309 pages. audit-phase2 broken links 54 → 54, missing images 0,
one old-docs smell fixed (156 → 155 total). #1073's anchor checker reports no new broken
anchors from these pages — the two on the API page are the `config-file` headings #1069
repairs. All five old URLs redirect, no duplicate heading ids.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jordanrburger
left a comment
There was a problem hiding this comment.
This is the best-reasoned PR in the batch right now. You went after why #1046 shipped dead anchors instead of just fixing them, and the anchor-on-redirect-stub case is a sharp catch — a meta-refresh stub has no headings at all, so any fragment into it is dead no matter how reasonable the link looks.
I checked the refactor: resolves() keeps its old semantics (empty path still returns true via cands.length === 0), and statSync is imported on line 12, so resolveFile() is fine.
One thing to fix before this merges — decodeURIComponent(frag) can throw:
node -e "decodeURIComponent('100%-off')"
# URIError: URI malformed
One link with a bare % in the fragment takes down the whole audit instead of reporting a finding, which is a rough failure mode for a quality gate specifically. Something like:
const decode = (s) => { try { return decodeURIComponent(s); } catch { return s; } };and use that in checkAnchor.
Two smaller notes, neither blocking:
- The id regex only matches double-quoted
id="...". Fine for what Astro emits today, just flagging the assumption. - Agreed on not wiring this into
branch.ymlyet with 45 pre-existing broken links sitting there. But worth a follow-up that diffs against a committed baseline so it gates on new findings only — otherwise this stays opt-in and rots the same way the anchors did.
On ordering: your base is #1070's branch, so today this is third behind #1069 → #1070. Consider rebasing straight onto main instead. It only touches AGENTS.md and scripts/audit-phase2.mjs, so there's no content overlap, and landing it early means it guards the rest of the queue — it would've caught the heading slug move in #1077 automatically.
Stacked on #1072 (base is
docs/stale-anchors), so its own output reads 0. Onmainit would report the 73 anchors that #1072 fixes.audit-phase2checked that a link's path resolves indist/but never that its#fragmentexists on the target page — and it skipped same-page#fraglinks entirely (audit-phase2.mjs:75). That blind spot is why #1046 shipped dead anchors, and why 73 links had quietly rotted by the time anyone looked: every heading rename since the Jekyll days broke its inbound links silently, with nothing in the build or the audit noticing.New
BROKEN LINK ANCHORSsection, two kinds:missing-anchor— the fragment has no matching id on the target page, cross-page or same-page.anchor-on-redirect-stub— the target is aredirect_frommeta-refresh stub, which has no headings at all, so the fragment is dead however plausible the link looks. Three of the 73 hid exactly here:/transformations/python/,/transformations/r/,/extractors/other/aws-s3/.Implementation:
resolves()keeps its exact semantics (a bare directory still counts as resolving); the candidate list moved intocandidates(), and the newresolveFile()returns only file candidates — a directory resolves but can't be read.<body>slice, like the existing link scan, and cached per file._topandstarlight__*are Starlight scaffolding, not authored anchors, so they're ignored; fragments compared afterdecodeURIComponent.Verified in a production build:
Not wired into
.github/workflows/branch.yml: that gate would also see the 45 pre-existing broken internal links and fail every PR. Worth doing once those are cleaned up — separate decision, separate PR.