influxdata/docs-v2 · Archived

hugo-template-dev

Hugo template development for InfluxData docs-v2, enforcing build and runtime testing to catch template errors that build-only validation misses. Use when creating or editing Hugo layouts, partials, or shortcodes, debugging template errors, or accessing site data in templates.

First seen Feb 11, 2026

Installation

$ npx skills add influxdata/docs-v2 --skill hugo-template-dev

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from influxdata/docs-v2.

npx skills add influxdata/docs-v2

Browse all from influxdata/docs-v2

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 82
License LICENSE
Default branch master
Open issues 623
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 22,705 B
  • docs SUMMARY.md 302 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 42 installs

SKILL.md

Hugo Template Development Skill

Purpose

This skill enforces proper Hugo template development practices, including mandatory runtime testing to catch errors that static builds miss.

Critical Testing Requirement

Hugo's npx hugo --quiet only validates template syntax, not runtime execution.

Template errors like accessing undefined fields, nil values, or incorrect type assertions only appear when Hugo actually renders pages. You MUST test templates by running the server.

Mandatory Testing Protocol

For ANY Hugo Template Change

After modifying files in layouts/, layouts/partials/, or layouts/shortcodes/:

Step 1: Start Hugo server in the background and capture output

rm -f /tmp/hugo-1315.log
npx hugo server --port 1315 >/tmp/hugo-1315.log 2>&1 &
sleep 5
head -50 /tmp/hugo-1315.log

This keeps the server running for the next steps while still showing startup output.

Success criteria:

  • No error calling partial messages
  • No can't evaluate field errors
  • No template: ... failed messages
  • Server shows "Web Server is available at <http://localhost:1315/>";

If errors appear: Fix the template and repeat Step 1 before proceeding.

Step 2: Verify the page renders

curl -s -o /dev/null -w "%{http_code}" http://localhost:1315/PATH/TO/PAGE/

Expected: HTTP 200 status code

Step 3: Browser testing (if MCP browser tools available)

If mcpclaude-in-chrome* tools are available, use them for visual inspection:

# Navigate and screenshot
mcp__claude-in-chrome__navigate({ url: "http://localhost:1315/PATH/", tabId: ... })
mcp__claude-in-chrome__computer({ action: "screenshot", tabId: ... })

# Check for JavaScript errors
mcp__claude-in-chrome__read_console_messages({ tabId: ..., onlyErrors: true })

This catches runtime JavaScript errors that template changes may introduce.

Step 4: Stop the test server

pkill -f "hugo server --port 1315"

Quick Test Command

Use this one-liner to test and get immediate feedback:

rm -f /tmp/hugo-1315.log
npx hugo server --port 1315 >/tmp/hugo-1315.log 2>&1 &
sleep 5
grep -E "(error|Error|ERROR|fail|FAIL)" /tmp/hugo-1315.log | head -20
pkill -f "hugo server --port 1315" 2>/dev/null

If output is empty, no errors were detected.

Preparing template changes for PR review

The repo's preview workflow (.github/workflows/pr-preview.yml) deploys the whole built site to a stable staging URL (https://test2.docs.influxdata.com/pr-preview/pr-<N>/) on every push — a hosted preview that replaces "check out my branch and run npx hugo server" for reviewers. It builds with --environment production, so it reflects production behavior (minification, fingerprinted/SRI JS, real analytics). Subdirectory-baseURL product detection is handled by layouts/partials/base-path-offset.html, not a per-environment config.

When you change layouts/, assets/, or data/, list the pages the reviewer should focus on in the PR body — this doesn't affect what deploys (the whole site always does), but it adds deep links to the sticky preview comment so reviewers don't have to navigate manually. The URL extractor (.github/scripts/parse-pr-urls.js) matches:

  • Production URLs (https://docs.influxdata.com/<path>)
  • Localhost URLs (http://localhost:1313/<path>)
  • Bare paths with a known product namespace from data/products.yml

(/influxdb3/..., /telegraf/..., etc.)

URLs inside fenced code blocks are stripped before extraction — list them as bare paths or markdown links, not inside backtick fences. Pair each URL with an "Expected" column (DOM element, attribute value, copy) so the reviewer knows what to verify rather than guessing.

For wider behavioral coverage (interactive UI, JS errors, navigation), prefer the cypress-e2e-testing skill. The preview-pages mechanism is the right tool for visual / structural verification — exactly the cases where Cypress is overkill or doesn't cover what changed.

Autodiscovery coherence guard (when touching Markdown-alternate paths)

If your template change affects <link rel="alternate" type="text/markdown">, the /sitemap-md.xml layout, the /llms.txt template, or any of the inputs to scripts/lib/corpus-paths.js (notably data/products.yml), run after the full build:

npx hugo --quiet && yarn build:md && yarn build:llms-full && yarn check:md-coherence

check:md-coherence runs two coherence checks: head-link → .md file existence, and Hugo /llms.txt ↔ getCorpusPaths() agreement. Catches drift between Hugo template logic and the JS derivation from products.yml. See DOCS-TESTING.md "Autodiscovery coherence guard" for details.

Validating structured data (JSON-LD)

The layouts/partials/header/*-jsonld.html partials emit schema.org JSON-LD (Organization, TechArticle, SoftwareApplication, FAQPage). When you add or change one:

Use the Schema Markup Validator (https://validator.schema.org), NOT the Google Rich Results Test.

The Rich Results Test only reports types eligible for a visual search enhancement. Most JSON-LD this repo emits is not eligible, so the Rich Results Test reports "no items detected" even for valid markup — a false negative that looks like failure:

Emitted type Rich Results Test Why
Organization Not reported Feeds the knowledge graph / entity resolution, never a rich result
TechArticle Not reported Google's Article rich result fires only for Article/NewsArticle/BlogPosting
SoftwareApplication Not reported Google retired the general software-app rich result
FAQPage Reported One of the few eligible types here

validator.schema.org validates every schema.org type regardless of rich-result eligibility — that's what confirms the node is well-formed.

Validation steps:

  1. Structural (local, scriptable): parse the emitted block to prove it's

valid JSON and schema-shaped. The minifier emits type=application/ld+json (unquoted) — match the attribute loosely:

``bash python3 - <<'EOF' import re, json html = open('public/index.html', encoding='utf-8', errors='replace').read() for b in re.findall(r'<script type=["\']?application/ld\+json["\']?>(.*?)</script>', html, re.S): j = json.loads(b) # raises on malformed JSON print('OK', j.get('@type')) EOF ``

  1. Schema (manual, reviewer): paste a deployed preview URL into

validator.schema.org; expect 0 errors. Note this in the PR description and do not ask reviewers to use the Rich Results Test for non-FAQPage nodes.

  1. Regression (Cypress): assertions depend on scope. Page-scoped nodes

(TechArticle/SoftwareApplication) — assert presence where they belong and absence where they don't (over-emission guard). Global nodes (Organization, emitted site-wide with a stable @id) — assert exactly one per page class, which catches both omission and duplicates. See the cypress-e2e-testing skill, "Testing structured data (JSON-LD)".

Common Hugo Template Errors

1. Accessing Keys with Hyphens or Dynamic Names

Hugo's dot notation only works for keys that are valid Go identifiers (letters, digits, underscores). This repo's data dir is articledata (underscore), so .Site.Data.articledata works. But a hyphenated key — or a key held in a variable — must use index:

Wrong (hyphen breaks dot notation; dataKey is a variable):

{{ .Site.Data.my-data.influxdb }}
{{ .Site.Data.article_data.dataKey }}

Correct:

{{ index .Site.Data "my-data" "influxdb" }}
{{ index .Site.Data "article_data" $dataKey }}

2. Nil Field Access

Wrong:

{{ range $articles }}
  {{ .path }}  {{/* Fails if item is nil or wrong type */}}
{{ end }}

Correct:

{{ range $articles }}
  {{ if . }}
    {{ with index . "path" }}
      {{ . }}
    {{ end }}
  {{ end }}
{{ end }}

