Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
5 changes: 3 additions & 2 deletions layouts/_shortcodes/link-hextra.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 -}}
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 104 additions & 0 deletions tests/link-hextra-lts-version.spec.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Loading