Skip to main content
The documentation joins three layers — the OP Stack specifications, the component source repositories, and these docs — with links. Links rot silently: a retired spec path keeps “working” through a hand-maintained redirect table, a commit-pinned contract link never 404s while teaching a two-generations-old architecture, and a misplaced tracking parameter breaks an anchor without breaking the page. This page defines one canonical form for each link target, and every rule on it is enforced by a deterministic linter (scripts/lint-link-policy.mjs). This policy is an annex of the content guide: the guide decides where content lives and when to link instead of restate; this page defines how those links must be written.

Linking the specs

Always link the rendered site, on its current paths.
  • Link https://specs.optimism.io/..., never a GitHub blob of a file under the specs repo’s specs/ directory — every specs/**.md source has a rendered page, and the rendered page is the canonical, navigable form. (GitHub links to non-rendered specs-repo files, such as book.toml, are fine.)
  • Use the page’s current path. Retired paths (for example /experimental/fault-proof/..., now /fault-proof/...) survive only through a hand-maintained redirect table in the specs repo’s book.toml and can disappear without notice. The linter vendors that redirect table and reports the current path to use.
  • Deep anchors must resolve. An anchor like #frame-format must match a real heading slug in the target page. The linter resolves every specs anchor against a checkout of the specs source (--specs-src), so a heading rename upstream surfaces as a lint failure here instead of a silently dead fragment.

Query parameters come before the fragment

UTM decoration (or any query string) goes before the #fragment, per the URL standard — a query appended after the fragment becomes part of the fragment, and the anchor never resolves.

Linking source code

Prefer floating links; badge every pin.
  • Links that track a branch (.../blob/develop/...) are the default: they follow the code and never teach a stale layout.
  • A link pinned to a commit sha or release tag (.../blob/v1.1.4/..., .../blob/op-contracts/v1.6.0/..., .../blob/62c7f3b0.../...) is allowed only when the pin is the point — quoting behavior at a specific release — and it must carry an as of `<tag>` badge on, or immediately adjacent to, the same line:
    The badge tells the reader the link is a snapshot, and tells the maintenance sweep which pins are deliberate. An unbadged pin is presumed to be accidental staleness and fails the linter.
  • Internal page links are root-relative: /chain-operators/..., never ./sibling-page or a bare word. The target page must exist (or be covered by a redirect in docs.json).
  • Static assets are referenced by their on-disk path from the docs root, including the public/ prefix: /public/img/....

The linter

scripts/lint-link-policy.mjs enforces all of the above plus dead-internal-link detection. It is dependency-free and offline-deterministic; Mintlify’s mint broken-links serves as an advisory second opinion on internal links.
Violations that predate the linter are recorded in scripts/lint-link-policy.baseline.json, so a run flags only new violations. The baseline is a burn-down list, not an allowlist: remediation batches shrink it with --update-baseline, and pull requests must never grow it. If the linter flags a link you believe is a deliberate exception, raise it in review — do not rebaseline silently. Enforcement runs as a scheduled, review-gated docs automation plus the local runs above; the linter, its baseline, and its embedded self-test fixtures all live under docs/public-docs/.