3. Type Assertion on Interface{}

Wrong:

{{ range $data }}
  {{ .fields.menuName }}
{{ end }}

Correct:

{{ range $data }}
  {{ if isset . "fields" }}
    {{ $fields := index . "fields" }}
    {{ if isset $fields "menuName" }}
      {{ index $fields "menuName" }}
    {{ end }}
  {{ end }}
{{ end }}

4. Empty Map vs Nil Check

Problem: Hugo's {{ if . }} passes for empty maps {}:

{{/* This doesn't catch empty maps */}}
{{ if $data }}
  {{ .field }}  {{/* Still fails if $data is {} */}}
{{ end }}

Solution: Check for specific keys:

{{ if and $data (isset $data "field") }}
  {{ index $data "field" }}
{{ end }}

Hugo Data Access Patterns

Safe Nested Access

{{/* Build up access with nil checks at each level */}}
{{ $articleDataRoot := index .Site.Data "article_data" }}
{{ if $articleDataRoot }}
  {{ $influxdbData := index $articleDataRoot "influxdb" }}
  {{ if $influxdbData }}
    {{ $productData := index $influxdbData $dataKey }}
    {{ if $productData }}
      {{ with $productData.articles }}
        {{/* Safe to use . here */}}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

Iterating Over Data Safely

{{ range $idx, $item := $articles }}
  {{/* Declare variables with defaults */}}
  {{ $path := "" }}
  {{ $name := "" }}

  {{/* Safely extract values */}}
  {{ if isset $item "path" }}
    {{ $path = index $item "path" }}
  {{ end }}

  {{ if $path }}
    {{/* Now safe to use $path */}}
  {{ end }}
{{ end }}

File Organization

Layouts Directory Structure

layouts/
├── _default/           # Default templates
├── partials/           # Reusable template fragments
│   └── api/            # API-specific partials
├── shortcodes/         # Content shortcodes
└── TYPE/               # Type-specific templates (api/, etc.)
    └── single.html     # Single page template

Partial Naming

  • Use descriptive names: api/sidebar-nav.html, not nav.html
  • 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:

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):

