Skip to content
Open
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
44 changes: 44 additions & 0 deletions .agents/instructions/layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
103 changes: 103 additions & 0 deletions .agents/skills/hugo-template-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
44 changes: 44 additions & 0 deletions .claude/rules/layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
44 changes: 44 additions & 0 deletions .github/instructions/layouts.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions data/products.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -284,6 +290,7 @@ influxdb:
versions:
- v2
- v1
supports_flux: true
latest: v2.9
latest_patches:
v2: 2.9.1
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading
Loading