gohugoio/hugo · error

cache dir contains unresolved placeholders

Error message

cache dir %q contains unresolved placeholders

What it means

In FileCacheConfig.init (filecache_config.go:123-128), when DirCompiled is empty but the raw Dir string contains a colon, it means a placeholder like :cacheDir was never resolved. This guard is primarily reached from unit tests that construct FileCacheConfig directly without running the full DecodeConfig pipeline.

Solutions

  1. Run the config through DecodeConfig to resolve placeholders before use
  2. Set DirCompiled explicitly if intentionally bypassing the decode pipeline
  3. Avoid colons in raw directory names used in test fixtures

Example fix

// before
cfg := FileCacheConfig{Dir: ":cacheDir/modules"}
cfg.init() // error: unresolved placeholder
// after
cfg.DirCompiled = "/tmp/hugo-cache/modules"
cfg.init()
Defensive patterns

Strategy: validation

Validate before calling

// In test code, resolve placeholders or set DirCompiled before calling init.
func mustResolveDir(t *testing.T, cfg *FileCacheConfig, baseCacheDir string) {
    t.Helper()
    if cfg.DirCompiled == "" && strings.Contains(cfg.Dir, ":") {
        cfg.DirCompiled = filepath.Join(baseCacheDir, strings.TrimPrefix(cfg.Dir, ":cacheDir"))
    }
}

Prevention

When it happens

Trigger: Constructing a FileCacheConfig in Go code/tests with Dir set to ":cacheDir/foo" but never calling DecodeConfig to resolve placeholders into DirCompiled.

Common situations: Unit tests of the filecache package that bypass config decoding; custom code that instantiates FileCacheConfig directly.

Related errors


AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09). Data as JSON: /api/errors/b7a691c1f0476058. Report an issue: GitHub.

Appendix: source

Thrown at cache/filecache/filecache_config.go:127

	// be removed and not returned from the cache.
	// A negative value means forever, 0 means cache is disabled.
	// Hugo is lenient with what types it accepts here, but we recommend using
	// a duration string, a sequence of  decimal numbers, each with optional fraction and a unit suffix,
	// such as "300ms", "1.5h" or "2h45m".
	// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
	MaxAge time.Duration

	// The directory where files are stored.
	Dir string

	fileCacheConfigInternal `json:"-"`
}

func (cfg *FileCacheConfig) init() error {
	if cfg.DirCompiled == "" {
		// From unit tests. Just check that it does not contain any placeholders.
		if strings.Contains(cfg.Dir, ":") {
			return fmt.Errorf("cache dir %q contains unresolved placeholders", cfg.Dir)
		}
		cfg.DirCompiled = cfg.Dir
	}
	// Sanity check the config.
	if len(cfg.DirCompiled) < 5 {
		panic(fmt.Sprintf("invalid cache dir: %q", cfg.DirCompiled))
	}
	return nil
}

type fileCacheConfigInternal struct {
	DirCompiled string

	name          string // The name of this cache, e.g. "images", "modules" etc.
	entryIsDir    bool   // when set, the cache entries represents directories directly below the base dir.
	isReadOnly    bool   // when set, the cache is read only and needs to be pruned differently. This is used for the Go modules cache.
	IsResourceDir bool   //  resources/_gen will get its own composite filesystem that also checks any theme. TODO(bep) unexport this.
}

View on GitHub (pinned to 52c9bd7908)