Splitting Large Hugo Sites with Segments
Hugo is fast enough that most sites never need to think about splitting the build. The exceptions are sites that are genuinely huge — hundreds of thousands of pages of generated API reference, many versions of the same documentation, a dozen languages — where even Hugo takes several minutes and every pull request preview waits for all of it. Before Hugo 0.124, the options were running separate Hugo projects per area, which broke cross-references and shared navigation, or accepting the wait.
Segments changed that. A segment is a named filter over the site — by language, path, page kind or output format — and hugo --renderSegments renders only the chosen segments while still loading the whole site, so links, menus and .GetPage lookups across segments keep working. This guide uses segments to render previews of only the changed area, to split a production build across parallel CI jobs, and to merge the outputs safely. It is part of Hugo Build Times for Large Repositories. The site is an API and product documentation portal of 162,000 pages across six versions and three languages.
Prerequisites
- Hugo 0.124 or newer (measured on 0.134.3 extended).
- A site where large areas can be described by path, language or kind.
- A CI system that can run parallel jobs and pass artifacts between them.
Step 1: Profile Before Splitting
Segments reduce rendering work, not loading work. Check which dominates with --templateMetrics and the build log's timing summary:
hugo --templateMetrics --templateMetricsHints --logLevel info 2>&1 | tee build.log
grep -E 'Total in|process|render' build.log
On this site, the full build took 6 min 40 s on an 8-vCPU runner: 1 min 5 s loading and assembling content (parsing 162,000 files, building taxonomies and the page tree) and 5 min 35 s rendering and writing output. Rendering dominated, so segments could help. On a site where loading dominated — for example, one with huge data files — they would not. Profiling Hugo Templates with Template Metrics covers the profiling in depth.
Step 2: Define Segments
Segments are declared in configuration with include and exclude filters. Filters match on lang, kind, path (a glob against the page path) and output:
# hugo.toml
[segments]
[segments.api]
[[segments.api.includes]]
path = "{/api,/api/**}"
[segments.docs-current]
[[segments.docs-current.includes]]
path = "{/docs/v6,/docs/v6/**}"
[segments.docs-older]
[[segments.docs-older.includes]]
path = "{/docs/v[1-5],/docs/v[1-5]/**}"
[segments.site]
[[segments.site.excludes]]
path = "{/api/**,/docs/**}"
The four segments partition the site: API reference (120,000 pages), current docs (9,000 pages per language), older doc versions (the rest of the docs) and everything else — home, blog, landing pages, sitemap and search index. Partitioning without overlaps matters for merging later; a page in two segments would be written by two jobs.
Language can be combined with path — lang = "de" inside an include — which lets a translation team preview only their language.
Step 3: Faster Previews for Pull Requests
Most pull requests touch one area. A small script maps changed files to segments, and the preview build renders only those:
changed=$(git diff --name-only origin/main...HEAD)
segs=site
grep -q '^content/api/\|^data/openapi/' <<<"$changed" && segs="$segs,api"
grep -q '^content/docs/v6/' <<<"$changed" && segs="$segs,docs-current"
grep -q '^content/docs/v[1-5]/' <<<"$changed" && segs="$segs,docs-older"
grep -q '^layouts/\|^assets/\|^hugo.toml' <<<"$changed" && segs="site,api,docs-current,docs-older"
hugo --renderSegments "$segs" --minify
Template and config changes render everything, since they can affect any page. A docs-only change rendered site,docs-current in 1 min 38 s instead of 6 min 40 s. The preview then contains only the rendered segments; links into unrendered areas 404 on the preview, which is acceptable for review but means the link checker in the preview job must be limited to rendered paths.
Step 4: Parallel Jobs for the Full Build
Production needs every page. Split the full build across parallel jobs, one per segment, and merge:
jobs:
render:
strategy:
matrix:
segment: [site, api, docs-current, docs-older]
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/cache/restore@v4
with: { path: '${{ runner.temp }}/hugo_cache', key: hugo-cache-${{ hashFiles('go.sum') }} }
- run: hugo --renderSegments ${{ matrix.segment }} --minify -d out
env: { HUGO_CACHEDIR: '${{ runner.temp }}/hugo_cache' }
- uses: actions/upload-artifact@v4
with: { name: 'out-${{ matrix.segment }}', path: out }
merge:
needs: render
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { pattern: 'out-*', path: parts }
- run: mkdir public && for d in parts/*; do cp -rn "$d"/. public/; done
- run: node scripts/verify-merge.mjs public
Each job loads the whole site (about 65 s) but renders only its share. The API job, the largest, finished in 3 min 55 s; the others in 1–2 minutes. The merged build was ready in 4 min 30 s including artifact transfer, against 6 min 40 s for a single job — a 32% saving, limited by the largest segment. Splitting the API segment further, by API version, brought the slowest job to 2 min 20 s and the merged build to 3 min 5 s.
Step 5: Verify the Merge
A merged build can silently lose or duplicate files. The verification script checks that every page in the sitemap exists in the merged output, that no file was present in two segment outputs (the cp -n flag would silently keep the first), and that the page count matches a full single-job build recorded nightly:
// scripts/verify-merge.mjs (core checks)
const sitemapUrls = await readSitemap('public/sitemap.xml');
const missing = sitemapUrls.filter((u) => !existsSync(join('public', new URL(u).pathname, 'index.html')));
const dupes = findDuplicatePathsAcross(['parts/out-site', 'parts/out-api', 'parts/out-docs-current', 'parts/out-docs-older']);
if (missing.length || dupes.length) { console.error({ missing: missing.slice(0, 20), dupes: dupes.slice(0, 20) }); process.exit(1); }
The first version of the segment config included /docs/v6/_index.md in both site (through a broad path) and docs-current; the duplicate check caught it immediately.
Search, Sitemaps and Other Whole-Site Outputs
Anything that summarises the whole site — sitemap.xml, RSS for the home page, a JSON search index built from .Site.Pages — belongs in one segment, and because every job loads the whole site, that segment's templates still see every page. Here the site segment wrote the sitemap index and a search index covering all 162,000 pages, even though it rendered only 3,000 HTML pages. Post-build indexers that read HTML files, such as Pagefind, must run after the merge instead, as described in Indexing Hugo Sites with Pagefind.
Measured Impact
| Measure | Single job | Segments |
|---|---|---|
| Production build wall-clock | 6 min 40 s | 3 min 5 s (7 parallel jobs) |
| Typical docs-only preview | 6 min 40 s | 1 min 38 s |
| CI minutes per production build | 6.7 | 17.2 (more jobs, more loading) |
| Largest job peak memory | 21 GB | 9 GB |
| Merge verification failures caught | — | 2 (overlapping segments) |
The parallel build uses more total CI minutes because every job loads the whole site; the gains are wall-clock time and lower memory per runner, which let the jobs run on standard-size machines.
Pitfalls & Rollback
- Overlapping segments. Two jobs write the same file; verify no duplicates.
- Whole-site outputs in several segments. Assign sitemaps and indexes to one segment.
- Link checking a partial preview. Restrict checks to rendered paths or links into unrendered areas fail.
- Expecting segments to speed loading. Every job still loads the full content tree.
- Rollback: remove
--renderSegmentsand build in one job; the segment config is ignored when the flag is absent.
Conclusion
Segments let a very large Hugo site render only what a build needs while keeping one content tree, so cross-references and whole-site outputs stay correct. Use them for targeted previews of the changed area, and for parallel production builds merged and verified in a final job. On a 162,000-page portal that cut preview builds to a quarter and production builds to under half, at the cost of more total CI minutes.
FAQ
What are Hugo segments?
Segments, added in Hugo 0.124, let you define named subsets of a site in configuration by language, path, kind or output format, and then render only the chosen segments with the --renderSegments flag. The whole site is still loaded, so cross-references and menus keep working, but only the selected pages are written.
Do segments make every build faster?
They make rendering faster when you only need part of the output, such as a preview of one section or parallel CI jobs that each render a share. Content loading and assembly still cover the whole site, so the saving is largest when rendering, not loading, dominates.
Can segments be built in parallel and merged?
Yes, if the segments do not overlap and shared files such as the home page, sitemap and search index are assigned to exactly one job. Each job uploads its output as an artifact and a final job merges the directories before deploying.
When is it worth splitting a Hugo build?
When a full build is long enough to slow pull request previews, typically several minutes, and the site has large, independent areas such as API reference, versioned docs or multiple languages. For builds under a minute, the orchestration usually costs more than it saves.
Related
- Parent: Hugo Build Times for Large Repositories — the full toolbox for big Hugo sites.
- Reducing Hugo Memory Usage on CI Runners — why smaller jobs help memory.
- Matrix Builds for Multi-Site Monorepos — the same matrix pattern across sites.
- Hugo partialCached for Faster Builds — reducing render time before splitting.
- Deploying Only Changed Files — shipping the merged output efficiently.