Skip to main content

Hugo Modules


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/theme

This 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/theme

Running 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/theme
  • Pin a specific version:

    hugo mod get -u github.com/user/theme@v0.42.0
  • Update all modules:

    hugo mod get -u
  • Update modules recursively:

    hugo mod get -u ./...
  • Clean up unused entries in go.mod/go.sum:

    hugo mod tidy
  • Clear 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:

go.mod
require example.com/othermodule v0.1.0

replace example.com/othermodule => example.com/myfork/othermodule v0.1.0

Or point it to a local directory:

go.mod
replace github.com/user/theme => /home/user/projects/theme

Because 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:

hugo.work
go 1.20

use .
use ../theme

Enable it temporarily with an environment variable:

HUGO_MODULE_WORKSPACE=hugo.work hugo server

Or enable workspace mode long-term in hugo.yaml:

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:

hugo.work
go 1.20

use .
use ../
hugo.yaml
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-html

You 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-content

Writers 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:

config/staging/hugo.yaml
module:
  imports:
    - path: config-staging
config/production/hugo.yaml
module:
  imports:
    - path: config-production

The 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.

Accessibility settings

Font size