Skip to content

feat(audit): validate link anchors, not just link paths - #1073

Open
Iamfle4ka wants to merge 2 commits into
mainfrom
docs/audit-anchors
Open

feat(audit): validate link anchors, not just link paths#1073
Iamfle4ka wants to merge 2 commits into
mainfrom
docs/audit-anchors

Conversation

@Iamfle4ka

Copy link
Copy Markdown
Collaborator

Stacked on #1072 (base is docs/stale-anchors), so its own output reads 0. On main it would report the 73 anchors that #1072 fixes.

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 (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 ANCHORS section, 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 a redirect_from meta-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 into candidates(), and the new resolveFile() returns only file candidates — a directory resolves but can't be read.
  • ids are parsed from the <body> slice, like the existing link scan, and cached per file.
  • _top and starlight__* are Starlight scaffolding, not authored anchors, so they're ignored; fragments compared after decodeURIComponent.
  • One line added to AGENTS.md: run the audit after renaming a heading, not just for link/image changes.

Verified in a production build:

  • BROKEN LINK ANCHORS: 0 on this branch, and every other category byte-identical — 45 broken internal links / 0 missing images / 101 old-docs smells / 3 multiple-h1 / 0 unclosed fences / 0 malformed tables, total 149.
  • Agrees with the standalone checker used to fix the 73 (8480 anchors, 0 broken).
  • Proved it actually fires by planting one of each case and reverting:
BROKEN LINK ANCHORS: 3
  [missing-anchor] /catalog/  →  /storage/jobs/#no-such-section
  [missing-anchor] /catalog/  →  #no-such-heading-here
  [anchor-on-redirect-stub] /catalog/  →  /transformations/python/#development-tutorial

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.

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>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
connection-docs Ready Ready Preview Aug 4, 2026 9:37am

Request Review

Base automatically changed from docs/stale-anchors to docs/em-dash-prose August 4, 2026 13:11
Iamfle4ka pushed a commit that referenced this pull request Aug 5, 2026
…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 jordanrburger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The id regex only matches double-quoted id="...". Fine for what Astro emits today, just flagging the assumption.
  2. Agreed on not wiring this into branch.yml yet 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.

Base automatically changed from docs/em-dash-prose to main August 5, 2026 20:28
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.

2 participants