Comprehensive guide to organizing HBStack module configurations using Hugo's configuration directory structure for better maintainability and separation of concerns.
This document details how we successfully achieved modular configuration organization for HBStack without breaking Hugo’s core functionality. We solved the challenge of separating module-specific configurations while maintaining proper site rendering and avoiding unintended multilingual site creation.
Initially, we attempted to separate HBStack module configurations into individual files to improve maintainability:
params.blog.yaml - Blog module configurationparams.docs.yaml - Documentation module configurationparams.pwa.yaml - PWA module configurationparams.seo.yaml - SEO module configurationProblem: Hugo interpreted these as language-specific configurations, creating separate language sites instead of module configurations, resulting in:
/blog/, /docs/) showing only sidebars| Metric | EN | BLOG | DOCS | PWA | SEO |
|---|---|---|---|---|---|
| Pages | 461 | 15 | 15 | 15 | 15 |
| Paginator | 12 | 0 | 0 | 0 | 0 |
| Non-page files | 35 | 0 | 0 | 0 | 0 |
| Static files | 27 | 27 | 27 | 27 | 27 |
| Processed images | 83 | 0 | 0 | 0 | 0 |
| Aliases | 165 | 1 | 1 | 1 | 1 |
| Cleaned | 0 | 0 | 0 | 0 | 0 |
The above metrics are shown when different languages are used. The hugo mistakenly took our separate configuration files as of being different languages. Though the site works but does not create blog and docs main _index page sections unless we do some tricks for example instead of /blogs/ we ask us to show /blog/featured-post where featured-post.md is a markdown file. A lot of struggle was done but it did not gave us satisfactory result.
Also the above does not show us clear pages that belong to Docs or Blog but same metrics, this is also not correct. In order to get the correct pages. We needed to add proper language settings either in language.en.yaml file or in hugo.yaml file.
For example, the following configuration in language.en.yaml shown below shows the correct result of pages belonging to blog and docs:
1en:
2 languageName: English
3 weight: 1
4
5blog:
6 contentDir: content/blog
7 languageName:: 'Blog'
8 weight: 2
9 outputs:
10 home: ['html', 'offline', 'rss', 'searchindex', 'webappmanifest']
11 section: ['html', 'rss']
12docs:
13 contentDir: content/docs
14 languageName: 'Docs'
15 weight: 3
16 outputs:
17 home: ['html', 'offline', 'rss', 'searchindex', 'webappmanifest']
18 section: ['html', 'rss']
The above configuration gives us the correct pages belonging to blog and docs but it creates separate language sites which is not what we want. We want a single main site with proper module configurations.
🔧 Key Discovery - Content Refresh Solution:
The problem of section content not refreshing automatically when treated as languages can be solved with two Hugo settings:
1defaultContentLanguage: en
2defaultContentLanguageInSubdir: true
With these settings, content refreshes automatically. However, this creates a new challenge - disappearing menus on language sites, as Hugo expects separate menu files for each language.
🎯 Menu Inheritance Solutions:
Create language-specific menu files (menus.blog.yaml, menus.docs.yaml)
Use menu merge configuration:
1menus:
2 _merge: 'shallow' # Enables menu inheritance across languages
Note: The above setting was not used as hugo and hbstack takes care of it automatically.
| Metric | EN | BLOG | DOCS |
|---|---|---|---|
| Pages | 468 | 32 | 243 |
| Paginator pages | 12 | 0 | 8 |
| Non-page files | 38 | 1 | 1 |
| Static files | 27 | 27 | 27 |
| Processed images | 74 | 4 | 0 |
| Aliases | 168 | 8 | 94 |
| Cleaned | 0 | 0 | 0 |
Note: The docs section contents were not shown in docs layout but in blog layout. To force the docs section to use docs layout, we need to set the
type: docsin the front matter of_index.mdfile incontent/docs/directory withcascade:as shown below:
1---
2type: docs
3cascade:
4 type: docs
5---
With cascade, all child pages will inherit the type: docs and use the docs layout.
To create separate menu files for blog and docs, we need to create two files menus.blog.yaml and menus.docs.yaml in the config/_default/ directory. And repeat the same menu structure as in menus.en.yaml but with menu items specific to blog and docs. This is not a good approach as it creates redundancy and maintenance overhead.. Rather we can use entirely different menus for Docs and Blog which serves the purpose and also providing a home menu for the main site.
We leveraged Hugo’s Configuration Directory feature with recursive parsing to properly organize module configurations.
1config/
2└── _default/
3 ├── hugo.yaml # Core Hugo settings
4 ├── params.yaml # Core HBStack parameters
5 ├── menus.en.yaml # Navigation menus
6 ├── languages.yaml # Language configuration
7 ├── module.yaml # Hugo modules
8 └── params/ # 📁 Module-specific parameters
9 ├── blog.yaml # Blog module configuration
10 ├── docs.yaml # Docs module configuration
11 ├── pwa.yaml # PWA module configuration
12 └── seo.yaml # SEO module configuration
1baseURL: https://agsayyed.github.io/hugo-with-docker/
2title: Hugo With Docker
3copyright: 'Copyright © 2024-{year} AG Sayyed. All Rights Reserved.'
4enableRobotsTXT: true
5timeout: 120s
6enableEmoji: true
7
8# Title configuration
9title_sections: true
10title_sections_depth: 0
11title_sections_depth_dir: end
12
13# URL configuration
14permalinks:
15 blog: /blog/:year/:month/:title
16
17# Output formats
18outputs:
19 home:
20 - HTML
21 - Offline
22 - RSS
23 - SearchIndex
24 - WebAppManifest
25
26# Taxonomies
27taxonomies:
28 authors: authors
29 tags: tags
30 categories: categories
31 series: series
Contains the main HBStack framework configuration including:
Key Insight: By placing module configs in config/_default/params/, Hugo treats them as parameter extensions rather than language configurations.
params/blog.yaml: Blog-specific settings (post display, sidebar, archives)params/docs.yaml: Documentation settings (navigation, TOC, breadcrumbs)params/pwa.yaml: Progressive Web App configurationparams/seo.yaml: SEO optimization settingsHugo uses a deep merge strategy for the params configuration key:
1[params]
2 _merge = 'deep'
This means all files in config/_default/params/ are recursively merged into the main params object, creating a unified configuration while maintaining organizational separation.
In the params/ subdirectory, we omit the root params: key from each file:
✅ Correct (in params/blog.yaml):
1# HBStack Blog Configuration
2hb:
3 blog:
4 list_cols_lg: 3
5 paginate: 12
❌ Wrong:
1params: # <- Don't include this in subdirectory files
2 hb:
3 blog:
4 list_cols_lg: 3
Hugo now properly watches the entire configuration structure:
1Watching for config changes in:
2- /config/_default
3- /config/_default/params # ✅ Recursive watching
4- /config/development
/blog/ and /docs/| Metric | EN | BLOG | DOCS |
|---|---|---|---|
| Pages | 468 | 32 | 243 |
| Paginator pages | 12 | 0 | 8 |
| Non-page files | 38 | 1 | 1 |
| Static files | 27 | 27 | 27 |
| Processed images | 74 | 4 | 0 |
| Aliases | 168 | 8 | 94 |
| Cleaned | 0 | 0 | 0 |
hugo.yamlparams.yaml for main HBStack framework configurationparams/ subdirectoryhugo.yaml - Core Hugo configurationparams.yaml - Main parametersparams/[module].yaml - Module-specific parametersmenus.[lang].yaml - Language-specific menusconfig/_default/params/ directoryparams: root key in subdirectory files--disableFastRender to clear cache1# View complete merged configuration
2hugo config
3
4# Check specific parameter merging
5hugo config | grep -A10 "hb.blog"
6
7# Validate YAML syntax
8hugo --debug
This configuration structure supports:
config/development/ or config/production/Building on our successful configuration organization, we implemented context-specific navigation menus to provide tailored user experiences for different site sections.
With our multilingual site structure treating /blog/ and /docs/ as separate language contexts, we faced menu duplication and navigation confusion:
We created dedicated menu files for each section context:
1# config/_default/menus.en.yaml - Main site navigation
2main:
3 - name: Blog
4 url: /blog/
5 weight: 4
6 - name: Docs
7 url: /docs/
8 weight: 1
1# config/_default/menus.blog.yaml - Blog-specific navigation
2main:
3 - name: Blog Home
4 url: /blog/
5 weight: 1
6 - name: Posts
7 url: /blog/posts/
8 weight: 2
9 - name: Categories
10 url: /categories/
11 weight: 3
1# config/_default/menus.docs.yaml - Documentation-specific navigation
2main:
3 - name: Home
4 url: /
5 weight: 1
6 - name: Getting Started
7 url: /docs/getting-started/
8 weight: 2
9 - name: HBStack Guide
10 url: /docs/hbstack-guide/
11 weight: 3
Menu Isolation: Prevents menu inheritance between language contexts:
1# config/_default/hugo.yaml
2menus:
3 _merge: 'none' # Complete menu isolation
Language Context Definition: Defines section-specific contexts:
1languages:
2 en:
3 languageName: 'English'
4 weight: 1
5 blog:
6 languageName: 'Blog'
7 weight: 2
8 docs:
9 languageName: 'Docs'
10 weight: 3
For proper context-specific navigation, we created corresponding content structures:
content/
├── blog/
│ ├── _index.md
│ └── posts/
│ └── _index.md
├── docs/
│ ├── _index.md
│ └── getting-started/
└── posts/
├── _index.md
└── [blog posts...]
This ensures that /blog/posts/ displays content within the blog language context with blog-specific menus, while /posts/ remains in the main site context.
We successfully achieved modular configuration organization by leveraging Hugo’s Configuration Directory feature with recursive parameter merging. This approach provides:
The solution demonstrates that proper understanding of Hugo’s configuration system can solve complex organizational challenges while maintaining full functionality.
This configuration pattern can be applied to any Hugo site using HBStack or similar modular frameworks.
Q: Why not use separate language configurations?
A: Hugo interprets params.[name].yaml files as language-specific configurations, creating separate sites that break navigation and content rendering.
Q: How does the recursive merging work?
A: Hugo recursively parses the config/_default/params/ directory and deeply merges all YAML files into the main params configuration object.
Q: Can I add more modules easily?
A: Yes, simply create a new params/[module-name].yaml file in the params directory, and Hugo will automatically include it in the configuration merge.