From ea96f4829855fdc8e724c42768121a1dcdd84ca9 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Mon, 27 Jul 2026 17:09:35 -0500 Subject: [PATCH 1/2] refactor(layouts): resolve products from data, not hardcoded lists Five templates inferred a product by parsing the URL path, then branched on hardcoded lists of product and version names. The inference is wrong under /influxdb3/, where the path segment is `influxdb3` but the products.yml keys are `influxdb3_core`, `influxdb3_cloud`, and so on, so the lookup always missed and each template papered over it with its own list. The lists drifted. `header/coveo-meta-data.html` and `header/search-attributes.html` both decided "is this page current", with different lists. InfluxDB 3 Explorer and Telegraf Controller were current for Algolia and stale for Coveo. Resolve the product from the cascade `product` param instead, which every product section already declares. Add product/get-context.html for the lookup and product/is-latest.html for the shared currency rule, so the two search systems cannot disagree again. Move the branching facts into data/products.yml: supports_flux influxdb, influxdb_cloud, enterprise_influxdb search_includes_resources false for the six InfluxDB 3 products has_support_contract false for chronograf, kapacitor Set `latest` on telegraf_controller and telegraf_enterprise to their version path segment, matching every other product. Both had `latest: 1.0` unquoted, which YAML parses as a float and Hugo renders as "1", so the currency check could never match. `latest_patch: 1.0.1` still carries the release version. The search tag stays path-derived. It is the key Algolia indexed every record under, and content_path does not reproduce it for the five products whose content path omits the version segment. No product names remain in the touched templates. --- data/products.yml | 15 +++++- layouts/partials/article/feedback.html | 46 ++++------------ layouts/partials/footer/search.html | 18 ++++--- layouts/partials/header/coveo-meta-data.html | 14 +++-- .../partials/header/search-attributes.html | 14 +++-- layouts/partials/product/get-context.html | 53 +++++++++++++++++++ layouts/partials/product/is-latest.html | 34 ++++++++++++ 7 files changed, 138 insertions(+), 56 deletions(-) create mode 100644 layouts/partials/product/get-context.html create mode 100644 layouts/partials/product/is-latest.html diff --git a/data/products.yml b/data/products.yml index 548cdf15a7..c45bd3e83b 100644 --- a/data/products.yml +++ b/data/products.yml @@ -11,6 +11,7 @@ influxdb3_core: menu_category: self-managed versions: [core] list_order: 2 + search_includes_resources: false latest: core latest_patch: 3.10.5 placeholder_host: localhost:8181 @@ -56,6 +57,7 @@ influxdb3_enterprise: menu_category: self-managed versions: [enterprise] list_order: 2 + search_includes_resources: false latest: enterprise latest_patch: 3.10.5 placeholder_host: localhost:8181 @@ -127,6 +129,7 @@ influxdb3_explorer: label_group: explorer menu_category: tools list_order: 1 + search_includes_resources: false latest: explorer latest_patch: 1.9.0 placeholder_host: localhost:8080 @@ -153,6 +156,7 @@ influxdb3_cloud_serverless: menu_category: managed versions: [cloud-serverless] list_order: 2 + search_includes_resources: false latest: cloud-serverless placeholder_host: cloud2.influxdata.com scheme: https @@ -192,6 +196,7 @@ influxdb3_cloud_dedicated: menu_category: managed versions: [cloud-dedicated] list_order: 3 + search_includes_resources: false latest: cloud-dedicated link: 'https://www.influxdata.com/contact-sales-cloud-dedicated/' latest_cli: 2.12.1 @@ -229,6 +234,7 @@ influxdb3_clustered: menu_category: self-managed versions: [clustered] list_order: 3 + search_includes_resources: false latest: clustered link: 'https://www.influxdata.com/contact-sales-influxdb-clustered/' placeholder_host: cluster-host.com @@ -284,6 +290,7 @@ influxdb: versions: - v2 - v1 + supports_flux: true latest: v2.9 latest_patches: v2: 2.9.1 @@ -333,6 +340,7 @@ influxdb_cloud: menu_category: managed versions: [cloud] list_order: 1 + supports_flux: true latest: cloud placeholder_host: cloud2.influxdata.com scheme: https @@ -396,7 +404,7 @@ telegraf_controller: menu_category: other link: 'https://influxdata.com/contact-sales-telegraf-enterprise/?utm_source=website&utm_medium=direct&utm_campaign=Telegraf-Enterprise' versions: [v1] - latest: 1.0 + latest: controller latest_patch: 1.0.1 schema: operating_system: "Docker" @@ -410,7 +418,7 @@ telegraf_enterprise: menu_category: other link: 'https://influxdata.com/contact-sales-telegraf-enterprise/?utm_source=website&utm_medium=direct&utm_campaign=Telegraf-Enterprise' versions: [v1] - latest: 1.0 + latest: enterprise latest_patch: 1.0.1 schema: operating_system: "Linux, macOS, Windows, Docker" @@ -428,6 +436,7 @@ chronograf: menu_category: other list_order: 7 versions: [v1] + has_support_contract: false latest: v1.11 latest_patches: v1: 1.11.4 @@ -455,6 +464,7 @@ kapacitor: menu_category: other list_order: 7 versions: [v1] + has_support_contract: false latest: v1.8 latest_patches: v1: 1.8.6 @@ -478,6 +488,7 @@ enterprise_influxdb: menu_category: self-managed list_order: 5 versions: [v1] + supports_flux: true latest: v1.12 latest_patches: v1: 1.12.4 diff --git a/layouts/partials/article/feedback.html b/layouts/partials/article/feedback.html index 081654c4d0..fb3bf3f4e7 100644 --- a/layouts/partials/article/feedback.html +++ b/layouts/partials/article/feedback.html @@ -1,44 +1,18 @@ -{{/* - Derive product and version from the content path. Prefer .File.Dir - (content-relative, immune to baseURL subpaths like /pr-preview/pr-N/); - fall back to .RelPermalink only for virtual pages that have no source - file. Using .RelPermalink under a subpath-mounted baseURL would pick - up `pr-preview`/`pr-N` as `$product`, breaking every product lookup. -*/}} -{{ $productPath := .RelPermalink }} -{{ with .File }} - {{ $productPath = .Dir }} -{{ end }} -{{ $productPathData := findRE "[^/]+" $productPath }} -{{ $product := "" }} -{{ $version := "" }} -{{ if gt (len $productPathData) 0 }}{{ $product = index $productPathData 0 }}{{ end }} -{{ if gt (len $productPathData) 1 }}{{ $version = index $productPathData 1 }}{{ end }} -{{ $productKey := $product }} -{{ if eq $product "influxdb3" }} - {{ $productKey = print "influxdb3_" (replaceRE "-" "_" $version) }} -{{ else if and (eq $product "influxdb") (eq $version "cloud") }} - {{/* /influxdb/cloud/* is Cloud (TSM), a separate product entry. */}} - {{ $productKey = "influxdb_cloud" }} -{{ else if and (eq $product "telegraf") (in (slice "controller" "enterprise") $version) }} - {{/* - Telegraf Controller and Telegraf Enterprise are licensed products with - their own entries. Without this, both fall back to the `telegraf` entry - and render its public issue-tracker button. - */}} - {{ $productKey = print "telegraf_" $version }} -{{ end }} -{{ $productData := dict }} -{{ if $productKey }} - {{ with index .Site.Data.products $productKey }}{{ $productData = . }}{{ end }} -{{ end }} +{{ $ctx := partial "product/get-context.html" . }} +{{ $product := $ctx.product }} +{{ $version := $ctx.version }} +{{ $productData := $ctx.data }} {{ $productName := $productData.name }} {{/* Prefer version-specific name (name__v2, name__v1) when available. */}} {{ with (index $productData (print "name__" $version)) }} {{ $productName = . }} {{ end }} {{ $productIssueUrl := $productData.product_issue_url }} -{{ $supportBlacklist := slice "chronograf" "kapacitor" }} +{{/* + Products without a paid support offering set has_support_contract: false + in data/products.yml, so this template needs no list of exceptions. +*/}} +{{ $hasSupportContract := $productData.has_support_contract | default true }} {{ if .File }} {{ .Scratch.Set "pageGithubLink" (print "https://github.com/influxdata/docs-v2/edit/master/content/" .File.Path) }} @@ -95,7 +69,7 @@

