Claude Skill

hugo-theme

Build, customize, and debug advanced Hugo CMS themes — template architecture, asset pipeline (CSS/JS/image processing), shortcodes and render hooks, page bundles, cover images, Hugo Modules, performance, SEO, and CI/CD. Use when working on a Hugo theme or site template layer. Do

LLM Mart · 0 points · 10 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download magnus919-agent-skills-hugo-theme-addad86.zip · 43 KB
Part of magnus919/agent-skills — 145 skills

Install

skills CLI npx skills add https://github.com/magnus919/agent-skills/tree/main/hugo-theme
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
Git git clone https://github.com/magnus919/agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.

README

Hugo Theme Development

Intermediate-to-advanced patterns for building and customizing Hugo CMS themes. Covers template architecture, asset pipeline, shortcodes, performance, SEO, and accessibility.

Why Install This Skill

When your agent loads this skill, it becomes a Hugo theme developer who can:

  • Set up template architecture — baseof.html with blocks, template lookup order, partials
  • Integrate Tailwind CSS — v4 with css.TailwindCSS or v3 with PostCSS
  • Build responsive images — srcset, Hugo Pipes processing
  • Create shortcodes and render hooks — complex nested shortcodes, Mermaid, custom link/image rendering
  • Optimize performance — partialCached, cache TTLs, build speed
  • Implement accessibility — semantic HTML landmarks, ARIA patterns, keyboard navigation
  • Configure SEO — JSON-LD structured data, Open Graph, Twitter Cards

What You Get

Directory Purpose
SKILL.md Quick-start theme bootstrap, reference file index
references/ 7 reference files: template architecture, asset pipeline, shortcodes & hooks, content & i18n, modules & performance, design & accessibility, SEO & output formats

Triggers

Load this when working on a Hugo theme or site template layer.

Requirements

Hugo v0.154+. Works with any agent framework supporting the Agent Skills format.

Quick Start

Start with the setup and first workflow in SKILL.md, then use the linked resources for the specific task you need to complete.

Skill manifest

Hugo Theme Development

Intermediate-to-advanced patterns for Hugo CMS theme development. Load the relevant reference file for your task.

Reference Files

Topic Hugo Min Load when... File
Template Architecture v0.120+ You need to set up base templates with blocks, understand template lookup order (kind/layout/type/section), create partials, use partial decorators (v0.154+), or work with shortcode fundamentals references/template-architecture.md
Asset Pipeline v0.161+ You're integrating Tailwind CSS v4 (css.TailwindCSS) or v3 (PostCSS), using Hugo Pipes for SCSS/JS bundling, setting up fingerprinting and SRI, building responsive images with srcset, or processing page/global/remote resources references/asset-pipeline.md
Shortcodes & Render Hooks v0.112+ You need complex nested shortcodes, raw HTML shortcodes, markdown rendering inside shortcodes, custom render hooks for links/images/headings/code blocks, or language-specific code block rendering (Mermaid, etc.) references/shortcodes-and-hooks.md
Content Organization & i18n v0.126+ You're working with leaf vs branch bundles, headless bundles, cover images, custom taxonomies, content adapters (v0.126+, dynamic pages), section-specific layouts, archetypes, or internationalization (translation tables, multilingual) references/content-and-i18n.md
Cover Images v0.120+ You need to add cover/hero images to articles, support both page bundle resources and frontmatter paths, generate responsive srcsets, or handle the no-cover case gracefully references/cover-images.md
Modules & Performance v0.109+ You're using Hugo Modules (init, import, vendor, workspace), building theme components with mount configuration, optimizing build speed with partialCached, configuring cache TTLs, or using configuration-driven theming (params, cascade) references/modules-and-performance.md
Design, UX & Accessibility v0.120+ You need typography systems, accessible color palettes, design tokens, semantic HTML landmarks, ARIA patterns, keyboard navigation, accessible forms, content-first layouts, responsive navigation, engagement patterns (reading progress, dark mode toggle, sharing), Core Web Vitals optimization, container queries, :has() selectors, or testing/QA automation (axe-core, Lighthouse CI, visual regression) references/design-accessibility.md
SEO, Output Formats & CI/CD v0.120+ You need JSON-LD structured data, Open Graph / Twitter Cards, custom output formats (JSON, AMP), sitemap customization, or CI/CD pipelines for themes (GitHub Actions, testing, deployment) references/seo-outputs-testing.md

Quick Start

{{/* Minimal theme baseof.html — start here */}}
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
  {{ block "styles" . }}{{ end }}
</head>
<body>
  {{ block "header" . }}{{ partial "header.html" . }}{{ end }}
  <main>{{ block "main" . }}{{ end }}</main>
  {{ block "footer" . }}{{ partial "footer.html" . }}{{ end }}
  {{ block "scripts" . }}{{ end }}
</body>
</html>

Step-by-Step: Bootstrap a New Theme

# 1. Create the theme directory
mkdir -p themes/my-theme/{layouts/{_default,_markup,partials,shortcodes},assets/{scss,css,js}}

# 2. Create baseof.html (use the template above) defining blocks:
#    title, styles, header, main, footer, scripts

# 3. Create partials for reusable components
#    layouts/partials/header.html, footer.html, css.html

# 4. Set up your asset pipeline
#    - SCSS  → assets/scss/main.scss + toCSS partial
#    - Tailwind  → assets/css/main.css + css.TailwindCSS partial
#    - JS  → assets/js/main.js + js.Build

# 5. Configure hugo.yaml
#    theme: my-theme
#    See the reference file for your chosen CSS approach.

# 6. Build and verify
hugo --gc
ls public/ | head

Tip: Project-level layouts/ overrides theme layouts/. If you want to test your theme in isolation, keep the project layouts/ directory empty until you need overrides.

Common Pitfalls

  • SCSS requires Hugo extended edition. The default macOS/Homebrew Hugo build is NOT extended. Verify with hugo version | grep extended.
  • Tailwind v4 uses css.TailwindCSS, not PostCSS. Don't install postcss-cli for v4 — use the native pipe directly. Tailwind v3 still needs the PostCSS pipeline.
  • partialCached stale with non-constant args. Variant strings (.Section, page.RelPermalink) must be unique per caller. Repeated section names produce stale results.
  • hugo new respects archetype directory structure. Place archetypes at archetypes/<section>/index.md to create page bundles instead of flat files.
  • resources.Get looks in assets/, not static/. Files in static/ are copied verbatim and not processed by Hugo Pipes. Use assets/ for any file that goes through Pipes.
  • Content adapter templates MUST use _content.gotmpl naming. Regular .md files in the same directory are ignored when a _content.gotmpl exists.
  • Render hook templates go in _markup/ subdirectories. Not in _default/ directly — they need layouts/_default/_markup/render-link.html or section-specific layouts/<type>/_markup/.
  • block in partials conflicts with define in page templates. {{ block "title" . }} inside a partial (e.g. head.html) uses the same Go template namespace as {{ define "title" }} in page templates (e.g. single.html). When both exist in the render tree, Hugo errors with multiple definition of template "title". Fix: use direct page variables (.Title, .Site.Title) in partials instead of block. Reserve block exclusively for the baseof.html shell.
Files (agent-skills)
  • evals
    • evals.json 11 KB
      {
        "schema_version": 1,
        "skill_name": "hugo-theme",
        "evals": [
          {
            "id": "audit-existing-hugo-site-seo",
            "prompt": "Our marketing site is built with Hugo and organic traffic is dropping. Audit the theme's SEO and fix what's missing: page titles, meta descriptions, canonical URLs, social sharing previews, structured data, and the sitemap.",
            "expected_output": "A structured SEO audit plus concrete template changes grounded in the skill's guidance. It should check that baseof.html emits per-page titles and descriptions, add JSON-LD structured data via partials like layouts/partials/jsonld/article.html (Article schema with headline, datePublished/dateModified, mainEntityOfPage) plus BreadcrumbList and Organization schemas, wire up Open Graph and Twitter Cards using Hugo's built-in {{ template \"_internal/opengraph.html\" . }} and {{ template \"_internal/twitter_cards.html\" . }} partials (called with `template`, not `partial`, passing full page context), emit canonical links that honor a front-matter canonicalURL override before falling back to .Permalink, and customize or verify sitemap output via hugo.yaml sitemap config or a layouts/sitemap.xml template that respects per-page sitemap.disable front matter. Fixes should be delivered as Hugo template code with verification via a production build (hugo --minify --gc) inspecting public/ HTML output.",
            "assertions": [
              "Provides JSON-LD structured-data templates (at minimum an Article schema partial) placed under layouts/partials/jsonld/ and wired into the head block of baseof.html",
              "Uses Hugo's built-in internal partials _internal/opengraph.html and _internal/twitter_cards.html invoked with {{ template ... . }} rather than `partial`, or explains overriding them in layouts/partials/",
              "Emits a <link rel=\"canonical\"> that prefers a page's canonicalURL front-matter param and otherwise falls back to .Permalink",
              "Addresses sitemap generation through config (sitemap: changefreq/priority/filename in hugo.yaml) or a custom layouts/sitemap.xml honoring sitemap.disable per page",
              "Verifies results by building the site (e.g., hugo --gc, ideally --minify) and inspecting the generated public/ HTML head"
            ]
          },
          {
            "id": "lighthouse-ci-quality-gate",
            "prompt": "We want every pull request on our Hugo theme repo to fail if Lighthouse performance, accessibility, or SEO scores drop below 0.9. Set up Lighthouse CI as a quality gate.",
            "expected_output": "A Lighthouse CI setup matching the skill's documented pattern: an lighthouserc JSON with collect.numberOfRuns set to at least 3 and desktop preset with simulated throttling, and an assert section using the lighthouse:recommended preset with error-level assertions on categories:performance, categories:accessibility, and categories:seo each requiring minScore 0.9. It should run the gate with npx @lhci/cli@0.14.x autorun (pinned version), typically as a 'Lighthouse CI' step in a GitHub Actions workflow after npm ci and the site build, serving the built site for collection. The deliverable should include the workflow YAML wiring and note that assertions at error level make the job fail when scores drop below threshold.",
            "assertions": [
              "Configures lighthouserc with collect.numberOfRuns >= 3 and desktop preset with throttlingMethod simulate",
              "Asserts on categories:performance, categories:accessibility, and categories:seo with minScore 0.9 at error level so failures break CI",
              "Runs the gate via npx @lhci/cli@0.14.x autorun with the version pinned",
              "Places the step in a CI pipeline (GitHub Actions) that builds the Hugo site before collection"
            ],
            "case_set": "dev"
          },
          {
            "id": "accessibility-audit-and-fixes",
            "prompt": "Run accessibility checks on our Hugo site and fix whatever they flag. We keep getting complaints that keyboard users can't navigate the menu and screen readers struggle with our article pages.",
            "expected_output": "An automated accessibility pass using axe-core followed by targeted fixes. The skill's approach is npx @axe-core/cli@4.13.0 against the locally served site (hugo server, default port 1313), optionally with Playwright + @axe-core/playwright tests asserting zero violations for the homepage and specific components like nav[aria-label=\"Main navigation\"]. For the reported symptoms it should apply the skill's design/accessibility guidance: semantic HTML landmarks, ARIA patterns and keyboard navigation for the menu (focus indicators visible on all interactive elements, Escape/arrow-key behavior), appropriate alt attributes on images, and contrast-checked color tokens. It should also offer the skill's manual QA checklist (keyboard-only navigation, VoiceOver/NVDA, 200-400% zoom, prefers-reduced-motion, 320px reflow without horizontal scroll, touch targets >= 24x24 CSS px) to catch what automation misses, and integrate the checks into CI alongside html-validate.",
            "assertions": [
              "Runs automated audits with axe-core (npx @axe-core/cli@4.13.0 against http://localhost:1313/ from hugo server) and/or @axe-core/playwright tests asserting zero violations",
              "Fixes the navigation complaints with semantic landmarks, ARIA patterns, keyboard operability, and visible focus indicators per the skill's design-accessibility guidance",
              "Includes a manual QA checklist covering keyboard-only navigation, screen readers, zoom levels, prefers-reduced-motion, 320px reflow, and touch target sizes",
              "Wires accessibility checks into CI so regressions are caught on future PRs"
            ]
          },
          {
            "id": "non-hugo-static-site-routes-away",
            "prompt": "I have a static documentation site built with plain HTML, CSS, and a tiny Node build script — no static site generator. Can you help me restructure its templates and improve its SEO?",
            "expected_output": "The agent should recognize this is outside the hugo-theme skill's scope: that skill is specifically for Hugo CMS themes and sites (template architecture, Hugo Pipes asset pipeline, shortcodes, Hugo Modules). It must not apply Hugo-specific solutions such as baseof.html blocks, layouts/_default lookup order, resources.Get/js.Build pipelines, or Hugo config in hugo.yaml. Instead it should decline or route away, noting the request involves a hand-rolled static build rather than a Hugo theme, and either help with generic HTML/CSS/Node techniques explicitly framed as outside this skill, or suggest adopting a generator like Hugo only if the user wants to migrate — without pretending Hugo template mechanics apply to a plain build-script site.",
            "assertions": [
              "Recognizes the project is not a Hugo site and does not prescribe Hugo-specific files such as baseof.html, layouts/, assets/ Pipes, or hugo.yaml config",
              "Explicitly frames the request as outside the hugo-theme skill's trigger boundary (Hugo themes/site template layer)",
              "Still responds helpfully: offers generic static-site guidance or a migration path to Hugo instead of refusing outright"
            ],
            "case_set": "regression"
          },
          {
            "id": "tailwind-v4-asset-pipeline-setup",
            "prompt": "Set up the CSS and JS pipeline for my new Hugo theme: I want Tailwind v4 for styling and a small bit of TypeScript, both minified with subresource integrity in production but untouched during development.",
            "expected_output": "Asset-pipeline partials following the skill's documented patterns. For Tailwind v4 it should use Hugo's native css.TailwindCSS pipe on an entry file under assets/css/ (e.g., resources.Get \"css/main.css\" | css.TailwindCSS $opts with minify tied to not hugo.IsDevelopment) — NOT a PostCSS/postcss-cli setup, which the skill flags as a v3-only requirement and a common pitfall. For TypeScript/JS it should use resources.Get \"js/main.js\" | js.Build (dict \"minify\" true) noting js.Build requires ES module import/export syntax, not CommonJS require(). In production both outputs should be fingerprinted with integrity attributes (fingerprint | then .Data.Integrity on the link/script tags); in development the raw unminified output is linked. It should also respect the skill's pitfalls: resources.Get reads from assets/ not static/ (static/ is copied verbatim, unprocessed), Tailwind v4 @source directives resolve relative to the project root, and SCSS would additionally require the extended Hugo build (hugo version | grep extended).",
            "assertions": [
              "Uses css.TailwindCSS for Tailwind v4 and does not introduce postcss-cli or a PostCSS chain for it",
              "Bundles JS via js.Build with ES module syntax noted, and gates minification on environment (hugo.IsDevelopment / hugo.IsProduction)",
              "Applies fingerprint in production and emits integrity=\"{{ .Data.Integrity }}\" on the stylesheet/script tags",
              "Loads entry files from assets/ via resources.Get and warns that static/ files bypass Hugo Pipes",
              "Mentions the extended-edition caveat if SCSS compilation is involved, or confirms it's unnecessary for the chosen pure-CSS/Tailwind path"
            ]
          },
          {
            "id": "json-search-index-and-sitemap-formats",
            "prompt": "For our Hugo theme I need two extra machine-readable outputs: a JSON search index at /index.json listing recent posts, and control over sitemap.xml so draft-ish pages can be excluded. How do I wire that up?",
            "expected_output": "Custom output format work straight from the skill's seo-outputs-testing reference. Define a JSON outputFormat in hugo.yaml (mediaType application/json, baseName index, isPlainText true, notAlternative true — the last one because hugo --minify can corrupt JSON output) and select it per kind via front matter or config outputs (e.g., home: [HTML, RSS, JSON]). The template must be named with the doubled extension layouts/_default/index.json.json — first token is the output format name, second the file suffix; omitting either means Hugo won't find the template — and should range over .Site.RegularPages with jsonify'd fields. For the sitemap: either configure sitemap options in hugo.yaml or provide a layouts/sitemap.xml template whose range skips pages with sitemap.disable: true front matter, letting the user mark draft-ish pages accordingly. Pitfalls to carry over: RSS/list templates must iterate .Site.RegularPages (not .Site.Pages, which includes section/taxonomy/home pages), custom formats still require baseName, and built-in templates are invoked with {{ template ... . }} passing full page context.",
            "assertions": [
              "Defines the JSON output format in config with mediaType application/json, baseName index, and notAlternative true to shield it from --minify",
              "Names the search-index template with the double extension (index.json.json) and explains the format-name/file-suffix convention",
              "Ranges over Site.RegularPages rather than Site.Pages for content listings",
              "Excludes pages from the sitemap via per-page sitemap.disable front matter handled by config or a custom layouts/sitemap.xml",
              "Selects output formats per kind (outputs: home: [HTML, RSS, JSON] or equivalent front matter)"
            ]
          }
        ]
      }
      
  • references
    • asset-pipeline.md 7.9 KB
      # Asset Pipeline
      
      Hugo Pipes processes assets through the `assets/` directory with a functional pipeline syntax. Requires **Hugo extended edition** for SCSS support.
      
      ## Hugo Pipes — SCSS/SASS Compilation
      
      ```go-html-template
      {{ $opts := dict "outputStyle" "compressed" "includePaths" (slice "node_modules") }}
      {{ $styles := resources.Get "scss/main.scss" | toCSS $opts }}
      <link rel="stylesheet" href="{{ $styles.Permalink }}">
      ```
      
      Source: [Hugo docs — SASS/SCSS](https://gohugo.io/hugo-pipes/transpile-sass-to-css/)
      
      ## Bundling, Minification, Fingerprinting
      
      **Full pipeline:**
      ```go-html-template
      {{ $css := resources.Get "css/main.css" | resources.PostCSS | minify | fingerprint }}
      <link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}">
      ```
      
      **JS bundling:**
      ```go-html-template
      {{ $js := resources.Get "js/main.js" | js.Build (dict "minify" true) | fingerprint }}
      <script src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}"></script>
      ```
      
      ## PostCSS Integration
      
      Create `assets/postcss.config.js`:
      ```js
      module.exports = {
        plugins: {
          autoprefixer: {},
          'postcss-import': {},
          'tailwindcss': {},
        }
      }
      ```
      
      **Template:**
      ```go-html-template
      {{ $css := resources.Get "css/main.css" | resources.PostCSS }}
      {{ if hugo.IsProduction }}{{ $css = $css | minify | fingerprint }}{{ end }}
      ```
      
      Source: [Bryce Wray's Hugo + Tailwind guide](https://www.brycewray.com/posts/2021/02/tailwind-head-hugo-pipes/)
      
      ## Tailwind CSS v4 (Native, v0.161+)
      
      Hugo v0.161+ has `css.TailwindCSS` — no PostCSS dependency required.
      
      **Install:**
      ```bash
      npm install --save-dev tailwindcss @tailwindcss/cli @tailwindcss/typography
      ```
      
      **Config (`hugo.yaml`):**
      ```yaml
      build:
        buildStats:
          enable: true
        cachebusters:
        - source: 'assets/notwatching/hugo_stats\.json'
          target: css
      module:
        mounts:
        - source: assets
          target: assets
        - disableWatch: true
          source: hugo_stats.json
          target: assets/notwatching/hugo_stats.json
      ```
      
      **Entry CSS (`assets/css/main.css`):**
      ```css
      @import "tailwindcss";
      @plugin "@tailwindcss/typography";
      @source "hugo_stats.json";
      ```
      
      **Template:**
      ```go-html-template
      {{ with resources.Get "css/main.css" }}
        {{ $opts := dict "minify" (not hugo.IsDevelopment) }}
        {{ with . | css.TailwindCSS $opts }}
          {{ if hugo.IsDevelopment }}
            <link rel="stylesheet" href="{{ .RelPermalink }}">
          {{ else }}
            {{ with . | fingerprint }}
              <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
            {{ end }}
          {{ end }}
        {{ end }}
      {{ end }}
      ```
      
      **Defer in baseof (`<head>`):**
      ```go-html-template
      {{ with (templates.Defer (dict "key" "global")) }}
        {{ partial "css.html" . }}
      {{ end }}
      ```
      
      Source: [Hugo docs — css.TailwindCSS](https://gohugo.io/functions/css/tailwindcss/)
      
      ### Tailwind v4 Deployment Checklist
      
      Follow these in order:
      
      ```yaml
      # 1. Config — add to hugo.yaml
      build:
        buildStats:
          enable: true
      module:
        mounts:
        - source: assets
          target: assets
        - disableWatch: true
          source: hugo_stats.json
          target: assets/notwatching/hugo_stats.json
      ```
      
      ```css
      /* 2. Entry CSS — assets/css/main.css */
      @import "tailwindcss";
      @plugin "@tailwindcss/typography";
      @source "hugo_stats.json";
      ```
      
      ```bash
      # 3. Install
      npm install --save-dev tailwindcss @tailwindcss/cli @tailwindcss/typography
      ```
      
      ```go-html-template
      {{/* 4. CSS partial — layouts/partials/css.html */}}
      {{ with resources.Get "css/main.css" }}
        {{ $opts := dict "minify" (not hugo.IsDevelopment) }}
        {{ with . | css.TailwindCSS $opts }}
          {{ if hugo.IsDevelopment }}
            <link rel="stylesheet" href="{{ .RelPermalink }}">
          {{ else }}
            {{ with . | fingerprint }}
              <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
            {{ end }}
          {{ end }}
        {{ end }}
      {{ end }}
      ```
      
      ```html
      {{/* 5. Dark mode toggle HTML + JS — add to header partial */}}
      <button id="theme-toggle" aria-label="Switch to dark mode" aria-pressed="false" type="button">
        <svg aria-hidden="true" class="sun-icon" width="20" height="20"><use href="#sun"/></svg>
        <svg aria-hidden="true" class="moon-icon" width="20" height="20" hidden><use href="#moon"/></svg>
      </button>
      
      <script>
      (function() {
        const theme = localStorage.getItem('theme');
        const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
        if (theme === 'dark' || (theme === null && prefersDark)) {
          document.documentElement.setAttribute('data-theme', 'dark');
        }
      })();
      </script>
      ```
      
      ```javascript
      // 6. Toggle handler — placed before closing </body>
      document.getElementById('theme-toggle')?.addEventListener('click', function() {
        const html = document.documentElement;
        const isDark = html.getAttribute('data-theme') === 'dark';
        const newTheme = isDark ? 'light' : 'dark';
        html.setAttribute('data-theme', newTheme);
        localStorage.setItem('theme', newTheme);
        this.setAttribute('aria-label', 'Switch to ' + (isDark ? 'light' : 'dark') + ' mode');
        this.setAttribute('aria-pressed', !isDark);
        this.querySelector('.sun-icon').hidden = !isDark;
        this.querySelector('.moon-icon').hidden = isDark;
      });
      ```
      
      ## Tailwind CSS v3 (PostCSS approach)
      
      ```bash
      npm install -D tailwindcss postcss postcss-cli autoprefixer
      npx tailwindcss init -p
      ```
      
      **`assets/css/postcss.config.js`:**
      ```js
      module.exports = {
        plugins: {
          tailwindcss: {},
          autoprefixer: {},
        }
      }
      ```
      
      **Template pipe:**
      ```go-html-template
      {{ $css := resources.Get "css/main.css" | resources.PostCSS }}
      {{ if hugo.IsProduction }}{{ $css = $css | minify | fingerprint }}{{ end }}
      ```
      
      ## Image Processing
      
      All image operations return cached results. Operations are idempotent.
      
      | Method | Description |
      |--------|-------------|
      | `.Resize "400x"` | Resize to width 400px |
      | `.Resize "400x webp"` | Resize and convert to WebP |
      | `.Fill "400x400 center"` | Crop and resize to exact dimensions |
      | `.Fit "400x400"` | Downscale to fit within box |
      | `.Crop "400x400"` | Crop to dimensions |
      | `.Filter (images.Grayscale)` | Apply image filters |
      | `.Process "resize 800x webp"` | Unified method (modern) |
      
      **Responsive images with srcset:**
      ```go-html-template
      {{ $image := .Resources.Get "photo.jpg" }}
      {{ $small := $image.Resize "500x" }}
      {{ $medium := $image.Resize "800x" }}
      {{ $large := $image.Resize "1200x" }}
      <img
        src="{{ $medium.RelPermalink }}"
        srcset="{{ $small.RelPermalink }} 500w,
                {{ $medium.RelPermalink }} 800w,
                {{ $large.RelPermalink }} 1200w"
        sizes="(max-width: 600px) 500px, (max-width: 900px) 800px, 1200px"
        alt=""
        loading="lazy"
        width="{{ $medium.Width }}" height="{{ $medium.Height }}">
      ```
      
      **Resource sources:**
      - **Global resource**: `resources.Get "images/photo.jpg"` (from `assets/`)
      - **Page resource**: `.Resources.Get "photo.jpg"` (from page bundle)
      - **Remote resource**: `resources.GetRemote "https://..."`
      
      Always set explicit `width` and `height` on images to prevent Cumulative Layout Shift (CLS). Hugo's `.Width` and `.Height` provide these after processing.
      
      Source: [Hugo docs — image processing](https://gohugo.io/content-management/image-processing/)
      
      ## Pitfalls
      
      - **`resources.Get` looks in `assets/`, not `static/`.** Files in `static/` are copied verbatim with no Pipes processing. Use `assets/` for any file you want to transform.
      - **`resources.GetRemote` respects network caching.** Subsequent builds won't re-fetch remote resources unless the cache is cleared. Force re-download with `hugo --ignoreCache`.
      - **`js.Build` requires ES module syntax.** CommonJS (`require()`) won't work — use `import`/`export`. Set `js.Build (dict "target" "es2015")` for broader browser support.
      - **SCSS `includePaths` must include `node_modules` explicitly.** Hugo doesn't auto-resolve npm packages for SCSS `@use`/`@import`. Always pass `includePaths` when using npm-installed SCSS dependencies.
      - **`css.TailwindCSS` directives load from project root.** `@source` paths in the entry CSS are relative to the project, not to `assets/`.
      
    • content-and-i18n.md 6.9 KB
      # Content Organization & Internationalization
      
      ## Taxonomies
      
      Default taxonomies: `tags` and `categories`. Customize in config:
      
      ```yaml
      taxonomies:
        tag: tags
        category: categories
        series: series
        author: authors
      ```
      
      **Template access:**
      ```go-html-template
      {{ range .Site.Taxonomies.tags }}
        <li><a href="{{ .Page.RelPermalink }}">{{ .Page.Title }}</a> ({{ .Count }})</li>
      {{ end }}
      ```
      
      **Weighted taxonomies** — use `tags_weight` (or `categories_weight`, `series_weight`) in front matter to influence sort order on term pages. Higher weights appear first.
      
      **Custom taxonomy templates:**
      ```
      layouts/
      ├── taxonomy/
      │   ├── taxonomy.html       # Lists all terms in a taxonomy (e.g., all tags)
      │   └── term.html           # Lists pages with a specific term (e.g., all "hugo" posts)
      └── _default/
          ├── taxonomy.html       # Fallback
          └── term.html           # Fallback
      ```
      
      ## Sections and Page Bundles
      
      ### Directory Structure
      
      ```
      content/
      ├── _index.md                  # Home page (kind: home)
      ├── posts/
      │   ├── _index.md              # Blog section (kind: section)
      │   ├── my-post/
      │   │   ├── index.md           # Leaf bundle (kind: page)
      │   │   ├── hero.jpg
      │   │   └── gallery/
      │   │       ├── img1.jpg
      │   │       └── img2.jpg
      │   └── flat-post.md           # Flat page, no bundle
      └── projects/
          ├── _index.md              # Branch bundle (kind: section)
          └── my-project.md
      ```
      
      ### Leaf vs Branch Bundle Comparison
      
      | | Leaf Bundle | Branch Bundle |
      |---|---|---|
      | Index file | `index.md` | `_index.md` |
      | Page kind | `page` | `home`, `section`, `taxonomy`, `term` |
      | Template type | `single` | `home`, `section`, `taxonomy`, `term` |
      | Descendants | None | Zero or more |
      | Resource types | `page`, `image`, `video`, etc. | All but `page` |
      
      ### Headless Bundles
      
      A leaf bundle that doesn't render a page — only its resources are accessible via `.Resources`:
      
      ```yaml
      ---
      title: Image Gallery
      headless: true
      ---
      ```
      
      Or using build options:
      ```yaml
      ---
      _build:
        list: never
        render: never
      ---
      ```
      
      Useful for: galleries, reusable content components, podcast episode assets, data fragments consumed by other pages.
      
      ## Content Adapters (v0.126.0+)
      
      Dynamically create pages from external data (APIs, JSON files, remote content) without on-disk content files. Place `_content.gotmpl` in a content directory:
      
      ```go-html-template
      {{/* content/books/_content.gotmpl — creates pages from remote JSON */}}
      {{ $data := dict }}
      {{ $url := "https://example.com/books.json" }}
      {{ with try (resources.GetRemote $url) }}
        {{ with .Err }}
          {{ errorf "Failed: %s" . }}
        {{ else with .Value }}
          {{ $data = . | transform.Unmarshal }}
        {{ end }}
      {{ end }}
      
      {{/* EnableAllLanguages must be called BEFORE iterating to create pages in all languages */}}
      {{ $.EnableAllLanguages }}
      
      {{ range $data }}
        {{ $content := dict "mediaType" "text/markdown" "value" .summary }}
        {{ $params := dict "author" .author "isbn" .isbn }}
        {{ $page := dict
          "content" $content
          "kind" "page"
          "params" $params
          "path" .title
          "title" .title
        }}
        {{ $.AddPage $page }}
      {{ end }}
      ```
      
      **Key methods on `$` (page generator context):**
      
      | Method | Description |
      |--------|-------------|
      | `AddPage $page` | Add a dynamically generated page |
      | `AddResource $resource` | Add a dynamically generated resource |
      | `Store` | Page-scoped memory store |
      | `Site` | Site context |
      | `EnableAllLanguages` | Create pages for all languages at once |
      | `EnableAllDimensions` | Create pages for all output format/dimension combinations |
      
      Regular `.md` files in the same directory are ignored when a `_content.gotmpl` exists.
      
      Source: [Hugo docs — content adapters](https://gohugo.io/content-management/content-adapters/)
      
      ## Internationalization (i18n)
      
      ### Configuration
      
      ```yaml
      defaultContentLanguage: en
      languages:
        en:
          languageName: English
          weight: 1
        fr:
          languageName: Français
          weight: 2
          params:
            description: "Site en français"
      ```
      
      ### Translation Approaches
      
      | Approach | How it works | Best for |
      |----------|-------------|----------|
      | **Translation tables** | `i18n/` YAML/TOML/JSON files with key-value pairs | UI strings, labels, static text |
      | **Content in subdirectories** | `content/en/`, `content/fr/` with parallel structure | Full content translation |
      | **Filename suffix** | `post.en.md`, `post.fr.md` in same directory | Single-page translation |
      | **translationKey** | Same key in front matter across content files | Cross-language page linking |
      
      **Translation table** (`i18n/en.yaml`):
      ```yaml
      - id: read_more
        translation: "Read more"
      - id: posted_on
        translation: "Posted on {{ .Date }}"
      ```
      
      **Template usage:**
      ```go-html-template
      {{ i18n "read_more" }}
      {{ i18n "posted_on" (dict "Date" (time.Format "January 2, 2006" .Date)) }}
      ```
      
      ### Multilingual Features
      
      - **`relLangURL` / `absLangURL`** — prefix URLs with the current language prefix
      - **`.Site.Languages`** — all configured languages
      - **`.Translations`** — page's translations in other languages
      - **`.AllTranslations`** — all translations including the current page
      - **`.IsTranslated`** — whether the page has translations
      
      **Language switcher:**
      ```go-html-template
      {{ range .Site.Home.AllTranslations }}
        <a href="{{ .RelPermalink }}">{{ .Language.LanguageName }}</a>
      {{ end }}
      ```
      
      ### Localization
      
      Dates, numbers, and currency can be localized:
      ```go-html-template
      {{ time.Format ":date_full" .Date }}        ← "Monday, January 2, 2006"
      {{ lang.NumberFormat 2 12345.6789 }}        ← "12,345.68" (locale-aware)
      ```
      
      ## Pitfalls
      
      - **`headless: true` prevents page rendering but resources remain accessible.** Use `.Resources.GetMatch` or `.Resources.ByType` from another page to access headless bundle resources.
      - **Content adapters re-run on every build.** Build-time data fetches from `resources.GetRemote` are cached during the build but re-fetched on each `hugo` invocation. Use a static data file and import it if the source rarely changes.
      - **Taxonomy `_index.md` supports cascade.** Place `_index.md` in a taxonomy section (e.g., `content/tags/_index.md`) with cascade rules to apply layouts or params to all term pages within that taxonomy.
      - **Translation keys must be unique across all translation files.** Duplicate IDs are silently ignored (first wins). Verify with `hugo server` and check for missing translation warnings.
      - **Leaf bundle `index.md` replaces the URL slug.** A leaf bundle at `content/posts/my-post/index.md` has URL `/posts/my-post/`. The directory name IS the slug — renaming the directory changes the URL.
      - **`.Site.LastChange` is not available on taxonomy, term, or some section pages.** It only returns a value when a regular page exists. Use `now.Format` as a fallback in footer partials that run across all page kinds: `{{ with .Site.LastChange }}{{ .Format \"2006\" }}{{ else }}{{ now.Format \"2006\" }}{{ end }}`.
      
    • cover-images.md 2.2 KB
      # Cover Images
      
      Use a page resource for a per-article cover image. Page resources live inside a
      page bundle, alongside its `index.md` or `_index.md` file. Hugo can process
      those images at build time.
      
      ```text
      content/
      └── posts/
          └── my-post/
              ├── index.md
              └── cover.jpg
      ```
      
      ## Template Pattern
      
      Use `GetMatch` so a missing cover does not break the page. Resize the image for
      the rendered width, and include intrinsic dimensions to avoid layout shift.
      
      ```go-html-template
      {{ with .Resources.GetMatch "cover.*" }}
        {{ $cover := .Resize "1200x webp" }}
        <img src="{{ $cover.RelPermalink }}"
             width="{{ $cover.Width }}"
             height="{{ $cover.Height }}"
             alt=""
             loading="lazy">
      {{ end }}
      ```
      
      Use meaningful alternative text when the image conveys content; leave `alt` empty
      only when it is decorative. For a hero image near the page title, omit lazy
      loading when it is likely to be in the initial viewport.
      
      ## Front Matter Path Fallback
      
      When a theme supports an explicit front matter path, resolve it as a page
      resource first. If the image instead belongs to the global asset pipeline, use
      `resources.Get` for paths below `assets/`.
      
      ```go-html-template
      {{ $path := .Params.cover | default "cover.jpg" }}
      {{ with .Resources.GetMatch $path }}
        {{ $cover := .Fill "1200x630 center webp" }}
        <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
      {{ end }}
      ```
      
      ## Responsive Variants
      
      Generate a small set of widths and use `srcset` when the same cover appears in
      both card and article layouts. Keep the source image in the page bundle; Hugo
      caches generated derivatives between builds.
      
      ```go-html-template
      {{ with .Resources.GetMatch "cover.*" }}
        {{ $small := .Resize "640x webp" }}
        {{ $large := .Resize "1200x webp" }}
        <img src="{{ $large.RelPermalink }}"
             srcset="{{ $small.RelPermalink }} 640w, {{ $large.RelPermalink }} 1200w"
             sizes="(max-width: 700px) 100vw, 1200px"
             width="{{ $large.Width }}" height="{{ $large.Height }}" alt="">
      {{ end }}
      ```
      
      Sources: [Hugo page resources](https://gohugo.io/content-management/page-resources/) and [Hugo image processing](https://gohugo.io/content-management/image-processing/).
      
    • design-accessibility.md 40.8 KB
      # Design, UX & Accessibility for CMS Themes
      
      Platform-agnostic guidance for building beautiful, inclusive, and performant CMS themes. Applies to Hugo, WordPress, Ghost, Statamic, Craft CMS, Jekyll, Eleventy — any system that renders templates to HTML.
      
      ---
      
      ## Table of Contents
      
      1. [Typography Systems](#1-typography-systems)
      2. [Accessible Color & Contrast](#2-accessible-color--contrast)
      3. [Spacing & Layout](#3-spacing--layout)
      4. [Design Tokens & Theming](#4-design-tokens--theming)
      5. [Semantic HTML & Landmarks](#5-semantic-html--landmarks)
      6. [ARIA & Dynamic Content](#6-aria--dynamic-content)
      7. [Keyboard Navigation & Focus](#7-keyboard-navigation--focus)
      8. [Accessible Forms & Search](#8-accessible-forms--search)
      9. [Content-First Design Patterns](#9-content-first-design-patterns)
      10. [Navigation & Information Architecture](#10-navigation--information-architecture)
      11. [Engagement Patterns](#11-engagement-patterns)
      12. [Performance & Core Web Vitals](#12-performance--core-web-vitals)
      13. [Modern CSS for Themes](#13-modern-css-for-themes)
      14. [Theme Testing & QA](#14-theme-testing--qa)
      15. [Sources & References](#15-sources--references)
      
      ---
      
      ## 1. Typography Systems
      
      ### 1.1 Fluid Type Scale
      
      A modular type scale ensures visual harmony across headings and body text. Use `clamp()` to size fluidly between viewport widths without media queries:
      
      ```css
      :root {
        --step--2: clamp(0.6944rem, 0.6515rem + 0.2144vw, 0.8333rem);
        --step--1: clamp(0.8333rem, 0.7708rem + 0.3125vw, 1rem);
        --step-0:  clamp(1rem, 0.9115rem + 0.4427vw, 1.25rem);
        --step-1:  clamp(1.2rem, 1.0755rem + 0.6224vw, 1.5625rem);
        --step-2:  clamp(1.44rem, 1.2665rem + 0.8671vw, 1.9531rem);
        --step-3:  clamp(1.728rem, 1.4885rem + 1.1979vw, 2.4414rem);
        --step-4:  clamp(2.074rem, 1.7466rem + 1.6372vw, 3.0518rem);
        --step-5:  clamp(2.488rem, 2.0463rem + 2.2084vw, 3.8147rem);
      }
      
      h1 { font-size: var(--step-5); }
      h2 { font-size: var(--step-3); }
      h3 { font-size: var(--step-2); }
      body { font-size: var(--step-0); }
      small { font-size: var(--step--1); }
      ```
      
      **Typography best practices:**
      - Body text: 16–18px (1rem–1.125rem) as base
      - Line height: 1.5–1.7 for body, 1.1–1.3 for headings
      - Measure (line length): 45–75 characters per line, ideal 66 CPL. WCAG 1.4.8 (AAA) mandates max 80 CPL.
      - Use `ch` units for text container width: `max-width: 65ch`
      - Limit to 2–3 font families and 3–4 weights total
      
      ### 1.2 Font Loading Strategy
      
      Self-host fonts as WOFF2 for performance and privacy:
      
      ```css
      @font-face {
        font-family: 'BodyFont';
        src: url('/fonts/body-regular.woff2') format('woff2');
        font-display: swap;        /* Show fallback text immediately */
        font-weight: 400;
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
          U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
          U+FEFF, U+FFFD;
      }
      ```
      
      ```html
      <!-- Preload critical fonts -->
      <link rel="preload" href="/fonts/body-regular.woff2" as="font" type="font/woff2" crossorigin>
      ```
      
      Never use `font-display: block` — it hides text for up to 3 seconds while the font loads, creating a flash of invisible text (FOIT). Use `swap` to render fallback text immediately, or `optional` if you'd rather fall back to the system font entirely on slow connections.
      
      ### 1.3 WCAG Text Spacing (SC 1.4.12)
      
      Users may override your text spacing for readability. Ensure no content loss when these overrides are applied:
      
      ```css
      .prose p {
        line-height: 1.5;          /* minimum 1.5× font size */
        margin-bottom: 1.5em;      /* 1.5× spacing between paragraphs */
        word-spacing: 0.16em;
        letter-spacing: 0.12em;
      }
      ```
      
      The WCAG text spacing bookmarklet applies these overrides — test with it during development.
      
      ---
      
      ## 2. Accessible Color & Contrast
      
      ### 2.1 WCAG Contrast Requirements
      
      **WCAG 2.2 minimum ratios (SC 1.4.3 & 1.4.6):**
      
      | Level | Normal text | Large text (18pt+ / 14pt bold) | UI components & graphics |
      |-------|-------------|--------------------------------|--------------------------|
      | AA    | ≥ 4.5:1     | ≥ 3:1                          | ≥ 3:1 (SC 1.4.11)       |
      | AAA   | ≥ 7:1       | ≥ 4.5:1                        | n/a                      |
      
      ### 2.2 Designing an Accessible Palette
      
      Choose color tokens that meet contrast from the start — don't fix them later:
      
      ```css
      :root {
        /* Text — all ≥8.6:1 on white */
        --color-text-primary: #1a1a1a;        /* 15:1 on white */
        --color-text-secondary: #4a4a4a;       /* 8.6:1 on white */
      
        /* Use muted text sparingly — it must still be readable */
        --color-text-muted: #6b6b6b;           /* 5.2:1 on white — small text minimum */
      
        /* Brand colors with accessible contrast on their expected backgrounds */
        --color-primary: #0055cc;              /* 4.8:1 on white, passes AA for text */
        --color-primary-text: #ffffff;         /* for buttons on --color-primary */
      
        /* Surface */
        --color-surface: #ffffff;
        --color-surface-secondary: #f5f5f5;    /* Sufficient contrast from white for borders */
        --color-border: #d4d4d4;
      }
      ```
      
      **Hard rules:**
      - Never convey information by color alone — add icons, underlines, or text labels
      - Links must have ≥ 3:1 contrast from body text AND an underline OR hover/focus underline
      - Test all color pairs with WebAIM contrast checker or axe DevTools before shipping
      - Test with `prefers-contrast: more` — a user preference for increased contrast
      
      ### 2.3 Dark Mode
      
      ```css
      @media (prefers-color-scheme: dark) {
        :root {
          --color-surface: #1a1a2e;
          --color-text-primary: #e8e8e8;
          --color-text-secondary: #a0a0a0;
          --color-border: #2a2a3e;
          --color-link: #6ba3ff;
          --color-link-hover: #8bb9ff;
          --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
        }
      }
      
      /* Manual toggle override */
      [data-theme="dark"] {
        --color-surface: #1a1a2e;
        --color-text-primary: #e8e8e8;
        /* ... same overrides ... */
      }
      ```
      
      **Modern browsers** support `light-dark()` for simpler theme switching (Chrome 123+, Firefox 128+):
      
      ```css
      :root {
        color-scheme: light dark;
        --color-surface: light-dark(#ffffff, #1a1a2e);
        --color-text-primary: light-dark(#1a1a1a, #e8e8e8);
        --color-link: light-dark(#0055cc, #6ba3ff);
      }
      ```
      
      ### 2.4 Theme Switching Without Flash
      
      Apply the user's preferred theme before any CSS renders to prevent a flash of incorrect theme:
      
      ```html
      <script>
        (function() {
          const theme = localStorage.getItem('theme');
          const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
          if (theme === 'dark' || (theme === null && prefersDark)) {
            document.documentElement.setAttribute('data-theme', 'dark');
          }
        })();
      </script>
      ```
      
      Place this inline in `<head>` before any stylesheets. It blocks rendering for microseconds but prevents the jarring light-to-dark flash.
      
      ---
      
      ## 3. Spacing & Layout
      
      ### 3.1 Consistent Spacing Scale
      
      Base on a 4px or 8px unit:
      
      ```css
      :root {
        --space-0: 0;
        --space-1: 0.25rem;   /*  4px */
        --space-2: 0.5rem;    /*  8px */
        --space-3: 0.75rem;   /* 12px */
        --space-4: 1rem;      /* 16px */
        --space-5: 1.5rem;    /* 24px */
        --space-6: 2rem;      /* 32px */
        --space-7: 3rem;      /* 48px */
        --space-8: 4rem;      /* 64px */
        --space-9: 6rem;      /* 96px */
      }
      ```
      
      **Vertical rhythm** — consistent spacing between elements without thinking about each one:
      
      ```css
      /* CUBE CSS flow utility */
      .flow > * + * {
        margin-top: var(--flow-space, 1em);
      }
      ```
      
      ### 3.2 Content-Out Page Layout
      
      A content-first grid that gives you full-bleed and constrained regions without nested wrappers:
      
      ```css
      .page-layout {
        display: grid;
        grid-template-columns:
          [full-start] minmax(1rem, 1fr)
          [main-start] minmax(0, 65ch)
          [main-end] minmax(1rem, 1fr)
          [full-end];
      }
      
      .page-layout > * {
        grid-column: main-start / main-end;   /* All children default to content column */
      }
      
      .page-layout > .full-width {
        grid-column: full-start / full-end;    /* Opt in to full bleed */
      }
      ```
      
      ### 3.3 Responsive Card Grid
      
      No media queries needed:
      
      ```css
      .card-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
        gap: var(--space-5);
      }
      ```
      
      **Responsive testing guidance:**
      - Use content-driven breakpoints, not device-driven ones
      - Test at every 100px width from 320px to 1600px
      - WCAG 1.4.10 (Reflow) requires no horizontal scroll at 320px equivalent width
      - Consider container queries for component-level responsiveness
      
      ---
      
      ## 4. Design Tokens & Theming
      
      ### 4.1 Token Architecture
      
      Layer tokens in a hierarchy: **Global → Semantic → Component**
      
      ```css
      /* Layer 1: Raw values (seldom change) */
      :root {
        --color-blue-600: #0055cc;
        --color-blue-700: #003d99;
        --font-body: 'Inter', system-ui, sans-serif;
        --font-heading: 'Inter', system-ui, sans-serif;
        --font-mono: 'JetBrains Mono', 'Cascadia Code', monospace;
      }
      
      /* Layer 2: Semantic tokens (theme-aware) */
      :root {
        --color-surface: #ffffff;
        --color-text: #1a1a1a;
        --color-link: var(--color-blue-600);
        --color-link-hover: var(--color-blue-700);
        --spacing-section: 4rem;
        --border-radius-sm: 4px;
        --border-radius-md: 8px;
        --shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
      }
      
      /* Layer 3: Component-level overrides (in component CSS files) */
      .card {
        --card-padding: var(--space-4);
        --card-radius: var(--border-radius-md);
      }
      ```
      
      ### 4.2 User Preference Detection
      
      Always respect these user preferences:
      
      ```css
      /* High contrast */
      @media (prefers-contrast: more) {
        :root {
          --color-text: #000000;
          --color-text-secondary: #1a1a1a;
          --color-border: #000000;
        }
      }
      
      /* Reduced transparency */
      @media (prefers-reduced-transparency: reduce) {
        * {
          opacity: 1 !important;
          backdrop-filter: none !important;
        }
      }
      ```
      
      ---
      
      ## 5. Semantic HTML & Landmarks
      
      ### 5.1 Page Landmarks (WCAG 1.3.1)
      
      Every CMS theme should provide these landmark regions:
      
      ```html
      <body>
        <a href="#main-content" class="skip-link">Skip to main content</a>
      
        <header role="banner">
          <nav aria-label="Main navigation">
            <!-- site nav -->
          </nav>
        </header>
      
        <main id="main-content">
          <article>
            <h1>Page Title</h1>
            <!-- content -->
          </article>
          <aside aria-label="Related content">
            <!-- sidebar -->
          </aside>
        </main>
      
        <footer role="contentinfo">
          <!-- footer content -->
        </footer>
      </body>
      ```
      
      ### 5.2 Heading Hierarchy (WCAG 1.3.1)
      
      - One `<h1>` per page (usually the page/post title in CMS)
      - Heading levels must not skip (h1 → h2 → h3, never h1 → h3)
      - For CMS themes: ensure editors can't break hierarchy — provide visual guidance in the editor, or use a render hook that maps heading levels to a semantic hierarchy
      
      ```html
      <article>
        <h1>Post Title</h1>
        <section aria-labelledby="section1-heading">
          <h2 id="section1-heading">Introduction</h2>
          <h3>Sub-point</h3>
        </section>
      </article>
      ```
      
      ### 5.3 Proper `<nav>` Usage
      
      - Use `<nav>` only for primary and secondary navigation blocks, not all link groups
      - Use `aria-label` to disambiguate multiple navs: `<nav aria-label="Breadcrumb">`, `<nav aria-label="Main">`
      - Footer links should be wrapped in `<nav aria-label="Footer">` only if they constitute navigation
      
      ### 5.4 Image Alt Text
      
      Every `<img>` must have appropriate `alt`:
      - Informative images: describe what's visually shown
      - Decorative images: `alt=""` (empty) — never omit the attribute
      - Linked images: describe the link destination, not the image
      - Complex images (charts, diagrams): `alt` for summary, plus a longer description nearby or via `aria-describedby`
      
      ---
      
      ## 6. ARIA & Dynamic Content
      
      ### 6.1 Golden Rule
      
      Use semantic HTML first. ARIA only when HTML semantics are insufficient.
      
      ### 6.2 Common ARIA Patterns for CMS Themes
      
      ```html
      <!-- Skip link -->
      <a class="skip-link" href="#main-content">Skip to main content</a>
      
      <!-- Breadcrumb nav -->
      <nav aria-label="Breadcrumb">
        <ol>
          <li><a href="/">Home</a></li>
          <li><a href="/blog">Blog</a></li>
          <li aria-current="page">Current Post</li>
        </ol>
      </nav>
      
      <!-- Mobile menu toggle -->
      <button
        aria-expanded="false"
        aria-controls="main-nav-menu"
        aria-label="Open navigation menu"
        type="button"
        class="nav-toggle">
        <span class="hamburger-icon"></span>
      </button>
      ```
      
      ### 6.3 Screen Reader Announcements (Live Regions)
      
      ```html
      <!-- Status after form submission -->
      <div role="status" aria-live="polite" class="form-status">
        <!-- Injected: "Thank you! Your comment is awaiting moderation." -->
      </div>
      
      <!-- Search results updates -->
      <div aria-live="polite" aria-atomic="true" class="search-results-count">
        <!-- "Showing 12 results" injected on filter change -->
      </div>
      ```
      
      **`aria-live` values:**
      - `polite` — announce when user is idle (default for status messages)
      - `assertive` — announce immediately (use sparingly, for critical errors)
      - `role="status"` — implicit `aria-live="polite"`, prefer this for status messages
      
      **Best practices:**
      - Live regions must exist in the DOM **before** content changes
      - Use `aria-atomic="true"` when replacing entire content so the whole region is read
      - Empty the region, then re-add content to trigger re-announcement
      
      ---
      
      ## 7. Keyboard Navigation & Focus
      
      ### 7.1 Visible Focus Indicators (WCAG 2.4.7)
      
      ```css
      :focus-visible {
        outline: 2px solid var(--color-primary);
        outline-offset: 2px;
        border-radius: 2px;
      }
      
      /* Never do: :focus { outline: none; } without providing a replacement */
      /* Never use: outline: none on :focus without also adding :focus-visible styles */
      ```
      
      ### 7.2 Skip Link Pattern
      
      ```css
      .skip-link {
        position: absolute;
        top: -100%;
        left: 0;
        z-index: 10000;
        padding: 0.5rem 1rem;
        background: var(--color-primary);
        color: white;
        text-decoration: none;
      }
      .skip-link:focus {
        top: 0;
      }
      ```
      
      ### 7.3 Dropdown Keyboard Support
      
      All navigation must work by keyboard (WCAG 2.1.1):
      
      ```javascript
      const menuButton = document.querySelector('[aria-haspopup="true"]');
      const menu = document.getElementById(menuButton.getAttribute('aria-controls'));
      
      menuButton.addEventListener('click', () => {
        const expanded = menuButton.getAttribute('aria-expanded') === 'true' ? false : true;
        menuButton.setAttribute('aria-expanded', expanded);
      });
      
      menuButton.addEventListener('keydown', (e) => {
        if (e.key === 'ArrowDown') {
          e.preventDefault();
          menu.querySelector('a, button')?.focus();
        }
      });
      
      // Within menu: Arrow keys navigate items, Escape closes
      menu.addEventListener('keydown', (e) => {
        const items = [...menu.querySelectorAll('a, button')];
        const currentIndex = items.indexOf(document.activeElement);
        switch (e.key) {
          case 'ArrowDown':  e.preventDefault(); items[(currentIndex + 1) % items.length]?.focus(); break;
          case 'ArrowUp':    e.preventDefault(); items[(currentIndex - 1 + items.length) % items.length]?.focus(); break;
          case 'Escape':     e.preventDefault(); menuButton.focus(); menuButton.setAttribute('aria-expanded', 'false'); break;
          case 'Home':       e.preventDefault(); items[0]?.focus(); break;
          case 'End':        e.preventDefault(); items[items.length - 1]?.focus(); break;
        }
      });
      ```
      
      ### 7.4 Reduced Motion (WCAG 2.3.3)
      
      ```css
      @media (prefers-reduced-motion: reduce) {
        *,
        *::before,
        *::after {
          animation-duration: 0.01ms !important;
          animation-iteration-count: 1 !important;
          transition-duration: 0.01ms !important;
          scroll-behavior: auto !important;
        }
      }
      ```
      
      This is the most commonly used pattern. Apply it globally in every theme that uses any animations.
      
      ### 7.5 Font Scaling (WCAG 1.4.4)
      
      Users must be able to zoom text to 200% without loss of content or functionality:
      
      ```css
      html {
        font-size: 100%; /* Respect user's default browser font size */
      }
      body {
        font-size: 1rem;  /* Scales with user preferences */
      }
      /* Never use px for font sizes in components */
      ```
      
      ---
      
      ## 8. Accessible Forms & Search
      
      ### 8.1 Search Form
      
      ```html
      <form role="search" action="/search" method="get">
        <div class="search-field">
          <label for="search-input" class="sr-only">Search</label>
          <input
            type="search"
            id="search-input"
            name="q"
            placeholder="Search articles..."
            aria-describedby="search-hint"
          />
          <span id="search-hint" hidden>Use Enter to search</span>
        </div>
        <button type="submit">Search</button>
      </form>
      ```
      
      ### 8.2 Screen-Reader-Only Utility
      
      ```css
      .sr-only {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border-width: 0;
      }
      ```
      
      Use `.sr-only` for labels that are visually obvious but need screen reader context (e.g., a search button with an icon but no text).
      
      ### 8.3 Comment Form with Inline Validation
      
      ```html
      <form method="post" action="/comments" novalidate>
        <div class="field">
          <label for="comment-name">Name <span aria-hidden="true">*</span></label>
          <input type="text" id="comment-name" name="name" required aria-required="true" />
          <span class="field-error" id="comment-name-error" role="alert" hidden>
            Name is required
          </span>
        </div>
        <div class="field">
          <label for="comment-text">Comment <span aria-hidden="true">*</span></label>
          <textarea id="comment-text" name="text" required aria-required="true" rows="5"></textarea>
        </div>
        <button type="submit">Post Comment</button>
      </form>
      ```
      
      ---
      
      ## 9. Content-First Design Patterns
      
      ### 9.1 Prose Container
      
      ```css
      .prose {
        max-width: 65ch;           /* 65 characters per line — ideal readability */
        margin-inline: auto;
        font-size: var(--step-0);
        line-height: 1.7;
      }
      
      /* Heading rhythm */
      .prose h1 { font-size: var(--step-5); margin-top: 2.5em; margin-bottom: 0.5em; }
      .prose h2 { font-size: var(--step-3); margin-top: 2em;   margin-bottom: 0.5em; }
      .prose h3 { font-size: var(--step-2); margin-top: 1.5em; margin-bottom: 0.5em; }
      .prose h4 { font-size: var(--step-1); margin-top: 1.25em; margin-bottom: 0.5em; }
      
      .prose p  { margin-bottom: 1.5em; }
      .prose li { margin-bottom: 0.5em; }
      
      /* Tighter spacing after headings */
      .prose h2 + p, .prose h3 + p { margin-top: 0; }
      ```
      
      ### 9.2 Responsive Images
      
      Always provide explicit dimensions plus responsive variants:
      
      ```html
      <picture>
        <source
          media="(max-width: 599px)"
          srcset="article-portrait-sm.jpg 400w, article-portrait-lg.jpg 600w"
          sizes="100vw"
        />
        <source
          media="(min-width: 600px)"
          srcset="article-landscape-sm.jpg 600w, article-landscape-md.jpg 900w, article-landscape-lg.jpg 1200w"
          sizes="(min-width: 900px) 65ch, 90vw"
        />
        <img
          src="article-landscape-md.jpg"
          alt="Descriptive alt text"
          width="900"
          height="506"
          loading="lazy"
          decoding="async"
        />
      </picture>
      ```
      
      **Aspect ratio boxes** — the modern way:
      
      ```css
      .featured-image {
        aspect-ratio: 16 / 9;
        width: 100%;
        height: auto;
        object-fit: cover;
      }
      ```
      
      ### 9.3 Figures
      
      ```html
      <figure>
        <picture>
          <img src="photo.jpg" alt="Mountain landscape at sunset" width="900" height="600" />
        </picture>
        <figcaption>Sunset over the Rocky Mountains, Colorado.</figcaption>
      </figure>
      ```
      
      ```css
      figure {
        margin: var(--space-6) 0;
      }
      figure img {
        width: 100%;
        height: auto;
        border-radius: var(--border-radius-md);
      }
      figcaption {
        margin-top: var(--space-2);
        font-size: var(--step--1);
        color: var(--color-text-secondary);
        text-align: center;
      }
      ```
      
      ### 9.4 Blockquotes
      
      ```css
      blockquote {
        margin: var(--space-6) 0;
        padding: var(--space-4) var(--space-5);
        border-inline-start: 4px solid var(--color-accent);
        font-style: italic;
        font-size: var(--step-1);
        color: var(--color-text-secondary);
        background: color-mix(in srgb, var(--color-accent) 8%, transparent);
      }
      blockquote cite {
        display: block;
        margin-top: var(--space-2);
        font-size: var(--step--1);
        font-style: normal;
        color: var(--color-text-muted);
      }
      blockquote cite::before {
        content: '\2014\00A0'; /* em dash + space */
      }
      ```
      
      ### 9.5 Code Blocks
      
      ```css
      /* Inline code */
      code {
        font-family: var(--font-mono);
        font-size: 0.9em;
        padding: 0.15em 0.3em;
        background: var(--color-surface-secondary, #f0f0f0);
        border-radius: var(--border-radius-sm);
        word-break: break-word;
      }
      
      /* Code blocks */
      pre {
        font-family: var(--font-mono);
        font-size: 0.9rem;
        line-height: 1.6;
        padding: var(--space-4);
        overflow-x: auto;
        border-radius: var(--border-radius-md);
        background: #1a1a2e;
        color: #e8e8e8;
        tab-size: 2;
      }
      ```
      
      WCAG 1.4.10 exception: Code blocks and tables may horizontally scroll at 320px viewport width when the content cannot reflow.
      
      ### 9.6 Responsive Tables
      
      Two approaches depending on table complexity:
      
      **Approach 1 — Overflow wrapper** (for data tables):
      ```css
      .table-wrapper {
        overflow-x: auto;
        max-width: 100%;
        -webkit-overflow-scrolling: touch;
      }
      table {
        width: 100%;
        border-collapse: collapse;
        font-size: var(--step--1);
      }
      th, td {
        padding: var(--space-2) var(--space-3);
        text-align: left;
        border-bottom: 1px solid var(--color-border);
      }
      th {
        font-weight: 600;
        background: color-mix(in srgb, var(--color-surface) 95%, black);
      }
      ```
      
      **Approach 2 — Card layout on small screens** (for simple tables):
      ```css
      @media (max-width: 600px) {
        table.responsive-card thead {
          position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0);
        }
        table.responsive-card tbody,
        table.responsive-card tr,
        table.responsive-card td { display: block; }
        table.responsive-card tr {
          padding: var(--space-3);
          margin-bottom: var(--space-3);
          border: 1px solid var(--color-border);
          border-radius: var(--border-radius-md);
        }
        table.responsive-card td { padding: var(--space-1) 0; border: none; }
        table.responsive-card td::before {
          content: attr(data-label);
          display: block;
          font-weight: 600;
          font-size: 0.75rem;
          text-transform: uppercase;
          color: var(--color-text-secondary);
        }
      }
      ```
      
      ---
      
      ## 10. Navigation & Information Architecture
      
      ### 10.1 Accessible Dropdown Menu
      
      ```html
      <nav aria-label="Main navigation">
        <ul class="nav-list" role="list">
          <li><a href="/">Home</a></li>
          <li class="nav-item-has-children">
            <button
              aria-haspopup="true"
              aria-expanded="false"
              aria-controls="sub-menu-1"
              class="nav-link"
            >
              Products
              <svg aria-hidden="true" class="chevron" width="12" height="12"><use href="#chevron"/></svg>
            </button>
            <ul id="sub-menu-1" class="sub-menu" role="menu" aria-label="Products">
              <li role="none"><a href="/products/a" role="menuitem">Service A</a></li>
              <li role="none"><a href="/products/b" role="menuitem">Service B</a></li>
            </ul>
          </li>
        </ul>
      </nav>
      ```
      
      ```css
      .sub-menu {
        display: none;
        position: absolute;
        top: 100%;
        left: 0;
        min-width: 200px;
        background: var(--color-surface);
        border: 1px solid var(--color-border);
        border-radius: var(--border-radius-md);
        box-shadow: var(--shadow-sm);
        z-index: 100;
      }
      .nav-item-has-children:hover .sub-menu,
      .nav-item-has-children:focus-within .sub-menu {
        display: block;
      }
      ```
      
      ### 10.2 Mobile Hamburger Menu
      
      ```html
      <button
        class="hamburger"
        aria-controls="mobile-menu"
        aria-expanded="false"
        aria-label="Open menu"
        type="button"
      >
        <span class="hamburger-box">
          <span class="hamburger-inner"></span>
        </span>
      </button>
      <nav id="mobile-menu" class="mobile-menu" aria-label="Mobile navigation" hidden>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/about">About</a></li>
          <li><a href="/blog">Blog</a></li>
        </ul>
      </nav>
      ```
      
      ### 10.3 Breadcrumbs with Structured Data
      
      ```html
      <nav aria-label="Breadcrumb">
        <ol itemscope itemtype="https://schema.org/BreadcrumbList">
          <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
            <a itemprop="item" href="/"><span itemprop="name">Home</span></a>
            <meta itemprop="position" content="1">
          </li>
          <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
            <a itemprop="item" href="/blog"><span itemprop="name">Blog</span></a>
            <meta itemprop="position" content="2">
          </li>
          <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem" aria-current="page">
            <span itemprop="name">Current Post</span>
            <meta itemprop="position" content="3">
          </li>
        </ol>
      </nav>
      ```
      
      ```css
      .breadcrumb li:not(:last-child)::after {
        content: '/';
        margin-left: var(--space-1);
        color: var(--color-text-muted);
      }
      ```
      
      ### 10.4 Table of Contents
      
      ```html
      <nav aria-label="Table of contents" class="toc">
        <h2 class="toc-title">On this page</h2>
        <ol class="toc-list">
          <li><a href="#section-1">Introduction</a></li>
          <li><a href="#section-2">Getting Started</a>
            <ol>
              <li><a href="#section-2-1">Prerequisites</a></li>
            </ol>
          </li>
        </ol>
      </nav>
      ```
      
      ```css
      .toc {
        position: sticky;
        top: var(--space-5);
        max-height: calc(100vh - 2rem);
        overflow-y: auto;
      }
      .toc-list a {
        color: var(--color-text-secondary);
        text-decoration: none;
        padding: var(--space-1) 0;
        display: block;
        border-left: 2px solid transparent;
        padding-left: var(--space-2);
      }
      .toc-list a:hover,
      .toc-list a:focus-visible {
        color: var(--color-link);
        border-left-color: var(--color-link);
      }
      ```
      
      ### 10.5 Pagination
      
      ```html
      <nav aria-label="Pagination">
        <ul class="pagination">
          <li><a href="/blog/page/2" aria-label="Previous page">&laquo; Prev</a></li>
          <li><a href="/blog" aria-label="Page 1">1</a></li>
          <li><a href="/blog/page/2" aria-label="Page 2" aria-current="page">2</a></li>
          <li><a href="/blog/page/3" aria-label="Page 3">3</a></li>
          <li><span class="pagination-ellipsis">&hellip;</span></li>
          <li><a href="/blog/page/10" aria-label="Page 10">10</a></li>
          <li><a href="/blog/page/3" aria-label="Next page">Next &raquo;</a></li>
        </ul>
      </nav>
      ```
      
      ### 10.6 Search UI with Live Region
      
      ```html
      <form role="search" action="/search" method="get" class="search-form">
        <label for="nav-search" class="sr-only">Search articles</label>
        <input type="search" id="nav-search" name="q" placeholder="Search..."
               aria-describedby="search-instructions" autocomplete="off" />
        <span id="search-instructions" class="sr-only">Type your query and press Enter.</span>
        <button type="submit" aria-label="Submit search">
          <svg aria-hidden="true" width="16" height="16"><use href="#search-icon"/></svg>
        </button>
      </form>
      
      <!-- Results live region -->
      <div role="status" aria-live="polite" aria-atomic="true" class="search-status" hidden></div>
      <div id="search-results" aria-label="Search results"></div>
      ```
      
      ---
      
      ## 11. Engagement Patterns
      
      ### 11.1 Reading Progress Indicator
      
      ```html
      <div class="reading-progress" role="progressbar" aria-label="Reading progress"
           aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
      ```
      
      ```css
      .reading-progress {
        position: fixed;
        top: 0; left: 0;
        height: 3px;
        background: var(--color-accent);
        width: 0%;
        z-index: 1000;
        transition: width 100ms linear;
      }
      ```
      
      ```javascript
      window.addEventListener('scroll', () => {
        const scrollTop = window.scrollY;
        const docHeight = document.documentElement.scrollHeight - window.innerHeight;
        const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
        const bar = document.querySelector('.reading-progress');
        if (bar) {
          bar.style.width = progress + '%';
          bar.setAttribute('aria-valuenow', Math.round(progress));
        }
      }, { passive: true });
      ```
      
      ### 11.2 Dark Mode Toggle
      
      ```html
      <button id="theme-toggle" aria-label="Switch to dark mode" aria-pressed="false" type="button">
        <svg aria-hidden="true" class="sun-icon" width="20" height="20"><use href="#sun"/></svg>
        <svg aria-hidden="true" class="moon-icon" width="20" height="20" hidden><use href="#moon"/></svg>
      </button>
      ```
      
      ```javascript
      const toggle = document.getElementById('theme-toggle');
      const html = document.documentElement;
      
      toggle.addEventListener('click', () => {
        const currentTheme = html.getAttribute('data-theme') || 'light';
        const newTheme = currentTheme === 'light' ? 'dark' : 'light';
        html.setAttribute('data-theme', newTheme);
        localStorage.setItem('theme', newTheme);
        toggle.setAttribute('aria-label', `Switch to ${currentTheme} mode`);
        toggle.setAttribute('aria-pressed', newTheme === 'dark');
        document.querySelector('.sun-icon').hidden = newTheme === 'dark';
        document.querySelector('.moon-icon').hidden = newTheme === 'light';
      });
      ```
      
      > **Important:** The flash-prevention script (section 2.4) MUST be placed inline in `<head>` BEFORE any stylesheets. Without it, users see a flash of the wrong theme on page load. The toggle button JS above handles the interaction; the flash-prevention script prevents the jarring light-to-dark transition.
      
      ### 11.3 Social Sharing (Lightweight)
      
      Use the native Web Share API with clipboard fallback — no third-party scripts:
      
      ```html
      <aside aria-label="Share this article">
        <button type="button" class="share-button" data-share-url="https://example.com/post">
          <svg aria-hidden="true" width="16" height="16"><use href="#share-icon"/></svg>
          Share
        </button>
      </aside>
      ```
      
      ```javascript
      document.querySelector('.share-button')?.addEventListener('click', async (e) => {
        const url = e.currentTarget.dataset.shareUrl || window.location.href;
        if (navigator.share) {
          try {
            await navigator.share({ title: document.title, url });
          } catch (err) {
            if (err.name !== 'AbortError') console.error(err);
          }
        } else {
          try {
            await navigator.clipboard.writeText(url);
            // Announce to screen reader
            const status = document.getElementById('share-status');
            if (status) status.textContent = 'Link copied to clipboard';
          } catch {
            window.location.href = `mailto:?body=${encodeURIComponent(url)}&subject=${encodeURIComponent(document.title)}`;
          }
        }
      });
      ```
      
      ### 11.4 Newsletter Signup
      
      ```html
      <section aria-labelledby="newsletter-heading" class="newsletter">
        <h2 id="newsletter-heading">Stay Updated</h2>
        <p>Get the latest articles delivered to your inbox.</p>
        <form method="post" action="/newsletter/subscribe" novalidate>
          <label for="newsletter-email" class="sr-only">Email address</label>
          <input type="email" id="newsletter-email" name="email"
                 placeholder="your@email.com" required
                 aria-describedby="newsletter-hint" />
          <span id="newsletter-hint" class="sr-only">We'll never share your email</span>
          <button type="submit">Subscribe</button>
        </form>
        <div role="status" aria-live="polite" class="newsletter-status" hidden></div>
      </section>
      ```
      
      ### 11.5 Related Content
      
      ```html
      <section aria-labelledby="related-posts-heading" class="related-posts">
        <h2 id="related-posts-heading">Related Articles</h2>
        <div class="card-grid">
          <article class="card">
            <a href="/blog/related-post" class="card-link" aria-label="Read: Related Post Title">
              <img src="thumb.jpg" alt="" width="400" height="225" loading="lazy" />
              <h3 class="card-title">Related Post Title</h3>
            </a>
            <p class="card-excerpt">Brief description...</p>
          </article>
        </div>
      </section>
      ```
      
      ---
      
      ## 12. Performance & Core Web Vitals
      
      ### 12.1 LCP (Largest Contentful Paint) — Target ≤ 2.5s
      
      Primary strategies for CMS themes:
      
      1. **Optimize hero/featured images** — WebP/AVIF, responsive srcset, explicit dimensions, `fetchpriority="high"` on the hero
      2. **Eliminate render-blocking resources** — inline critical CSS, defer full CSS with preload, async/defer JS
      3. **Resource hints** for critical origins:
      
      ```html
      <link rel="preconnect" href="https://fonts.googleapis.com">
      <link rel="dns-prefetch" href="https://analytics.example.com">
      <link rel="preload" href="/images/hero.webp" as="image" fetchpriority="high">
      ```
      
      **Critical CSS pattern:**
      ```html
      <style>
        /* Above-the-fold styles: header, hero, navigation only */
        header { ... }
        .hero { ... }
        nav { ... }
      </style>
      <link rel="preload" href="/css/theme.css" as="style"
            onload="this.onload=null;this.rel='stylesheet'">
      ```
      
      ### 12.2 CLS (Cumulative Layout Shift) — Target ≤ 0.1
      
      1. **Always declare image dimensions** — width and height on every `<img>`
      2. **Use `aspect-ratio`** for dynamic/variable images:
      ```css
      .image-wrapper {
        aspect-ratio: 16 / 9;
        overflow: hidden;
      }
      .image-wrapper img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
      ```
      3. **Use `font-display: swap`** — prevents FOIT which causes layout shifts when fonts load
      4. **Reserve space for dynamic content** (ads, embeds):
      ```css
      .ad-container { min-height: 250px; width: 100%; }
      ```
      5. **Don't insert content above existing content** — reserve space or insert at the end
      
      ### 12.3 INP (Interaction to Next Paint) — Target ≤ 200ms
      
      - Keep main thread responsive: break up long tasks (< 50ms)
      - Defer non-critical JavaScript
      - Use `requestAnimationFrame` for visual updates
      - Lazy load below-the-fold content and images
      - Minimize third-party script impact — delay or load only on interaction:
      
      ```javascript
      document.getElementById('open-chat')?.addEventListener('click', () => {
        const script = document.createElement('script');
        script.src = 'https://chat-widget.example.com/widget.js';
        script.async = true;
        document.body.appendChild(script);
      }, { once: true });
      ```
      
      ### 12.4 Lazy Loading
      
      ```html
      <!-- Native lazy loading for below-fold images -->
      <img src="photo.jpg" alt="..." loading="lazy" width="800" height="600" decoding="async">
      
      <!-- For iframes (comments, embeds) -->
      <iframe src="https://example.com/widget" loading="lazy" title="Widget"></iframe>
      ```
      
      ### 12.5 Performance Budget Reference
      
      | Metric | Target |
      |--------|--------|
      | LCP | ≤ 2.5s |
      | INP | ≤ 200ms |
      | CLS | ≤ 0.1 |
      | FCP | ≤ 1.8s |
      | TBT | ≤ 200ms |
      | Total page weight | ≤ 500KB |
      | CSS | ≤ 50KB |
      | JS | ≤ 100KB |
      | Fonts | ≤ 50KB |
      
      ---
      
      ## 13. Modern CSS for Themes
      
      ### 13.1 Container Queries
      
      Component-based responsiveness without knowing the viewport:
      
      ```css
      .card-grid > * {
        container-type: inline-size;
        container-name: card;
      }
      
      @container card (min-width: 400px) {
        .card {
          display: grid;
          grid-template-columns: 200px 1fr;
          gap: var(--space-4);
        }
      }
      
      @container card (min-width: 600px) {
        .card__title { font-size: var(--step-2); }
      }
      ```
      
      **Container query length units** for fluid sizing inside containers:
      - `cqi` — 1% of container inline-size
      - `cqw` / `cqh` / `cqmin` / `cqmax`
      
      ### 13.2 The `:has()` Selector
      
      Style parents based on their children — powerful for CMS where content varies:
      
      ```css
      /* Card that contains an image gets a different layout */
      .card:has(img) {
        grid-template-columns: 1fr 2fr;
      }
      
      /* Different layout when the list has more than 3 items */
      .post-list:has(> :nth-child(4)) .post-item:first-child {
        grid-column: 1 / -1;
      }
      
      /* Style a form field that's currently invalid */
      .field:has(:invalid:not(:placeholder-shown)) .field-error {
        display: block;
      }
      ```
      
      ### 13.3 Logical Properties
      
      Write once, work across writing modes (LTR, RTL, vertical):
      
      ```css
      .container {
        margin-inline: auto;                    /* Instead of margin-left: auto; margin-right: auto */
        padding-inline: var(--space-4);         /* Instead of padding-left/right */
        border-inline-start: 3px solid var(--color-accent);  /* Instead of border-left */
        padding-block: var(--space-6);          /* Instead of padding-top/bottom */
      }
      ```
      
      **Works for:** `dir="ltr"` → margin-right, `dir="rtl"` → margin-left, RTL text automatically mirrored.
      
      ### 13.4 Subgrid
      
      Align nested elements to the parent grid — keeps cards aligned in a grid even when content varies:
      
      ```css
      .post-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
        gap: var(--space-5);
      }
      .post-card {
        display: grid;
        grid-template-rows: subgrid;   /* inherit parent row tracks */
        grid-row: span 3;              /* span 3 rows */
      }
      .post-card__meta {
        align-self: end;               /* all meta sections align to the bottom */
      }
      ```
      
      ### 13.5 Scroll-Driven Animations
      
      Only when user hasn't requested reduced motion:
      
      ```css
      @keyframes fade-in {
        from { opacity: 0; transform: translateY(20px); }
        to { opacity: 1; transform: translateY(0); }
      }
      
      @media (prefers-reduced-motion: no-preference) {
        .animate-on-scroll {
          animation: fade-in linear both;
          animation-timeline: view();
          animation-range: entry 0% entry 100%;
        }
      }
      ```
      
      ### 13.6 CSS Methodology Comparison
      
      | Methodology | Approach | Best For |
      |-------------|----------|----------|
      | **CUBE CSS** | Composition → Utility → Block → Exception | CMS themes, progressive enhancement — leverages cascade, tiny CSS output |
      | **BEM** | `.block__element--modifier` | Component libraries, strict naming, large teams |
      | **Utility-first** (Tailwind) | Atomic classes in HTML | Rapid prototyping, design systems |
      | **ITCSS** | Specificity layers (Settings→Tools→Generic→Elements→Objects→Components→Trumps) | Large-scale applications |
      
      For CMS themes, **CUBE CSS** is the recommended approach — it produces very little CSS, handles content variance gracefully, and composes well with `@layer`:
      
      ```css
      @layer composition, utilities, blocks, exceptions;
      
      @import 'composition/_grid.css' layer(composition);
      @import 'composition/_flow.css' layer(composition);
      @import 'utilities/_tokens.css' layer(utilities);
      @import 'blocks/_card.css' layer(blocks);
      @import 'exceptions/_states.css' layer(exceptions);
      ```
      
      ---
      
      ## 14. Theme Testing & QA
      
      ### 14.1 Accessibility Automation
      
      ```javascript
      import { test, expect } from '@playwright/test';
      import AxeBuilder from '@axe-core/playwright';
      
      test('homepage should have no accessibility violations', async ({ page }) => {
        await page.goto('/');
        const results = await new AxeBuilder({ page }).analyze();
        expect(results.violations).toEqual([]);
      });
      
      // Test specific components
      test('navigation should be accessible', async ({ page }) => {
        await page.goto('/');
        const nav = page.locator('nav[aria-label="Main navigation"]');
        const results = await new AxeBuilder({ page }).include(nav).analyze();
        expect(results.violations).toEqual([]);
      });
      ```
      
      ### 14.2 Visual Regression Testing
      
      ```javascript
      import { test, expect } from '@playwright/test';
      
      test('homepage visual regression', async ({ page }) => {
        await page.goto('/');
        await expect(page).toHaveScreenshot('homepage.png', {
          fullPage: true,
          maxDiffPixelRatio: 0.01,
        });
      });
      
      test('dark mode visual regression', async ({ page }) => {
        await page.goto('/');
        await page.click('#theme-toggle');
        await expect(page).toHaveScreenshot('homepage-dark.png', { fullPage: true });
      });
      ```
      
      ### 14.3 Lighthouse CI
      
      ```json
      {
        "ci": {
          "collect": {
            "numberOfRuns": 3,
            "settings": { "preset": "desktop", "throttlingMethod": "simulate" }
          },
          "assert": {
            "preset": "lighthouse:recommended",
            "assertions": {
              "categories:performance": ["error", { "minScore": 0.9 }],
              "categories:accessibility": ["error", { "minScore": 0.9 }],
              "categories:seo": ["error", { "minScore": 0.9 }]
            }
          }
        }
      }
      ```
      
      ### 14.4 Manual QA Checklist
      
      - [ ] Navigate entire site using only keyboard (Tab, Enter, Escape, Arrow keys)
      - [ ] Test with screen reader (VoiceOver on macOS, NVDA on Windows)
      - [ ] Test with browser zoom at 200%, 300%, 400%
      - [ ] Verify focus indicators visible on all interactive elements
      - [ ] Test all color combinations with WebAIM contrast checker
      - [ ] Test with `prefers-reduced-motion: reduce` enabled
      - [ ] Test with `prefers-color-scheme: dark` enabled
      - [ ] Test forms with and without JavaScript
      - [ ] Test print stylesheet
      - [ ] Verify all images have appropriate `alt` attributes
      - [ ] 320px reflow — no horizontal scroll (WCAG 1.4.10)
      - [ ] Touch targets ≥ 24×24 CSS px (WCAG 2.5.8)
      - [ ] Works in both portrait and landscape orientations
      
      ### 14.5 CI Pipeline
      
      ```yaml
      name: Theme Quality
      on: [push, pull_request]
      jobs:
        quality:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - run: npm ci
            - run: npm run build
            - name: Lighthouse CI
              run: npx @lhci/cli@0.14.x autorun
            - name: Accessibility
              run: npx playwright test --project=accessibility
            - name: Visual regression
              run: npx playwright test --project=visual
            - name: Validate HTML
              run: npx html-validate 'dist/**/*.html'
      ```
      
      ---
      
      ## 15. Sources & References
      
      | Topic | Source |
      |-------|--------|
      | WCAG 2.2 Full Specification | https://www.w3.org/TR/WCAG22/ |
      | WebAIM WCAG Checklist | https://webaim.org/standards/wcag/checklist |
      | WAI Accessible Navigation Tutorial | https://www.w3.org/WAI/tutorials/menus/flyout/ |
      | WAI Form Validation | https://www.w3.org/WAI/tutorials/forms/validation/ |
      | MDN Responsive Images Guide | https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images |
      | Core Web Vitals | https://web.dev/articles/top-cwv |
      | CUBE CSS Methodology | https://piccalil.li/blog/cube-css/ |
      | CSS Container Queries | https://css-tricks.com/css-container-queries/ |
      | MDN Media Queries for Accessibility | https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using_for_accessibility |
      | MDN ARIA Live Regions | https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions |
      | U.S. Web Design System Typography | https://designsystem.digital.gov/components/typography/ |
      | CSS Subgrid by Josh Comeau | https://www.joshwcomeau.com/css/subgrid/ |
      | Design Tokens Guide (Penpot) | https://penpot.app/blog/the-developers-guide-to-design-tokens-and-css-variables/ |
      | Axe + Playwright Testing | https://dev.to/subito/how-we-automate-accessibility-testing-with-playwright-and-axe-3ok5 |
      | Web Font Optimization | https://web.dev/learn/performance/optimize-web-fonts |
      
    • modules-and-performance.md 10.9 KB
      # Hugo Modules & Performance
      
      ## Hugo Modules
      
      Hugo Modules replace the older theme system with Go Modules-based dependency management.
      
      ### Initialization
      
      ```bash
      hugo mod init github.com/user/repo
      ```
      
      ### Adding Dependencies
      
      ```bash
      hugo mod get github.com/theNewDynamic/gohugo-theme-ananke
      hugo mod get github.com/gohugoio/hugo-mod-bootstrap-scss/v5
      ```
      
      ### Module Configuration (`hugo.yaml` / `hugo.toml`)
      
      ```yaml
      module:
        imports:
        - path: github.com/theNewDynamic/gohugo-theme-ananke
        - path: github.com/gohugoio/hugo-mod-bootstrap-scss/v5
          mounts:
          - source: assets/scss
            target: assets/bootstrap-scss
        - path: my-local-component
          path: ../components/table-of-contents
      ```
      
      ### Mount Configuration
      
      Maps filesystem paths to virtual paths Hugo understands:
      
      ```yaml
      module:
        mounts:
        - source: mycontent
          target: content
        - source: layouts
          target: layouts
        - source: assets
          target: assets
        - source: static
          target: static
        - source: node_modules/jquery/dist
          target: assets/js/vendor/jquery
        - disableWatch: true
          source: hugo_stats.json
          target: assets/notwatching/hugo_stats.json
      ```
      
      ### Common Operations
      
      | Command | Description |
      |---------|-------------|
      | `hugo mod get -u` | Update all module dependencies |
      | `hugo mod tidy` | Remove unused module entries from go.sum |
      | `hugo mod vendor` | Copy module files into `_vendor/` for offline builds |
      | `hugo mod graph` | Print module dependency tree |
      | `hugo mod verify` | Verify module integrity |
      | `hugo mod clean` | Clean module cache |
      
      ### Workspace Mode (v0.109.0+)
      
      For developing multiple modules together without publishing:
      
      ```bash
      hugo mod init github.com/user/mytheme
      ```
      
      **`hugo.workspace`** file:
      ```
      workspace:
        - /path/to/component-a
        - /path/to/component-b
      ```
      
      Run with: `hugo server --workspace`
      
      ### Module Replacement
      
      Replace remote modules with local copies during development:
      
      ```bash
      hugo mod replace github.com/example/theme -> ../local-theme
      ```
      
      ### Theme Components
      
      A Hugo Module can contain any combination of these component types:
      
      | Component | Directory | Purpose |
      |-----------|-----------|---------|
      | Templates | `layouts/` | Template overrides |
      | Content | `content/` | Content additions |
      | Assets | `assets/` | CSS, JS, images (processed by Hugo Pipes) |
      | i18n | `i18n/` | Translation bundles |
      | Static | `static/` | Raw static files |
      | Data | `data/` | Structured data |
      | Archetypes | `archetypes/` | Content templates |
      
      ### Component Composition Pattern
      
      Build reusable theme components as single-purpose modules:
      
      ```
      table-of-contents/
      ├── layouts/
      │   └── partials/
      │       └── toc.html          # Renders a table of contents
      ├── assets/
      │   └── css/
      │       └── toc.css           # Styling for the ToC
      ├── data/
      │   └── toc-config.yaml       # Default configuration
      ├── README.md
      └── theme.toml
      ```
      
      ### Cross-Component Data Communication
      
      Use `Page.Store` (page-scoped) or `.Scratch` (template-scoped) to pass data between modules:
      
      ```go-html-template
      {{/* Component A sets data */}}
      {{ .Page.Store.Set "component-data" (dict "items" .Pages) }}
      
      {{/* Component B reads data */}}
      {{ with .Page.Store.Get "component-data" }}
        {{ range .items }}
          <li>{{ .Title }}</li>
        {{ end }}
      {{ end }}
      ```
      
      ## Performance & Caching
      
      ### partialCached
      
      `partialCached` caches the rendered output of a partial the first time it's called. Subsequent calls with the same arguments return the cached result.
      
      ```go-html-template
      {{ partialCached "sidebar.html" . }}
      {{ partialCached "sidebar.html" . "sidebar" }}
      ```
      
      **With variant keys** (cache is unique per combination of variant strings):
      ```go-html-template
      {{ partialCached "article-nav.html" . .Section }}
      {{ partialCached "article-nav.html" . .Section .CurrentSection.RelPermalink }}
      ```
      
      Variant keys prevent stale cross-contamination — each unique variant string gets its own cache entry.
      
      **Performance impact:** Can reduce build times by up to 40% on sites with hundreds of pages, especially for expensive partials like related-content queries, syntax highlighting, or image galleries.
      
      ### When NOT to use partialCached
      
      - Partials that depend on the **current page context** without a unique variant key (`partialCached "header.html" .` with no variant — header is usually the same for all pages, so this is safe)
      - Partials containing **`{{ hugo.Generator }}`** or other unique-per-page content
      - Partials that run **shortcode rendering** via `.RenderString` or `.RenderShortcodes`
      
      ### Cache Configuration
      
      Hugo uses LRU caches with configurable TTLs:
      
      ```yaml
      caches:
        assets:
          dir: :resourceDir/_gen
          maxAge: -1h          # Negative = expire after build
        images:
          dir: :resourceDir/_gen
          maxAge: 720h         # 30 days — Go duration syntax (h/m/s only, no 'd')
        modules:
          maxAge: 720h         # 30 days
        getresource:
          maxAge: 10m
        getjson:
          maxAge: 0            # Always re-fetch
        getcsv:
          maxAge: 0
      ```
      
      ### Cache Configuration Reference
      
      | Cache | Default TTL | Contains |
      |-------|-------------|---------|
      | `assets` | -1 (expire after build) | Processed CSS, JS |
      | `images` | -1 (expire after build) | Resized/Fit/Filled images |
      | `modules` | 720h | Downloaded module files |
      | `getresource` | 10m | `resources.GetRemote` results |
      | `getjson` | 0 (no cache) | `getJSON` results |
      
      ### Template Metrics
      
      Enable template execution metrics to find slow partials:
      
      ```bash
      hugo --templateMetrics --templateMetricsHints
      ```
      
      This reports execution time per template, including counts of `partialCached` hits and misses.
      
      ### Resource Bundling Strategies
      
      - **Concatenate CSS**: Use `resources.Concat` to combine small CSS files: `{{ $bundle := slice $reset $typography $layout | resources.Concat "css/bundle.css" }}`
      - **Separate critical CSS**: Extract above-the-fold styles and inline them in `<head>`; load deferred async CSS via `resources.PostCSS` + `defer`
      - **JS modules**: Bundle third-party JS via `js.Build` with `"minify": true`. Lazy-load non-critical scripts
      
      ### Build Performance Tips
      
      - **Use `partialCached` liberally** with section-specific variant keys
      - **Avoid `resources.GetRemote` in loops** — fetch once, reuse
      - **Use `hugo --gc`** to garbage-collect stale cache files
      - **Increase `--maxPageSize`** if you have pages with thousands of shortcodes
      - **Prefer `resources.Match` over `resources.Get` with wildcards** for bulk operations
      - **Set `build.buildStats.enable: true`** in config for Tailwind v4 to track CSS class usage
      
      ### Build Performance Troubleshooting
      
      If your build is slow, run this diagnostic first:
      
      ```bash
      hugo --templateMetrics --templateMetricsHints --gc
      ```
      
      Then check these common bottlenecks:
      
      | Symptom | Most Likely Cause | Fix |
      |---------|-------------------|-----|
      | Build time scales linearly with page count | Missing `partialCached` on expensive partials (related content, syntax highlighting, image galleries) | Add `partialCached` with section-specific variant keys. Each section gets its own cache entry. |
      | One partial dominates execution time | Identified by `--templateMetrics` — look for high cumulative time with low cache hit rate | Either add better variant keys, or move the expensive operation to build time (data file, content adapter) |
      | `resources.GetRemote` calls slow the build | Fetching the same URL on every page iteration | Fetch once in a `_content.gotmpl` or `data/` file, store results, then iterate locally |
      | CSS/SASS rebuild on every page | `includePaths` missing `node_modules`, or SCSS imports not cached | Verify `partialCached` on the CSS partial. Use `--gc` to clear stale cache. |
      | Module resolution slow | `hugo mod graph` shows deep dependency trees, or no `go.sum` | Run `hugo mod tidy && hugo mod vendor` for CI. Use `--ignoreVendorPaths` in dev. |
      | Image processing dominates build | Hundreds of images without cached resizes | Increase `images` cache TTL. Use `hugo --gc` only when stale. `--ignoreCache` re-processes everything. |
      | Build crashes on taxonomy/term pages | `.Site.LastChange` or `.Site.RegularPages` nil | Check for page-kind-specific template access. Wrap in `{{ with .Site.LastChange }}...{{ end }}`. |
      | Tailwind v4 build is slow or missing classes | `build.buildStats.enable` not set, or `@source` path incorrect | Verify `hugo_stats.json` is generated and mounted. Check `@source "hugo_stats.json"` path in entry CSS. |
      
      **Quick wins in order of impact:**
      1. Add `partialCached` with `.Section` variant to your most expensive partial
      2. Move `resources.GetRemote` calls from templates to `data/` files
      3. Increase `getresource` cache TTL from `10m` to `24h` if remote data changes infrequently
      4. Run `hugo mod tidy && hugo mod vendor` to freeze module versions
      5. Set `images` cache `maxAge` to `720h` (30 days) if images rarely change
      
      ## Configuration-Driven Theming
      
      ### Theme Params
      
      Make themes configurable via `hugo.yaml`:
      
      ```yaml
      params:
        theme:
          primaryColor: "#3b82f6"
          fontFamily: "Inter, sans-serif"
          layout: "grid"          # "grid" or "list"
          features:
            darkMode: true
            comments: false
      ```
      
      Accessed in templates as:
      ```go-html-template
      {{ .Site.Params.theme.primaryColor }}
      ```
      
      ### Front Matter Cascade
      
      Apply default front matter to groups of pages. Placed in `_index.md`:
      
      ```yaml
      ---
      title: Blog
      cascade:
        - _target:
            kind: page
            path: /blog/**
          layout: post
          show_sidebar: true
        - _target:
            kind: page
            path: /blog/archive/**
          show_sidebar: false
          params:
            section: archive
      ---
      ```
      
      **Cascade target filters:**
      
      | Target Param | Values |
      |-------------|--------|
      | `kind` | `page`, `section`, `home`, `taxonomy`, `term` |
      | `path` | Glob pattern (e.g., `/blog/**`) |
      | `type` | Content type |
      | `lang` | Language code |
      | `environment` | `development` or `production` |
      
      ### Per-Section Defaults via `_index.md`
      
      Each section's `_index.md` can set section-wide defaults:
      ```yaml
      ---
      title: Projects
      show_sidebar: false
      date: 2024-01-01
      params:
        section: projects
        icon: briefcase
      ---
      ```
      
      ## Pitfalls
      
      - **`partialCached` variant order matters.** Only the first call with a given key set caches the result. If the first call lacks a critical variant, the cached result is shared across all callers. Ensure the variant tuple fully describes the partial's dependencies.
      - **Hugo Modules use Go's semver, not git tags.** Module paths must follow Go conventions. If you see `404` on `hugo mod get`, verify the module path has a valid `go.mod` or `theme.toml`.
      - **`hugo mod vendor` is one-way.** Once vendored, `hugo mod get -u` won't update modules until you remove `_vendor/` and re-run. Use vendor for CI/deployment, not development.
      - **Cascade targets use glob patterns, not regex.** `path: /blog/**` matches all descendants of `/blog/`. Use `path: /blog/*` for immediate children only. No regex support.
      - **Mount source paths can be outside the project directory.** Modules often mount `node_modules` paths into `assets/`. This works for development but may fail in CI if `node_modules` isn't installed. Always verify mounts in clean builds.
      
    • seo-outputs-testing.md 10.8 KB
      # SEO, Output Formats & CI/CD
      
      ## SEO & Structured Data
      
      ### JSON-LD Structured Data
      
      Add schema.org structured data to your theme for search engines. Best placed in the `<head>` block of `baseof.html`.
      
      **Article schema (`layouts/partials/jsonld/article.html`):**
      ```go-html-template
      {{ if eq .Kind "page" }}
      <script type="application/ld+json">
      {
        "@context": "https://schema.org",
        "@type": "Article",
        "headline": {{ .Title | jsonify }},
        "description": {{ .Params.description | default .Summary | jsonify }},
        "author": {
          "@type": "Person",
          "name": {{ .Params.author | default .Site.Params.author | jsonify }}
        },
        "datePublished": {{ .Date.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
        "dateModified": {{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
        {{ with .Params.tags }}"keywords": {{ . | jsonify }},{{ end }}
        {{ with .Params.featureimage }}"image": {{ . | jsonify }},{{ end }}
        "mainEntityOfPage": {
          "@type": "WebPage",
          "@id": {{ .Permalink | jsonify }}
        }
      }
      </script>
      {{ end }}
      ```
      
      **Breadcrumb schema (`layouts/partials/jsonld/breadcrumb.html`):**
      ```go-html-template
      {{ $breadcrumb := collections.Slice }}
      {{ range .Ancestors.Reverse }}
        {{ $breadcrumb = $breadcrumb | append (dict
          "@type" "ListItem"
          "position" (len $breadcrumb | add 1)
          "name" .Title
          "item" .Permalink
        )}}
      {{ end }}
      {{ $breadcrumb = $breadcrumb | append (dict
        "@type" "ListItem"
        "position" (len $breadcrumb | add 1)
        "name" .Title
        "item" .Permalink
      )}}
      <script type="application/ld+json">
      {
        "@context": "https://schema.org",
        "@type": "BreadcrumbList",
        "itemListElement": {{ $breadcrumb | jsonify }}
      }
      </script>
      ```
      
      **Organization/Brand schema (`layouts/partials/jsonld/organization.html`):**
      ```go-html-template
      <script type="application/ld+json">
      {
        "@context": "https://schema.org",
        "@type": "Organization",
        "name": {{ .Site.Title | jsonify }},
        {{ with .Site.Params.logo }}"logo": {{ . | jsonify }},{{ end }}
        {{ with .Site.Params.social.linkedin }}"sameAs": ["{{ . }}"],{{ end }}
        "url": {{ .Site.BaseURL | jsonify }}
      }
      </script>
      ```
      
      ### Open Graph & Twitter Cards
      
      Hugo ships built-in `internal/opengraph.html` and `internal/twitter_cards.html` partials:
      
      ```go-html-template
      {{ template "_internal/opengraph.html" . }}
      {{ template "_internal/twitter_cards.html" . }}
      ```
      
      **Override the built-in templates** by placing your own at:
      ```
      layouts/partials/opengraph.html
      layouts/partials/twitter_cards.html
      ```
      
      The built-in templates read from:
      - `.Title`, `.Description`, `.Permalink`
      - `.Params.images` (array, first image used)
      - `.Site.Params.images` (default images)
      - `.Date`, `.Lastmod` (for `article:published_time`, `article:modified_time`)
      
      ### Canonical URLs
      
      ```go-html-template
      {{ if .Params.canonicalURL }}
        <link rel="canonical" href="{{ .Params.canonicalURL }}">
      {{ else }}
        <link rel="canonical" href="{{ .Permalink }}">
      {{ end }}
      ```
      
      Enable in config: `canonifyURLs: true`
      
      ### Sitemap Customization
      
      **Config:**
      ```yaml
      sitemap:
        changefreq: weekly
        priority: 0.5
        filename: sitemap.xml
      ```
      
      **Per-page overrides** in front matter:
      ```yaml
      ---
      sitemap:
        priority: 0.8
        changefreq: monthly
        disable: true
      ---
      ```
      
      **Custom sitemap template** (`layouts/sitemap.xml`):
      ```go-html-template
      {{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
      <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      {{ range .Data.Pages }}{{ if not .Params.sitemap.disable }}
        <url>
          <loc>{{ .Permalink }}</loc>
          <lastmod>{{ .Lastmod.Format "2006-01-02" }}</lastmod>
          {{ with .Params.sitemap.changefreq }}<changefreq>{{ . }}</changefreq>{{ end }}
          {{ if ge .Params.sitemap.priority 0 }}{{ with .Params.sitemap.priority }}<priority>{{ . }}</priority>{{ end }}{{ else }}<priority>{{ if .IsHome }}1.0{{ else }}0.8{{ end }}</priority>{{ end }}
        </url>
      {{ end }}{{ end }}
      </urlset>
      ```
      
      ## Custom Output Formats
      
      Create non-HTML output formats (JSON, AMP, etc.).
      
      **Config:**
      ```yaml
      outputFormats:
        JSON:
          mediaType: application/json
          baseName: index
          isPlainText: true
          notAlternative: true
        AMP:
          mediaType: text/html
          baseName: amp
          path: amp
          isHTML: true
      ```
      
      **Per-page output format selection:**
      ```yaml
      ---
      outputs:
        home: [HTML, RSS, JSON]
        page: [HTML, AMP]
        section: [HTML, RSS, JSON]
      ---
      ```
      
      **JSON output template** (`layouts/_default/index.json.json`):
      ```go-html-template
      {{- $pages := .Site.RegularPages -}}
      {{- $limit := .Site.Params.jsonLimit | default 20 -}}
      {
        "site": {
          "title": {{ .Site.Title | jsonify }},
          "url": {{ .Site.BaseURL | jsonify }},
          "pages": {{ len $pages }}
        },
        "pages": [
          {{- range first $limit $pages }}
          {
            "title": {{ .Title | jsonify }},
            "url": {{ .Permalink | jsonify }},
            "summary": {{ .Summary | plainify | jsonify }},
            "date": {{ .Date.Format "2006-01-02" | jsonify }},
            {{- with .Params.tags }}"tags": {{ . | jsonify }},{{ end }}
            "wordCount": {{ .WordCount }}
          }{{- if not (eq . (index (first $limit $pages) (sub (len (first $limit $pages)) 1))) }},{{ end }}
          {{- end }}
        ]
      }
      ```
      
      ### Output Format Template Naming Convention
      
      Templates for custom formats follow: `layouts/<section>/<template>.<format>.<suffix>`
      
      Examples:
      - `layouts/_default/list.json.json` — JSON list output
      - `layouts/_default/single.amp.html` — AMP single page
      - `layouts/_default/single.txt.txt` — Plain text output
      
      ### RSS Customization
      
      Override the built-in RSS template at `layouts/_default/rss.xml`:
      ```go-html-template
      {{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
      <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"
           xmlns:content="http://purl.org/rss/1.0/modules/content/">
        <channel>
          <title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
          <link>{{ .Permalink }}</link>
          <description>{{ .Site.Params.description }}</description>
          {{ with .Site.LanguageCode }}<language>{{ . }}</language>{{ end }}
          <lastBuildDate>{{ now.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</lastBuildDate>
          {{ range .Pages }}
          <item>
            <title>{{ .Title }}</title>
            <link>{{ .Permalink }}</link>
            <guid>{{ .Permalink }}</guid>
            <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</pubDate>
            {{ with .Params.tags }}{{ range . }}<category>{{ . }}</category>{{ end }}{{ end }}
            <description>{{ .Summary | html }}</description>
            <content:encoded><![CDATA[{{ .Content }}]]></content:encoded>
          </item>
          {{ end }}
        </channel>
      </rss>
      ```
      
      ### `llms.txt` for Agent Consumption
      
      An `llms.txt` file helps AI agents and LLMs understand your site. Generate it as a custom output format:
      
      **`layouts/index.llmtxt.txt`:**
      ```go-html-template
      # {{ .Site.Title }}
      
      > {{ .Site.Params.description }}
      
      ## Navigation
      
      {{ range .Site.Sections }}
      - [{{ .Title }}]({{ .Permalink }}): {{ .Params.description }}
      {{ end }}
      
      ## Important Pages
      
      {{ range where .Site.RegularPages "Params.llm_important" true }}
      - [{{ .Title }}]({{ .Permalink }})
      {{ end }}
      
      ## Content
      
      {{ range .Site.RegularPages }}
      ### {{ .Title }}
      
      {{ .Summary | plainify }}
      
      [Read more]({{ .Permalink }})
      {{ end }}
      ```
      
      ## CI/CD for Themes
      
      ### GitHub Actions — Full CI Pipeline
      
      ```yaml
      name: build-and-test
      on: [push, pull_request]
      jobs:
        test:
          runs-on: ubuntu-latest
          strategy:
            matrix:
              hugo-version: [latest, v0.145.0, v0.140.0]
          steps:
          - uses: actions/checkout@v4
      
          - name: Setup Hugo
            uses: peaceiris/actions-hugo@v3
            with:
              hugo-version: ${{ matrix.hugo-version }}
              extended: true
      
          - name: Install dependencies
            run: |
              npm ci
              npx playwright install-deps chromium 2>/dev/null || true
      
          - name: Build
            run: hugo --minify --gc
      
          - name: HTML validation
            run: pip install html5validator && html5validator --root public/
      
          - name: Broken link check
            run: npx broken-link-checker --recursive http://localhost:1313 &
                 hugo server -D &
                 sleep 10
                 npx broken-link-checker --filter-level 3 --recursive http://localhost:1313 || true
      
          - name: Accessibility audit
            run: |
              npx @axe-core/cli@4.13.0 http://localhost:1313/ --exit --stdout || true
      
      ```
      
      ### Testing Strategies
      
      | Tool | Purpose | Integration |
      |------|---------|-------------|
      | `html5validator` | HTML spec compliance | `pip install html5validator` |
      | `broken-link-checker` | Dead links | `npm install broken-link-checker` |
      | `@axe-core/cli` | Accessibility | `npx @axe-core/cli@4.13.0` |
      | `hugo --templateMetrics` | Template performance | Built-in CLI flag |
      | `hugo --renderToMemory` | Build without writing to disk | `hugo --renderToMemory --gc` |
      | `hugo mod verify` | Module integrity | Built-in |
      
      ### Deployment Configurations
      
      **Netlify** (`netlify.toml`):
      ```toml
      [build]
      command = "hugo --minify --gc"
      publish = "public"
      
      [build.environment]
      HUGO_VERSION = "0.154.0"
      HUGO_EXTENDED = "true"
      ```
      
      **Vercel** (`vercel.json`):
      ```json
      {
        "buildCommand": "hugo --minify --gc",
        "outputDirectory": "public",
        "framework": null
      }
      ```
      
      **Cloudflare Pages**:
      ```bash
      hugo --minify --gc
      # Output directory: public
      # Environment variable: HUGO_VERSION=0.154.0
      ```
      
      ### Theme Testing with Hugo Sites
      
      Create a test site inside the theme repo:
      
      ```
      my-theme/
      ├── layouts/
      ├── assets/
      ├── exampleSite/
      │   ├── hugo.yaml
      │   ├── content/
      │   └── archetypes/
      ├── tests/
      │   └── screenshot.spec.js
      └── .github/
          └── workflows/
              └── test.yml
      ```
      
      Build the example site in CI:
      ```yaml
      - name: Build example site
        run: |
          hugo --source exampleSite --minify --gc --themesDir ../..
      ```
      
      ## Pitfalls
      
      - **Canonical URL scheme matters.** Always use absolute URLs with the correct protocol (`https://`). Hugo's `.Permalink` returns absolute by default. Use `absURL` for relative-to-absolute conversion: `{{ .Title | absURL }}`.
      - **JSON output templates need `.json.json` extension.** The first `.json` is the output format name, the second is the file suffix. Without both, Hugo won't find the template.
      - **`hugo --minify` may break JSON output.** HTML minifiers collapse whitespace aggressively. Set `notAlternative: true` on JSON output formats to exclude them from minification.
      - **Custom output formats still require `baseName`.** Hugo uses the baseName as the filename. For index pages, set `baseName: index`. For list pages that generate one file per page, omit baseName.
      - **RSS templates should use `.Site.RegularPages` not `.Site.Pages`.** `.Site.Pages` includes section pages, taxonomies, and the home page — you'll get empty entries. Use `.Site.RegularPages` for content pages only.
      - **`template "_internal/opengraph.html" .` works because it's a partial.** The built-in templates are registered as partials, not standalone templates. Always call them with `template` (not `partial`) and pass `.` (the full page context).
      
    • shortcodes-and-hooks.md 7.7 KB
      # Shortcodes & Render Hooks
      
      Hugo provides two systems for custom content rendering: **shortcodes** (explicitly invoked in content) and **render hooks** (automatically applied to Markdown elements).
      
      ## Shortcodes
      
      Saved in `layouts/shortcodes/` and invoked in content files. Two notations:
      
      | Notation | Syntax | Inner content processed |
      |----------|--------|------------------------|
      | **Markdown** | `{{% %}}` | Before Markdown renderer — headings appear in ToC |
      | **Standard** | `{{< >}}` | After Markdown renderer — headings excluded from ToC |
      
      ### Complex Nested Shortcodes
      
      Shortcodes can nest via `.Inner` for parent-child composition:
      
      **Parent** (`layouts/shortcodes/gallery.html`):
      ```go-html-template
      <div class="gallery {{ .Get "class" }}">
        {{ .Inner }}
      </div>
      ```
      
      **Child** (`layouts/shortcodes/image.html`):
      ```go-html-template
      {{ with .Get "src" }}
        {{ with $.Page.Resources.GetMatch . }}
          <img src="{{ .RelPermalink }}" alt="{{ $.Get "alt" }}">
        {{ end }}
      {{ end }}
      ```
      
      **Usage:**
      ```markdown
      {{< gallery class="content-gallery" >}}
        {{< image src="/images/a.jpg" alt="Photo A" >}}
        {{< image src="/images/b.jpg" alt="Photo B" >}}
      {{< /gallery >}}
      ```
      
      ### Raw HTML Shortcodes
      
      Pass raw HTML through without Markdown processing:
      ```go-html-template
      {{/* layouts/shortcodes/html-block.html */}}
      {{ .Inner }}
      ```
      
      ```markdown
      {{< html-block >}}
      <div class="custom-html">
        <h2>Raw HTML Here</h2>
        <p>Not processed by Markdown.</p>
      </div>
      {{< /html-block >}}
      ```
      
      ### Markdown Rendering Inside Shortcodes
      
      Use `markdownify` to render inner Markdown — use `{{% %}}` notation:
      ```go-html-template
      {{/* layouts/shortcodes/notice.html */}}
      <div class="notice notice-{{ .Get "type" }}">
        {{ .Inner | markdownify }}
      </div>
      ```
      
      ```markdown
      {{% notice type="warning" %}}
      This is a **warning** with _markdown_ inside.
      {{% /notice %}}
      ```
      
      ### Shortcode Variables
      
      | Variable | Description |
      |----------|-------------|
      | `.Name` | Shortcode name |
      | `.Ordinal` | Zero-based ordinal in the page |
      | `.Position` | File path and line number in source content |
      | `.IsNamedParams` | True when called with `key=value` syntax |
      | `.Params` | All parameters (map when named, slice when positional) |
      | `.Get "key"` | Named parameter value |
      | `.Get 0` | Positional parameter (0-indexed) |
      | `.Inner` | Content between opening and closing tags |
      | `.InnerDeindent` | Inner with common whitespace stripped |
      | `.Page` | The containing page (use `$.Page` inside nested shortcodes) |
      
      ### PageInner for Nested Content Context (v0.112.0+)
      
      When a shortcode renders `.Inner` that contains other shortcodes, the inner shortcodes lose page context. Use `.PageInner` to preserve it:
      
      ```go-html-template
      {{/* Parent shortcode that wraps inner content */}}
      <div class="wrapper">
        {{ .Inner }}
      </div>
      {{/* Save the page context for inner shortcodes */}}
      {{ .PageInner }}
      ```
      
      This is critical for nested shortcodes that need `.Page` resources (images, page links).
      
      ## Custom Render Hooks
      
      Render hooks override how Markdown elements are rendered to HTML. Place them in `layouts/_markup/` or `layouts/<type>/_markup/`.
      
      **Layout structure:**
      ```
      layouts/
      └── _default/
          └── _markup/
              ├── render-codeblock.html
              ├── render-codeblock-mermaid.html
              ├── render-heading.html
              ├── render-image.html
              ├── render-image.rss.xml
              └── render-link.html
      ```
      
      ### Link Render Hook
      
      `layouts/_default/_markup/render-link.html`:
      ```go-html-template
      <a href="{{ .Destination }}" {{ with .Title }}title="{{ . }}"{{ end }}>
        {{ .Text | safeHTML }}
      </a>
      ```
      
      Accessible variables: `.Destination`, `.Title`, `.Text`, `.Page`
      
      ### Image Render Hook
      
      `layouts/_default/_markup/render-image.html`:
      ```go-html-template
      <figure>
        {{ if .Page.Resources.GetMatch .Destination }}
          {{ $image := .Page.Resources.GetMatch .Destination }}
          {{ $resized := $image.Resize "800x" }}
          <img src="{{ $resized.RelPermalink }}" alt="{{ .Text }}" loading="lazy"
               width="{{ $resized.Width }}" height="{{ $resized.Height }}">
        {{ else }}
          <img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" loading="lazy">
        {{ end }}
        {{ with .Title }}<figcaption>{{ . }}</figcaption>{{ end }}
      </figure>
      ```
      
      Accessible variables: `.Destination`, `.Title`, `.Text`, `.Page`
      
      ### Heading Render Hook
      
      `layouts/_default/_markup/render-heading.html`:
      ```go-html-template
      <h{{ .Level }} id="{{ .Anchor | safeURL }}">
        {{ .Text | safeHTML }}
        <a href="#{{ .Anchor | safeURL }}" class="anchor">#</a>
      </h{{ .Level }}>
      ```
      
      Accessible variables: `.Level` (1-6), `.Anchor`, `.Text`, `.Page`, `.Attributes` (map of HTML attributes)
      
      ### Code Block Render Hook
      
      `layouts/_default/_markup/render-codeblock.html`:
      ```go-html-template
      {{ $lang := .Type | default "text" }}
      {{ if .Attributes.copy }}
        <button class="copy-btn" data-code="{{ .Inner | htmlEscape }}">Copy</button>
      {{ end }}
      <div class="code-block" lang="{{ $lang }}">
        <pre><code class="language-{{ $lang }}">{{ .Inner }}</code></pre>
      </div>
      ```
      
      ### Language-Specific Render Hooks
      
      Create hooks for specific languages by appending the language to the filename:
      
      - `render-codeblock-mermaid.html` — renders mermaid code blocks only
      - `render-codeblock-go.html` — renders Go code blocks only
      - `render-codeblock-python.html` — renders Python code blocks only
      
      Example — Mermaid code block renderer:
      `layouts/_default/_markup/render-codeblock-mermaid.html`:
      ```go-html-template
      <pre class="mermaid"{{ with .Attributes.theme }} data-theme="{{ . }}"{{ end }}>
        {{ .Inner }}
      </pre>
      {{/* Only loads Mermaid JS when a mermaid code block exists */}}
      {{ with .Page.Store.Get "mermaid" }}{{ else }}
        {{ .Page.Store.Set "mermaid" true }}
        <script defer src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
      {{ end }}
      ```
      
      Accessible variables: `.Type` (language), `.Inner` (code content), `.Attributes` (map of HTML attributes map), `.Position`
      
      ### Render Hook Variables (common)
      
      | Variable | Type | Description |
      |----------|------|-------------|
      | `.Page` | Page | The containing page |
      | `.Destination` | string | Link/image URL destination |
      | `.Title` | string | Title attribute |
      | `.Text` | string | Display text (link) or alt text (image) |
      | `.PlainText` | string | Plain text without formatting |
      | `.Level` | int | Heading level (1-6) |
      | `.Anchor` | string | Auto-generated heading anchor |
      | `.Type` | string | Code block language (e.g., "python") |
      | `.Inner` | string | Code block content |
      | `.Attributes` | map | HTML attributes from markdown attributes syntax |
      | `.Position` | string | File:line of the markdown element |
      
      ## Pitfalls
      
      - **`{{% %}}` vs `{{< >}}` is about rendering order, not syntax preference.** Use `{{% %}}` when the shortcode's inner content contains Markdown that should be rendered. Use `{{< >}}` for raw HTML inner content or when you want to exclude inner shortcode headings from the ToC.
      - **Render hooks fire for all Markdown elements of that type.** You cannot disable a render hook selectively. Use conditional logic in the template (check `.Type`, `.Page`, or `.Attributes`) to handle different cases in one hook.
      - **`$.Page` is required in nested shortcodes.** Inside a child shortcode, `.Page` refers to the shortcode itself, not the containing page. Use `$.Page` (the dollar sign refers to the top-level template context) to access the actual page object.
      - **Render hook filenames use hyphens and dots.** The pattern is `render-{element}.{variant}.{suffix}`, e.g., `render-codeblock-mermaid.html`, `render-image.rss.xml`. Language-specific code block hooks use `render-codeblock-{language}.html`.
      - **Mermaid render hooks need `Page.Store` to avoid duplicate script loads.** The store is per-page and persists across the build. Use `.Page.Store.Get`/`.Set` as shown above to inject dependency scripts exactly once.
      
    • template-architecture.md 5.4 KB
      # Template Architecture
      
      Hugo's template system uses Go templates with a cascading lookup order. Everything starts with a base template (`baseof.html`) that defines blocks child templates fill in.
      
      ## Base Templates and Blocks
      
      A `baseof.html` defines the outer HTML shell. Child templates use `{{ define }}` to fill specific blocks.
      
      **`layouts/_default/baseof.html`:**
      ```go-html-template
      <!DOCTYPE html>
      <html lang="{{ .Site.Language.Lang }}">
      <head>
        <meta charset="utf-8">
        <title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
        {{ block "styles" . }}{{ end }}
      </head>
      <body>
        {{ block "header" . }}{{ partial "header.html" . }}{{ end }}
        <main>
          {{ block "main" . }}{{ end }}
        </main>
        {{ block "footer" . }}{{ partial "footer.html" . }}{{ end }}
        {{ block "scripts" . }}{{ end }}
      </body>
      </html>
      ```
      
      **Child overriding blocks** (`layouts/_default/single.html`):
      ```go-html-template
      {{ define "title" }}{{ .Title }} | {{ .Site.Title }}{{ end }}
      {{ define "main" }}
        <article>{{ .Content }}</article>
      {{ end }}
      ```
      
      ### Base template lookup order
      
      1. `layouts/<type>/<layout>.html` → e.g., `layouts/post/single.html`
      2. `layouts/<section>/baseof.html` (section-specific base)
      3. `layouts/<type>/baseof.html` (type-specific base)
      4. `layouts/_default/baseof.html` (fallback)
      5. `themes/<theme>/layouts/...` (same order)
      
      Source: [Hugo docs — lookup order](https://gohugo.io/templates/lookup-order/)
      
      ## Template Lookup Order (Full)
      
      Hugo selects the most specific template based on page parameters. Parameters applied in order of specificity:
      
      | Parameter | Description |
      |-----------|-------------|
      | **Kind** | `home`, `page`, `section`, `taxonomy`, `term` |
      | **Layout** | Set in front matter via `layout:` field |
      | **Output Format** | Name (e.g. `rss`) and suffix (e.g. `xml`) |
      | **Language** | Language tag in filename (e.g., `index.fr.amp.html`) |
      | **Type** | Value of `type` in front matter, else root section name |
      | **Section** | Relevant for `section`, `taxonomy`, `term` kinds |
      
      **Targeting specific pages** — set both `type` and `layout` in front matter:
      ```yaml
      ---
      title: Contact
      type: miscellaneous
      layout: contact
      ---
      ```
      This renders via `layouts/miscellaneous/contact.html`.
      
      The project's `layouts/` directory always wins over the theme's `layouts/`. Templates interleave between project and theme — the most specific match wins regardless of location.
      
      ## Partials
      
      Partials live in `layouts/partials/` and are called with the dot (`.`) passing full page context:
      
      ```go-html-template
      {{ partial "header.html" . }}
      {{ partial "nav.html" (dict "menu" .Site.Menus.main "current" .) }}
      ```
      
      Use `dict` to pass custom data instead of the full page context — saves memory and avoids unnecessary re-renders.
      
      ## Partial Decorators (v0.154.0+)
      
      Reusable wrapper components that enclose template content using `templates.Inner`:
      
      **Calling template:**
      ```go-html-template
      {{ with partial "components/wrapper.html" . }}
        <p>Everything in this block will be wrapped.</p>
        <p>{{ .Content | transform.Plainify | strings.Truncate 200 }}</p>
      {{ end }}
      ```
      
      **Decorator definition** (`layouts/partials/components/wrapper.html`):
      ```go-html-template
      <div class="wrapper-styling">
        {{ templates.Inner . }}
      </div>
      ```
      
      This pattern replaces what previously required inline partials or duplication — partial decorators compose like higher-order components.
      
      Source: [Hugo docs — partial decorators](https://gohugo.io/templates/partial-decorators/)
      
      ## Page-Level Theming
      
      ### Section-Specific Layouts
      
      Create `layouts/<section>/` directories for section-specific templates:
      ```
      layouts/
      ├── _default/
      │   ├── baseof.html
      │   ├── list.html
      │   └── single.html
      ├── posts/
      │   ├── list.html
      │   └── single.html
      └── projects/
          └── single.html
      ```
      
      ### Type/Kind Switching
      
      Set `type` in front matter to use a different layout directory:
      ```yaml
      ---
      title: About
      type: docs
      ---
      ```
      This looks in `layouts/docs/` instead of the default section.
      
      ### Archetype Patterns
      
      Archetypes define content defaults for `hugo new`. Directory structure maps to content paths:
      ```
      archetypes/
      ├── default.md              # Default archetype (hugo new post/my-post.md)
      ├── posts.md                # Section-specific (hugo new posts/my-post.md)
      └── projects/
          ├── banner.png          # Files alongside the archetype
          └── index.md            # Creates a leaf bundle
      ```
      
      **Archetype template:**
      ```yaml
      ---
      title: "{{ replace .Name "-" " " | title }}"
      date: {{ .Date }}
      draft: true
      tags: []
      ---
      ```
      
      ### Layout Param
      
      Override which template renders a page without changing its type:
      ```yaml
      ---
      title: "My Page"
      layout: "wide"
      ---
      ```
      Renders `layouts/<type>/wide.html` instead of `layouts/<type>/single.html`.
      
      ### Pitfall: `block` in partials conflicts with `define` in page templates
      
      `block` and `define` share the same Go template namespace. A `{{ block "title" . }}` in a partial (e.g. `head.html`) conflicts with a `{{ define "title" }}` in a page template, producing `"partials/head.html: template: multiple definition of template 'title'"`.
      
      **Fix:** Do not use `block` in partials. Use direct template expressions instead:
      
      ```go-html-template
      {{- /* Good: partial without block */ -}}
      <meta property="og:title" content="{{ .Title }} | {{ .Site.Title }}">
      ```
      
      Leave `block` only in `baseof.html` for child templates to fill via `define` at the page level.
      
  • README.md 1.5 KB
    # Hugo Theme Development
    
    Intermediate-to-advanced patterns for building and customizing Hugo CMS themes. Covers template architecture, asset pipeline, shortcodes, performance, SEO, and accessibility.
    
    ## Why Install This Skill
    
    When your agent loads this skill, it becomes a **Hugo theme developer** who can:
    
    - **Set up template architecture** — baseof.html with blocks, template lookup order, partials
    - **Integrate Tailwind CSS** — v4 with `css.TailwindCSS` or v3 with PostCSS
    - **Build responsive images** — srcset, Hugo Pipes processing
    - **Create shortcodes and render hooks** — complex nested shortcodes, Mermaid, custom link/image rendering
    - **Optimize performance** — partialCached, cache TTLs, build speed
    - **Implement accessibility** — semantic HTML landmarks, ARIA patterns, keyboard navigation
    - **Configure SEO** — JSON-LD structured data, Open Graph, Twitter Cards
    
    ## What You Get
    
    | Directory | Purpose |
    |-----------|---------|
    | `SKILL.md` | Quick-start theme bootstrap, reference file index |
    | `references/` | 7 reference files: template architecture, asset pipeline, shortcodes & hooks, content & i18n, modules & performance, design & accessibility, SEO & output formats |
    
    ## Triggers
    
    Load this when working on a Hugo theme or site template layer.
    
    ## Requirements
    
    Hugo v0.154+. Works with any agent framework supporting the Agent Skills format.
    
    
    ## Quick Start
    
    Start with the setup and first workflow in SKILL.md, then use the linked resources for the specific task you need to complete.
    
  • SKILL.md 6.5 KB
    ---
    name: hugo-theme
    description: >-
      Build, customize, and debug advanced Hugo CMS themes — template architecture, asset
      pipeline (CSS/JS/image processing), shortcodes and render hooks, page bundles, cover
      images, Hugo Modules, performance, SEO, and CI/CD. Use when working on a Hugo theme or
      site template layer. Do not use this skill for unrelated requests; route to the nearest
      named specialist.
    license: MIT
    compatibility: Works with any agent framework supporting the Agent Skills format.
    metadata:
      source: >-
        Research compiled from official Hugo docs, major theme repositories, Hugo
        power user blogs, and community discussions.
      hugo-version: v0.154+
    ---
    
    # Hugo Theme Development
    
    Intermediate-to-advanced patterns for Hugo CMS theme development. Load the relevant reference file for your task.
    
    ## Reference Files
    
    | Topic | Hugo Min | Load when... | File |
    |-------|----------|-------------|------|
    | **Template Architecture** | v0.120+ | You need to set up base templates with blocks, understand template lookup order (kind/layout/type/section), create partials, use partial decorators (v0.154+), or work with shortcode fundamentals | `references/template-architecture.md` |
    | **Asset Pipeline** | v0.161+ | You're integrating Tailwind CSS v4 (`css.TailwindCSS`) or v3 (PostCSS), using Hugo Pipes for SCSS/JS bundling, setting up fingerprinting and SRI, building responsive images with srcset, or processing page/global/remote resources | `references/asset-pipeline.md` |
    | **Shortcodes & Render Hooks** | v0.112+ | You need complex nested shortcodes, raw HTML shortcodes, markdown rendering inside shortcodes, custom render hooks for links/images/headings/code blocks, or language-specific code block rendering (Mermaid, etc.) | `references/shortcodes-and-hooks.md` |
    | **Content Organization & i18n** | v0.126+ | You're working with leaf vs branch bundles, headless bundles, cover images, custom taxonomies, content adapters (v0.126+, dynamic pages), section-specific layouts, archetypes, or internationalization (translation tables, multilingual) | `references/content-and-i18n.md` |
    | **Cover Images** | v0.120+ | You need to add cover/hero images to articles, support both page bundle resources and frontmatter paths, generate responsive srcsets, or handle the no-cover case gracefully | `references/cover-images.md` |
    | **Modules & Performance** | v0.109+ | You're using Hugo Modules (init, import, vendor, workspace), building theme components with mount configuration, optimizing build speed with `partialCached`, configuring cache TTLs, or using configuration-driven theming (params, cascade) | `references/modules-and-performance.md` |
    | **Design, UX & Accessibility** | v0.120+ | You need typography systems, accessible color palettes, design tokens, semantic HTML landmarks, ARIA patterns, keyboard navigation, accessible forms, content-first layouts, responsive navigation, engagement patterns (reading progress, dark mode toggle, sharing), Core Web Vitals optimization, container queries, `:has()` selectors, or testing/QA automation (axe-core, Lighthouse CI, visual regression) | `references/design-accessibility.md` |
    | **SEO, Output Formats & CI/CD** | v0.120+ | You need JSON-LD structured data, Open Graph / Twitter Cards, custom output formats (JSON, AMP), sitemap customization, or CI/CD pipelines for themes (GitHub Actions, testing, deployment) | `references/seo-outputs-testing.md` |
    
    ## Quick Start
    
    ```go-html-template
    {{/* Minimal theme baseof.html — start here */}}
    <!DOCTYPE html>
    <html lang="{{ .Site.Language.Lang }}">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
      {{ block "styles" . }}{{ end }}
    </head>
    <body>
      {{ block "header" . }}{{ partial "header.html" . }}{{ end }}
      <main>{{ block "main" . }}{{ end }}</main>
      {{ block "footer" . }}{{ partial "footer.html" . }}{{ end }}
      {{ block "scripts" . }}{{ end }}
    </body>
    </html>
    ```
    
    ### Step-by-Step: Bootstrap a New Theme
    
    ```bash
    # 1. Create the theme directory
    mkdir -p themes/my-theme/{layouts/{_default,_markup,partials,shortcodes},assets/{scss,css,js}}
    
    # 2. Create baseof.html (use the template above) defining blocks:
    #    title, styles, header, main, footer, scripts
    
    # 3. Create partials for reusable components
    #    layouts/partials/header.html, footer.html, css.html
    
    # 4. Set up your asset pipeline
    #    - SCSS  → assets/scss/main.scss + toCSS partial
    #    - Tailwind  → assets/css/main.css + css.TailwindCSS partial
    #    - JS  → assets/js/main.js + js.Build
    
    # 5. Configure hugo.yaml
    #    theme: my-theme
    #    See the reference file for your chosen CSS approach.
    
    # 6. Build and verify
    hugo --gc
    ls public/ | head
    ```
    
    > **Tip:** Project-level `layouts/` overrides theme `layouts/`. If you want to test your theme in isolation, keep the project `layouts/` directory empty until you need overrides.
    
    ## Common Pitfalls
    
    - **SCSS requires Hugo extended edition.** The default macOS/Homebrew Hugo build is NOT extended. Verify with `hugo version | grep extended`.
    - **Tailwind v4 uses `css.TailwindCSS`, not PostCSS.** Don't install `postcss-cli` for v4 — use the native pipe directly. Tailwind v3 still needs the PostCSS pipeline.
    - **`partialCached` stale with non-constant args.** Variant strings (`.Section`, `page.RelPermalink`) must be unique per caller. Repeated section names produce stale results.
    - **`hugo new` respects archetype directory structure.** Place archetypes at `archetypes/<section>/index.md` to create page bundles instead of flat files.
    - **`resources.Get` looks in `assets/`, not `static/`.** Files in `static/` are copied verbatim and not processed by Hugo Pipes. Use `assets/` for any file that goes through Pipes.
    - **Content adapter templates MUST use `_content.gotmpl` naming.** Regular `.md` files in the same directory are ignored when a `_content.gotmpl` exists.
    - **Render hook templates go in `_markup/` subdirectories.** Not in `_default/` directly — they need `layouts/_default/_markup/render-link.html` or section-specific `layouts/<type>/_markup/`.
    - **`block` in partials conflicts with `define` in page templates.** `{{ block "title" . }}` inside a partial (e.g. `head.html`) uses the same Go template namespace as `{{ define "title" }}` in page templates (e.g. `single.html`). When both exist in the render tree, Hugo errors with `multiple definition of template "title"`. Fix: use direct page variables (`.Title`, `.Site.Title`) in partials instead of `block`. Reserve `block` exclusively for the `baseof.html` shell.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related