How Hugo Generates Taxonomies (Tags, Categories) and Their Term Pages
Hugo generates taxonomies by creating two virtual page types—taxonomy list pages (KindTaxonomy) for overview pages like /tags/ and term pages (KindTerm) for individual terms like /tags/go/—through a four-step pipeline that parses configuration, walks the content tree, assembles virtual pages, and exposes data to templates.
In the gohugoio/hugo static site generator, taxonomy generation is handled entirely in memory during the site build process. Unlike static files, taxonomy pages are virtual resources created by the page assembler based on content relationships defined in front matter. Understanding this pipeline is essential for customizing taxonomy templates and optimizing site architecture.
Understanding Hugo's Taxonomy Architecture
Hugo distinguishes between two distinct virtual page kinds when generating taxonomy structures, each serving a different purpose in the content hierarchy.
Taxonomy List Pages (KindTaxonomy)
The taxonomy list page represents the top-level overview for an entire taxonomy classification. In resources/kinds/kinds.go, this is defined as KindTaxonomy. For example, the page at /tags/ or /categories/ is a KindTaxonomy page that lists all terms within that taxonomy. These pages are generated automatically when the site configuration includes the taxonomy, regardless of whether individual term content files exist.
Term Pages (KindTerm)
Term pages represent individual taxonomy values, such as /tags/go/ or /categories/tutorial/. Defined as KindTerm in kinds.go, these pages aggregate all content pages that reference that specific term. Hugo creates these pages in two ways: automatically for terms referenced in content front matter, or from explicit content files (e.g., content/tags/go.md) that allow authors to add custom metadata to term pages.
The Four-Step Taxonomy Generation Pipeline
The taxonomy generation process in gohugoio/hugo follows a strict pipeline defined across several files in the hugolib package. Each step transforms configuration and content into renderable virtual pages.
Step 1: Parsing Taxonomy Configuration
Hugo begins by reading the site configuration to identify which taxonomies to generate. In hugolib/site.go, the configuration is parsed into an internal taxonomiesConfig type:
// hugolib/site.go – internal type for the config map
type taxonomiesConfig map[string]string
The Taxonomies() method (lazy-loaded) converts this configuration into a list of viewName structs defined in hugolib/content_map_page.go (lines 33-36). Each viewName stores the singular and plural forms of the taxonomy along with a tree key used for URL construction.
Step 2: Walking the Content Tree with CreateSiteTaxonomies
The core aggregation logic resides in CreateSiteTaxonomies within hugolib/content_map_page.go. This function walks the content tree to collect all term references and build the TaxonomyList data structure:
// hugolib/content_map_page.go – CreateSiteTaxonomies
func (m *pageMap) CreateSiteTaxonomies(ctx context.Context) (page.TaxonomyList, error) {
taxonomies := make(page.TaxonomyList)
…
for _, viewName := range m.cfg.taxonomyConfig.views {
// Walk the page tree under the taxonomy’s tree key
w := &doctree.NodeShiftTreeWalker[contentNode]{ … }
w.Handle = func(s string, n contentNode) (radix.WalkFlag, error) {
p := n.(*pageState)
switch p.Kind() {
case kinds.KindTerm:
if !p.m.shouldList(true) { return radix.WalkContinue, nil }
taxonomy := taxonomies[viewName.plural]
termKey := strings.ToLower(p.m.term) // e.g. “go”
// Gather all pages that belong to this term
m.treeTaxonomyEntries.WalkPrefix(doctree.LockTypeRead,
paths.AddTrailingSlash(s),
func(ss string, wn *weightedContentNode) (bool, error) {
taxonomy[termKey] = append(taxonomy[termKey],
page.NewWeightedPage(wn.weight, wn.n.(page.Page), wn.term.Page()))
return false, nil
})
}
return radix.WalkContinue, nil
}
if err := w.Walk(ctx); err != nil { … }
}
// Sort each term’s pages
for _, taxonomy := range taxonomies {
for _, v := range taxonomy { v.Sort() }
}
return taxonomies, nil
}
This function specifically handles KindTerm pages, using treeTaxonomyEntries.WalkPrefix to gather all content pages associated with each term. It populates the TaxonomyList map where keys are taxonomy names (plural) and values are maps of term keys to WeightedPages collections.
Step 3: Creating Virtual Pages in the Assembler
While CreateSiteTaxonomies builds the data structures, the actual virtual pages are instantiated in hugolib/content_map_page_assembler.go. The assembler creates two types of pages:
For term pages (KindTerm):
// hugolib/content_map_page_assembler.go – term page creation (excerpt)
case kinds.KindTerm:
p := &pageState{
// …
Kind: kinds.KindTerm,
// term, singular/plural etc. are stored in the page’s meta
}
For taxonomy list pages (KindTaxonomy), the process is similar but sets Kind = kinds.KindTaxonomy. These pages are inserted into treePages and assigned URLs based on the taxonomy's tree key and the lower-cased term name. They exist only in memory during the build, derived from the content relationships rather than physical files.
Step 4: Exposing Data to Templates
Finally, Hugo exposes the taxonomy data through the Site object and template functions. In hugolib/site.go (around line 90), the Taxonomies() method returns the page.TaxonomyList built during the creation phase.
The resources/page/taxonomy.go file defines helper methods that templates use to access taxonomy data:
// resources/page/taxonomy.go – helpers
func (i Taxonomy) Page() Page // returns the taxonomy list page
func (i Taxonomy) Get(key string) WeightedPages // pages for a term
func (i Taxonomy) Alphabetical() OrderedTaxonomy // sorted terms
Template usage examples:
{{/* List all tags */}}
{{ range .Site.Taxonomies.tags.Alphabetical }}
<a href="{{ .Pages | first.Permalink }}">{{ .Term }}</a> ({{ .Count }})
{{ end }}
{{/* Render a term page */}}
{{ range .Pages }}
<h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
{{ end }}
Configuring Taxonomies in Hugo
Before Hugo can generate taxonomy pages, you must define them in your site configuration. The default configuration includes tags and categories, but you can customize or add taxonomies in config.toml:
[taxonomies]
tag = "tags"
category = "categories"
series = "series"
Each entry maps the singular form (key) to the plural form (value). The plural form determines the URL structure (e.g., /tags/) and the access key in templates (e.g., .Site.Taxonomies.tags).
Key Source Files in the Hugo Repository
Understanding the taxonomy generation pipeline requires familiarity with these specific files in gohugoio/hugo:
| File | Purpose |
|---|---|
hugolib/site.go |
Parses taxonomy configuration via taxonomiesConfig and exposes Site.Taxonomies() |
hugolib/content_map_page.go |
Contains CreateSiteTaxonomies() which walks the content tree and builds TaxonomyList |
hugolib/content_map_page_assembler.go |
Creates virtual KindTaxonomy and KindTerm pages and inserts them into treePages |
resources/page/taxonomy.go |
Defines Taxonomy, WeightedPages, and helper methods like Alphabetical() |
resources/kinds/kinds.go |
Defines KindTaxonomy and KindTerm constants |
create/skeletons/theme/layouts/taxonomy.html |
Default layout template for taxonomy lists |
Summary
-
Hugo generates taxonomies by creating virtual pages for both taxonomy lists (
KindTaxonomy) and individual terms (KindTerm), rather than relying on physical files. -
Configuration parsing happens in
hugolib/site.go, where thetaxonomiesmap is converted into internalviewNamestructs defining singular/plural pairs. -
Data aggregation occurs in
CreateSiteTaxonomieswithinhugolib/content_map_page.go, which walks the content tree, identifiesKindTermpages, and populatesTaxonomyListwithWeightedPagesfor each term. -
Virtual page creation is handled by
hugolib/content_map_page_assembler.go, which instantiates the taxonomy pages and assigns them URLs based on tree keys and lower-cased term names. -
Template exposure happens through
.Site.Taxonomies, defined inresources/page/taxonomy.go, providing methods likeAlphabetical()andGet()for rendering taxonomy data.
Frequently Asked Questions
How does Hugo determine the URL structure for taxonomy pages?
Hugo builds taxonomy URLs using the tree key defined in the taxonomy configuration (typically the plural form) combined with the lower-cased term name. For example, a "go" tag creates the URL /tags/go/ where tags is the plural configuration key. This logic is implemented in hugolib/content_map_page_assembler.go when virtual pages are inserted into treePages with their respective paths.
What is the difference between KindTaxonomy and KindTerm in Hugo?
KindTaxonomy represents the list page for an entire taxonomy classification (e.g., /tags/ or /categories/), showing all available terms within that taxonomy. KindTerm represents the page for a specific taxonomy value (e.g., /tags/go/), displaying all content pages associated with that specific term. These constants are defined in resources/kinds/kinds.go and determine which layout templates Hugo selects and how the pages are rendered.
How can I customize the layout for taxonomy pages?
Hugo looks for specific layout templates based on the page kind and lookup order. For taxonomy list pages (KindTaxonomy), create layouts/_default/taxonomy.html or layouts/_default/list.html. For individual term pages (KindTerm), use layouts/_default/term.html or layouts/_default/list.html. You can also create taxonomy-specific layouts like layouts/tags/list.html or layouts/tags/tag.html (depending on your taxonomy name) to target specific taxonomies with custom designs.
Where does Hugo store the mapping between content and taxonomy terms?
Hugo stores the taxonomy-to-content mapping in the TaxonomyList data structure, which is built during the site assembly process in hugolib/content_map_page.go. Specifically, the CreateSiteTaxonomies function populates this structure by walking treeTaxonomyEntries—a radix tree containing all page-term associations. The resulting map organizes content by taxonomy plural name, then by lower-cased term key, storing WeightedPages collections that link back to the actual content pages. This structure is then exposed to templates via .Site.Taxonomies.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →