gohugoio/hugo · error

invalid module path %q; must be relative to themesDir when d

Error message

invalid module path %q; must be relative to themesDir when defined outside of the project

What it means

`createThemeDirname` (modules/client.go:877) maps a non-Go-module import path to a directory under `themesDir`. For imports declared by a module other than the project itself, the resolved path must stay inside `themesDir`: absolute paths and relative paths that escape via `..` are rejected. This is a path-traversal guard — a third-party theme module must not be able to point Hugo at arbitrary filesystem locations.

Source

Thrown at modules/client.go:878

	}

	if goModOnly {
		return nil
	}

	if err := c.rewriteGoMod(goSumFilename, isGoMod); err != nil {
		return err
	}

	return nil
}

func (c *Client) shouldNotVendor(path string) bool {
	return c.noVendor != nil && c.noVendor.Match(path)
}

func (c *Client) createThemeDirname(modulePath string, isProjectMod bool) (string, error) {
	invalid := fmt.Errorf("invalid module path %q; must be relative to themesDir when defined outside of the project", modulePath)

	modulePath = filepath.Clean(modulePath)
	if filepath.IsAbs(modulePath) {
		if isProjectMod {
			return modulePath, nil
		}
		return "", invalid
	}

	moduleDir := filepath.Join(c.ccfg.ThemesDir, modulePath)
	if !isProjectMod && !strings.HasPrefix(moduleDir, c.ccfg.ThemesDir) {
		return "", invalid
	}
	return moduleDir, nil
}

// ClientConfig configures the module Client.
type ClientConfig struct {

View on GitHub ↗ (pinned to 8a468df065)

Solutions

  1. In the imported module's config, reference the theme by its module path or by a name relative to themesDir, not an absolute or `../` path.
  2. If you need a local directory override, declare it in the PROJECT's config via `module.replacements` (or the HUGO_MODULE_REPLACEMENTS env var) — project-level paths are allowed to be absolute.
  3. Move the local theme into the project's themes directory so a plain name resolves.
  4. Run `hugo mod graph` to see which module declares the offending import.

Example fix

# before (inside an imported theme's config)
[[module.imports]]
path = "/home/dev/my-component"
# after (in the project's hugo.toml)
[module]
replacements = "github.com/org/my-component -> /home/dev/my-component"
Defensive patterns

Strategy: validation

Validate before calling

abs, _ := filepath.Abs(modPath)
if !strings.HasPrefix(abs, projectDir) && !strings.HasPrefix(abs, themesDir) {
    return fmt.Errorf("module path %q must live under themesDir when outside the project", modPath)
}

Prevention

When it happens

Trigger: Module collection where a non-project module (a theme/component you import) declares an import whose path is absolute (e.g. `/home/me/theme`) or resolves outside themesDir after `filepath.Clean` (e.g. `../../other`), and the path was not replaced via a project-level `replace` directive.

Common situations: Copying a theme's dev config that used absolute local paths into a published module; using `../sibling-theme` relative imports inside a nested module; expecting `module.replacements` semantics but declaring the local path directly in the imported module's config instead of the project's.

Related errors


AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31). Data as JSON: /data/errors/a9934fa8f04208ae.json. Report an issue: GitHub ↗.