Support and feedback

  • InfluxData Community
  • InfluxDB Subreddit
  • - {{ if not (in $supportBlacklist $product) }} + {{ if $hasSupportContract }}

    Customers with an annual or support contract can contact InfluxData Support. {{ if eq $version "enterprise" }}Customers using a trial license can email trial@influxdata.com for assistance.

    {{ end }} diff --git a/layouts/partials/footer/search.html b/layouts/partials/footer/search.html index f61459af54..708d617916 100644 --- a/layouts/partials/footer/search.html +++ b/layouts/partials/footer/search.html @@ -1,11 +1,13 @@ -{{ $productPathData := findRE "[^/]+.*?" .RelPermalink }} -{{ $product := index $productPathData 0 }} -{{ $version := index $productPathData 1 }} -{{ $latestV2 := .Site.Data.products.influxdb.latest }} -{{ $fluxSupported := slice "influxdb" "enterprise_influxdb" }} -{{ $influxdbFluxSupport := slice "v1" "v2" "cloud" }} -{{ $includeFlux := and (in $fluxSupported $product) (in $influxdbFluxSupport $version) }} -{{ $includeResources := not (in (slice "cloud-serverless" "cloud-dedicated" "clustered" "core" "enterprise" "explorer") $version) }} +{{ $ctx := partial "product/get-context.html" . }} +{{ $product := $ctx.product }} +{{ $version := $ctx.version }} +{{/* + Both flags come from data/products.yml so adding a product never requires + editing this template. See `supports_flux` and `search_includes_resources` + in that file for what each one means. +*/}} +{{ $includeFlux := $ctx.data.supports_flux | default false }} +{{ $includeResources := $ctx.data.search_includes_resources | default true }}
    \ No newline at end of file diff --git a/layouts/partials/header/search-attributes.html b/layouts/partials/header/search-attributes.html index 34a44a05d3..48fd80e928 100644 --- a/layouts/partials/header/search-attributes.html +++ b/layouts/partials/header/search-attributes.html @@ -1,13 +1,17 @@ -{{ $productPathData := findRE "[^/]+.*?" .RelPermalink }} -{{ $product := index $productPathData 0 | default "" }} -{{ $version := index $productPathData 1 | default "" }} +{{ $ctx := partial "product/get-context.html" . }} +{{ $product := $ctx.product }} +{{ $version := $ctx.version }} {{ $fluxPath := print "/flux/v0/" }} {{ $isFluxLatest := cond (in .RelPermalink $fluxPath) true false }} -{{ $isResources := cond (eq $product "resources") true false}} +{{/* + The search tag stays path-derived. It is the key Algolia already indexed + every record under, so it must keep matching the crawled URL rather than + follow a products.yml field. +*/}} {{ $searchTag := print $product "-" $version }} {{ if not .IsHome }} - {{ if or (eq $version (replaceRE `\.[0-9x]+$` "" (index $.Site.Data.products $product).latest)) (eq $product "platform") (in (slice "cloud" "core" "enterprise" "cloud-serverless" "cloud-dedicated" "clustered" "explorer" "controller") $version ) }} + {{ if partial "product/is-latest.html" . }} {{ end }} {{ if and (ne $product "platform") (ne $product "resources") (ne $version "") }} diff --git a/layouts/partials/product/get-context.html b/layouts/partials/product/get-context.html new file mode 100644 index 0000000000..8399098ed2 --- /dev/null +++ b/layouts/partials/product/get-context.html @@ -0,0 +1,53 @@ +{{- /* + Resolve a page's product without inferring it from the URL. + + Every product section declares its product through cascade in its + section _index.md, so the products.yml key is stated, not guessed. + Templates that inferred the key from the first path segment got it wrong + under /influxdb3/, where the segment is `influxdb3` but the keys are + `influxdb3_core`, `influxdb3_cloud`, and so on. + + Hub pages (/influxdb3/), /platform/, and /resources/ declare no product. + They get an empty key and empty data, which callers already handle. + + Path segments are still returned. Search tags and some path-scoped UI + rules key off the URL itself rather than off the product. + + Prefers .File.Dir over .RelPermalink so a subpath-mounted baseURL + (/pr-preview/pr-N/) does not leak into the segments. + + Returns a dict: + key - products.yml key ("influxdb3_cloud"), or "" + data - the products.yml entry, or an empty dict + product - first path segment ("influxdb3") + version - second path segment ("cloud") + + Usage: + {{ $ctx := partial "product/get-context.html" . }} + {{ $ctx.data.latest }} +*/ -}} +{{- $productPath := .RelPermalink -}} +{{- with .File -}} + {{- $productPath = .Dir -}} +{{- end -}} +{{- $pathData := findRE "[^/]+.*?" $productPath -}} +{{- $product := index $pathData 0 | default "" -}} +{{- $version := index $pathData 1 | default "" -}} +{{- $key := .Params.product | default "" -}} +{{- if not $key -}} + {{- /* + Product hub pages (/telegraf/, /chronograf/, /influxdb/) sit above the + versioned sections that carry the cascade, so they declare no product. + Fall back to the first path segment when it names a product. + */ -}} + {{- with index $.Site.Data.products $product -}} + {{- $key = $product -}} + {{- end -}} +{{- end -}} +{{- $data := dict -}} +{{- with $key -}} + {{- with index $.Site.Data.products . -}} + {{- $data = . -}} + {{- end -}} +{{- end -}} +{{- return dict "key" $key "data" $data "product" $product "version" $version -}} diff --git a/layouts/partials/product/is-latest.html b/layouts/partials/product/is-latest.html new file mode 100644 index 0000000000..7f91d35c17 --- /dev/null +++ b/layouts/partials/product/is-latest.html @@ -0,0 +1,34 @@ +{{- /* + Report whether a page documents the current version of its product. + + The rule, in one place, with no product names in it: + + 1. The site home page is always current. + 2. A page outside any versioned product (/platform/, /resources/) has + no older counterpart, so it is current. + 3. Otherwise the page's version path segment must equal the product's + `latest` in data/products.yml, with any trailing patch digits + trimmed so /influxdb/v2/ matches `latest: v2.9`. + + Rule 3 is why every product sets `latest` to the version segment that + appears in its URL. Products with a named version rather than a number + set it to that name (core, cloud, explorer, controller). Keeping that + convention is what stops this check from needing a list of exceptions. + + Consumed by header/coveo-meta-data.html and header/search-attributes.html + so the two search systems cannot drift apart on the same question. + + Usage: + {{ $isLatest := partial "product/is-latest.html" . }} +*/ -}} +{{- $ctx := partial "product/get-context.html" . -}} +{{- $isLatest := false -}} +{{- if eq .Kind "home" -}} + {{- $isLatest = true -}} +{{- else if not $ctx.key -}} + {{- $isLatest = true -}} +{{- else -}} + {{- $latest := replaceRE `\.[0-9x]+$` "" (string ($ctx.data.latest | default "")) -}} + {{- $isLatest = and (ne $latest "") (eq $ctx.version $latest) -}} +{{- end -}} +{{- return $isLatest -}} From bef595a741f2757c566d303bb2db1e11bbd79759 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Mon, 27 Jul 2026 17:14:29 -0500 Subject: [PATCH 2/2] docs(agents): ban magic product values in layout templates Templates operate on data and stay ignorant of the values in that data. A product name, version segment, or products.yml key must never appear as a string literal in template logic, so renaming or adding a product never requires a template edit. Document the banned shapes, the three replacements (a per-product field in data/products.yml, product/get-data.html or product/get-context.html for resolution, one shared partial for a decision two templates make), and the incident that motivated it: coveo-meta-data.html and search-attributes.html each kept their own list for the same question, the lists drifted, and InfluxDB 3 Explorer and Telegraf Controller were current for one search system and stale for the other. Carve out one exception. A value that must match an external system rather than a product fact stays as it is, with a comment saying why. The Algolia search tag is path-derived because Algolia indexed every record under the crawled URL, and a well-meaning cleanup would orphan those records. Rule lives in .agents/instructions/layouts.md, how-to in the hugo-template-dev skill, one-line constraint in AGENTS.md. The three adapters are regenerated by yarn build:agent:instructions. --- .agents/instructions/layouts.md | 44 ++++++++ .agents/skills/hugo-template-dev/SKILL.md | 103 +++++++++++++++++++ .claude/rules/layouts.md | 44 ++++++++ .github/instructions/layouts.instructions.md | 44 ++++++++ AGENTS.md | 2 + layouts/AGENTS.md | 44 ++++++++ 6 files changed, 281 insertions(+) diff --git a/.agents/instructions/layouts.md b/.agents/instructions/layouts.md index 95afbde1c0..6a18b77794 100644 --- a/.agents/instructions/layouts.md +++ b/.agents/instructions/layouts.md @@ -18,6 +18,50 @@ paths: - Shortcode implementation best practices - Complete TDD workflow for Hugo templates +## No Magic Values in Template Logic + +Templates operate on data and stay ignorant of the values in that data. +A product name, version segment, or `data/products.yml` key must never appear +as a string literal in template logic. +Nobody should have to edit a template because a product was renamed or added. + +Never write any of these in `layouts/**`: + +- A slice of product names or version segments used in a condition, such as a + list of the versions that count as current or the products that support Flux. +- A single hardcoded product comparison that branches behavior, such as testing + whether the first path segment equals a specific product. +- Deriving a `data/products.yml` key by matching the URL path when the page + already declares one. + +This file is generated into `layouts/AGENTS.md`, and Hugo parses every file +under `layouts/` as a template, so it carries no Go template examples. +For the annotated before and after, see the +[hugo-template-dev skill](../skills/hugo-template-dev/SKILL.md). + +Do this instead: + +1. Put the fact in `data/products.yml` as a per-product field — a boolean such + as `supports_flux`, `has_support_contract`, or `search_includes_resources` — + and read it with a `| default` that covers products that don't set it. +2. Resolve the product with `partial "product/get-data.html"` or + `partial "product/get-context.html"`, which read the page's cascade `product` + param. + Every product section declares `product` and `version` by cascade in its + section `_index.md`, so the key is stated rather than guessed. +3. When two templates need the same decision, extract it into one partial so + the two can't drift. + `layouts/partials/product/is-latest.html` is the worked example. + +The one exception is a value that must match an external system rather than a +product fact. +The Algolia search tag in `layouts/partials/header/search-attributes.html` +stays path-derived because Algolia indexed every record under the crawled URL. +Comment any such case in the template so the next reader doesn't "fix" it. + +For the before/after example and the incident behind this rule, see +[hugo-template-dev skill](../skills/hugo-template-dev/SKILL.md). + ## Implementing Shortcodes When creating or modifying Hugo layouts and shortcodes: diff --git a/.agents/skills/hugo-template-dev/SKILL.md b/.agents/skills/hugo-template-dev/SKILL.md index aed9bf2819..56bccfaa50 100644 --- a/.agents/skills/hugo-template-dev/SKILL.md +++ b/.agents/skills/hugo-template-dev/SKILL.md @@ -330,6 +330,107 @@ layouts/ - Group related partials in subdirectories - Include comments at the top describing purpose and required context +## No Magic Values in Template Logic + +**Principle:** Templates operate on data and stay ignorant of the values in that data. A product name, version segment, or `data/products.yml` key must never appear as a string literal in template logic. Nobody should have to edit a template because a product was renamed or added. + +### Why this rule exists + +`header/coveo-meta-data.html` and `header/search-attributes.html` both answered the same question — does this page document the current version of its product? — and each answered it with its own hardcoded list of version segments. The lists drifted. `explorer` and `controller` made it into the Algolia list but not the Coveo list, so InfluxDB 3 Explorer and Telegraf Controller were indexed as current by one search system and as stale by the other. Neither list was wrong on its face. The duplication was. + +### What counts as a magic value + +| Pattern | Example | +| ------------------------------------------- | --------------------------------------------------------------- | +| A list of product names or version segments | `{{ $alwaysLatest := slice "cloud" "core" "enterprise" }}` | +| A hardcoded comparison that branches | `{{ if eq $product "platform" }}` | +| An exclusion list | `{{ if not (in (slice "chronograf" "kapacitor") $product) }}` | +| A key inferred from the URL | `{{ findRE "[^/]+.*?" .RelPermalink }}` to build a products key | + +Find them with: + +```bash +grep -rnE '(slice|in |eq |ne )[^}]*"(core|enterprise|cloud|clustered|explorer|controller|platform|resources|influxdb|telegraf|chronograf|kapacitor|flux)' layouts/ --exclude=AGENTS.md +``` + +Not every hit is a violation. String literals in class names, URLs, and display text are fine. The rule is about **branching on product identity**. + +### Fix 1: Move the fact into products.yml + +Name the field after the fact, not the product, and choose the `default` so only +the exceptions need the field. + +**Before** (`layouts/partials/footer/search.html`): + +```go +{{ $productPathData := findRE "[^/]+.*?" .RelPermalink }} +{{ $product := index $productPathData 0 }} +{{ $version := index $productPathData 1 }} +{{ $fluxSupported := slice "influxdb" "enterprise_influxdb" }} +{{ $influxdbFluxSupport := slice "v1" "v2" "cloud" }} +{{ $includeFlux := and (in $fluxSupported $product) (in $influxdbFluxSupport $version) }} +{{ $includeResources := not (in (slice "cloud-serverless" "cloud-dedicated" "clustered" "core" "enterprise" "explorer") $version) }} +``` + +**After:** + +```go +{{ $ctx := partial "product/get-context.html" . }} +{{/* + Both flags come from data/products.yml so adding a product never requires + editing this template. +*/}} +{{ $includeFlux := $ctx.data.supports_flux | default false }} +{{ $includeResources := $ctx.data.search_includes_resources | default true }} +``` + +`data/products.yml`: + +```yaml +influxdb: + supports_flux: true +influxdb3_core: + search_includes_resources: false +``` + +### Fix 2: Resolve the product from the page, not the path + +`layouts/partials/product/get-data.html` and +`layouts/partials/product/get-context.html` read the page's cascade `product` +param. Every product section declares `product` and `version` by cascade in its +section `_index.md`, so the key is stated rather than guessed. + +```go +{{ $productData := partial "product/get-data.html" . }} +{{ $ctx := partial "product/get-context.html" . }} +{{ $ctx.key }} {{/* "influxdb3_cloud_dedicated" */}} +{{ $ctx.data }} {{/* the products.yml entry */}} +{{ $ctx.product }} {{/* first path segment, for path-scoped rules */}} +``` + +Parsing `.RelPermalink` gets the key wrong under `/influxdb3/`, where the path +segment is `influxdb3` but the keys are `influxdb3_core`, `influxdb3_cloud`, and +so on. It also breaks under a subpath-mounted baseURL, where the PR preview's +`/pr-preview/pr-N/` prefix becomes the "product." + +### Fix 3: Extract a shared decision into one partial + +When two templates need the same answer, give them one partial to call. +`layouts/partials/product/is-latest.html` is the worked example: both search +templates now call it, so they can't disagree about which pages are current. + +```go +{{ $isLatest := partial "product/is-latest.html" . }} +``` + +### The one exception + +A value that must match an external system rather than a product fact stays as +it is. The Algolia search tag in `header/search-attributes.html` is path-derived +because Algolia indexed every record under the crawled URL, and changing the tag +would orphan those records. Comment any such case in the template so the next +reader doesn't "fix" it. + ## Separation of Concerns: Templates vs TypeScript **Principle:** Hugo templates handle structure and data binding. TypeScript handles behavior and interactivity. @@ -561,6 +662,8 @@ pre-commit: 3. **Check error output first** before declaring success 4. **Use `isset` and `index`** for safe data access 5. **Hyphenated keys require `index` function** - dot notation fails +6. **No product names in template logic** - put the fact in `data/products.yml` + and resolve the product with the `product/get-context.html` partial ## Related Resources diff --git a/.claude/rules/layouts.md b/.claude/rules/layouts.md index fb3e7a285a..ce0b0ebea7 100644 --- a/.claude/rules/layouts.md +++ b/.claude/rules/layouts.md @@ -20,6 +20,50 @@ paths: - Shortcode implementation best practices - Complete TDD workflow for Hugo templates +## No Magic Values in Template Logic + +Templates operate on data and stay ignorant of the values in that data. +A product name, version segment, or `data/products.yml` key must never appear +as a string literal in template logic. +Nobody should have to edit a template because a product was renamed or added. + +Never write any of these in `layouts/**`: + +- A slice of product names or version segments used in a condition, such as a + list of the versions that count as current or the products that support Flux. +- A single hardcoded product comparison that branches behavior, such as testing + whether the first path segment equals a specific product. +- Deriving a `data/products.yml` key by matching the URL path when the page + already declares one. + +This file is generated into `layouts/AGENTS.md`, and Hugo parses every file +under `layouts/` as a template, so it carries no Go template examples. +For the annotated before and after, see the +[hugo-template-dev skill](../../.agents/skills/hugo-template-dev/SKILL.md). + +Do this instead: + +1. Put the fact in `data/products.yml` as a per-product field — a boolean such + as `supports_flux`, `has_support_contract`, or `search_includes_resources` — + and read it with a `| default` that covers products that don't set it. +2. Resolve the product with `partial "product/get-data.html"` or + `partial "product/get-context.html"`, which read the page's cascade `product` + param. + Every product section declares `product` and `version` by cascade in its + section `_index.md`, so the key is stated rather than guessed. +3. When two templates need the same decision, extract it into one partial so + the two can't drift. + `layouts/partials/product/is-latest.html` is the worked example. + +The one exception is a value that must match an external system rather than a +product fact. +The Algolia search tag in `layouts/partials/header/search-attributes.html` +stays path-derived because Algolia indexed every record under the crawled URL. +Comment any such case in the template so the next reader doesn't "fix" it. + +For the before/after example and the incident behind this rule, see +[hugo-template-dev skill](../../.agents/skills/hugo-template-dev/SKILL.md). + ## Implementing Shortcodes When creating or modifying Hugo layouts and shortcodes: diff --git a/.github/instructions/layouts.instructions.md b/.github/instructions/layouts.instructions.md index 7055dae225..7921707bfc 100644 --- a/.github/instructions/layouts.instructions.md +++ b/.github/instructions/layouts.instructions.md @@ -19,6 +19,50 @@ applyTo: "layouts/**/*.html" - Shortcode implementation best practices - Complete TDD workflow for Hugo templates +## No Magic Values in Template Logic + +Templates operate on data and stay ignorant of the values in that data. +A product name, version segment, or `data/products.yml` key must never appear +as a string literal in template logic. +Nobody should have to edit a template because a product was renamed or added. + +Never write any of these in `layouts/**`: + +- A slice of product names or version segments used in a condition, such as a + list of the versions that count as current or the products that support Flux. +- A single hardcoded product comparison that branches behavior, such as testing + whether the first path segment equals a specific product. +- Deriving a `data/products.yml` key by matching the URL path when the page + already declares one. + +This file is generated into `layouts/AGENTS.md`, and Hugo parses every file +under `layouts/` as a template, so it carries no Go template examples. +For the annotated before and after, see the +[hugo-template-dev skill](../../.agents/skills/hugo-template-dev/SKILL.md). + +Do this instead: + +1. Put the fact in `data/products.yml` as a per-product field — a boolean such + as `supports_flux`, `has_support_contract`, or `search_includes_resources` — + and read it with a `| default` that covers products that don't set it. +2. Resolve the product with `partial "product/get-data.html"` or + `partial "product/get-context.html"`, which read the page's cascade `product` + param. + Every product section declares `product` and `version` by cascade in its + section `_index.md`, so the key is stated rather than guessed. +3. When two templates need the same decision, extract it into one partial so + the two can't drift. + `layouts/partials/product/is-latest.html` is the worked example. + +The one exception is a value that must match an external system rather than a +product fact. +The Algolia search tag in `layouts/partials/header/search-attributes.html` +stays path-derived because Algolia indexed every record under the crawled URL. +Comment any such case in the template so the next reader doesn't "fix" it. + +For the before/after example and the incident behind this rule, see +[hugo-template-dev skill](../../.agents/skills/hugo-template-dev/SKILL.md). + ## Implementing Shortcodes When creating or modifying Hugo layouts and shortcodes: diff --git a/AGENTS.md b/AGENTS.md index 3c55e1be09..e3bcb11b89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,6 +68,8 @@ See [README.md](README.md) and the - For InfluxDB 3, shared directories contain the prose; product directories are usually thin stubs with `source:` references. - Product names and versions come from `data/products.yml`. + Templates under `layouts/` read that data; they never hardcode a product name + or version segment in template logic. - Commit format is `type(scope): description`. - Network-restricted environments may fail on Cypress downloads, Docker builds, or Alpine package installs. diff --git a/layouts/AGENTS.md b/layouts/AGENTS.md index 95c4b9afe7..10d686e719 100644 --- a/layouts/AGENTS.md +++ b/layouts/AGENTS.md @@ -19,6 +19,50 @@ These instructions apply when working in `layouts/`. - Shortcode implementation best practices - Complete TDD workflow for Hugo templates +### No Magic Values in Template Logic + +Templates operate on data and stay ignorant of the values in that data. +A product name, version segment, or `data/products.yml` key must never appear +as a string literal in template logic. +Nobody should have to edit a template because a product was renamed or added. + +Never write any of these in `layouts/**`: + +- A slice of product names or version segments used in a condition, such as a + list of the versions that count as current or the products that support Flux. +- A single hardcoded product comparison that branches behavior, such as testing + whether the first path segment equals a specific product. +- Deriving a `data/products.yml` key by matching the URL path when the page + already declares one. + +This file is generated into `layouts/AGENTS.md`, and Hugo parses every file +under `layouts/` as a template, so it carries no Go template examples. +For the annotated before and after, see the +[hugo-template-dev skill](../.agents/skills/hugo-template-dev/SKILL.md). + +Do this instead: + +1. Put the fact in `data/products.yml` as a per-product field — a boolean such + as `supports_flux`, `has_support_contract`, or `search_includes_resources` — + and read it with a `| default` that covers products that don't set it. +2. Resolve the product with `partial "product/get-data.html"` or + `partial "product/get-context.html"`, which read the page's cascade `product` + param. + Every product section declares `product` and `version` by cascade in its + section `_index.md`, so the key is stated rather than guessed. +3. When two templates need the same decision, extract it into one partial so + the two can't drift. + `layouts/partials/product/is-latest.html` is the worked example. + +The one exception is a value that must match an external system rather than a +product fact. +The Algolia search tag in `layouts/partials/header/search-attributes.html` +stays path-derived because Algolia indexed every record under the crawled URL. +Comment any such case in the template so the next reader doesn't "fix" it. + +For the before/after example and the incident behind this rule, see +[hugo-template-dev skill](../.agents/skills/hugo-template-dev/SKILL.md). + ### Implementing Shortcodes When creating or modifying Hugo layouts and shortcodes: