# 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 }}
  • {{ .Title }}
  • {{ 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 ``; 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.