mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 23:38:53 +00:00
config: Allow repeating the root key in /config files
If a non-default-name file in the config folder parses to a map with a single top-level key matching the file's basename, unwrap it. This lets TOML/YAML express slice-typed roots (cascade, permalinks), which can't have a headless top-level array, and also lets users copy-paste docs examples that include the root container (e.g. params.yaml with a top-level params: block). Fixes #12899 Fixes #14882 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-1
@@ -173,9 +173,11 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid
|
||||
}
|
||||
|
||||
var keyPath []string
|
||||
var unwrapKey string
|
||||
if !DefaultConfigNamesSet[name] {
|
||||
// Can be params.jp, menus.en etc.
|
||||
name, lang := paths.FileAndExtNoDelimiter(name)
|
||||
unwrapKey = name
|
||||
|
||||
keyPath = []string{name}
|
||||
|
||||
@@ -190,13 +192,23 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid
|
||||
}
|
||||
}
|
||||
|
||||
// TOML/YAML can't represent a headless top-level array, so allow a
|
||||
// file to wrap its content under a single top-level key matching
|
||||
// the basename (e.g. cascade.yaml with `cascade: [...]`).
|
||||
var itemValue any = item
|
||||
if unwrapKey != "" && len(item) == 1 {
|
||||
if inner, ok := item[unwrapKey]; ok {
|
||||
itemValue = inner
|
||||
}
|
||||
}
|
||||
|
||||
root := item
|
||||
if len(keyPath) > 0 {
|
||||
root = make(map[string]any)
|
||||
m := root
|
||||
for i, key := range keyPath {
|
||||
if i >= len(keyPath)-1 {
|
||||
m[key] = item
|
||||
m[key] = itemValue
|
||||
} else {
|
||||
nm := make(map[string]any)
|
||||
m[key] = nm
|
||||
|
||||
@@ -46,3 +46,140 @@ Params: map[a:acp1 b:bc1 c:c1 d:dcp1]
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
// TOML/YAML can't represent a top-level array, so a basename-matched wrapper
|
||||
// key in the config file unwraps to the slice-typed root key.
|
||||
func TestConfigDirCascadeSliceIssue12899(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "home", "section"]
|
||||
-- config/_default/cascade.yaml --
|
||||
cascade:
|
||||
- target:
|
||||
path: /books/**
|
||||
params:
|
||||
color: red
|
||||
- target:
|
||||
path: /films/**
|
||||
params:
|
||||
color: blue
|
||||
-- content/books/b1.md --
|
||||
---
|
||||
title: B1
|
||||
---
|
||||
-- content/films/f1.md --
|
||||
---
|
||||
title: F1
|
||||
---
|
||||
-- layouts/page.html --
|
||||
{{ .Title }}|color:{{ .Params.color }}|
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/books/b1/index.html", "B1|color:red|")
|
||||
b.AssertFileContent("public/films/f1/index.html", "F1|color:blue|")
|
||||
}
|
||||
|
||||
func TestConfigDirPermalinksSliceIssue12899(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "home", "section"]
|
||||
-- config/_default/permalinks.yaml --
|
||||
permalinks:
|
||||
- target:
|
||||
path: /books/**
|
||||
pattern: /shelf/:slug/
|
||||
-- content/books/b1.md --
|
||||
---
|
||||
title: B1
|
||||
slug: novel
|
||||
---
|
||||
-- layouts/page.html --
|
||||
{{ .Title }}|{{ .RelPermalink }}|
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/shelf/novel/index.html", "B1|/shelf/novel/|")
|
||||
}
|
||||
|
||||
func TestConfigDirCascadeEnvironmentOverrideIssue12899(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "home", "section"]
|
||||
-- config/_default/cascade.yaml --
|
||||
cascade:
|
||||
- target:
|
||||
path: /**
|
||||
params:
|
||||
color: default
|
||||
-- config/production/cascade.yaml --
|
||||
cascade:
|
||||
- target:
|
||||
path: /**
|
||||
params:
|
||||
color: production
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: P1
|
||||
---
|
||||
-- layouts/page.html --
|
||||
{{ .Title }}|color:{{ .Params.color }}|
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "P1|color:production|")
|
||||
}
|
||||
|
||||
// The basename-match unwrap is type-agnostic — also unwraps maps.
|
||||
func TestConfigDirRepeatRootMapIssue14882(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "section"]
|
||||
-- config/_default/params.yaml --
|
||||
params:
|
||||
a: aval
|
||||
b: bval
|
||||
-- layouts/home.html --
|
||||
a:{{ site.Params.a }}|b:{{ site.Params.b }}|
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "a:aval|b:bval|")
|
||||
}
|
||||
|
||||
// The unwrap fires only when the basename-matched key is the sole top-level
|
||||
// key in the file. A mixed map must be left alone.
|
||||
func TestConfigDirUnwrapOnlySoleKeyIssue12899(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "home", "section"]
|
||||
-- config/_default/params.yaml --
|
||||
params:
|
||||
nested: yes
|
||||
other: top
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: P1
|
||||
---
|
||||
-- layouts/page.html --
|
||||
{{ .Title }}|params.params.nested:{{ site.Params.params.nested }}|params.other:{{ site.Params.other }}|
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "P1|params.params.nested:yes|params.other:top|")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user