hugo-template-dev by influxdata
Hugo template development skill for InfluxData docs-v2. Enforces proper build and runtime testing to catch template errors that build-only validation misses.
Content & Writing
81 Stars
322 Forks
Updated Jan 18, 2026, 12:19 AM
Why Use This
This skill provides specialized capabilities for influxdata's codebase.
Use Cases
- Developing new features in the influxdata repository
- Refactoring existing code to follow influxdata standards
- Understanding and working with influxdata's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 551 Lines
Total Files 1
Total Size 0 B
License NOASSERTION
---
name: hugo-template-dev
description: Hugo template development skill for InfluxData docs-v2. Enforces proper build and runtime testing to catch template errors that build-only validation misses.
author: InfluxData
version: "1.2"
---
# 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 and capture output**
```bash
npx hugo server --port 1315 2>&1 | head -50
```
**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**
```bash
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 `mcp__claude-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**
```bash
pkill -f "hugo server --port 1315"
```
### Quick Test Command
Use this one-liner to test and get immediate feedback:
```bash
timeout 15 npx hugo server --port 1315 2>&1 | grep -E "(error|Error|ERROR|fail|FAIL)" | 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 only
the pages listed in the PR description to a stable GitHub Pages preview URL —
a targeted hosted preview that replaces "check out my branch and run
`npx hugo server`" for reviewers.
**When you change `layouts/`, `assets/`, or `data/`, list every page the
reviewer should verify in the PR body.** 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:
```bash
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
```
2. **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.
3. **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 Hyphenated Keys
**Wrong:**
```go
{{ .Site.Data.article-data.influxdb }}
```
**Correct:**
```go
{{ index .Site.Data "article-data" "influxdb" }}
```
### 2. Nil Field Access
**Wrong:**
```go
{{ range $articles }}
{{ .path }} {{/* Fails if item is nil or wrong type */}}
{{ end }}
```
**Correct:**
```go
{{ range $articles }}
{{ if . }}
{{ with index . "path" }}
{{ . }}
{{ end }}
{{ end }}
{{ end }}
```
### 3. Type Assertion on Interface{}
**Wrong:**
```go
{{ range $data }}
{{ .fields.menuName }}
{{ end }}
```
**Correct:**
```go
{{ 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 `{}`:
```go
{{/* This doesn't catch empty maps */}}
{{ if $data }}
{{ .field }} {{/* Still fails if $data is {} */}}
{{ end }}
```
**Solution:** Check for specific keys:
```go
{{ if and $data (isset $data "field") }}
{{ index $data "field" }}
{{ end }}
```
## Hugo Data Access Patterns
### Safe Nested Access
```go
{{/* 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
```go
{{ 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
## 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:**
```html
{{/* 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`):
```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`):
```typescript
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`:
```javascript
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:
```html
<div
data-component="api-toc"
data-headings="{{ .headings | jsonify | safeHTMLAttr }}"
data-scroll-offset="80"
>
</div>
```
TypeScript:
```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:
```html
{{/* 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
```bash
npx hugo server --port 1315 --verbose 2>&1 | head -100
```
### Print Variables for Debugging
```go
{{/* Temporary debugging - REMOVE before committing */}}
<pre>{{ printf "%#v" $myVariable }}</pre>
```
### Check Data File Loading
```bash
# Verify data files exist and are valid YAML
cat data/article-data/influxdb/influxdb3-core/articles.yml | head -20
```
## Integration with CI/CD
### Pre-commit Hook (Recommended)
Add to `.lefthook.yml` or pre-commit configuration:
```yaml
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
```yaml
- 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
## 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
Name Size