Repository: gohugoio/hugo (language: go)

Issue:
defaultContentLanguage must be set if no English lang is defined
Which is a little inconvenient ...

This fails with:

```
func TestDefaaultConfigLanguageBlankWhenNoEnglishExists(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
baseURL = "https://example.com"
[languages]
[languages.nn]
weight = 20
[languages.sv]
weight = 10
-- layouts/all.html --
All.
`

	b := hugolib.Test(t, files)

	b.Assert(b.H.Conf.DefaultContentLanguage(), qt.Equals, "sv")
}
```


```
failed to decode "languages": config value "en" for defaultContentLanguage does not match any language definition`
```

We should be smarter here and pick the first (`sv`) language.

Edit in: Looking at some failing tests, I would say that to avoid breakage we need to:

* If `en` is one of the languages, we pick that as the default.
* Else the first language.






Discussion / hints:

Fix patch:
diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
index e73153a94b5..d3ee28490c0 100644
--- a/config/allconfig/allconfig.go
+++ b/config/allconfig/allconfig.go
@@ -800,28 +800,56 @@ func (c *Configs) IsZero() bool {
 
 func (c *Configs) Init() error {
 	var languages langs.Languages
-	defaultContentLanguage := c.Base.DefaultContentLanguage
-	for k, v := range c.LanguageConfigMap {
-		languageConf := v.Languages[k]
-		language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
-		if err != nil {
-			return err
+
+	var langKeys []string
+	var hasEn bool
+
+	const en = "en"
+
+	for k := range c.LanguageConfigMap {
+		langKeys = append(langKeys, k)
+		if k == en {
+			hasEn = true
 		}
-		languages = append(languages, language)
 	}
 
-	// Sort the sites by language weight (if set) or lang.
-	sort.Slice(languages, func(i, j int) bool {
-		li := languages[i]
-		lj := languages[j]
+	// Sort the LanguageConfigSlice by language weight (if set) or lang.
+	sort.Slice(langKeys, func(i, j int) bool {
+		ki := langKeys[i]
+		kj := langKeys[j]
+		lki := c.LanguageConfigMap[ki]
+		lkj := c.LanguageConfigMap[kj]
+		li := lki.Languages[ki]
+		lj := lkj.Languages[kj]
 		if li.Weight != lj.Weight {
 			return li.Weight < lj.Weight
 		}
-		return li.Lang < lj.Lang
+		return ki < kj
 	})
 
-	for _, l := range languages {
-		c.LanguageConfigSlice = append(c.LanguageConfigSlice, c.LanguageConfigMap[l.Lang])
+	// See issue #13646.
+	defaultConfigLanguageFallback := en
+	if !hasEn {
+		// Pick the first one.
+		defaultConfigLanguageFallback = langKeys[0]
+	}
+
+	if c.Base.DefaultContentLanguage == "" {
+		c.Base.DefaultContentLanguage = defaultConfigLanguageFallback
+	}
+
+	for _, k := range langKeys {
+		v := c.LanguageConfigMap[k]
+		if v.DefaultContentLanguage == "" {
+			v.DefaultContentLanguage = defaultConfigLanguageFallback
+		}
+		c.LanguageConfigSlice = append(c.LanguageConfigSlice, v)
+		languageConf := v.Languages[k]
+		language, err := langs.NewLanguage(k, c.Base.DefaultContentLanguage, v.TimeZone, languageConf)
+		if err != nil {
+			return err
+		}
+		languages = append(languages, language)
 	}
 
 	// Filter out disabled languages.
@@ -836,12 +864,12 @@ func (c *Configs) Init() error {
 
 	var languagesDefaultFirst langs.Languages
 	for _, l := range languages {
-		if l.Lang == defaultContentLanguage {
+		if l.Lang == c.Base.DefaultContentLanguage {
 			languagesDefaultFirst = append(languagesDefaultFirst, l)
 		}
 	}
 	for _, l := range languages {
-		if l.Lang != defaultContentLanguage {
+		if l.Lang != c.Base.DefaultContentLanguage {
 			languagesDefaultFirst = append(languagesDefaultFirst, l)
 		}
 	}
@@ -927,17 +955,48 @@ func (c Configs) GetByLang(lang string) config.AllProvider {
 	return nil
 }
 
+func newDefaultConfig() *Config {
+	return &Config{
+		Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
+		Sitemap:    config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
+		RootConfig: RootConfig{
+			Environment:          hugo.EnvironmentProduction,
+			TitleCaseStyle:       "AP",
+			PluralizeListTitles:  true,
+			CapitalizeListTitles: true,
+			StaticDir:            []string{"static"},
+			SummaryLength:        70,
+			Timeout:              "60s",
+
+			CommonDirs: config.CommonDirs{
+				ArcheTypeDir: "archetypes",
+				ContentDir:   "content",
+				ResourceDir:  "resources",
+				PublishDir:   "public",
+				ThemesDir:    "themes",
+				AssetDir:     "assets",
+				LayoutDir:    "layouts",
+				I18nDir:      "i18n",
+				DataDir:      "data",
+			},
+		},
+	}
+}
+
 // fromLoadConfigResult creates a new Config from res.
 func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadConfigResult) (*Configs, error) {
 	if !res.Cfg.IsSet("languages") {
 		// We need at least one
 		lang := res.Cfg.GetString("defaultContentLanguage")
+		if lang == "" {
+			lang = "en"
+		}
 		res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
 	}
 	bcfg := res.BaseConfig
 	cfg := res.Cfg
 
-	all := &Config{}
+	all := newDefaultConfig()
 
 	err := decodeConfigFromParams(fs, logger, bcfg, cfg, all, nil)
 	if err != nil {
@@ -947,6 +1006,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
 	langConfigMap := make(map[string]*Config)
 
 	languagesConfig := cfg.GetStringMap("languages")
+
 	var isMultihost bool
 
 	if err := all.CompileConfig(logger); err != nil {
diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go
index 0bf8508d96c..1cfa1afc4e0 100644
--- a/config/allconfig/alldecoders.go
+++ b/config/allconfig/alldecoders.go
@@ -249,14 +249,18 @@ var allDecoderSetups = map[string]decodeWeight{
 		key: "sitemap",
 		decode: func(d decodeWeight, p decodeConfig) error {
 			var err error
-			p.c.Sitemap, err = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.p.GetStringMap(d.key))
+			if p.p.IsSet(d.key) {
+				p.c.Sitemap, err = config.DecodeSitemap(p.c.Sitemap, p.p.GetStringMap(d.key))
+			}
 			return err
 		},
 	},
 	"taxonomies": {
 		key: "taxonomies",
 		decode: func(d decodeWeight, p decodeConfig) error {
-			p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
+			if p.p.IsSet(d.key) {
+				p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
+			}
 			return nil
 		},
 	},
@@ -306,15 +310,17 @@ var allDecoderSetups = map[string]decodeWeight{
 			}
 
 			// Validate defaultContentLanguage.
-			var found bool
-			for lang := range p.c.Languages {
-				if lang == p.c.DefaultContentLanguage {
-					found = true
-					break
+			if p.c.DefaultContentLanguage != "" {
+				var found bool
+				for lang := range p.c.Languages {
+					if lang == p.c.DefaultContentLanguage {
+						found = true
+						break
+					}
+				}
+				if !found {
+					return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage)
 				}
-			}
-			if !found {
-				return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage)
 			}
 
 			return nil
diff --git a/config/allconfig/load.go b/config/allconfig/load.go
index 9289cf294f2..2d9185f6f52 100644
--- a/config/allconfig/load.go
+++ b/config/allconfig/load.go
@@ -159,63 +159,9 @@ func (l configLoader) applyConfigAliases() error {
 
 func (l configLoader) applyDefaultConfig() error {
 	defaultSettings := maps.Params{
-		"baseURL":                              "",
-		"cleanDestinationDir":                  false,
-		"watch":                                false,
-		"contentDir":                           "content",
-		"resourceDir":                          "resources",
-		"publishDir":                           "public",
-		"publishDirOrig":                       "public",
-		"themesDir":                            "themes",
-		"assetDir":                             "assets",
-		"layoutDir":                            "layouts",
-		"i18nDir":                              "i18n",
-		"dataDir":                              "data",
-		"archetypeDir":                         "archetypes",
-		"configDir":                            "config",
-		"staticDir":                            "static",
-		"buildDrafts":                          false,
-		"buildFuture":                          false,
-		"buildExpired":                         false,
-		"params":                               maps.Params{},
-		"environment":                          hugo.EnvironmentProduction,
-		"uglyURLs":                             false,
-		"verbose":                              false,
-		"ignoreCache":                          false,
-		"canonifyURLs":                         false,
-		"relativeURLs":                         false,
-		"removePathAccents":                    false,
-		"titleCaseStyle":                       "AP",
-		"taxonomies":                           maps.Params{"tag": "tags", "category": "categories"},
-		"permalinks":                           maps.Params{},
-		"sitemap":                              maps.Params{"priority": -1, "filename": "sitemap.xml"},
-		"menus":                                maps.Params{},
-		"disableLiveReload":                    false,
-		"pluralizeListTitles":                  true,
-		"capitalizeListTitles":                 true,
-		"forceSyncStatic":                      false,
-		"footnoteAnchorPrefix":                 "",
-		"footnoteReturnLinkContents":           "",
-		"newContentEditor":                     "",
-		"paginate":                             0,  // Moved into the paginator struct in Hugo v0.128.0.
-		"paginatePath":                         "", // Moved into the paginator struct in Hugo v0.128.0.
-		"summaryLength":                        70,
-		"rssLimit":                             -1,
-		"sectionPagesMenu":                     "",
-		"disablePathToLower":                   false,
-		"hasCJKLanguage":                       false,
-		"enableEmoji":                          false,
-		"defaultContentLanguage":               "en",
-		"defaultContentLanguageInSubdir":       false,
-		"enableMissingTranslationPlaceholders": false,
-		"enableGitInfo":                        false,
-		"ignoreFiles":                          make([]string, 0),
-		"disableAliases":                       false,
-		"debug":                                false,
-		"disableFastRender":                    false,
-		"timeout":                              "60s",
-		"timeZone":                             "",
-		"enableInlineShortcodes":               false,
+		// These dirs are used early/before we build the config struct.
+		"themesDir": "themes",
+		"configDir": "config",
 	}
 
 	l.cfg.SetDefaults(defaultSettings)
diff --git a/config/commonConfig.go b/config/commonConfig.go
index 3dfd9b40969..9470786724f 100644
--- a/config/commonConfig.go
+++ b/config/commonConfig.go
@@ -17,6 +17,7 @@ import (
 	"fmt"
 	"net/http"
 	"regexp"
+	"slices"
 	"sort"
 	"strings"
 
@@ -28,7 +29,6 @@ import (
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/mitchellh/mapstructure"
 	"github.com/spf13/cast"
-	"slices"
 )
 
 type BaseConfig struct {
diff --git a/config/services/servicesConfig.go b/config/services/servicesConfig.go
index f302244d4aa..f9d5e1a6e6c 100644
--- a/config/services/servicesConfig.go
+++ b/config/services/servicesConfig.go
@@ -101,6 +101,9 @@ func DecodeConfig(cfg config.Provider) (c Config, err error) {
 
 	if c.RSS.Limit == 0 {
 		c.RSS.Limit = cfg.GetInt(rssLimitKey)
+		if c.RSS.Limit == 0 {
+			c.RSS.Limit = -1
+		}
 	}
 
 	return


Test patch:
diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go
index cae04ba8546..03394720302 100644
--- a/config/allconfig/allconfig_integration_test.go
+++ b/config/allconfig/allconfig_integration_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/common/hugo"
 	"github.com/gohugoio/hugo/config/allconfig"
 	"github.com/gohugoio/hugo/hugolib"
 	"github.com/gohugoio/hugo/media"
@@ -234,3 +235,62 @@ baseURL = "https://example.com"
 	b.Assert(c.IsContentFile("foo.md"), qt.Equals, true)
 	b.Assert(len(s), qt.Equals, 6)
 }
+
+func TestMergeDeep(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+theme = ["theme1", "theme2"]
+_merge = "deep"
+-- themes/theme1/hugo.toml --
+[sitemap]
+filename = 'mysitemap.xml'
+[services]
+[services.googleAnalytics]
+id = 'foo bar'
+[taxonomies]
+  foo = 'bars'
+-- themes/theme2/config/_default/hugo.toml --
+[taxonomies]
+  bar = 'baz'
+-- layouts/home.html --
+GA ID: {{ site.Config.Services.GoogleAnalytics.ID }}.
+
+`
+
+	b := hugolib.Test(t, files)
+
+	conf := b.H.Configs
+	base := conf.Base
+
+	b.Assert(base.Environment, qt.Equals, hugo.EnvironmentProduction)
+	b.Assert(base.BaseURL, qt.Equals, "https://example.com")
+	b.Assert(base.Sitemap.Filename, qt.Equals, "mysitemap.xml")
+	b.Assert(base.Taxonomies, qt.DeepEquals, map[string]string{"bar": "baz", "foo": "bars"})
+
+	b.AssertFileContent("public/index.html", "GA ID: foo bar.")
+}
+
+func TestDefaultConfigLanguageBlankWhenNoEnglishExists(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+[languages]
+[languages.nn]
+weight = 20
+[languages.sv]
+weight = 10
+[languages.sv.taxonomies]
+  tag = "taggar"
+-- layouts/all.html --
+All.
+`
+
+	b := hugolib.Test(t, files)
+
+	b.Assert(b.H.Conf.DefaultContentLanguage(), qt.Equals, "sv")
+}
diff --git a/hugolib/config_test.go b/hugolib/config_test.go
index cbf821ee76a..4275fb02aa1 100644
--- a/hugolib/config_test.go
+++ b/hugolib/config_test.go
@@ -475,7 +475,7 @@ name = "menu-theme"
 		})
 	})
 
-	// Issue #8724
+	// Issue #8724 ##13643
 	for _, mergeStrategy := range []string{"none", "shallow"} {
 		c.Run(fmt.Sprintf("Merge with sitemap config in theme, mergestrategy %s", mergeStrategy), func(c *qt.C) {
 			smapConfigTempl := `[sitemap]
@@ -495,7 +495,7 @@ name = "menu-theme"
 				b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "", Disable: false, Priority: -1, Filename: "sitemap.xml"})
 				b.AssertFileContent("public/sitemap.xml", "schemas/sitemap")
 			} else {
-				b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "monthly", Disable: false, Priority: -1, Filename: "sitemap.xml"})
+				b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "monthly", Disable: false, Priority: 0.5, Filename: "sitemap.xml"})
 				b.AssertFileContent("public/sitemap.xml", "<changefreq>monthly</changefreq>")
 			}
 		})