4.7 Module Configuration for Hugo Modules
Right, let’s talk about modules. This is where Hugo goes from being a nifty static site generator to a full-blown dependency management powerhouse. Think of it as npm or go mod for your Hugo site, but thankfully, with far less node_modules-induced existential dread. You’re not just managing themes anymore; you’re stitching together content, layouts, and assets from multiple sources into one coherent site. It’s brilliant, but it demands a bit of configuration finesse.
The [[imports]] Section: Your Module Dependency Manifest
Forget config.toml for a second. The real magic for modules happens in a dedicated [[imports]] section. This is your site’s dependency list, and Hugo is ruthlessly specific about the syntax. You must use double brackets ([[imports]]); a single bracket ([imports]) will be ignored with the silent judgment only a compiler can muster. Each module you pull in gets its own [[imports]] table.
Here’s the most straightforward example: pulling in a remote theme from GitHub. Notice how we disable the local theme setting. The module system takes precedence, and this avoids any confusing fallback behavior.
theme = ["none"] # Disable the old way of defining a theme
[[imports]]
path = "github.com/gohugoio/hugo-theme-slack"
But why stop at themes? This is the killer feature. You can import a module that only provides archetypes from your company’s internal repository, or one that just has a folder of JSON data files for headless content. You break your site into logical, reusable components.
Going Beyond Git: Local Paths and Mounts
Not every module needs to live on GitHub. During development, you’ll often work with a local copy of your theme. The module system has you covered. Using a local path is trivial, and the ../ pathing works exactly as you’d expect from the root of your project.
[[imports]]
path = "../my-awesome-theme"
This is where it gets genuinely clever. Let’s say the theme you’re importing has its own content directory, but you want to override a specific page. You don’t have to fork the whole theme and change its structure. You use a mount. This allows you to superimpose your own project’s directories over the ones provided by the module. It’s filesystem overlaying, and it’s incredibly powerful.
[[imports]]
path = "github.com/gohugoio/hugo-theme-slack"
[[imports.mounts]]
source = "content/posts/my-special-post.md" # In *your* project
target = "content/posts/some-post.md" # In the *theme's* structure
This configuration tells Hugo: “Hey, for the theme module you just imported, when you go to look for content/posts/some-post.md, ignore the one in the theme and use my my-special-post.md file instead.” You can do this for layouts, static files, data—almost anything. It’s your get-out-of-jail-free card for customizing modules without breaking them.
The config.* Idiom: Taming Multi-Environment Chaos
This isn’t strictly a module feature, but you’ll use it to manage modules differently across environments, so pay attention. You don’t have to cram every setting into one massive hugo.toml file. Hugo lets you split your configuration using the config directory idiom.
Create a config directory (not configs, mind you) in your project root. Inside, you can have environment-specific files:
config/production/config.tomlconfig/staging/config.tomlconfig/dev/config.toml
Hugo will merge settings from these files, with environment-specific files taking precedence. This is how you avoid accidentally publishing your Google Analytics tag to your staging server. You define the module imports in your base config.toml, but then you can, for example, use a different local path for the theme in dev mode than you use in production which points to the git repository.
# config.toml (base)
[[imports]]
path = "github.com/your-team/hugo-theme-prod" # The production default
# config/dev/config.toml
[[imports]]
path = "../hugo-theme" # Your local, in-development copy of the same theme
When you run hugo server --environment dev, it will use your local theme, allowing for rapid iteration. When you build for production, it seamlessly switches to the pinned git version. This is the professional-grade workflow you’ve been looking for.