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
Install
npx skills add https://github.com/magnus919/agent-skills/tree/main/hugo-theme
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
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.TailwindCSSor 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 themelayouts/. If you want to test your theme in isolation, keep the projectlayouts/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 installpostcss-clifor v4 — use the native pipe directly. Tailwind v3 still needs the PostCSS pipeline. partialCachedstale with non-constant args. Variant strings (.Section,page.RelPermalink) must be unique per caller. Repeated section names produce stale results.hugo newrespects archetype directory structure. Place archetypes atarchetypes/<section>/index.mdto create page bundles instead of flat files.resources.Getlooks inassets/, notstatic/. Files instatic/are copied verbatim and not processed by Hugo Pipes. Useassets/for any file that goes through Pipes.- Content adapter templates MUST use
_content.gotmplnaming. Regular.mdfiles in the same directory are ignored when a_content.gotmplexists. - Render hook templates go in
_markup/subdirectories. Not in_default/directly — they needlayouts/_default/_markup/render-link.htmlor section-specificlayouts/<type>/_markup/. blockin partials conflicts withdefinein 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 withmultiple definition of template "title". Fix: use direct page variables (.Title,.Site.Title) in partials instead ofblock. Reserveblockexclusively for thebaseof.htmlshell.
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">« 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">…</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 »</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.
Reviews (0)
No reviews yet.
No comments yet.