{{ $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:

{{ $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:

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.

{{ $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 influxdb3core, influxdb3cloud, 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.

{{ $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.

What Goes Where

Concern Location Example
HTML structure layouts/**/*.html Navigation markup, tab containers
Data binding layouts/**/*.html {{ .Title }}, {{ range .Data }}
Static styling assets/styles/**/*.scss Layout, colors, typography
User interaction assets/js/components/*.ts Click handlers, scroll behavior
State management assets/js/components/*.ts Active tabs, collapsed sections
DOM manipulation assets/js/components/*.ts Show/hide, class toggling

Anti-Pattern: Inline JavaScript in Templates

Wrong - JavaScript mixed with template:

{{/* DON'T DO THIS */}}
<nav class="api-nav">
  {{ range $articles }}
    <button onclick="toggleSection('{{ .id }}')">{{ .name }}</button>
  {{ end }}
</nav>

<script>
function toggleSection(id) {
  document.getElementById(id).classList.toggle('is-open');
}
</script>

Correct - Clean separation:

Template (layouts/partials/api/sidebar-nav.html):

<nav class="api-nav" data-component="api-nav">
  {{ range $articles }}
    <button class="api-nav-group-header" aria-expanded="false">
      {{ .name }}
    </button>
    <ul class="api-nav-group-items">
      {{/* items */}}
    </ul>
  {{ end }}
</nav>

TypeScript (assets/js/components/api-nav.ts):

interface ApiNavOptions {
  component: HTMLElement;
}

export default function initApiNav({ component }: ApiNavOptions): void {
  const headers = component.querySelectorAll('.api-nav-group-header');

  headers.forEach((header) => {
    header.addEventListener('click', () => {
      const isOpen = header.classList.toggle('is-open');
      header.setAttribute('aria-expanded', String(isOpen));
      header.nextElementSibling?.classList.toggle('is-open', isOpen);
    });
  });
}

Register in main.js:

import initApiNav from './components/api-nav.js';

const componentRegistry = {
  'api-nav': initApiNav,
  // ... other components
};

Data Passing Pattern

Pass Hugo data to TypeScript via data-* attributes:

Template:

<div
  data-component="api-toc"
  data-headings="{{ .headings | jsonify | safeHTMLAttr }}"
  data-scroll-offset="80"
>
</div>

TypeScript:

interface TocOptions {
  component: HTMLElement;
}

interface TocData {
  headings: string[];
  scrollOffset: number;
}

function parseData(component: HTMLElement): TocData {
  const headingsRaw = component.dataset.headings;
  const headings = headingsRaw ? JSON.parse(headingsRaw) : [];
  const scrollOffset = parseInt(component.dataset.scrollOffset || '0', 10);

  return { headings, scrollOffset };
}

export default function initApiToc({ component }: TocOptions): void {
  const data = parseData(component);
  // Use data.headings and data.scrollOffset
}

Minimal Inline Scripts (Exception)

The only acceptable inline scripts are minimal initialization that MUST run before component registration:

{{/* Acceptable: Critical path, no logic, runs immediately */}}
<script>
  document.documentElement.dataset.theme =
    localStorage.getItem('theme') || 'light';
</script>

Everything else belongs in assets/js/.

File Organization for Components

assets/
├── js/
│   ├── main.js                    # Entry point, component registry
│   ├── components/
│   │   └── api-toc.ts             # API table of contents behavior
│   └── utils/
│       └── dom-helpers.ts         # Shared DOM utilities
└── styles/
    └── layouts/
        ├── _api-layout.scss       # API page layout (3-column, sidebar, TOC)
        └── _api-operations.scss   # Operation rendering (methods, params, responses)

TypeScript Component Checklist

When creating a new interactive feature:

  1. [ ] Create TypeScript file in assets/js/components/
  2. [ ] Define interface for component options
  3. [ ] Export default initializer function
  4. [ ] Register in main.js componentRegistry
  5. [ ] Add data-component attribute to HTML element
  6. [ ] Pass data via data-* attributes (not inline JS)
  7. [ ] NO inline <script> tags in templates

Debugging Templates

Enable Verbose Mode

npx hugo server --port 1315 --verbose 2>&1 | head -100

Print Variables for Debugging

{{/* Temporary debugging - REMOVE before committing */}}
<pre>{{ printf "%#v" $myVariable }}</pre>

Check Data File Loading

# Verify data files exist and are valid YAML
cat data/article_data/influxdb3/core/api/articles.yml | head -20

Integration with CI/CD

Pre-commit Hook (Recommended)

Add to .lefthook.yml or pre-commit configuration:

pre-commit:
  commands:
    hugo-template-test:
      glob: "layouts/**/*.html"
      run: |
        timeout 20 npx hugo server --port 1315 2>&1 | grep -E "error|Error" && exit 1 || exit 0
        pkill -f "hugo server --port 1315" 2>/dev/null

GitHub Actions Workflow

- name: Test Hugo templates
  run: |
    npx hugo server --port 1315 &
    sleep 10
    curl -f http://localhost:1315/ || exit 1
    pkill -f hugo

Quick Reference

Action Command
Test templates (runtime) `npx hugo server --port 1315 2>&1 \ head -50`
Build only (insufficient) npx hugo --quiet
Check specific page curl -s -o /dev/null -w "%{http_code}" http://localhost:1315/path/
Stop test server pkill -f "hugo server --port 1315"
Debug data access <pre>{{ printf "%#v" $var }}</pre>

Remember

  1. Never trust npx hugo --quiet alone - it only checks syntax
  2. Always run the server to test template changes
  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

  • api-docs/README.md: API documentation workflow, tags.yml format, overlays, generation pipeline
  • cypress-e2e-testing skill: E2E testing of UI components and pages
  • docs-cli-workflow skill: Creating/editing documentation content
  • ts-component-dev agent: TypeScript component behavior and interactivity
  • ui-testing agent: Cypress E2E testing for UI components