This page covers Hugo Modules and the corresponding hugo mod command in detail.
What Is a Module
A module is Hugo's basic unit for organizing content. A module can be a full Hugo project, or it can be a small reusable package that provides just one type of component (content, layouts, assets, data, i18n, static, archetypes). A theme you install is, at its core, a module.
Modules can be combined freely, referenced in a nested way, and can even mount external directories, including directories from non-Hugo projects. Everything gets merged into the same UFS.
Initialization and Imports
Your project must become a module itself before it can import other modules:
hugo mod init github.com/user/themeThis generates go.mod. If you don't plan on letting others import your module, the exact name doesn't matter much, pick anything reasonable.
Declare the module you want to import in hugo.yaml:
module:
imports:
- path: github.com/user/themeRunning hugo to build the site automatically downloads the module, caches it, and generates go.sum to record its version and checksum.
When you import multiple modules at once, files with the same name merge according to UFS rules.
Common Commands
Here is a summary of commonly used hugo mod commands:
Update a single module:
hugo mod get -u github.com/user/themePin a specific version:
hugo mod get -u github.com/user/theme@v0.42.0Update all modules:
hugo mod get -uUpdate modules recursively:
hugo mod get -u ./...Clean up unused entries in
go.mod/go.sum:hugo mod tidyClear the module cache:
hugo mod clean
Vendor
hugo mod vendor is used for local inspection and temporary debugging.
This command copies all imported modules into the _vendor directory, which has higher priority, letting you modify and test modules directly within the _vendor directory. Directly modifying files inside _vendor is only for quick debugging. Running vendor again will overwrite those changes. The proper way to customize is still to override the same path in the project root through the UFS.
Replace
Use the replace directive to permanently swap out a dependency, for example when you switch to your own fork:
require example.com/othermodule v0.1.0
replace example.com/othermodule => example.com/myfork/othermodule v0.1.0Or point it to a local directory:
replace github.com/user/theme => /home/user/projects/themeBecause replace lives in go.mod and ships with your project, it applies to everyone who builds this project, including CI.
Workspace
Use a workspace to configure modules for local development. Think of it as a temporary version of replace. For example, when you're developing a local module, you can point directly to your local files:
go 1.20
use .
use ../themeEnable it temporarily with an environment variable:
HUGO_MODULE_WORKSPACE=hugo.work hugo serverOr enable workspace mode long-term in hugo.yaml:
workspace: 'hugo.work'The key difference between workspace and replace: workspace can be enabled temporarily and never gets written to go.mod. So when other people depend on your module, your replace settings won't affect them.
Practical Examples
Multilingual Sites
As described in Multilingual Sites, you can mount a given directory to a given site at a given location to complete multilingual configuration.
Shared Component Libraries
The most basic use case: multiple sites sharing the same set of shortcodes, partials, or CSS. Extract them into an independent module that every site can import:
[module]
[[module.imports]]
path = 'github.com/your-org/shared-components'exampleSite
When building a theme, it's common to include an exampleSite/ directory inside the theme's repo as a demo site. But that exampleSite/ site depends on the theme sitting in the project's own root.
A workspace solves this cleanly:
go 1.20
use .
use ../workspace: 'hugo.work'node_modules
Mounts also work with node_modules, letting you mount its contents directly into a target directory without manually vendoring the package. Configure it like this:
module:
mounts:
- source: assets
target: assets
- source: node_modules/@awmottaz/prettier-plugin-void-html/
target: assets/prettier-plugin-void-htmlYou can then use the contents of that directory directly in your JS and templates.
Separating Content From Source Code
Split content/ and assets/ into their own module, backed by an independent git repo, so writers never need to touch the theme's source code:
module:
imports:
- path: github.com/your-org/site-contentWriters only need access to the independent site-content repo, so they can't accidentally touch layouts/ or other code. Engineers only need to configure CI to download this module at build time, which cleanly separates responsibilities.
For local development where you need to see content changes immediately, combine this with the replace approach mentioned earlier, pointing to a local path, or mount it through a workspace instead.
Separating Configuration Across Environments
Switch which data/ or params gets imported depending on whether you're building the production site or the preview site:
module:
imports:
- path: config-stagingmodule:
imports:
- path: config-productionThe config/staging and config/production directories switch automatically based on an environment variable, or manually set via hugo -e staging. With CI/CD, the configuration module to import switches based on the deployment environment.