Working With Hugo Modules

Hugo

thumbnail

As Hugo has evolved to handle larger and more complex static sites, it has added more features to manage that complexity. Perhaps the largest feature to enable complex Hugo sites is the introduction of Hugo’s module system. This post discusses how modules work in Hugo.

Terminology

There are several terms in use throughout this note which are very similar semantically, but have very different meanings.

  • Project: A Hugo site. The “largest” term in this scope, consisting of all the files needed for Hugo to statically assemble the site.
  • Module: A unit of composition supported by Hugo to make it very easy to reuse parts across multiple projects. All modules are maintained via hugo mod commands.
  • Package: A unit of composition supported by NPM. Hugo supports interfacing with NPM packages in its module system via hugo mod npm commands.

Hugo Modules Are Go Modules

A Hugo module is a Go Module. Hugo leverages Go Modules to locate and obtain modules when generating a static site. Hugo also uses the Go Module system to manage updating modules. This means that every module contains a go.mod file containing the module’s name, Go version, and any of its dependencies.

Hugo manages the contents of the go.mod file via the hugo CLI, so it doesn’t need to be edited manually. The example provided later shows some commands used to manage modules.

A Hierarchy of Modules

Logically, a Hugo project is organized in a tree of modules. At the root of the tree is the module where the hugo CLI is invoked. Each branch of the tree is specified by a module dependency. Organizing a project into several distinct modules allows for maximum flexibility and reuse of existing code.

The Virtual Union Filesystem

In order to assemble multiple modules located across the system into a single static site, Hugo introduces the virtual, unified filesystem. Each module specifies a set of folders that it “mounts” into the filesystem, and Hugo uses the files referenced in the virtual, unified filesystem to generate a site. For example, module A may provide an assets folder and mount it into the filesystem, and module B can reference the files in there as if it was stored locally.

This system removes the headache of specifying paths to other files; content that is separated across multiple modules can specify paths relative to the root of this filesystem rather than maintaining a relative path between modules. As a benefit, this makes it possible to easily rearrange the modules on a system without breaking the build and needing to fix all the relative paths.

When generating a module-based static site, Hugo will download and mount all modules into the virtual, unified filesystem prior to performing the actual transformations. After all the modules have been downloaded and mounted, Hugo generates a static site from all the files across all the modules.

Configuring Modules in Hugo

Whereas the task of discovering, obtaining, and updating modules is configured using the go.mod file in the Go Module system, the task of mounting folders from a module into the virtual, unified filesystem is configured via Hugo’s main configuration file. Below is the minimal configuration required to specify another module as a dependency:

# config.toml
[module]
    [[module.imports]]
        path = "gitlab.com/nicholasnooney/augustus"

Note that it is still possible to use Hugo without configuring modules. In this case, the entire project is treated as a single module, and default settings are applied.

Default Mount Points

When a module is used as a part of a Hugo project, Hugo adds folders from the module to the virtual, unified filesystem. The following folders are mounted by default into a folder of the same name:

  • content
  • static
  • layouts
  • data
  • assets
  • i18n
  • archetypes

However, a module can override the default mount folders by specifying which folders to mount. If a module specifies any folder, then none of the default folders are mounted. Below is the minimal configuration required to specify a folder to mount.

[module]
  [[module.mounts]]
    source = "local/folder/path"
    target = "virtual/unified/filesystem/path"

This mechanism enables very flexible arrangements of modules and dependencies within a Hugo project, supporting maximal reuse of code.

Complete Customization

There are several settings to configure modules in addition to specifying dependencies on other modules and naming which folders should be added to the virtual, unified filesystem. The full list of configuration options for modules can be found here.

Access to Node and NPM

Hugo uses NPM modules to leverage PostCSS when processing static sites. By default, it searches for the postcss CLI tool relative to the root Hugo module. However, NPM will by default install packages in the module where the dependency is declared. Since Hugo expects NPM packages to be contained in the root module, Hugo will emit a not found error when trying to build the site. Thankfully, Hugo provides additional tooling to support installing NPM packages from modules into the root module.

To manage installing NPM packages in the root Hugo module, Hugo provides the command hugo mod npm pack. This command will “pack” all the files named package.hugo.json across all Hugo modules specified as dependencies and then generate a single package.json file in the root module.

After running hugo mod npm pack, a user can run npm install to install all dependencies for a project with multiple NPM packages across multiple Hugo modules, and Hugo will be able to find each package it needs.

Therefore, the full setup process for using a module that uses NPM is the following:

hugo mod get -u    # Update module dependencies
hugo mod npm pack  # Generate NPM package.json
npm install        # Install NPM packages
hugo               # Build the static site

There’s one caveat to making NPM packages work in Hugo. Hugo installs all the packages relative to the root module’s base directory. If any sub-module tries to reference an NPM package via a relative path, it is treated as relative to the sub-module’s base directory. To solve this, I created a symlink in each sub-module to the root module’s node_modules folder.

Mounting NPM Packages

If a module specifies an NPM package as a mount point, it can refer to the package as if it was locally installed, even if Hugo installed the package in the root module instead of the module that mounts the folder. Typically, files from NPM packages are mounted into the static folder, so that Hugo copies the content as-is.

Example: A Site and a Theme

One of the most common patterns for using Hugo modules is to have a site’s content located in the root module and a site’s theme in another module. The content module then specifies a dependency on the theme module that it wants to use.

It’s possible to initialize both modules via the following commands:

hugo new site mysite
hugo new theme mytheme

Note that Hugo generates the theme’s config in theme.toml. This file should be copied to config.toml so that modules can be used (Hugo doesn’t process module settings in theme.toml).

Once Hugo creates the scaffolding files for the project, each module must be initialized with hugo mod init <module-name>. The module name should match the spec for Go modules, namely that it should have the same import path and repository URL (use a service like GitHub or GitLab to host the module). Initializing the module will create the go.mod files automatically.

Next, in the root module, specify a dependency on the theme module by adding a module.imports setting to the root module’s config. Because the theme module mounts the relevant folders directly into the virtual, unified filesystem, the site does not use the theme config setting.

# mysite config.toml
[module]
    [[module.imports]]
        path = "gitlab.com/username/mytheme"

If any of the non-default paths should be mounted (for example, including JavaScript from an NPM package), then specify them using the module.mounts setting in the config file of the module that uses the non-default path.

# mytheme config.toml
[module]
  [[module.mounts]]
    source = "assets"
    target = "assets"

  [[module.mounts]]
    source = "layouts"
    target = "layouts"

  [[module.mounts]]
    source = "static"
    target = "static"

  [[module.mounts]]
    source = "node_modules/package/dist"
    target = "static/package"

With the modules configured, it is possible to build the site. Run the hugo command from the site’s root folder and Hugo will generate the content in the public folder.

This example configures two modules: the root module containing a site’s content and a theme module specified as a dependency. The theme module uses code from an NPM package and adds it to the virtual, unified filesystem. Using modules allows for flexibility and reusability of components.