# How Hugo Manages Module Dependencies Using Go Modules

> Discover how Hugo manages module dependencies using Go modules. Learn how Hugo enhances Go modules for themes and components to build a robust dependency graph.

- Repository: [GoHugo.io/hugo](https://github.com/gohugoio/hugo)
- Tags: internals
- Published: 2026-02-28

---

**Hugo treats every theme and component as a Hugo Module—a Go module enhanced with mounts, vendoring, and replacement handling—and builds the dependency graph by recursively parsing `go.mod` files using Go's standard tooling.**

When you run `hugo mod graph`, Hugo is not reinventing package management. Instead, the `gohugoio/hugo` repository implements a thin wrapper around Go Modules that adds Hugo-specific functionality while preserving full compatibility with the Go toolchain. This architecture allows Hugo to resolve theme dependencies, handle local replacements, and vendor assets using the same `go.mod` files that standard Go projects use.

## What Is a Hugo Module?

A **Hugo Module** is a regular Go module defined by a `go.mod` file, but Hugo enriches it with additional metadata. According to the source code in [`modules/module.go`](https://github.com/gohugoio/hugo/blob/main/modules/module.go), each module implements the `Module` interface, which exposes methods like `Path()`, `Version()`, `Replace()`, and `Vendor()` to track the module's identity, replacement paths, and vendoring status. This abstraction allows Hugo to treat themes, shortcodes, and asset libraries as interchangeable units in a dependency tree.

## Core Data Structures

The module system rests on four primary source files that handle configuration, representation, collection, and client operations.

### modules/module.go

This file defines the `Module` interface and the `moduleAdapter` struct that wraps Go modules for Hugo's consumption. The interface requires implementations to expose the module path, semantic version, replacement module (if any), and whether the module is vendored. This is the foundation of how Hugo manage dependencies at the structural level.

### modules/config.go

Hugo-specific configuration—such as proxy settings, replacement directives, and workspace mode—is decoded here into the `Config` struct. This allows you to specify module settings in your [`config.yaml`](https://github.com/gohugoio/hugo/blob/main/config.yaml) or [`config.toml`](https://github.com/gohugoio/hugo/blob/main/config.toml) that Go itself does not natively understand, such as mounting specific directories from a theme into your project.

### modules/collect.go

The `collector` struct in this file performs the heavy lifting of walking the module tree. It records seen modules, handles circular dependencies, and constructs the final `ModulesConfig`. A critical step occurs in `filterDuplicateMounts`, which ensures that the first module defining a given mount point wins, enforcing Hugo's "first-module-wins" rule.

### modules/client.go

This is the public API surface. The `Client` struct provides `NewClient`, `Graph`, `Vendor`, and `Tidy` methods. It bootstraps the collector, executes the Go binary when downloading is required, and writes the dependency graph to an `io.Writer`.

## The Dependency Resolution Flow

Hugo builds the dependency graph through a six-stage pipeline that bridges Hugo configuration with Go module semantics.

### 1. Client Initialization

When you invoke a module command, `NewClient` checks for the presence of a `go.mod` file in the working directory using `afero.Exists(fs, n)`. If found, the path is stored in `Client.GoModulesFilename`, enabling Go-based operations. The client also detects the Go binary status to provide helpful error messages if Go is not installed.

### 2. Tree Collection

The `Client.collect` method instantiates a `collector` and calls `c.collect()`. This process reads the project's `go.mod` and parses `require` directives using `golang.org/x/mod/module`. For each requirement, Hugo creates a `moduleAdapter`. It also applies replacement directives from the site configuration (`Config.Replacements`), allowing local development overrides.

### 3. Mount Deduplication

After constructing the tree, `ModulesConfig.finalize` calls `filterDuplicateMounts(m.mounts)` to remove duplicate mount definitions. This guarantees deterministic behavior where the first module in the dependency list controls shared mount points.

### 4. Vendoring Support

The `Client.Vendor` method creates a `_vendor` directory (distinct from Go's standard `vendor` folder) and copies module sources into it. It skips modules where `Vendor()` returns `true` to avoid nesting vendor directories. A [`modules.txt`](https://github.com/gohugoio/hugo/blob/main/modules.txt) manifest is written to track the vendored versions, mirroring `go mod vendor` functionality but adapted for Hugo's asset structure.

### 5. Graph Output

Running `hugo mod graph` invokes `Client.Graph`, which iterates over `mc.AllModules` and prints lines in the format `<parent-module> <child-module>`. The method resolves replacement directives, displaying local directory paths when replacements are active rather than semantic versions.

### 6. Tidying Dependencies

`Client.Tidy` runs a fresh collection without vendoring (`c.collect(false)`) and then invokes `c.tidy` to prune unused entries from `go.mod` and `go.sum`. This ensures the dependency graph remains minimal and accurate.

## Interacting with the Go Toolchain

Hugo does not reimplement Go's module resolver. Instead, it delegates to the Go binary when necessary.

- **Module Downloading**: When a required module is missing locally, Hugo executes `go get` via `exec.Command` to fetch it, then re-collects the tree to update the graph.
- **Workspace Support**: If `Config.Workspace` is enabled, Hugo leverages Go's workspace mechanism to resolve multiple modules within a single repository, facilitating monorepo development.
- **Binary Detection**: The client tracks `goBinaryStatus` to differentiate between missing Go installations and configuration errors.

## Practical Commands and Code Examples

To inspect your project's dependency graph, including replacement directives:

```bash
hugo mod graph

```

Programmatically, you can initialize a client and vendor modules:

```go
c := modules.NewClient(modules.ClientConfig{
    WorkingDir:  projectDir,
    Fs:          afero.NewOsFs(),
    Logger:      loggers.NewDefault(),
    ModuleConfig: modules.DefaultModuleConfig,
})
if err := c.Vendor(); err != nil {
    log.Fatalf("vendor failed: %s", err)
}

```

To walk the collected modules and inspect their properties:

```go
mc, coll := c.Collect()
if coll.err != nil {
    panic(coll.err)
}
for _, m := range mc.AllModules {
    fmt.Printf("%s@%s (path=%s, vendor=%t)\n",
        m.Path(), m.Version(), m.Dir(), m.Vendor())
}

```

## Summary

- Hugo Modules are standard Go modules augmented with Hugo-specific metadata like mounts and replacements.
- The resolution flow in [`modules/collect.go`](https://github.com/gohugoio/hugo/blob/main/modules/collect.go) recursively parses `go.mod` files and applies site-specific replacements from [`modules/config.go`](https://github.com/gohugoio/hugo/blob/main/modules/config.go).
- **First-module-wins**: Duplicate mounts are filtered to ensure deterministic override behavior.
- Vendoring stores dependencies in `_vendor` with a manifest file, while `hugo mod tidy` prunes unused dependencies.
- The system delegates to the Go binary for downloading but manages the graph enrichment internally.

## Frequently Asked Questions

### How does Hugo resolve conflicting versions of the same module?

Hugo relies on Go's minimal version selection (MVS) algorithm to resolve versions, but applies its own **replacement** and **mount** logic afterward. When multiple modules require the same dependency, Go selects the highest required version, and Hugo then deduplicates mounts so the first module in the dependency list controls shared directories.

### What is the difference between `vendor` and `_vendor` in Hugo?

Standard Go projects use a `vendor` directory, but Hugo uses `_vendor` (as defined in [`modules/client.go`](https://github.com/gohugoio/hugo/blob/main/modules/client.go)) to store vendored module files. This distinction prevents conflicts with Go's native vendoring while allowing Hugo to maintain its own manifest format in [`modules.txt`](https://github.com/gohugoio/hugo/blob/main/modules.txt) and handle asset-specific requirements.

### Can Hugo manage dependencies without Go installed?

No. While Hugo caches module information, the `Client` in [`modules/client.go`](https://github.com/gohugoio/hugo/blob/main/modules/client.go) checks for the Go binary (`goBinaryStatus`) and executes `go get` commands when modules are missing. Without Go installed, Hugo cannot download new dependencies or resolve the full graph, though it can operate with previously vendored modules.

### How do replacement directives work in Hugo Modules?

Replacements are specified in your Hugo configuration file (not just `go.mod`) under the `modules` section and are parsed by [`modules/config.go`](https://github.com/gohugoio/hugo/blob/main/modules/config.go). When the `collector` builds the dependency tree, it substitutes the replacement path for the original module path, allowing you to develop themes locally or fork modules without modifying import paths.