diff --git a/CHANGELOG.md b/CHANGELOG.md index da9e37c..7fcaddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,14 @@ how to verify it, e.g. view-source or a validator). State how the change was ver --- +## [v0.1.24] — 2026-07-31 + +### Fix — infer fully qualified (LTS) versions in `link-hextra` (`layouts/_shortcodes/link-hextra.html`, `tests/link-hextra-lts-version.spec.ts`) + +- **Every reuse-nested cross-doc link on a fully qualified version tree — e.g. agentgateway's new `2026.7.1` LTS docs — resolves to `/latest/…` instead of `/2026.7.1/…`, and the build logs a `link-hextra called with no version and could not infer one from …` WARN for each one (which fails `hugo-warnings.spec.ts`, so the LTS branch can't go green).** `link-hextra` infers the target version by regex-matching the page's permalink when no `version=` was passed, and both of its patterns accepted only `\d+\.\d+\.x`, `latest`, or `main`. A three-segment version matched neither, so inference fell through to the last-resort `$ver = "latest"` fallback. This is not a version-tree-only concern: the pages that hit it are ordinary `{{< reuse "…" >}}` stubs, and `reuse.html` injects `version=` into nested `link-hextra` calls only when the page passed a version POSITIONALLY (`$parentVersion`) — the version it resolves from the permalink for asset lookup (`$resolvedVersion`, which matches against `params.versions` and therefore handled `2026.7.1` fine) is never injected. So on any product whose reuse stubs omit the positional version, the shortcode's own regex is the only thing standing between the page and a wrong-version link. `2.3.x`-style trees were unaffected only because they happen to match the narrow pattern. Fix: widen both alternations to `\d+\.\d+\.(?:x|\d+)`, so `X.Y.x` and `X.Y.Z` both infer. Nothing else in the shortcode changes — the language-prefix strip, the `reference/api` subpage routing, and the baseURL handling are untouched, and a version that still can't be inferred keeps warning and falling back to `latest`. +- **Consumer note — `solo-io/docs` carries a LOCAL `layouts/_shortcodes/link-hextra.html` override with the identical widening**, because the LTS branch there can't wait on a pin bump (it pins v0.1.23). That override is marked for deletion once the hub bumps to a release carrying this fix; while it exists, module-side `link-hextra` changes do NOT reach the hub. Same shape as the v0.1.22 `reuse.html` override note. +- Observable in production: on the [agentgateway 2026.7.1 WAF overview](https://docs.solo.io/agentgateway/2026.7.1/security/waf/overview/), the in-body "Custom rules" / "IP filtering" links point at `/agentgateway/latest/security/waf/…` before the fix and `/agentgateway/2026.7.1/security/waf/…` after — links that silently retarget as soon as `latest` moves on. Verified against the hub's real build (`make build-preview PRODUCT=agentgateway`): the pre-fix log carried 60+ `link-hextra called with no version` WARNs across the `2026.7.1` tree and zero after, the only remaining WARN being the allowlisted `.Site.Data` deprecation; the rendered page's `custom-rules` hrefs are `/agentgateway/2026.7.1/security/waf/custom-rules/` (plus the `#body-inspection` anchor), with `/latest/` left only in the version-dropdown and nav index where it belongs; and the hub's `content` project passes 130/130. `tests/link-hextra-lts-version.spec.ts` extracts both `findRE` patterns from the shipped shortcode and exercises them directly (source-level, since the bundled fixture has no fully qualified version tree and adding one would shift the URLs the rest of the suite asserts on) — it fails if either alternation is narrowed back to `X.Y.x`-only. Full `static` project green (1307 passed) on the enterprise fixture. Takes effect on the hub via its override; for other consumers, when they bump their extras pin. + ## [v0.1.23] — 2026-07-29 ### Fix — actually hide the llms.txt discovery directive from sighted readers (`layouts/_partials/docs-llms-directive.html`) diff --git a/layouts/_shortcodes/link-hextra.html b/layouts/_shortcodes/link-hextra.html index b58fe73..7852d3c 100644 --- a/layouts/_shortcodes/link-hextra.html +++ b/layouts/_shortcodes/link-hextra.html @@ -24,11 +24,12 @@ {{- with .Site.LanguagePrefix -}} {{- $relURL = replace $relURL (printf "%s/" .) "/" -}} {{- end -}} - {{- with findRE `(?:kgateway|agentgateway|gateway|envoy)/(\d+\.\d+\.x|latest|main)` $relURL 1 -}} + {{- /* Versions are either `X.Y.x` (2.3.x) or fully qualified `X.Y.Z` (2026.7.1, LTS). */ -}} + {{- with findRE `(?:kgateway|agentgateway|gateway|envoy)/(\d+\.\d+\.(?:x|\d+)|latest|main)` $relURL 1 -}} {{- $ver = replaceRE `^.*/` "" (index . 0) -}} {{- end -}} {{- if not $ver -}} - {{- with findRE `^/(\d+\.\d+\.x|latest|main)` $relURL 1 -}} + {{- with findRE `^/(\d+\.\d+\.(?:x|\d+)|latest|main)` $relURL 1 -}} {{- $ver = index . 0 | strings.TrimPrefix "/" -}} {{- end -}} {{- end -}} diff --git a/playwright.config.ts b/playwright.config.ts index 698b5d2..9a7a39f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -58,7 +58,7 @@ export default defineConfig({ { name: "static", testMatch: - /static\.spec\.ts$|versioning\.spec\.ts$|versioned-image-auto\.spec\.ts$|version-nested-list\.spec\.ts$|version-inside-fence\.spec\.ts$|version-table-row\.spec\.ts$|version-cards\.spec\.ts$|shortcode-contexts\.spec\.ts$|conditional-block\.spec\.ts$|cond-reuse-table\.spec\.ts$|reuse-version-remap\.spec\.ts$|llms-directive\.spec\.ts$|callout-in-table-cell\.spec\.ts$|callout-in-reuse-tab\.spec\.ts$|auto-cards\.spec\.ts$|card-image\.spec\.ts$|presence\.spec\.ts$|github-shortcode\.spec\.ts$|language-switch\.spec\.ts$|redirect\.spec\.ts$|sidebar-linktitle\.spec\.ts$|sidebar-flat\.spec\.ts$|link-hextra-apiref\.spec\.ts$|build-resilience\.spec\.ts$|page-feedback\.spec\.ts$|footnotes-after-cards\.spec\.ts$|callout-icon\.spec\.ts$|custom-alert\.spec\.ts$|docs-tabs\.spec\.ts$|meta-description\.spec\.ts$|link-hextra-lang-prefix\.spec\.ts$|warn-missing-description\.spec\.ts$/, + /static\.spec\.ts$|versioning\.spec\.ts$|versioned-image-auto\.spec\.ts$|version-nested-list\.spec\.ts$|version-inside-fence\.spec\.ts$|version-table-row\.spec\.ts$|version-cards\.spec\.ts$|shortcode-contexts\.spec\.ts$|conditional-block\.spec\.ts$|cond-reuse-table\.spec\.ts$|reuse-version-remap\.spec\.ts$|llms-directive\.spec\.ts$|callout-in-table-cell\.spec\.ts$|callout-in-reuse-tab\.spec\.ts$|auto-cards\.spec\.ts$|card-image\.spec\.ts$|presence\.spec\.ts$|github-shortcode\.spec\.ts$|language-switch\.spec\.ts$|redirect\.spec\.ts$|sidebar-linktitle\.spec\.ts$|sidebar-flat\.spec\.ts$|link-hextra-apiref\.spec\.ts$|build-resilience\.spec\.ts$|page-feedback\.spec\.ts$|footnotes-after-cards\.spec\.ts$|callout-icon\.spec\.ts$|custom-alert\.spec\.ts$|docs-tabs\.spec\.ts$|meta-description\.spec\.ts$|link-hextra-lang-prefix\.spec\.ts$|link-hextra-lts-version\.spec\.ts$|warn-missing-description\.spec\.ts$/, }, // Consumer-content specs. Every spec here reads the CONSUMER's own content // — either the built HTML tree (target.builtRoot) or the markdown source diff --git a/tests/link-hextra-lts-version.spec.ts b/tests/link-hextra-lts-version.spec.ts new file mode 100644 index 0000000..b74e310 --- /dev/null +++ b/tests/link-hextra-lts-version.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; + +// Source-level guard for fully qualified (LTS) versions in link-hextra. +// +// link-hextra infers the target version by regex-matching the page's permalink. +// The regexes originally accepted only `X.Y.x` (2.3.x), `latest`, and `main`. +// When a product ships a fully qualified LTS tree (e.g. +// `/agentgateway/2026.7.1/...`), inference fell through to the "latest" +// fallback: every reuse-nested link on those pages pointed at `/latest/...` +// instead of the LTS tree, and the build emitted a `link-hextra called with no +// version` WARN for each one (which hugo-warnings.spec.ts fails on). +// +// Why a SOURCE check, not a rendered-output check: the bundled fixture has no +// fully qualified version tree, and adding one shifts the page URLs the rest of +// the suite asserts on. So this extracts the version alternation from the +// shipped shortcode and exercises it directly. Self-skips when the file isn't +// at the module-relative path (a consumer build, where the module lives under +// hugo_cache rather than ../layouts). + +const SHORTCODE = path.resolve( + __dirname, + "../layouts/_shortcodes/link-hextra.html", +); + +// Strip Go/Hugo template comments (`{{- /* … */ -}}`) so the assertions match +// ACTIVE code, not the explanatory comments (which also spell out version +// patterns). +function activeSrc(): string { + return fs + .readFileSync(SHORTCODE, "utf8") + .replace(/\{\{-?\s*\/\*[\s\S]*?\*\/\s*-?\}\}/g, ""); +} + +// The two version-inference regexes, as written in the template: +// 1. product-prefixed, e.g. `/agentgateway/2026.7.1/security/waf/overview/` +// 2. root-relative, e.g. `/2026.7.1/security/waf/overview/` (preview builds +// whose baseURL path is the product) +const PATTERNS: Array<{ label: string; extract: RegExp }> = [ + { + label: "product-prefixed", + extract: /findRE\s+`((?:\(\?:)?kgateway[^`]*)`/, + }, + { + label: "root-relative", + extract: /findRE\s+`(\^\/\([^`]*)`/, + }, +]; + +// Versions that must infer, and the permalinks they appear in. +const MUST_MATCH: Array<[string, string, string]> = [ + ["2026.7.1", "/agentgateway/2026.7.1/security/waf/overview/", "/2026.7.1/security/waf/overview/"], + ["2.3.x", "/agentgateway/2.3.x/security/waf/overview/", "/2.3.x/security/waf/overview/"], + ["latest", "/agentgateway/latest/security/waf/overview/", "/latest/security/waf/overview/"], + ["main", "/kgateway/main/security/waf/overview/", "/main/security/waf/overview/"], +]; + +test.describe("link-hextra infers fully qualified (LTS) versions", () => { + test.skip( + !fs.existsSync(SHORTCODE), + "link-hextra.html not at the module-relative path (consumer build)", + ); + + for (const { label, extract } of PATTERNS) { + test(`the ${label} version regex accepts X.Y.Z as well as X.Y.x`, () => { + const src = activeSrc(); + const m = src.match(extract); + expect( + m, + `${label} version-inference \`findRE\` not found — the shortcode ` + + "changed shape; re-check that fully qualified versions still infer.", + ).not.toBeNull(); + + // Go's regexp syntax is RE2, but these patterns use only constructs JS + // shares, so they can be exercised directly. + const re = new RegExp(m![1]); + const idx = label === "product-prefixed" ? 1 : 2; + + for (const row of MUST_MATCH) { + const [version, ...permalinks] = row; + const permalink = permalinks[idx - 1]; + const hit = permalink.match(re); + expect( + hit?.[1], + `\`${version}\` did not infer from ${permalink} — links on that ` + + "version tree fall back to `latest` and the build WARNs.", + ).toBe(version); + } + }); + } + + test("the version alternation is not narrowed back to X.Y.x only", () => { + const src = activeSrc(); + // Both regexes must allow a numeric third segment. Guards against a + // revert of one branch while the other keeps working. + const narrowed = src.match(/findRE\s+`[^`]*\\d\+\\\.\\d\+\\\.x[|)]/g) ?? []; + expect( + narrowed, + "a version-inference regex still accepts only `\\d+\\.\\d+\\.x` — " + + "fully qualified LTS versions (e.g. 2026.7.1) will not infer.", + ).toEqual([]); + }); +});