The main motivation behind this is simplicity and correctnes, but the new small config library is also faster:
```
BenchmarkDefaultConfigProvider/Viper-16         	  252418	      4546 ns/op	    2720 B/op	      30 allocs/op
BenchmarkDefaultConfigProvider/Custom-16        	  450756	      2651 ns/op	    1008 B/op	       6 allocs/op
```
Fixes #8633
Fixes #8618
Fixes #8630
Updates #8591
Closes #6680
Closes #5192
        "strings"
        "time"
 
+       "github.com/gohugoio/hugo/common/maps"
+
        "github.com/gohugoio/hugo/config"
 
        "github.com/gohugoio/hugo/helpers"
        _, isOsFs := fs.(*afero.OsFs)
 
        for k, v := range m {
+               if _, ok := v.(maps.Params); !ok {
+                       continue
+               }
                cc := defaultCacheConfig
 
                dc := &mapstructure.DecoderConfig{
                }
 
                if err := decoder.Decode(v); err != nil {
-                       return nil, err
+                       return nil, errors.Wrap(err, "failed to decode filecache config")
                }
 
                if cc.Dir == "" {
 
        "github.com/gohugoio/hugo/config"
 
        qt "github.com/frankban/quicktest"
-       "github.com/spf13/viper"
 )
 
 func TestDecodeConfig(t *testing.T) {
        c.Assert(err, qt.Not(qt.IsNil))
 }
 
-func newTestConfig() *viper.Viper {
-       cfg := viper.New()
+func newTestConfig() config.Provider {
+       cfg := config.New()
        cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
        cfg.Set("contentDir", "content")
        cfg.Set("dataDir", "data")
 
        }
        config.Set("cacheDir", cacheDir)
 
-       cfg.Logger.Infoln("Using config file:", config.ConfigFileUsed())
-
        return nil
 }
 
        "path/filepath"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/htesting"
 
        "github.com/spf13/afero"
        "github.com/gohugoio/hugo/common/types"
 
        "github.com/spf13/cobra"
-       "github.com/spf13/viper"
 
        qt "github.com/frankban/quicktest"
 )
                        name: "ignoreVendor as bool",
                        args: []string{"server", "--ignoreVendor"},
                        check: func(c *qt.C, cmd *serverCmd) {
-                               cfg := viper.New()
+                               cfg := config.New()
                                cmd.flagsToConfig(cfg)
                                c.Assert(cfg.Get("ignoreVendor"), qt.Equals, true)
                        },
                        name: "ignoreVendorPaths",
                        args: []string{"server", "--ignoreVendorPaths=github.com/**"},
                        check: func(c *qt.C, cmd *serverCmd) {
-                               cfg := viper.New()
+                               cfg := config.New()
                                cmd.flagsToConfig(cfg)
                                c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**")
                        },
                                c.Assert(sc.serverPort, qt.Equals, 1366)
                                c.Assert(sc.environment, qt.Equals, "testing")
 
-                               cfg := viper.New()
+                               cfg := config.New()
                                sc.flagsToConfig(cfg)
                                c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
                                c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
 
        "sort"
        "strings"
 
+       "github.com/gohugoio/hugo/common/maps"
+
        "github.com/gohugoio/hugo/parser"
        "github.com/gohugoio/hugo/parser/metadecoders"
 
        "github.com/gohugoio/hugo/modules"
 
        "github.com/spf13/cobra"
-       "github.com/spf13/viper"
 )
 
 var _ cmder = (*configCmd)(nil)
                return err
        }
 
-       allSettings := cfg.Cfg.(*viper.Viper).AllSettings()
+       allSettings := cfg.Cfg.Get("").(maps.Params)
 
        // We need to clean up this, but we store objects in the config that
        // isn't really interesting to the end user, so filter these.
 
        "path/filepath"
        "strings"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/parser/metadecoders"
 
        _errors "github.com/pkg/errors"
        "github.com/gohugoio/hugo/parser"
        "github.com/spf13/cobra"
        jww "github.com/spf13/jwalterweatherman"
-       "github.com/spf13/viper"
 )
 
 var _ cmder = (*newSiteCmd)(nil)
 
        forceNew, _ := cmd.Flags().GetBool("force")
 
-       return n.doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew)
+       return n.doNewSite(hugofs.NewDefault(config.New()), createpath, forceNew)
 }
 
 func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
 
        "testing"
        "time"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/helpers"
 
        qt "github.com/frankban/quicktest"
-       "github.com/spf13/viper"
 )
 
 func TestServer(t *testing.T) {
                t.Run(test.TestName, func(t *testing.T) {
                        b := newCommandsBuilder()
                        s := b.newServerCmd()
-                       v := viper.New()
+                       v := config.New()
                        baseURL := test.CLIBaseURL
                        v.Set("baseURL", test.CfgBaseURL)
                        s.serverAppend = test.AppendPort
 
        "strings"
 
        "github.com/gobwas/glob"
-
        "github.com/spf13/cast"
 )
 
-// ToLower makes all the keys in the given map lower cased and will do so
-// recursively.
-// Notes:
-// * This will modify the map given.
-// * Any nested map[interface{}]interface{} will be converted to Params.
-func ToLower(m Params) {
-       for k, v := range m {
-               var retyped bool
-               switch v.(type) {
-               case map[interface{}]interface{}:
-                       var p Params = cast.ToStringMap(v)
-                       v = p
-                       ToLower(p)
-                       retyped = true
-               case map[string]interface{}:
-                       var p Params = v.(map[string]interface{})
-                       v = p
-                       ToLower(p)
-                       retyped = true
+// ToStringMapE converts in to map[string]interface{}.
+func ToStringMapE(in interface{}) (map[string]interface{}, error) {
+       switch vv := in.(type) {
+       case Params:
+               return vv, nil
+       case map[string]string:
+               var m = map[string]interface{}{}
+               for k, v := range vv {
+                       m[k] = v
                }
+               return m, nil
 
-               lKey := strings.ToLower(k)
-               if retyped || k != lKey {
-                       delete(m, k)
-                       m[lKey] = v
-               }
+       default:
+               return cast.ToStringMapE(in)
        }
 }
 
-func ToStringMapE(in interface{}) (map[string]interface{}, error) {
-       switch in.(type) {
-       case Params:
-               return in.(Params), nil
-       default:
-               return cast.ToStringMapE(in)
+// ToParamsAndPrepare converts in to Params and prepares it for use.
+// See PrepareParams.
+func ToParamsAndPrepare(in interface{}) (Params, bool) {
+       m, err := ToStringMapE(in)
+       if err != nil {
+               return nil, false
        }
+       PrepareParams(m)
+       return m, true
 }
 
+// ToStringMap converts in to map[string]interface{}.
 func ToStringMap(in interface{}) map[string]interface{} {
        m, _ := ToStringMapE(in)
        return m
 }
 
+// ToStringMapStringE converts in to map[string]string.
+func ToStringMapStringE(in interface{}) (map[string]string, error) {
+       m, err := ToStringMapE(in)
+       if err != nil {
+               return nil, err
+       }
+       return cast.ToStringMapStringE(m)
+}
+
+// ToStringMapString converts in to map[string]string.
+func ToStringMapString(in interface{}) map[string]string {
+       m, _ := ToStringMapStringE(in)
+       return m
+}
+
+// ToStringMapBool converts in to bool.
+func ToStringMapBool(in interface{}) map[string]bool {
+       m, _ := ToStringMapE(in)
+       return cast.ToStringMapBool(m)
+}
+
+// ToSliceStringMap converts in to []map[string]interface{}.
 func ToSliceStringMap(in interface{}) ([]map[string]interface{}, error) {
        switch v := in.(type) {
        case []map[string]interface{}:
        k1, k2 = strings.ToLower(k1), strings.ToLower(k2)
        if k1 == "" {
                return k2
-       } else {
-               return k1 + "/" + k2
        }
+       return k1 + "/" + k2
 }
 
 func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) {
 
        for i, test := range tests {
                t.Run(fmt.Sprint(i), func(t *testing.T) {
                        // ToLower modifies input.
-                       ToLower(test.input)
+                       PrepareParams(test.input)
                        if !reflect.DeepEqual(test.expected, test.input) {
                                t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
                        }
 
 package maps
 
 import (
+       "fmt"
        "strings"
 
        "github.com/spf13/cast"
        return v
 }
 
+// Set overwrites values in p with values in pp for common or new keys.
+// This is done recursively.
+func (p Params) Set(pp Params) {
+       for k, v := range pp {
+               vv, found := p[k]
+               if !found {
+                       p[k] = v
+               } else {
+                       switch vvv := vv.(type) {
+                       case Params:
+                               if pv, ok := v.(Params); ok {
+                                       vvv.Set(pv)
+                               } else {
+                                       p[k] = v
+                               }
+                       default:
+                               p[k] = v
+                       }
+               }
+       }
+}
+
+// Merge transfers values from pp to p for new keys.
+// This is done recursively.
+func (p Params) Merge(pp Params) {
+       p.merge("", pp)
+}
+
+func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
+       ns, found := p.GetMergeStrategy()
+
+       var ms = ns
+       if !found && ps != "" {
+               ms = ps
+       }
+
+       noUpdate := ms == ParamsMergeStrategyNone
+       noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
+
+       for k, v := range pp {
+
+               if k == mergeStrategyKey {
+                       continue
+               }
+               vv, found := p[k]
+
+               if found {
+                       // Key matches, if both sides are Params, we try to merge.
+                       if vvv, ok := vv.(Params); ok {
+                               if pv, ok := v.(Params); ok {
+                                       vvv.merge(ms, pv)
+                               }
+
+                       }
+
+               } else if !noUpdate {
+                       p[k] = v
+
+               }
+
+       }
+}
+
+func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
+       if v, found := p[mergeStrategyKey]; found {
+               if s, ok := v.(ParamsMergeStrategy); ok {
+                       return s, true
+               }
+       }
+       return ParamsMergeStrategyShallow, false
+}
+
+func (p Params) DeleteMergeStrategy() bool {
+       if _, found := p[mergeStrategyKey]; found {
+               delete(p, mergeStrategyKey)
+               return true
+       }
+       return false
+}
+
+func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) {
+       switch s {
+       case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
+       default:
+               panic(fmt.Sprintf("invalid merge strategy %q", s))
+       }
+       p[mergeStrategyKey] = s
+}
+
 func getNested(m map[string]interface{}, indices []string) (interface{}, string, map[string]interface{}) {
        if len(indices) == 0 {
                return nil, "", nil
 
        return nil, "", nil, nil
 }
+
+// ParamsMergeStrategy tells what strategy to use in Params.Merge.
+type ParamsMergeStrategy string
+
+const (
+       // Do not merge.
+       ParamsMergeStrategyNone ParamsMergeStrategy = "none"
+       // Only add new keys.
+       ParamsMergeStrategyShallow ParamsMergeStrategy = "shallow"
+       // Add new keys, merge existing.
+       ParamsMergeStrategyDeep ParamsMergeStrategy = "deep"
+
+       mergeStrategyKey = "_merge"
+)
+
+func toMergeStrategy(v interface{}) ParamsMergeStrategy {
+       s := ParamsMergeStrategy(cast.ToString(v))
+       switch s {
+       case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
+               return s
+       default:
+               return ParamsMergeStrategyDeep
+       }
+}
+
+// PrepareParams
+// * makes all the keys in the given map lower cased and will do so
+// * This will modify the map given.
+// * Any nested map[interface{}]interface{} will be converted to Params.
+// * Any _merge value will be converted to proper type and value.
+func PrepareParams(m Params) {
+       for k, v := range m {
+               var retyped bool
+               lKey := strings.ToLower(k)
+               if lKey == mergeStrategyKey {
+                       v = toMergeStrategy(v)
+                       retyped = true
+               } else {
+                       switch v.(type) {
+                       case map[interface{}]interface{}:
+                               var p Params = cast.ToStringMap(v)
+                               v = p
+                               PrepareParams(p)
+                               retyped = true
+                       case map[string]interface{}:
+                               var p Params = v.(map[string]interface{})
+                               v = p
+                               PrepareParams(p)
+                               retyped = true
+                       }
+               }
+
+               if retyped || k != lKey {
+                       delete(m, k)
+                       m[lKey] = v
+               }
+       }
+}
 
        c.Assert(nestedKey, qt.Equals, "new")
        c.Assert(owner, qt.DeepEquals, nested)
 }
+
+func TestParamsSetAndMerge(t *testing.T) {
+       c := qt.New(t)
+
+       createParamsPair := func() (Params, Params) {
+               p1 := Params{"a": "av", "c": "cv", "nested": Params{"al2": "al2v", "cl2": "cl2v"}}
+               p2 := Params{"b": "bv", "a": "abv", "nested": Params{"bl2": "bl2v", "al2": "al2bv"}, mergeStrategyKey: ParamsMergeStrategyDeep}
+               return p1, p2
+       }
+
+       p1, p2 := createParamsPair()
+
+       p1.Set(p2)
+
+       c.Assert(p1, qt.DeepEquals, Params{
+               "a": "abv",
+               "c": "cv",
+               "nested": Params{
+                       "al2": "al2bv",
+                       "cl2": "cl2v",
+                       "bl2": "bl2v",
+               },
+               "b":              "bv",
+               mergeStrategyKey: ParamsMergeStrategyDeep,
+       })
+
+       p1, p2 = createParamsPair()
+
+       p1.Merge(p2)
+
+       // Default is to do a shallow merge.
+       c.Assert(p1, qt.DeepEquals, Params{
+               "c": "cv",
+               "nested": Params{
+                       "al2": "al2v",
+                       "cl2": "cl2v",
+               },
+               "b": "bv",
+               "a": "av",
+       })
+
+       p1, p2 = createParamsPair()
+       p1.SetDefaultMergeStrategy(ParamsMergeStrategyNone)
+       p1.Merge(p2)
+       p1.DeleteMergeStrategy()
+
+       c.Assert(p1, qt.DeepEquals, Params{
+               "a": "av",
+               "c": "cv",
+               "nested": Params{
+                       "al2": "al2v",
+                       "cl2": "cl2v",
+               },
+       })
+
+       p1, p2 = createParamsPair()
+       p1.SetDefaultMergeStrategy(ParamsMergeStrategyShallow)
+       p1.Merge(p2)
+       p1.DeleteMergeStrategy()
+
+       c.Assert(p1, qt.DeepEquals, Params{
+               "a": "av",
+               "c": "cv",
+               "nested": Params{
+                       "al2": "al2v",
+                       "cl2": "cl2v",
+               },
+               "b": "bv",
+       })
+
+       p1, p2 = createParamsPair()
+       p1.SetDefaultMergeStrategy(ParamsMergeStrategyDeep)
+       p1.Merge(p2)
+       p1.DeleteMergeStrategy()
+
+       c.Assert(p1, qt.DeepEquals, Params{
+               "nested": Params{
+                       "al2": "al2v",
+                       "cl2": "cl2v",
+                       "bl2": "bl2v",
+               },
+               "b": "bv",
+               "a": "av",
+               "c": "cv",
+       })
+
+}
 
        "github.com/gohugoio/hugo/common/types"
 
        qt "github.com/frankban/quicktest"
-
-       "github.com/spf13/viper"
 )
 
 func TestBuild(t *testing.T) {
        c := qt.New(t)
 
-       v := viper.New()
+       v := New()
        v.Set("build", map[string]interface{}{
                "useResourceCacheWhen": "always",
        })
 
--- /dev/null
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+       "github.com/gohugoio/hugo/common/maps"
+)
+
+// NewCompositeConfig creates a new composite Provider with a read-only base
+// and a writeable layer.
+func NewCompositeConfig(base, layer Provider) Provider {
+       return &compositeConfig{
+               base:  base,
+               layer: layer,
+       }
+}
+
+// compositeConfig contains a read only config base with
+// a possibly writeable config layer on top.
+type compositeConfig struct {
+       base  Provider
+       layer Provider
+}
+
+func (c *compositeConfig) GetBool(key string) bool {
+       if c.layer.IsSet(key) {
+               return c.layer.GetBool(key)
+       }
+       return c.base.GetBool(key)
+}
+
+func (c *compositeConfig) GetInt(key string) int {
+       if c.layer.IsSet(key) {
+               return c.layer.GetInt(key)
+       }
+       return c.base.GetInt(key)
+}
+
+func (c *compositeConfig) Merge(key string, value interface{}) {
+       c.layer.Merge(key, value)
+}
+
+func (c *compositeConfig) GetParams(key string) maps.Params {
+       if c.layer.IsSet(key) {
+               return c.layer.GetParams(key)
+       }
+       return c.base.GetParams(key)
+}
+
+func (c *compositeConfig) GetStringMap(key string) map[string]interface{} {
+       if c.layer.IsSet(key) {
+               return c.layer.GetStringMap(key)
+       }
+       return c.base.GetStringMap(key)
+}
+
+func (c *compositeConfig) GetStringMapString(key string) map[string]string {
+       if c.layer.IsSet(key) {
+               return c.layer.GetStringMapString(key)
+       }
+       return c.base.GetStringMapString(key)
+}
+
+func (c *compositeConfig) GetStringSlice(key string) []string {
+       if c.layer.IsSet(key) {
+               return c.layer.GetStringSlice(key)
+       }
+       return c.base.GetStringSlice(key)
+}
+
+func (c *compositeConfig) Get(key string) interface{} {
+       if c.layer.IsSet(key) {
+               return c.layer.Get(key)
+       }
+       return c.base.Get(key)
+}
+
+func (c *compositeConfig) IsSet(key string) bool {
+       if c.layer.IsSet(key) {
+               return true
+       }
+       return c.base.IsSet(key)
+}
+
+func (c *compositeConfig) GetString(key string) string {
+       if c.layer.IsSet(key) {
+               return c.layer.GetString(key)
+       }
+       return c.base.GetString(key)
+}
+
+func (c *compositeConfig) Set(key string, value interface{}) {
+       c.layer.Set(key, value)
+}
+
+func (c *compositeConfig) WalkParams(walkFn func(params ...KeyParams) bool) {
+       panic("not supported")
+}
+
+func (c *compositeConfig) SetDefaultMergeStrategy() {
+       panic("not supported")
+}
 
--- /dev/null
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+       "testing"
+
+       qt "github.com/frankban/quicktest"
+)
+
+func TestCompositeConfig(t *testing.T) {
+       c := qt.New(t)
+
+       c.Run("Set and get", func(c *qt.C) {
+               base, layer := New(), New()
+               cfg := NewCompositeConfig(base, layer)
+
+               layer.Set("a1", "av")
+               base.Set("b1", "bv")
+               cfg.Set("c1", "cv")
+
+               c.Assert(cfg.Get("a1"), qt.Equals, "av")
+               c.Assert(cfg.Get("b1"), qt.Equals, "bv")
+               c.Assert(cfg.Get("c1"), qt.Equals, "cv")
+               c.Assert(cfg.IsSet("c1"), qt.IsTrue)
+               c.Assert(layer.IsSet("c1"), qt.IsTrue)
+               c.Assert(base.IsSet("c1"), qt.IsFalse)
+       })
+}
 
        "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/parser/metadecoders"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
 var (
 
 // FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
 func FromConfigString(config, configType string) (Provider, error) {
-       v := newViper()
        m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
        if err != nil {
                return nil, err
        }
-
-       v.MergeConfigMap(m)
-
-       return v, nil
+       return NewFrom(m), nil
 }
 
 // FromFile loads the configuration from the given filename.
        if err != nil {
                return nil, err
        }
-
-       v := newViper()
-
-       err = v.MergeConfigMap(m)
-       if err != nil {
-               return nil, err
-       }
-
-       return v, nil
+       return NewFrom(m), nil
 }
 
 // FromFileToMap is the same as FromFile, but it returns the config values
 func RenameKeys(m map[string]interface{}) {
        keyAliases.Rename(m)
 }
-
-func newViper() *viper.Viper {
-       v := viper.New()
-
-       return v
-}
 
 package config
 
 import (
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/common/types"
 )
 
        GetString(key string) string
        GetInt(key string) int
        GetBool(key string) bool
+       GetParams(key string) maps.Params
        GetStringMap(key string) map[string]interface{}
        GetStringMapString(key string) map[string]string
        GetStringSlice(key string) []string
        Get(key string) interface{}
        Set(key string, value interface{})
+       Merge(key string, value interface{})
+       SetDefaultMergeStrategy()
+       WalkParams(walkFn func(params ...KeyParams) bool)
        IsSet(key string) bool
 }
 
 
        "testing"
 
        qt "github.com/frankban/quicktest"
-       "github.com/spf13/viper"
 )
 
 func TestGetStringSlicePreserveString(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := New()
 
        s := "This is a string"
        sSlice := []string{"This", "is", "a", "slice"}
 
--- /dev/null
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+       "fmt"
+       "sort"
+       "strings"
+       "sync"
+
+       "github.com/spf13/cast"
+
+       "github.com/gohugoio/hugo/common/maps"
+)
+
+var (
+
+       // ConfigRootKeysSet contains all of the config map root keys.
+       // TODO(bep) use this for something (docs etc.)
+       ConfigRootKeysSet = map[string]bool{
+               "build":         true,
+               "caches":        true,
+               "frontmatter":   true,
+               "languages":     true,
+               "imaging":       true,
+               "markup":        true,
+               "mediatypes":    true,
+               "menus":         true,
+               "minify":        true,
+               "module":        true,
+               "outputformats": true,
+               "params":        true,
+               "permalinks":    true,
+               "related":       true,
+               "sitemap":       true,
+               "taxonomies":    true,
+       }
+
+       // ConfigRootKeys is a sorted version of ConfigRootKeysSet.
+       ConfigRootKeys []string
+)
+
+func init() {
+       for k := range ConfigRootKeysSet {
+               ConfigRootKeys = append(ConfigRootKeys, k)
+       }
+       sort.Strings(ConfigRootKeys)
+}
+
+// New creates a Provider backed by an empty maps.Params.
+func New() Provider {
+       return &defaultConfigProvider{
+               root: make(maps.Params),
+       }
+}
+
+// NewFrom creates a Provider backed by params.
+func NewFrom(params maps.Params) Provider {
+       maps.PrepareParams(params)
+       return &defaultConfigProvider{
+               root: params,
+       }
+}
+
+// defaultConfigProvider is a Provider backed by a map where all keys are lower case.
+// All methods are thread safe.
+type defaultConfigProvider struct {
+       mu   sync.RWMutex
+       root maps.Params
+
+       keyCache sync.Map
+}
+
+func (c *defaultConfigProvider) Get(k string) interface{} {
+       if k == "" {
+               return c.root
+       }
+       c.mu.RLock()
+       key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
+       if m == nil {
+               return nil
+       }
+       v := m[key]
+       c.mu.RUnlock()
+       return v
+}
+
+func (c *defaultConfigProvider) GetBool(k string) bool {
+       v := c.Get(k)
+       return cast.ToBool(v)
+}
+
+func (c *defaultConfigProvider) GetInt(k string) int {
+       v := c.Get(k)
+       return cast.ToInt(v)
+}
+
+func (c *defaultConfigProvider) IsSet(k string) bool {
+       var found bool
+       c.mu.RLock()
+       key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
+       if m != nil {
+               _, found = m[key]
+       }
+       c.mu.RUnlock()
+       return found
+}
+
+func (c *defaultConfigProvider) GetString(k string) string {
+       v := c.Get(k)
+       return cast.ToString(v)
+}
+
+func (c *defaultConfigProvider) GetParams(k string) maps.Params {
+       v := c.Get(k)
+       if v == nil {
+               return nil
+       }
+       return v.(maps.Params)
+}
+
+func (c *defaultConfigProvider) GetStringMap(k string) map[string]interface{} {
+       v := c.Get(k)
+       return maps.ToStringMap(v)
+}
+
+func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string {
+       v := c.Get(k)
+       return maps.ToStringMapString(v)
+}
+
+func (c *defaultConfigProvider) GetStringSlice(k string) []string {
+       v := c.Get(k)
+       return cast.ToStringSlice(v)
+}
+
+func (c *defaultConfigProvider) Set(k string, v interface{}) {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+
+       k = strings.ToLower(k)
+
+       if k == "" {
+               if p, ok := maps.ToParamsAndPrepare(v); ok {
+                       // Set the values directly in root.
+                       c.root.Set(p)
+               } else {
+                       c.root[k] = v
+               }
+
+               return
+       }
+
+       switch vv := v.(type) {
+       case map[string]interface{}:
+               var p maps.Params = vv
+               v = p
+               maps.PrepareParams(p)
+       }
+
+       key, m := c.getNestedKeyAndMap(k, true)
+
+       if existing, found := m[key]; found {
+               if p1, ok := existing.(maps.Params); ok {
+                       if p2, ok := v.(maps.Params); ok {
+                               p1.Set(p2)
+                               return
+                       }
+               }
+       }
+
+       m[key] = v
+}
+
+func (c *defaultConfigProvider) Merge(k string, v interface{}) {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+       k = strings.ToLower(k)
+
+       if k == "" {
+               rs, f := c.root.GetMergeStrategy()
+               if f && rs == maps.ParamsMergeStrategyNone {
+                       // The user has set a "no merge" strategy on this,
+                       // nothing more to do.
+                       return
+               }
+
+               if p, ok := maps.ToParamsAndPrepare(v); ok {
+                       // As there may be keys in p not in root, we need to handle
+                       // those as a special case.
+                       for kk, vv := range p {
+                               if pp, ok := vv.(maps.Params); ok {
+                                       if ppp, ok := c.root[kk]; ok {
+                                               ppp.(maps.Params).Merge(pp)
+                                       } else {
+                                               // We need to use the default merge strategy for
+                                               // this key.
+                                               np := make(maps.Params)
+                                               strategy := c.determineMergeStrategy(KeyParams{Key: "", Params: c.root}, KeyParams{Key: kk, Params: np})
+                                               np.SetDefaultMergeStrategy(strategy)
+                                               np.Merge(pp)
+                                               if len(np) > 0 {
+                                                       c.root[kk] = np
+                                               }
+                                       }
+                               }
+                       }
+                       // Merge the rest.
+                       c.root.Merge(p)
+               } else {
+                       panic(fmt.Sprintf("unsupported type %T received in Merge", v))
+               }
+
+               return
+       }
+
+       switch vv := v.(type) {
+       case map[string]interface{}:
+               var p maps.Params = vv
+               v = p
+               maps.PrepareParams(p)
+       }
+
+       key, m := c.getNestedKeyAndMap(k, true)
+
+       if existing, found := m[key]; found {
+               if p1, ok := existing.(maps.Params); ok {
+                       if p2, ok := v.(maps.Params); ok {
+                               p1.Merge(p2)
+                       }
+               }
+       } else {
+               m[key] = v
+       }
+}
+
+func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool) {
+       var walk func(params ...KeyParams)
+       walk = func(params ...KeyParams) {
+               if walkFn(params...) {
+                       return
+               }
+               p1 := params[len(params)-1]
+               i := len(params)
+               for k, v := range p1.Params {
+                       if p2, ok := v.(maps.Params); ok {
+                               paramsplus1 := make([]KeyParams, i+1)
+                               copy(paramsplus1, params)
+                               paramsplus1[i] = KeyParams{Key: k, Params: p2}
+                               walk(paramsplus1...)
+                       }
+               }
+       }
+       walk(KeyParams{Key: "", Params: c.root})
+}
+
+func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps.ParamsMergeStrategy {
+       if len(params) == 0 {
+               return maps.ParamsMergeStrategyNone
+       }
+
+       var (
+               strategy   maps.ParamsMergeStrategy
+               prevIsRoot bool
+               curr       = params[len(params)-1]
+       )
+
+       if len(params) > 1 {
+               prev := params[len(params)-2]
+               prevIsRoot = prev.Key == ""
+
+               // Inherit from parent (but not from the root unless it's set by user).
+               s, found := prev.Params.GetMergeStrategy()
+               if !prevIsRoot && !found {
+                       panic("invalid state, merge strategy not set on parent")
+               }
+               if found || !prevIsRoot {
+                       strategy = s
+               }
+       }
+
+       switch curr.Key {
+       case "":
+       // Don't set a merge strategy on the root unless set by user.
+       // This will be handled as a special case.
+       case "params":
+               strategy = maps.ParamsMergeStrategyDeep
+       case "outputformats", "mediatypes":
+               if prevIsRoot {
+                       strategy = maps.ParamsMergeStrategyShallow
+               }
+       case "menus":
+               isMenuKey := prevIsRoot
+               if !isMenuKey {
+                       // Can also be set below languages.
+                       // root > languages > en > menus
+                       if len(params) == 4 && params[1].Key == "languages" {
+                               isMenuKey = true
+                       }
+               }
+               if isMenuKey {
+                       strategy = maps.ParamsMergeStrategyShallow
+               }
+       default:
+               if strategy == "" {
+                       strategy = maps.ParamsMergeStrategyNone
+               }
+       }
+
+       return strategy
+}
+
+type KeyParams struct {
+       Key    string
+       Params maps.Params
+}
+
+func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
+       c.WalkParams(func(params ...KeyParams) bool {
+               if len(params) == 0 {
+                       return false
+               }
+               p := params[len(params)-1].Params
+               var found bool
+               if _, found = p.GetMergeStrategy(); found {
+                       // Set by user.
+                       return false
+               }
+               strategy := c.determineMergeStrategy(params...)
+               if strategy != "" {
+                       p.SetDefaultMergeStrategy(strategy)
+               }
+               return false
+       })
+
+}
+
+func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) {
+       var parts []string
+       v, ok := c.keyCache.Load(key)
+       if ok {
+               parts = v.([]string)
+       } else {
+               parts = strings.Split(key, ".")
+               c.keyCache.Store(key, parts)
+       }
+       current := c.root
+       for i := 0; i < len(parts)-1; i++ {
+               next, found := current[parts[i]]
+               if !found {
+                       if create {
+                               next = make(maps.Params)
+                               current[parts[i]] = next
+                       } else {
+                               return "", nil
+                       }
+               }
+               current = next.(maps.Params)
+       }
+       return parts[len(parts)-1], current
+}
 
--- /dev/null
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+       "context"
+       "errors"
+       "fmt"
+       "strconv"
+       "strings"
+       "testing"
+
+       "github.com/spf13/viper"
+
+       "github.com/gohugoio/hugo/common/para"
+
+       "github.com/gohugoio/hugo/common/maps"
+
+       qt "github.com/frankban/quicktest"
+)
+
+func TestDefaultConfigProvider(t *testing.T) {
+       c := qt.New(t)
+
+       c.Run("Set and get", func(c *qt.C) {
+               cfg := New()
+               var k string
+               var v interface{}
+
+               k, v = "foo", "bar"
+               cfg.Set(k, v)
+               c.Assert(cfg.Get(k), qt.Equals, v)
+               c.Assert(cfg.Get(strings.ToUpper(k)), qt.Equals, v)
+               c.Assert(cfg.GetString(k), qt.Equals, v)
+
+               k, v = "foo", 42
+               cfg.Set(k, v)
+               c.Assert(cfg.Get(k), qt.Equals, v)
+               c.Assert(cfg.GetInt(k), qt.Equals, v)
+
+               c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
+                       "foo": 42,
+               })
+       })
+
+       c.Run("Set and get map", func(c *qt.C) {
+               cfg := New()
+
+               cfg.Set("foo", map[string]interface{}{
+                       "bar": "baz",
+               })
+
+               c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
+                       "bar": "baz",
+               })
+
+               c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]interface{}{"bar": string("baz")})
+               c.Assert(cfg.GetStringMapString("foo"), qt.DeepEquals, map[string]string{"bar": string("baz")})
+       })
+
+       c.Run("Set and get nested", func(c *qt.C) {
+               cfg := New()
+
+               cfg.Set("a", map[string]interface{}{
+                       "B": "bv",
+               })
+               cfg.Set("a.c", "cv")
+
+               c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
+                       "b": "bv",
+                       "c": "cv",
+               })
+               c.Assert(cfg.Get("a.c"), qt.Equals, "cv")
+
+               cfg.Set("b.a", "av")
+               c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
+                       "a": "av",
+               })
+
+               cfg.Set("b", map[string]interface{}{
+                       "b": "bv",
+               })
+
+               c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
+                       "a": "av",
+                       "b": "bv",
+               })
+
+               cfg = New()
+
+               cfg.Set("a", "av")
+
+               cfg.Set("", map[string]interface{}{
+                       "a": "av2",
+                       "b": "bv2",
+               })
+
+               c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
+                       "a": "av2",
+                       "b": "bv2",
+               })
+
+               cfg = New()
+
+               cfg.Set("a", "av")
+
+               cfg.Set("", map[string]interface{}{
+                       "b": "bv2",
+               })
+
+               c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
+                       "a": "av",
+                       "b": "bv2",
+               })
+
+               cfg = New()
+
+               cfg.Set("", map[string]interface{}{
+                       "foo": map[string]interface{}{
+                               "a": "av",
+                       },
+               })
+
+               cfg.Set("", map[string]interface{}{
+                       "foo": map[string]interface{}{
+                               "b": "bv2",
+                       },
+               })
+
+               c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
+                       "a": "av",
+                       "b": "bv2",
+               })
+       })
+
+       c.Run("Merge default strategy", func(c *qt.C) {
+               cfg := New()
+
+               cfg.Set("a", map[string]interface{}{
+                       "B": "bv",
+               })
+
+               cfg.Merge("a", map[string]interface{}{
+                       "B": "bv2",
+                       "c": "cv2",
+               })
+
+               c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
+                       "b": "bv",
+                       "c": "cv2",
+               })
+
+               cfg = New()
+
+               cfg.Set("a", "av")
+
+               cfg.Merge("", map[string]interface{}{
+                       "a": "av2",
+                       "b": "bv2",
+               })
+
+               c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
+                       "a": "av",
+                       "b": "bv2",
+               })
+       })
+
+       c.Run("Merge shallow", func(c *qt.C) {
+               cfg := New()
+
+               cfg.Set("a", map[string]interface{}{
+                       "_merge": "shallow",
+                       "B":      "bv",
+                       "c": map[string]interface{}{
+                               "b": "bv",
+                       },
+               })
+
+               cfg.Merge("a", map[string]interface{}{
+                       "c": map[string]interface{}{
+                               "d": "dv2",
+                       },
+                       "e": "ev2",
+               })
+
+               c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
+                       "e":      "ev2",
+                       "_merge": maps.ParamsMergeStrategyShallow,
+                       "b":      "bv",
+                       "c": maps.Params{
+                               "b": "bv",
+                       },
+               })
+       })
+
+       c.Run("IsSet", func(c *qt.C) {
+               cfg := New()
+
+               cfg.Set("a", map[string]interface{}{
+                       "B": "bv",
+               })
+
+               c.Assert(cfg.IsSet("A"), qt.IsTrue)
+               c.Assert(cfg.IsSet("a.b"), qt.IsTrue)
+               c.Assert(cfg.IsSet("z"), qt.IsFalse)
+       })
+
+       c.Run("Para", func(c *qt.C) {
+               cfg := New()
+               p := para.New(4)
+               r, _ := p.Start(context.Background())
+
+               setAndGet := func(k string, v int) error {
+                       vs := strconv.Itoa(v)
+                       cfg.Set(k, v)
+                       err := errors.New("get failed")
+                       if cfg.Get(k) != v {
+                               return err
+                       }
+                       if cfg.GetInt(k) != v {
+                               return err
+                       }
+                       if cfg.GetString(k) != vs {
+                               return err
+                       }
+                       if !cfg.IsSet(k) {
+                               return err
+                       }
+                       return nil
+               }
+
+               for i := 0; i < 20; i++ {
+                       i := i
+                       r.Run(func() error {
+                               const v = 42
+                               k := fmt.Sprintf("k%d", i)
+                               if err := setAndGet(k, v); err != nil {
+                                       return err
+                               }
+
+                               m := maps.Params{
+                                       "new": 42,
+                               }
+
+                               cfg.Merge("", m)
+
+                               return nil
+                       })
+               }
+
+               c.Assert(r.Wait(), qt.IsNil)
+       })
+}
+
+func BenchmarkDefaultConfigProvider(b *testing.B) {
+       type cfger interface {
+               Get(key string) interface{}
+               Set(key string, value interface{})
+               IsSet(key string) bool
+       }
+
+       newMap := func() map[string]interface{} {
+               return map[string]interface{}{
+                       "a": map[string]interface{}{
+                               "b": map[string]interface{}{
+                                       "c": 32,
+                                       "d": 43,
+                               },
+                       },
+                       "b": 62,
+               }
+       }
+
+       runMethods := func(b *testing.B, cfg cfger) {
+               m := newMap()
+               cfg.Set("mymap", m)
+               cfg.Set("num", 32)
+               if !(cfg.IsSet("mymap") && cfg.IsSet("mymap.a") && cfg.IsSet("mymap.a.b") && cfg.IsSet("mymap.a.b.c")) {
+                       b.Fatal("IsSet failed")
+               }
+
+               if cfg.Get("num") != 32 {
+                       b.Fatal("Get failed")
+               }
+
+               if cfg.Get("mymap.a.b.c") != 32 {
+                       b.Fatal("Get failed")
+               }
+       }
+
+       b.Run("Viper", func(b *testing.B) {
+               v := viper.New()
+               for i := 0; i < b.N; i++ {
+                       runMethods(b, v)
+               }
+       })
+
+       b.Run("Custom", func(b *testing.B) {
+               cfg := New()
+               for i := 0; i < b.N; i++ {
+                       runMethods(b, cfg)
+               }
+       })
+}
 
--- /dev/null
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+       "github.com/gohugoio/hugo/common/maps"
+       "github.com/gohugoio/hugo/docshelper"
+)
+
+// This is is just some helpers used to create some JSON used in the Hugo docs.
+func init() {
+       docsProvider := func() docshelper.DocProvider {
+
+               cfg := New()
+               for _, configRoot := range ConfigRootKeys {
+                       cfg.Set(configRoot, make(maps.Params))
+               }
+               lang := maps.Params{
+                       "en": maps.Params{
+                               "menus":  maps.Params{},
+                               "params": maps.Params{},
+                       },
+               }
+               cfg.Set("languages", lang)
+               cfg.SetDefaultMergeStrategy()
+
+               configHelpers := map[string]interface{}{
+                       "mergeStrategy": cfg.Get(""),
+               }
+               return docshelper.DocProvider{"config": configHelpers}
+       }
+
+       docshelper.AddDocProviderFunc(docsProvider)
+}
 
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/config"
-       "github.com/spf13/viper"
 )
 
 func TestDecodeConfigFromTOML(t *testing.T) {
 func TestDecodeConfigDefault(t *testing.T) {
        c := qt.New(t)
 
-       pc, err := DecodeConfig(viper.New())
+       pc, err := DecodeConfig(config.New())
        c.Assert(err, qt.IsNil)
        c.Assert(pc, qt.Not(qt.IsNil))
        c.Assert(pc.YouTube.PrivacyEnhanced, qt.Equals, false)
 
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/config"
-       "github.com/spf13/viper"
+       
 )
 
 func TestDecodeConfigFromTOML(t *testing.T) {
 func TestUseSettingsFromRootIfSet(t *testing.T) {
        c := qt.New(t)
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("disqusShortname", "root_short")
        cfg.Set("googleAnalytics", "ga_root")
 
 
        "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/deps"
 
        "github.com/gohugoio/hugo/hugolib"
        "github.com/gohugoio/hugo/create"
        "github.com/gohugoio/hugo/helpers"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
 func TestNewContent(t *testing.T) {
        return string(b)
 }
 
-func newTestCfg(c *qt.C, mm afero.Fs) (*viper.Viper, *hugofs.Fs) {
+func newTestCfg(c *qt.C, mm afero.Fs) (config.Provider, *hugofs.Fs) {
        cfg := `
 
 theme = "mytheme"
 
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/config"
-       "github.com/spf13/viper"
+       
 )
 
 func TestDecodeConfigFromTOML(t *testing.T) {
 func TestDecodeConfigDefault(t *testing.T) {
        c := qt.New(t)
 
-       dcfg, err := decodeConfig(viper.New())
+       dcfg, err := decodeConfig(config.New())
        c.Assert(err, qt.IsNil)
        c.Assert(len(dcfg.Targets), qt.Equals, 0)
        c.Assert(len(dcfg.Matchers), qt.Equals, 0)
 
 languageCode = "en-us"
 title = "Hugo"
 
- ignoreErrors = ["error-remote-getjson"]
+ ignoreErrors = ["error-remote-getjson", "err-missing-instagram-accesstoken"]
 
 
 googleAnalytics = "UA-7131036-4"
 
 {{% note %}}
 Default environments are __development__ with `hugo server` and __production__ with `hugo`.
 {{%/ note %}}
+
+## Merge Configuration from Themes
+
+{{< new-in "0.84.0" >}} The configuration merge described below was improved in Hugo 0.84.0 and made fully configurable. The big change/improvement was that we now, by default, do deep merging of `params` maps from themes.
+
+The configuration value for `_merge` can be one of:
+
+none
+: No merge.
+
+shallow
+: Only add values for new keys.
+
+shallow
+: Add values for new keys, merge existing.
+
+Note that you don't need to be so verbose as in the default setup below; a `_merge` value higher up will be inherited if not set.
+
+{{< code-toggle config="mergeStrategy" skipHeader=true />}}
+
 ## All Configuration Settings
 
 The following is the full list of Hugo-defined variables with their default
 
         "preserveTOC": false
       }
     },
+    "mergeStrategy": {
+      "build": {
+        "_merge": "none"
+      },
+      "caches": {
+        "_merge": "none"
+      },
+      "frontmatter": {
+        "_merge": "none"
+      },
+      "imaging": {
+        "_merge": "none"
+      },
+      "languages": {
+        "_merge": "none",
+        "en": {
+          "_merge": "none",
+          "menus": {
+            "_merge": "shallow"
+          },
+          "params": {
+            "_merge": "deep"
+          }
+        }
+      },
+      "markup": {
+        "_merge": "none"
+      },
+      "mediatypes": {
+        "_merge": "shallow"
+      },
+      "menus": {
+        "_merge": "shallow"
+      },
+      "minify": {
+        "_merge": "none"
+      },
+      "module": {
+        "_merge": "none"
+      },
+      "outputformats": {
+        "_merge": "shallow"
+      },
+      "params": {
+        "_merge": "deep"
+      },
+      "permalinks": {
+        "_merge": "none"
+      },
+      "related": {
+        "_merge": "none"
+      },
+      "sitemap": {
+        "_merge": "none"
+      },
+      "taxonomies": {
+        "_merge": "none"
+      }
+    },
     "minify": {
       "minifyOutput": false,
       "disableHTML": false,
 
 {{ $file := .Get "file" }}
 {{ $code := "" }}
 {{ with .Get "config" }}
-{{ $file = $file | default "config" }}
-{{ $sections := (split . ".") }}
-{{ $configSection := index $.Site.Data.docs.config $sections }}
-{{ $code = dict $sections $configSection  }}
+  {{ $file = $file | default "config" }}
+  {{ $sections := (split . ".") }}
+  {{ $configSection := index $.Site.Data.docs.config $sections }}
+  {{ $code = dict $sections $configSection  }}
+  {{ if $.Get "skipHeader"}}
+    {{ $code = $configSection  }}
+  {{ end }}
 {{ else }}
-{{ $code = $.Inner }}
+  {{ $code = $.Inner }}
 {{ end  }}
 {{ $langs := (slice "yaml" "toml" "json") }}
 <div class="code relative" {{ with $file }}id="{{ . | urlize}}"{{ end }}>
-       <div class="code-nav flex flex-nowrap items-stretch">
-               {{- with $file -}}
-                       <div class="san-serif f6 dib lh-solid pl2 pv2 mr2">{{ . }}.</div>
-               {{- end -}}
-               {{ range $langs }}
-                       <button data-toggle-tab="{{ . }}" class="tab-button {{ cond (eq . "yaml") "active" ""}} ba san-serif f6 dib lh-solid ph2 pv2">{{ . }}</button> 
-               {{ end }}
-       </div>
-       <div class="tab-content">
-               {{ range $langs }}
-                       <div data-pane="{{ . }}" class="code-copy-content nt3 tab-pane {{ cond (eq . "yaml") "active" ""}}">
-                               {{ highlight ($code | transform.Remarshal . | safeHTML) . ""}}
-                       </div>
-                       {{ if ne ($.Get "copy") "false" }}
-                               <button class="needs-js copy copy-toggle bg-accent-color-dark f6 absolute top-0 right-0 lh-solid hover-bg-primary-color-dark bn white ph3 pv2" title="Copy this code to your clipboard." data-clipboard-action="copy" aria-label="copy button">
-                               </button>
-                               {{/* Functionality located within filesaver.js The copy here is located in the css with .copy class so it can be replaced with JS on success */}}
-                       {{end}}
-               {{ end }}
-       </div>
-
+  <div class="code-nav flex flex-nowrap items-stretch">
+    {{- with $file -}}
+      <div class="san-serif f6 dib lh-solid pl2 pv2 mr2">
+        {{ . }}.
+      </div>
+    {{- end -}}
+    {{ range $langs }}
+      <button data-toggle-tab="{{ . }}" class="tab-button {{ cond (eq . "yaml") "active" ""}} ba san-serif f6 dib lh-solid ph2 pv2">
+        {{ . }}
+      </button>
+       
+    {{ end }}
+  </div>
+  <div class="tab-content">
+    {{ range $langs }}
+      <div data-pane="{{ . }}" class="code-copy-content nt3 tab-pane {{ cond (eq . "yaml") "active" ""}}">
+        {{ highlight ($code | transform.Remarshal . | safeHTML) . ""}}
+      </div>
+      {{ if ne ($.Get "copy") "false" }}
+        <button class="needs-js copy copy-toggle bg-accent-color-dark f6 absolute top-0 right-0 lh-solid hover-bg-primary-color-dark bn white ph3 pv2" title="Copy this code to your clipboard." data-clipboard-action="copy" aria-label="copy button"></button>
+        {{/* Functionality located within filesaver.js The copy here is located in the css with .copy class so it can be replaced with JS on success */}}
+      {{end}}
+    {{ end }}
+  </div>
+  
 </div>
 
        github.com/spf13/fsync v0.9.0
        github.com/spf13/jwalterweatherman v1.1.0
        github.com/spf13/pflag v1.0.5
-       github.com/spf13/viper v1.7.1
+       github.com/spf13/viper v1.7.0
        github.com/tdewolff/minify/v2 v2.9.16
        github.com/yuin/goldmark v1.3.5
        github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
 
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
 github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 
        "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/spf13/afero"
 
        "github.com/gohugoio/hugo/common/loggers"
 
-       "github.com/spf13/viper"
-
        qt "github.com/frankban/quicktest"
 )
 
 }
 
 func TestNewContentSpec(t *testing.T) {
-       cfg := viper.New()
+       cfg := config.New()
        c := qt.New(t)
 
        cfg.Set("summaryLength", 32)
 
        "strings"
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 
        "github.com/gohugoio/hugo/common/loggers"
 
 
 func TestResolveMarkup(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs())
        c.Assert(err, qt.IsNil)
 
 
 
        "github.com/gohugoio/hugo/hugofs"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
 func TestMakePath(t *testing.T) {
 }
 
 func TestAbsPathify(t *testing.T) {
-       defer viper.Reset()
-
        type test struct {
                inPath, workingDir, expected string
        }
        }
 
        for i, d := range data {
-               viper.Reset()
                // todo see comment in AbsPathify
                ps := newTestDefaultPathSpec("workingDir", d.workingDir)
 
 
 
 import (
        "github.com/gohugoio/hugo/common/loggers"
+       "github.com/gohugoio/hugo/config"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/langs"
        "github.com/gohugoio/hugo/modules"
 )
 
-func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *PathSpec {
+func newTestPathSpec(fs *hugofs.Fs, v config.Provider) *PathSpec {
        l := langs.NewDefaultLanguage(v)
        ps, _ := NewPathSpec(fs, l, nil)
        return ps
 }
 
 func newTestDefaultPathSpec(configKeyValues ...interface{}) *PathSpec {
-       v := viper.New()
+       v := config.New()
        fs := hugofs.NewMem(v)
-       cfg := newTestCfgFor(fs)
+       cfg := newTestCfg()
 
        for i := 0; i < len(configKeyValues); i += 2 {
                cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
        return newTestPathSpec(fs, cfg)
 }
 
-func newTestCfgFor(fs *hugofs.Fs) *viper.Viper {
-       v := newTestCfg()
-       v.SetFs(fs.Source)
-
-       return v
-}
-
-func newTestCfg() *viper.Viper {
-       v := viper.New()
+func newTestCfg() config.Provider {
+       v := config.New()
        v.Set("contentDir", "content")
        v.Set("dataDir", "data")
        v.Set("i18nDir", "i18n")
 }
 
 func newTestContentSpec() *ContentSpec {
-       v := viper.New()
+       v := config.New()
        spec, err := NewContentSpec(v, loggers.NewErrorLogger(), afero.NewMemMapFs())
        if err != nil {
                panic(err)
 
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/htesting/hqt"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
 func TestNewDefault(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        f := NewDefault(v)
 
        c.Assert(f.Source, qt.Not(qt.IsNil))
 
 func TestNewMem(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        f := NewMem(v)
 
        c.Assert(f.Source, qt.Not(qt.IsNil))
 
 func TestWorkingDir(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
 
        v.Set("workingDir", "/a/b/")
 
 
        "sort"
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/htesting"
 
 func TestLanguageRootMapping(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
 
        fs := NewBaseFileDecorator(afero.NewMemMapFs())
 
        "github.com/gohugoio/hugo/config/services"
        "github.com/gohugoio/hugo/helpers"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
-// SiteConfig represents the config in .Site.Config.
-type SiteConfig struct {
-       // This contains all privacy related settings that can be used to
-       // make the YouTube template etc. GDPR compliant.
-       Privacy privacy.Config
+var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n       Run `hugo help new` for details.\n")
 
-       // Services contains config for services such as Google Analytics etc.
-       Services services.Config
-}
+// LoadConfig loads Hugo configuration into a new Viper and then adds
+// a set of defaults.
+func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (config.Provider, []string, error) {
 
-func loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) {
-       privacyConfig, err := privacy.DecodeConfig(cfg)
+       if d.Environment == "" {
+               d.Environment = hugo.EnvironmentProduction
+       }
+
+       if len(d.Environ) == 0 {
+               d.Environ = os.Environ()
+       }
+
+       var configFiles []string
+
+       l := configLoader{ConfigSourceDescriptor: d, cfg: config.New()}
+
+       if err := l.applyConfigDefaults(); err != nil {
+               return l.cfg, configFiles, err
+       }
+
+       for _, name := range d.configFilenames() {
+               var filename string
+               filename, err := l.loadConfig(name)
+               if err == nil {
+                       configFiles = append(configFiles, filename)
+               } else if err != ErrNoConfigFile {
+                       return nil, nil, err
+               }
+       }
+
+       if d.AbsConfigDir != "" {
+               dirnames, err := l.loadConfigFromConfigDir()
+               if err == nil {
+                       configFiles = append(configFiles, dirnames...)
+               } else if err != ErrNoConfigFile {
+                       return nil, nil, err
+               }
+       }
+
+       // TODO(bep) improve this. This is currently needed to get the merge correctly.
+       if l.cfg.IsSet("languages") {
+               langs := l.cfg.GetParams("languages")
+               for _, lang := range langs {
+                       langp := lang.(maps.Params)
+                       if _, ok := langp["menus"]; !ok {
+                               langp["menus"] = make(maps.Params)
+                       }
+                       if _, ok := langp["params"]; !ok {
+                               langp["params"] = make(maps.Params)
+                       }
+               }
+
+       }
+       l.cfg.SetDefaultMergeStrategy()
+
+       // We create languages based on the settings, so we need to make sure that
+       // all configuration is loaded/set before doing that.
+       for _, d := range doWithConfig {
+               if err := d(l.cfg); err != nil {
+                       return l.cfg, configFiles, err
+               }
+       }
+
+       // We made this a Glob pattern in Hugo 0.75, we don't need both.
+       if l.cfg.GetBool("ignoreVendor") {
+               helpers.Deprecated("--ignoreVendor", "--ignoreVendorPaths **", false)
+               l.cfg.Set("ignoreVendorPaths", "**")
+       }
+
+       // Some settings are used before we're done collecting all settings,
+       // so apply OS environment both before and after.
+       if err := l.applyOsEnvOverrides(d.Environ); err != nil {
+               return l.cfg, configFiles, err
+       }
+
+       modulesConfig, err := l.loadModulesConfig()
        if err != nil {
-               return
+               return l.cfg, configFiles, err
        }
 
-       servicesConfig, err := services.DecodeConfig(cfg)
+       // Need to run these after the modules are loaded, but before
+       // they are finalized.
+       collectHook := func(m *modules.ModulesConfig) error {
+               // We don't need the merge strategy configuration anymore,
+               // remove it so it doesn't accidentaly show up in other settings.
+               l.cfg.WalkParams(func(params ...config.KeyParams) bool {
+                       params[len(params)-1].Params.DeleteMergeStrategy()
+                       return false
+               })
+
+               if err := l.loadLanguageSettings(nil); err != nil {
+                       return err
+               }
+
+               mods := m.ActiveModules
+
+               // Apply default project mounts.
+               if err := modules.ApplyProjectConfigDefaults(l.cfg, mods[0]); err != nil {
+                       return err
+               }
+
+               return nil
+       }
+
+       _, modulesConfigFiles, err := l.collectModules(modulesConfig, l.cfg, collectHook)
        if err != nil {
-               return
+               return l.cfg, configFiles, err
        }
 
-       scfg.Privacy = privacyConfig
-       scfg.Services = servicesConfig
+       configFiles = append(configFiles, modulesConfigFiles...)
 
-       return
+       if err := l.applyOsEnvOverrides(d.Environ); err != nil {
+               return l.cfg, configFiles, err
+       }
+
+       if err = l.applyConfigAliases(); err != nil {
+               return l.cfg, configFiles, err
+       }
+
+       return l.cfg, configFiles, err
+}
+
+// LoadConfigDefault is a convenience method to load the default "config.toml" config.
+func LoadConfigDefault(fs afero.Fs) (config.Provider, error) {
+       v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"})
+       return v, err
 }
 
 // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
        Environ []string
 }
 
+func (d ConfigSourceDescriptor) configFileDir() string {
+       if d.Path != "" {
+               return d.Path
+       }
+       return d.WorkingDir
+}
+
 func (d ConfigSourceDescriptor) configFilenames() []string {
        if d.Filename == "" {
                return []string{"config"}
        return strings.Split(d.Filename, ",")
 }
 
-func (d ConfigSourceDescriptor) configFileDir() string {
-       if d.Path != "" {
-               return d.Path
-       }
-       return d.WorkingDir
+// SiteConfig represents the config in .Site.Config.
+type SiteConfig struct {
+       // This contains all privacy related settings that can be used to
+       // make the YouTube template etc. GDPR compliant.
+       Privacy privacy.Config
+
+       // Services contains config for services such as Google Analytics etc.
+       Services services.Config
 }
 
-// LoadConfigDefault is a convenience method to load the default "config.toml" config.
-func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) {
-       v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"})
-       return v, err
+type configLoader struct {
+       cfg config.Provider
+       ConfigSourceDescriptor
 }
 
-var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n       Run `hugo help new` for details.\n")
+// Handle some legacy values.
+func (l configLoader) applyConfigAliases() error {
+       aliases := []types.KeyValueStr{{Key: "taxonomies", Value: "indexes"}}
 
-// LoadConfig loads Hugo configuration into a new Viper and then adds
-// a set of defaults.
-func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) {
-       if d.Environment == "" {
-               d.Environment = hugo.EnvironmentProduction
+       for _, alias := range aliases {
+               if l.cfg.IsSet(alias.Key) {
+                       vv := l.cfg.Get(alias.Key)
+                       l.cfg.Set(alias.Value, vv)
+               }
        }
 
-       if len(d.Environ) == 0 {
-               d.Environ = os.Environ()
-       }
+       return nil
+}
 
-       var configFiles []string
+func (l configLoader) applyConfigDefaults() error {
+       defaultSettings := maps.Params{
+               "cleanDestinationDir":                  false,
+               "watch":                                false,
+               "resourceDir":                          "resources",
+               "publishDir":                           "public",
+               "themesDir":                            "themes",
+               "buildDrafts":                          false,
+               "buildFuture":                          false,
+               "buildExpired":                         false,
+               "environment":                          hugo.EnvironmentProduction,
+               "uglyURLs":                             false,
+               "verbose":                              false,
+               "ignoreCache":                          false,
+               "canonifyURLs":                         false,
+               "relativeURLs":                         false,
+               "removePathAccents":                    false,
+               "titleCaseStyle":                       "AP",
+               "taxonomies":                           map[string]string{"tag": "tags", "category": "categories"},
+               "permalinks":                           make(map[string]string),
+               "sitemap":                              config.Sitemap{Priority: -1, Filename: "sitemap.xml"},
+               "disableLiveReload":                    false,
+               "pluralizeListTitles":                  true,
+               "forceSyncStatic":                      false,
+               "footnoteAnchorPrefix":                 "",
+               "footnoteReturnLinkContents":           "",
+               "newContentEditor":                     "",
+               "paginate":                             10,
+               "paginatePath":                         "page",
+               "summaryLength":                        70,
+               "rssLimit":                             -1,
+               "sectionPagesMenu":                     "",
+               "disablePathToLower":                   false,
+               "hasCJKLanguage":                       false,
+               "enableEmoji":                          false,
+               "pygmentsCodeFencesGuessSyntax":        false,
+               "defaultContentLanguage":               "en",
+               "defaultContentLanguageInSubdir":       false,
+               "enableMissingTranslationPlaceholders": false,
+               "enableGitInfo":                        false,
+               "ignoreFiles":                          make([]string, 0),
+               "disableAliases":                       false,
+               "debug":                                false,
+               "disableFastRender":                    false,
+               "timeout":                              "30s",
+               "enableInlineShortcodes":               false,
+       }
+
+       l.cfg.Merge("", defaultSettings)
 
-       v := viper.New()
-       l := configLoader{ConfigSourceDescriptor: d}
+       return nil
+}
 
-       for _, name := range d.configFilenames() {
-               var filename string
-               filename, err := l.loadConfig(name, v)
-               if err == nil {
-                       configFiles = append(configFiles, filename)
-               } else if err != ErrNoConfigFile {
-                       return nil, nil, err
-               }
+func (l configLoader) applyOsEnvOverrides(environ []string) error {
+       if len(environ) == 0 {
+               return nil
        }
 
-       if d.AbsConfigDir != "" {
-               dirnames, err := l.loadConfigFromConfigDir(v)
-               if err == nil {
-                       configFiles = append(configFiles, dirnames...)
-               } else if err != ErrNoConfigFile {
-                       return nil, nil, err
-               }
-       }
+       const delim = "__env__delim"
 
-       if err := loadDefaultSettingsFor(v); err != nil {
-               return v, configFiles, err
-       }
+       // Extract all that start with the HUGO prefix.
+       // The delimiter is the following rune, usually "_".
+       const hugoEnvPrefix = "HUGO"
+       var hugoEnv []types.KeyValueStr
+       for _, v := range environ {
+               key, val := config.SplitEnvVar(v)
+               if strings.HasPrefix(key, hugoEnvPrefix) {
+                       delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
+                       if len(delimiterAndKey) < 2 {
+                               continue
+                       }
+                       // Allow delimiters to be case sensitive.
+                       // It turns out there isn't that many allowed special
+                       // chars in environment variables when used in Bash and similar,
+                       // so variables on the form HUGOxPARAMSxFOO=bar is one option.
+                       key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim)
+                       key = strings.ToLower(key)
+                       hugoEnv = append(hugoEnv, types.KeyValueStr{
+                               Key:   key,
+                               Value: val,
+                       })
 
-       // We create languages based on the settings, so we need to make sure that
-       // all configuration is loaded/set before doing that.
-       for _, d := range doWithConfig {
-               if err := d(v); err != nil {
-                       return v, configFiles, err
                }
        }
 
-       // This is invoked both after we load the main config and at the end
-       // to support OS env override of config options used in the module collector.
-       applyOsEnvOverrides := func() error {
-               if d.Environ == nil {
-                       return nil
-               }
-
-               const delim = "__env__delim"
-
-               // Extract all that start with the HUGO prefix.
-               // The delimiter is the following rune, usually "_".
-               const hugoEnvPrefix = "HUGO"
-               var hugoEnv []types.KeyValueStr
-               for _, v := range d.Environ {
-                       key, val := config.SplitEnvVar(v)
-                       if strings.HasPrefix(key, hugoEnvPrefix) {
-                               delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
-                               if len(delimiterAndKey) < 2 {
-                                       continue
-                               }
-                               // Allow delimiters to be case sensitive.
-                               // It turns out there isn't that many allowed special
-                               // chars in environment variables when used in Bash and similar,
-                               // so variables on the form HUGOxPARAMSxFOO=bar is one option.
-                               key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim)
-                               key = strings.ToLower(key)
-                               hugoEnv = append(hugoEnv, types.KeyValueStr{
-                                       Key:   key,
-                                       Value: val,
-                               })
-
-                       }
+       for _, env := range hugoEnv {
+               existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
+               if err != nil {
+                       return err
                }
 
-               for _, env := range hugoEnv {
-                       existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, v.Get)
+               if existing != nil {
+                       val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
                        if err != nil {
-                               return err
+                               continue
                        }
 
-                       if existing != nil {
-                               val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
-                               if err != nil {
-                                       continue
-                               }
-
-                               if owner != nil {
-                                       owner[nestedKey] = val
-                               } else {
-                                       v.Set(env.Key, val)
-                               }
-                       } else if nestedKey != "" {
-                               owner[nestedKey] = env.Value
+                       if owner != nil {
+                               owner[nestedKey] = val
                        } else {
-                               v.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
+                               l.cfg.Set(env.Key, val)
                        }
+               } else if nestedKey != "" {
+                       owner[nestedKey] = env.Value
+               } else {
+                       // The container does not exist yet.
+                       l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
                }
+       }
 
-               return nil
+       return nil
+}
 
+func (l configLoader) collectModules(modConfig modules.Config, v1 config.Provider, hookBeforeFinalize func(m *modules.ModulesConfig) error) (modules.Modules, []string, error) {
+       workingDir := l.WorkingDir
+       if workingDir == "" {
+               workingDir = v1.GetString("workingDir")
        }
 
-       if err := applyOsEnvOverrides(); err != nil {
-               return v, configFiles, err
-       }
+       themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
 
-       // We made this a Glob pattern in Hugo 0.75, we don't need both.
-       if v.GetBool("ignoreVendor") {
-               helpers.Deprecated("--ignoreVendor", "--ignoreVendorPaths **", false)
-               v.Set("ignoreVendorPaths", "**")
+       var ignoreVendor glob.Glob
+       if s := v1.GetString("ignoreVendorPaths"); s != "" {
+               ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
        }
 
-       modulesConfig, err := l.loadModulesConfig(v)
+       filecacheConfigs, err := filecache.DecodeConfig(l.Fs, v1)
        if err != nil {
-               return v, configFiles, err
+               return nil, nil, err
        }
 
-       // Need to run these after the modules are loaded, but before
-       // they are finalized.
-       collectHook := func(m *modules.ModulesConfig) error {
-               if err := loadLanguageSettings(v, nil); err != nil {
-                       return err
-               }
+       v1.Set("filecacheConfigs", filecacheConfigs)
 
-               mods := m.ActiveModules
+       var configFilenames []string
 
-               // Apply default project mounts.
-               if err := modules.ApplyProjectConfigDefaults(v, mods[0]); err != nil {
-                       return err
+       hook := func(m *modules.ModulesConfig) error {
+               for _, tc := range m.ActiveModules {
+                       if tc.ConfigFilename() != "" {
+                               if tc.Watch() {
+                                       configFilenames = append(configFilenames, tc.ConfigFilename())
+                               }
+
+                               // Merge from theme config into v1 based on configured
+                               // merge strategy.
+                               v1.Merge("", tc.Cfg().Get(""))
+
+                       }
+               }
+
+               if hookBeforeFinalize != nil {
+                       return hookBeforeFinalize(m)
                }
 
                return nil
        }
 
-       _, modulesConfigFiles, err := l.collectModules(modulesConfig, v, collectHook)
+       modulesClient := modules.NewClient(modules.ClientConfig{
+               Fs:                 l.Fs,
+               Logger:             l.Logger,
+               HookBeforeFinalize: hook,
+               WorkingDir:         workingDir,
+               ThemesDir:          themesDir,
+               CacheDir:           filecacheConfigs.CacheDirModules(),
+               ModuleConfig:       modConfig,
+               IgnoreVendor:       ignoreVendor,
+       })
 
-       if err == nil && len(modulesConfigFiles) > 0 {
-               configFiles = append(configFiles, modulesConfigFiles...)
-       }
+       v1.Set("modulesClient", modulesClient)
 
-       if err := applyOsEnvOverrides(); err != nil {
-               return v, configFiles, err
-       }
+       moduleConfig, err := modulesClient.Collect()
 
-       return v, configFiles, err
-}
+       // Avoid recreating these later.
+       v1.Set("allModules", moduleConfig.ActiveModules)
 
-func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
-       _, err := langs.LoadLanguageSettings(cfg, oldLangs)
-       return err
-}
+       if moduleConfig.GoModulesFilename != "" {
+               // We want to watch this for changes and trigger rebuild on version
+               // changes etc.
+               configFilenames = append(configFilenames, moduleConfig.GoModulesFilename)
+       }
 
-type configLoader struct {
-       ConfigSourceDescriptor
+       return moduleConfig.ActiveModules, configFilenames, err
 }
 
-func (l configLoader) loadConfig(configName string, v *viper.Viper) (string, error) {
+func (l configLoader) loadConfig(configName string) (string, error) {
        baseDir := l.configFileDir()
        var baseFilename string
        if filepath.IsAbs(configName) {
                return "", l.wrapFileError(err, filename)
        }
 
-       if err = v.MergeConfigMap(m); err != nil {
-               return "", l.wrapFileError(err, filename)
-       }
+       // Set overwrites keys of the same name, recursively.
+       l.cfg.Set("", m)
 
        return filename, nil
 }
 
-func (l configLoader) wrapFileError(err error, filename string) error {
-       err, _ = herrors.WithFileContextForFile(
-               err,
-               filename,
-               filename,
-               l.Fs,
-               herrors.SimpleLineMatcher)
-       return err
-}
-
-func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) {
+func (l configLoader) loadConfigFromConfigDir() ([]string, error) {
        sourceFs := l.Fs
        configDir := l.AbsConfigDir
 
                        // Migrate menu => menus etc.
                        config.RenameKeys(root)
 
-                       if err := v.MergeConfigMap(root); err != nil {
-                               return l.wrapFileError(err, path)
-                       }
+                       // Set will overwrite keys with the same name, recursively.
+                       l.cfg.Set("", root)
 
                        return nil
                })
        return dirnames, nil
 }
 
-func (l configLoader) loadModulesConfig(v1 *viper.Viper) (modules.Config, error) {
-       modConfig, err := modules.DecodeConfig(v1)
+func (l configLoader) loadLanguageSettings(oldLangs langs.Languages) error {
+       _, err := langs.LoadLanguageSettings(l.cfg, oldLangs)
+       return err
+}
+
+func (l configLoader) loadModulesConfig() (modules.Config, error) {
+       modConfig, err := modules.DecodeConfig(l.cfg)
        if err != nil {
                return modules.Config{}, err
        }
        return modConfig, nil
 }
 
-func (l configLoader) collectModules(modConfig modules.Config, v1 *viper.Viper, hookBeforeFinalize func(m *modules.ModulesConfig) error) (modules.Modules, []string, error) {
-       workingDir := l.WorkingDir
-       if workingDir == "" {
-               workingDir = v1.GetString("workingDir")
-       }
-
-       themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
-
-       var ignoreVendor glob.Glob
-       if s := v1.GetString("ignoreVendorPaths"); s != "" {
-               ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
-       }
-
-       filecacheConfigs, err := filecache.DecodeConfig(l.Fs, v1)
+func (configLoader) loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) {
+       privacyConfig, err := privacy.DecodeConfig(cfg)
        if err != nil {
-               return nil, nil, err
-       }
-
-       v1.Set("filecacheConfigs", filecacheConfigs)
-
-       var configFilenames []string
-
-       hook := func(m *modules.ModulesConfig) error {
-               for _, tc := range m.ActiveModules {
-                       if tc.ConfigFilename() != "" {
-                               if tc.Watch() {
-                                       configFilenames = append(configFilenames, tc.ConfigFilename())
-                               }
-                               if err := l.applyThemeConfig(v1, tc); err != nil {
-                                       return err
-                               }
-                       }
-               }
-
-               if hookBeforeFinalize != nil {
-                       return hookBeforeFinalize(m)
-               }
-
-               return nil
-       }
-
-       modulesClient := modules.NewClient(modules.ClientConfig{
-               Fs:                 l.Fs,
-               Logger:             l.Logger,
-               HookBeforeFinalize: hook,
-               WorkingDir:         workingDir,
-               ThemesDir:          themesDir,
-               CacheDir:           filecacheConfigs.CacheDirModules(),
-               ModuleConfig:       modConfig,
-               IgnoreVendor:       ignoreVendor,
-       })
-
-       v1.Set("modulesClient", modulesClient)
-
-       moduleConfig, err := modulesClient.Collect()
-
-       // Avoid recreating these later.
-       v1.Set("allModules", moduleConfig.ActiveModules)
-
-       if moduleConfig.GoModulesFilename != "" {
-               // We want to watch this for changes and trigger rebuild on version
-               // changes etc.
-               configFilenames = append(configFilenames, moduleConfig.GoModulesFilename)
-       }
-
-       return moduleConfig.ActiveModules, configFilenames, err
-}
-
-func (l configLoader) applyThemeConfig(v1 *viper.Viper, theme modules.Module) error {
-       const (
-               paramsKey    = "params"
-               languagesKey = "languages"
-               menuKey      = "menus"
-       )
-
-       v2 := theme.Cfg()
-
-       for _, key := range []string{paramsKey, "outputformats", "mediatypes"} {
-               l.mergeStringMapKeepLeft("", key, v1, v2)
-       }
-
-       // Only add params and new menu entries, we do not add language definitions.
-       if v1.IsSet(languagesKey) && v2.IsSet(languagesKey) {
-               v1Langs := v1.GetStringMap(languagesKey)
-               for k := range v1Langs {
-                       langParamsKey := languagesKey + "." + k + "." + paramsKey
-                       l.mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2)
-               }
-               v2Langs := v2.GetStringMap(languagesKey)
-               for k := range v2Langs {
-                       if k == "" {
-                               continue
-                       }
-
-                       langMenuKey := languagesKey + "." + k + "." + menuKey
-                       if v2.IsSet(langMenuKey) {
-                               // Only add if not in the main config.
-                               v2menus := v2.GetStringMap(langMenuKey)
-                               for k, v := range v2menus {
-                                       menuEntry := menuKey + "." + k
-                                       menuLangEntry := langMenuKey + "." + k
-                                       if !v1.IsSet(menuEntry) && !v1.IsSet(menuLangEntry) {
-                                               v1.Set(menuLangEntry, v)
-                                       }
-                               }
-                       }
-               }
-       }
-
-       // Add menu definitions from theme not found in project
-       if v2.IsSet(menuKey) {
-               v2menus := v2.GetStringMap(menuKey)
-               for k, v := range v2menus {
-                       menuEntry := menuKey + "." + k
-                       if !v1.IsSet(menuEntry) {
-                               v1.SetDefault(menuEntry, v)
-                       }
-               }
-       }
-
-       return nil
-}
-
-func (configLoader) mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) {
-       if !v2.IsSet(key) {
                return
        }
 
-       if !v1.IsSet(key) && !(rootKey != "" && rootKey != key && v1.IsSet(rootKey)) {
-               v1.Set(key, v2.Get(key))
+       servicesConfig, err := services.DecodeConfig(cfg)
+       if err != nil {
                return
        }
 
-       m1 := v1.GetStringMap(key)
-       m2 := v2.GetStringMap(key)
+       scfg.Privacy = privacyConfig
+       scfg.Services = servicesConfig
 
-       for k, v := range m2 {
-               if _, found := m1[k]; !found {
-                       if rootKey != "" && v1.IsSet(rootKey+"."+k) {
-                               continue
-                       }
-                       m1[k] = v
-               }
-       }
+       return
 }
 
-func loadDefaultSettingsFor(v *viper.Viper) error {
-       v.RegisterAlias("indexes", "taxonomies")
-
-       /*
-
-               TODO(bep) from 0.56 these are configured as module mounts.
-                       v.SetDefault("contentDir", "content")
-                       v.SetDefault("layoutDir", "layouts")
-                       v.SetDefault("assetDir", "assets")
-                       v.SetDefault("staticDir", "static")
-                       v.SetDefault("dataDir", "data")
-                       v.SetDefault("i18nDir", "i18n")
-                       v.SetDefault("archetypeDir", "archetypes")
-       */
-
-       v.SetDefault("cleanDestinationDir", false)
-       v.SetDefault("watch", false)
-       v.SetDefault("resourceDir", "resources")
-       v.SetDefault("publishDir", "public")
-       v.SetDefault("themesDir", "themes")
-       v.SetDefault("buildDrafts", false)
-       v.SetDefault("buildFuture", false)
-       v.SetDefault("buildExpired", false)
-       v.SetDefault("environment", hugo.EnvironmentProduction)
-       v.SetDefault("uglyURLs", false)
-       v.SetDefault("verbose", false)
-       v.SetDefault("ignoreCache", false)
-       v.SetDefault("canonifyURLs", false)
-       v.SetDefault("relativeURLs", false)
-       v.SetDefault("removePathAccents", false)
-       v.SetDefault("titleCaseStyle", "AP")
-       v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"})
-       v.SetDefault("permalinks", make(map[string]string))
-       v.SetDefault("sitemap", config.Sitemap{Priority: -1, Filename: "sitemap.xml"})
-       v.SetDefault("disableLiveReload", false)
-       v.SetDefault("pluralizeListTitles", true)
-       v.SetDefault("forceSyncStatic", false)
-       v.SetDefault("footnoteAnchorPrefix", "")
-       v.SetDefault("footnoteReturnLinkContents", "")
-       v.SetDefault("newContentEditor", "")
-       v.SetDefault("paginate", 10)
-       v.SetDefault("paginatePath", "page")
-       v.SetDefault("summaryLength", 70)
-       v.SetDefault("rssLimit", -1)
-       v.SetDefault("sectionPagesMenu", "")
-       v.SetDefault("disablePathToLower", false)
-       v.SetDefault("hasCJKLanguage", false)
-       v.SetDefault("enableEmoji", false)
-       v.SetDefault("pygmentsCodeFencesGuessSyntax", false)
-       v.SetDefault("defaultContentLanguage", "en")
-       v.SetDefault("defaultContentLanguageInSubdir", false)
-       v.SetDefault("enableMissingTranslationPlaceholders", false)
-       v.SetDefault("enableGitInfo", false)
-       v.SetDefault("ignoreFiles", make([]string, 0))
-       v.SetDefault("disableAliases", false)
-       v.SetDefault("debug", false)
-       v.SetDefault("disableFastRender", false)
-       v.SetDefault("timeout", "30s")
-       v.SetDefault("enableInlineShortcodes", false)
-
-       return nil
+func (l configLoader) wrapFileError(err error, filename string) error {
+       err, _ = herrors.WithFileContextForFile(
+               err,
+               filename,
+               filename,
+               l.Fs,
+               herrors.SimpleLineMatcher)
+       return err
 }
 
        "bytes"
        "fmt"
        "path/filepath"
+       "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/media"
+       "github.com/google/go-cmp/cmp"
+
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
 func TestLoadConfig(t *testing.T) {
 
        c := qt.New(t)
 
-       mainConfigBasic := `
-theme = "test-theme"
-baseURL = "https://example.com/"
-
-`
-       mainConfig := `
+       mainConfigTemplate := `
 theme = "test-theme"
 baseURL = "https://example.com/"
 
 date = ["date","publishDate"]
 
 [params]
+MERGE_PARAMS
 p1 = "p1 main"
-p2 = "p2 main"
-top = "top"
+[params.b]
+b1 = "b1 main"
+[params.b.c]
+bc1 = "bc1 main"
 
 [mediaTypes]
 [mediaTypes."text/m1"]
 [params]
 p1 = "p1 theme"
 p2 = "p2 theme"
-p3 = "p3 theme"
+[params.b]
+b1 = "b1 theme"
+b2 = "b2 theme"
+[params.b.c]
+bc1 = "bc1 theme"
+bc2 = "bc2 theme"
+[params.b.c.d]
+bcd1 = "bcd1 theme"
 
 [mediaTypes]
 [mediaTypes."text/m1"]
 
 `
 
-       b := newTestSitesBuilder(t)
-       b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
-       b.CreateSites().Build(BuildCfg{})
-
-       got := b.Cfg.(*viper.Viper).AllSettings()
-
-       b.AssertObject(`
-map[string]interface {}{
-  "p1": "p1 main",
-  "p2": "p2 main",
-  "p3": "p3 theme",
-  "top": "top",
-}`, got["params"])
-
-       b.AssertObject(`
-map[string]interface {}{
-  "date": []interface {}{
-    "date",
-    "publishDate",
-  },
-}`, got["frontmatter"])
-
-       b.AssertObject(`
-map[string]interface {}{
-  "text/m1": map[string]interface {}{
-    "suffixes": []interface {}{
-      "m1main",
-    },
-  },
-  "text/m2": map[string]interface {}{
-    "suffixes": []interface {}{
-      "m2theme",
-    },
-  },
-}`, got["mediatypes"])
-
-       b.AssertObject(`
-map[string]interface {}{
-  "o1": map[string]interface {}{
-    "basename": "o1main",
-    "mediatype": Type{
-      MainType: "text",
-      SubType: "m1",
-      Delimiter: ".",
-      FirstSuffix: SuffixInfo{
-        Suffix: "m1main",
-        FullSuffix: ".m1main",
-      },
-    },
-  },
-  "o2": map[string]interface {}{
-    "basename": "o2theme",
-    "mediatype": Type{
-      MainType: "text",
-      SubType: "m2",
-      Delimiter: ".",
-      FirstSuffix: SuffixInfo{
-        Suffix: "m2theme",
-        FullSuffix: ".m2theme",
-      },
-    },
-  },
-}`, got["outputformats"])
-
-       b.AssertObject(`map[string]interface {}{
-  "en": map[string]interface {}{
-    "languagename": "English",
-    "menus": map[string]interface {}{
-      "theme": []map[string]interface {}{
-        map[string]interface {}{
-          "name": "menu-lang-en-theme",
-        },
-      },
-    },
-    "params": map[string]interface {}{
-      "pl1": "p1-en-main",
-      "pl2": "p2-en-theme",
-    },
-  },
-  "nb": map[string]interface {}{
-    "languagename": "Norsk",
-    "menus": map[string]interface {}{
-      "theme": []map[string]interface {}{
-        map[string]interface {}{
-          "name": "menu-lang-nb-theme",
-        },
-      },
-    },
-    "params": map[string]interface {}{
-      "pl1": "p1-nb-main",
-      "pl2": "p2-nb-theme",
-    },
-  },
-}
-`, got["languages"])
-
-       b.AssertObject(`
-map[string]interface {}{
-  "main": []map[string]interface {}{
-    map[string]interface {}{
-      "name": "menu-main-main",
-    },
-  },
-  "thememenu": []map[string]interface {}{
-    map[string]interface {}{
-      "name": "menu-theme",
-    },
-  },
-  "top": []map[string]interface {}{
-    map[string]interface {}{
-      "name": "menu-top-main",
-    },
-  },
-}
-`, got["menus"])
+       buildForStrategy := func(t testing.TB, s string) *sitesBuilder {
+               mainConfig := strings.ReplaceAll(mainConfigTemplate, "MERGE_PARAMS", s)
+               b := newTestSitesBuilder(t)
+               b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
+               return b.CreateSites().Build(BuildCfg{})
+       }
 
-       c.Assert(got["baseurl"], qt.Equals, "https://example.com/")
+       c.Run("Merge default", func(c *qt.C) {
+               b := buildForStrategy(c, "")
+
+               got := b.Cfg.Get("").(maps.Params)
+
+               b.Assert(got["params"], qt.DeepEquals, maps.Params{
+                       "b": maps.Params{
+                               "b1": "b1 main",
+                               "c": maps.Params{
+                                       "bc1": "bc1 main",
+                                       "bc2": "bc2 theme",
+                                       "d":   maps.Params{"bcd1": string("bcd1 theme")},
+                               },
+                               "b2": "b2 theme",
+                       },
+                       "p2": "p2 theme",
+                       "p1": "p1 main",
+               })
+
+               b.Assert(got["mediatypes"], qt.DeepEquals, maps.Params{
+                       "text/m2": maps.Params{
+                               "suffixes": []interface{}{
+                                       "m2theme",
+                               },
+                       },
+                       "text/m1": maps.Params{
+                               "suffixes": []interface{}{
+                                       "m1main",
+                               },
+                       },
+               })
+
+               var eq = qt.CmpEquals(
+                       cmp.Comparer(func(m1, m2 media.Type) bool {
+                               if m1.SubType != m2.SubType {
+                                       return false
+                               }
+                               return m1.FirstSuffix == m2.FirstSuffix
+                       }),
+               )
+
+               mediaTypes := b.H.Sites[0].mediaTypesConfig
+               m1, _ := mediaTypes.GetByType("text/m1")
+               m2, _ := mediaTypes.GetByType("text/m2")
+
+               b.Assert(got["outputformats"], eq, maps.Params{
+                       "o1": maps.Params{
+                               "mediatype": m1,
+                               "basename":  "o1main",
+                       },
+                       "o2": maps.Params{
+                               "basename":  "o2theme",
+                               "mediatype": m2,
+                       },
+               })
+
+               b.Assert(got["languages"], qt.DeepEquals, maps.Params{
+                       "en": maps.Params{
+                               "languagename": "English",
+                               "params": maps.Params{
+                                       "pl2": "p2-en-theme",
+                                       "pl1": "p1-en-main",
+                               },
+                               "menus": maps.Params{
+                                       "main": []map[string]interface{}{
+                                               {
+                                                       "name": "menu-lang-en-main",
+                                               },
+                                       },
+                                       "theme": []map[string]interface{}{
+                                               {
+                                                       "name": "menu-lang-en-theme",
+                                               },
+                                       },
+                               },
+                       },
+                       "nb": maps.Params{
+                               "languagename": "Norsk",
+                               "params": maps.Params{
+                                       "top": "top-nb-theme",
+                                       "pl1": "p1-nb-main",
+                                       "pl2": "p2-nb-theme",
+                               },
+                               "menus": maps.Params{
+                                       "main": []map[string]interface{}{
+                                               {
+                                                       "name": "menu-lang-nb-main",
+                                               },
+                                       },
+                                       "theme": []map[string]interface{}{
+                                               {
+                                                       "name": "menu-lang-nb-theme",
+                                               },
+                                       },
+                                       "top": []map[string]interface{}{
+                                               {
+                                                       "name": "menu-lang-nb-top",
+                                               },
+                                       },
+                               },
+                       },
+               })
+
+               c.Assert(got["baseurl"], qt.Equals, "https://example.com/")
+       })
+
+       c.Run("Merge shallow", func(c *qt.C) {
+               b := buildForStrategy(c, fmt.Sprintf("_merge=%q", "shallow"))
+
+               got := b.Cfg.Get("").(maps.Params)
+
+               // Shallow merge, only add new keys to params.
+               b.Assert(got["params"], qt.DeepEquals, maps.Params{
+                       "p1": "p1 main",
+                       "b": maps.Params{
+                               "b1": "b1 main",
+                               "c": maps.Params{
+                                       "bc1": "bc1 main",
+                               },
+                       },
+                       "p2": "p2 theme",
+               })
+       })
 
-       if true {
-               return
-       }
-       // Test variants with only values from theme
-       b = newTestSitesBuilder(t)
-       b.WithConfigFile("toml", mainConfigBasic).WithThemeConfigFile("toml", themeConfig)
-       b.CreateSites().Build(BuildCfg{})
-
-       got = b.Cfg.(*viper.Viper).AllSettings()
-
-       b.AssertObject(`map[string]interface {}{
-  "p1": "p1 theme",
-  "p2": "p2 theme",
-  "p3": "p3 theme",
-  "test-theme": map[string]interface {}{
-    "p1": "p1 theme",
-    "p2": "p2 theme",
-    "p3": "p3 theme",
-  },
-}`, got["params"])
-
-       c.Assert(got["languages"], qt.IsNil)
-       b.AssertObject(`
-map[string]interface {}{
-  "text/m1": map[string]interface {}{
-    "suffix": "m1theme",
-  },
-  "text/m2": map[string]interface {}{
-    "suffix": "m2theme",
-  },
-}`, got["mediatypes"])
-
-       b.AssertObject(`
-map[string]interface {}{
-  "o1": map[string]interface {}{
-    "basename": "o1theme",
-    "mediatype": Type{
-      MainType: "text",
-      SubType: "m1",
-      Suffix: "m1theme",
-      Delimiter: ".",
-    },
-  },
-  "o2": map[string]interface {}{
-    "basename": "o2theme",
-    "mediatype": Type{
-      MainType: "text",
-      SubType: "m2",
-      Suffix: "m2theme",
-      Delimiter: ".",
-    },
-  },
-}`, got["outputformats"])
-       b.AssertObject(`
-map[string]interface {}{
-  "main": []interface {}{
-    map[string]interface {}{
-      "name": "menu-main-theme",
-    },
-  },
-  "thememenu": []interface {}{
-    map[string]interface {}{
-      "name": "menu-theme",
-    },
-  },
-}`, got["menu"])
 }
 
 func TestPrivacyConfig(t *testing.T) {
 floatSlice = [3.14, 5.19]
 stringSlice = ["a", "b"]
 
+[outputFormats]
+[outputFormats.ofbase]
+mediaType = "text/plain"
+
 [params]
+paramWithNoEnvOverride="nooverride"
 [params.api_config]
 api_key="default_key"
 another_key="default another_key"
 
        b.WithSourceFile("themes/mytheme/config.toml", `
 
+[outputFormats]
+[outputFormats.oftheme]
+mediaType = "text/plain"
+[outputFormats.ofbase]
+mediaType = "application/xml"
+
 [params]
 [params.mytheme_section]
 theme_param="themevalue"
+theme_param_nooverride="nooverride"
 [params.mytheme_section2]
 theme_param="themevalue2"
 
                "HUGOxPARAMSxMYTHEME_SECTION2xTHEME_PARAM", "themevalue2_changed",
                "HUGO_PARAMS_EMPTY", ``,
                "HUGO_PARAMS_HTML", `<a target="_blank" />`,
-               //
+               // Issue #8618
                "HUGO_SERVICES_GOOGLEANALYTICS_ID", `gaid`,
+               "HUGO_PARAMS_A_B_C", "abc",
        )
 
        b.Build(BuildCfg{})
 
        cfg := b.H.Cfg
-       scfg := b.H.Sites[0].siteConfigConfig.Services
+       s := b.H.Sites[0]
+       scfg := s.siteConfigConfig.Services
 
        c.Assert(cfg.Get("environment"), qt.Equals, "test")
        c.Assert(cfg.GetBool("enablegitinfo"), qt.Equals, false)
        c.Assert(cfg.Get("params.api_config.api_key"), qt.Equals, "new_key")
        c.Assert(cfg.Get("params.api_config.another_key"), qt.Equals, "default another_key")
        c.Assert(cfg.Get("params.mytheme_section.theme_param"), qt.Equals, "themevalue_changed")
+       c.Assert(cfg.Get("params.mytheme_section.theme_param_nooverride"), qt.Equals, "nooverride")
        c.Assert(cfg.Get("params.mytheme_section2.theme_param"), qt.Equals, "themevalue2_changed")
        c.Assert(cfg.Get("params.empty"), qt.Equals, ``)
        c.Assert(cfg.Get("params.html"), qt.Equals, `<a target="_blank" />`)
 
+       params := cfg.Get("params").(maps.Params)
+       c.Assert(params["paramwithnoenvoverride"], qt.Equals, "nooverride")
+       c.Assert(cfg.Get("params.paramwithnoenvoverride"), qt.Equals, "nooverride")
        c.Assert(scfg.GoogleAnalytics.ID, qt.Equals, "gaid")
+       c.Assert(cfg.Get("params.a.b"), qt.DeepEquals, maps.Params{
+               "c": "abc",
+       })
+
+       ofBase, _ := s.outputFormatsConfig.GetByName("ofbase")
+       ofTheme, _ := s.outputFormatsConfig.GetByName("oftheme")
+
+       c.Assert(ofBase.MediaType, qt.Equals, media.TextType)
+       c.Assert(ofTheme.MediaType, qt.Equals, media.TextType)
+
 }
 
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/hugolib/paths"
        "github.com/gohugoio/hugo/modules"
-       "github.com/spf13/viper"
+       
 )
 
 func initConfig(fs afero.Fs, cfg config.Provider) error {
 
 func TestNewBaseFs(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
 
        fs := hugofs.NewMem(v)
 
        }
 }
 
-func createConfig() *viper.Viper {
-       v := viper.New()
+func createConfig() config.Provider {
+       v := config.New()
        v.Set("contentDir", "mycontent")
        v.Set("i18nDir", "myi18n")
        v.Set("staticDir", "mystatic")
        return counter, filenames, nil
 }
 
-func setConfigAndWriteSomeFilesTo(fs afero.Fs, v *viper.Viper, key, val string, num int) {
+func setConfigAndWriteSomeFilesTo(fs afero.Fs, v config.Provider, key, val string, num int) {
        workingDir := v.GetString("workingDir")
        v.Set(key, val)
        fs.Mkdir(val, 0755)
 
        "testing"
        "time"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/modules/npm"
 
        "github.com/gohugoio/hugo/common/loggers"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/testmodBuilder/mods"
-       "github.com/spf13/viper"
 )
 
 func TestHugoModulesVariants(t *testing.T) {
                t.Skip("skip (relative) long running modules test when running locally")
        }
 
-       config := `
+       tomlConfig := `
 baseURL="https://example.org"
 workingDir = %q
 
 `
 
        createConfig := func(workingDir, moduleOpts string) string {
-               return fmt.Sprintf(config, workingDir, moduleOpts)
+               return fmt.Sprintf(tomlConfig, workingDir, moduleOpts)
        }
 
        newTestBuilder := func(t testing.TB, moduleOpts string) (*sitesBuilder, func()) {
                b.Assert(err, qt.IsNil)
                workingDir := filepath.Join(tempDir, "myhugosite")
                b.Assert(os.MkdirAll(workingDir, 0777), qt.IsNil)
-               b.Fs = hugofs.NewDefault(viper.New())
+               b.Fs = hugofs.NewDefault(config.New())
                b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts))
                b.WithTemplates(
                        "index.html", `
        for _, m := range testmods[:2] {
                c := qt.New(t)
 
-               v := viper.New()
+               v := config.New()
 
                workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-test")
                c.Assert(err, qt.IsNil)
 
        c := qt.New(t)
        // We need to use the OS fs for this.
-       cfg := viper.New()
+       cfg := config.New()
        fs := hugofs.NewFrom(hugofs.Os, cfg)
 
        workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mod-sym")
 
 `
 
-       config := fmt.Sprintf(configTemplate, workingDir)
+       tomlConfig := fmt.Sprintf(configTemplate, workingDir)
 
        b := newTestSitesBuilder(t).Running()
 
-       b.Fs = hugofs.NewDefault(viper.New())
+       b.Fs = hugofs.NewDefault(config.New())
 
-       b.WithWorkingDir(workingDir).WithConfigFile("toml", config)
+       b.WithWorkingDir(workingDir).WithConfigFile("toml", tomlConfig)
        b.WithTemplatesAdded("index.html", `
 {{ .Title }}
 {{ .Content }}
 %s
 
 `
-               config := fmt.Sprintf(configTemplate, workingDir, mounts)
-               config = strings.Replace(config, "WORKING_DIR", workingDir, -1)
+               tomlConfig := fmt.Sprintf(configTemplate, workingDir, mounts)
+               tomlConfig = strings.Replace(tomlConfig, "WORKING_DIR", workingDir, -1)
 
                b := newTestSitesBuilder(c).Running()
 
-               b.Fs = hugofs.NewDefault(viper.New())
+               b.Fs = hugofs.NewDefault(config.New())
 
                os.MkdirAll(filepath.Join(workingDir, "content", "blog"), 0777)
 
-               b.WithWorkingDir(workingDir).WithConfigFile("toml", config)
+               b.WithWorkingDir(workingDir).WithConfigFile("toml", tomlConfig)
 
                return test{
                        b:          b,
        workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-no-mod")
        c.Assert(err, qt.IsNil)
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("workingDir", workDir)
        fs := hugofs.NewFrom(hugofs.Os, cfg)
 
        absContentDir, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-content")
        c.Assert(err, qt.IsNil)
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("workingDir", workDir)
        fs := hugofs.NewFrom(hugofs.Os, cfg)
 
 
                s.h = h
        }
 
-       if err := applyDeps(cfg, sites...); err != nil {
+       var l configLoader
+       if err := l.applyDeps(cfg, sites...); err != nil {
                return nil, errors.Wrap(err, "add site dependencies")
        }
 
        return nil
 }
 
-func applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
+func (l configLoader) applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
        if cfg.TemplateProvider == nil {
                cfg.TemplateProvider = tplimpl.DefaultTemplateProvider
        }
 
                        d.Site = s.Info
 
-                       siteConfig, err := loadSiteConfig(s.language)
+                       siteConfig, err := l.loadSiteConfig(s.language)
                        if err != nil {
                                return errors.Wrap(err, "load site config")
                        }
 func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error {
        oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages)
 
-       if err := loadLanguageSettings(h.Cfg, oldLangs); err != nil {
+       l := configLoader{cfg: h.Cfg}
+       if err := l.loadLanguageSettings(oldLangs); err != nil {
                return err
        }
 
-       depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: cfg}
+       depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: l.cfg}
 
        sites, err := createSitesFromConfig(depsCfg)
        if err != nil {
                s.h = h
        }
 
-       if err := applyDeps(depsCfg, sites...); err != nil {
+       var cl configLoader
+       if err := cl.applyDeps(depsCfg, sites...); err != nil {
                return err
        }
 
 
        "path/filepath"
        "strings"
        "testing"
-       "time"
 
-       "github.com/fortytw2/leaktest"
        "github.com/gohugoio/hugo/htesting"
 
        qt "github.com/frankban/quicktest"
 // https://github.com/gohugoio/hugo/issues/5375
 func TestSiteBuildTimeout(t *testing.T) {
        if !htesting.IsCI() {
-               defer leaktest.CheckTimeout(t, 10*time.Second)()
+               //defer leaktest.CheckTimeout(t, 10*time.Second)()
        }
 
        b := newTestSitesBuilder(t)
 
        "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/htesting"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/hugofs"
-       "github.com/spf13/viper"
 )
 
 // We have many tests for the different resize operations etc. in the resource package,
        defer clean()
 
        newBuilder := func(timeout interface{}) *sitesBuilder {
-               v := viper.New()
+               v := config.New()
                v.Set("workingDir", workDir)
                v.Set("baseURL", "https://example.org")
                v.Set("timeout", timeout)
 
        "testing"
 
        "github.com/gohugoio/hugo/common/hexec"
+       "github.com/gohugoio/hugo/config"
 
        "github.com/gohugoio/hugo/htesting"
 
-       "github.com/spf13/viper"
-
        qt "github.com/frankban/quicktest"
 
        "github.com/gohugoio/hugo/hugofs"
        c.Assert(err, qt.IsNil)
        defer clean()
 
-       v := viper.New()
+       v := config.New()
        v.Set("workingDir", workDir)
        v.Set("disableKinds", []string{"taxonomy", "term", "page"})
        b := newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger())
        c.Assert(err, qt.IsNil)
        defer clean()
 
-       config := fmt.Sprintf(`
+       tomlConfig := fmt.Sprintf(`
 baseURL = "https://example.org"
 workingDir = %q
 
 `, workDir)
 
        b := newTestSitesBuilder(t)
-       b.Fs = hugofs.NewDefault(viper.New())
-       b.WithWorkingDir(workDir).WithConfigFile("toml", config).WithLogger(loggers.NewInfoLogger())
+       b.Fs = hugofs.NewDefault(config.New())
+       b.WithWorkingDir(workDir).WithConfigFile("toml", tomlConfig).WithLogger(loggers.NewInfoLogger())
        b.WithSourceFile("go.mod", `module github.com/gohugoio/tests/testHugoModules
         
 go 1.15
 
 import (
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 )
 
 func TestMinifyPublisher(t *testing.T) {
        t.Parallel()
 
-       v := viper.New()
+       v := config.New()
        v.Set("minify", true)
        v.Set("baseURL", "https://example.org/")
 
 
 
        if frontmatter != nil {
                // Needed for case insensitive fetching of params values
-               maps.ToLower(frontmatter)
+               maps.PrepareParams(frontmatter)
                if p.bucket != nil {
                        // Check for any cascade define on itself.
                        if cv, found := frontmatter["cascade"]; found {
 
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/resources/resource"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
        c := qt.New(t)
 
        // We need to use the OS fs for this.
-       cfg := viper.New()
+       cfg := config.New()
        fs := hugofs.NewFrom(hugofs.Os, cfg)
        fs.Destination = &afero.MemMapFs{}
 
 
 func TestPageWithEmoji(t *testing.T) {
        for _, enableEmoji := range []bool{true, false} {
-               v := viper.New()
+               v := config.New()
                v.Set("enableEmoji", enableEmoji)
 
                b := newTestSitesBuilder(t).WithViper(v)
 
        "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/hugofs/files"
 
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/htesting"
 
        "github.com/gohugoio/hugo/deps"
-       "github.com/spf13/viper"
 
        qt "github.com/frankban/quicktest"
 )
 
        c := qt.New(t)
        _, cfg := newTestBundleSourcesMultilingual(t)
-
        cfg.Set("disableLanguages", []string{"en"})
-
-       err := loadDefaultSettingsFor(cfg)
+       l := configLoader{cfg: cfg}
+       err := l.applyConfigDefaults()
        c.Assert(err, qt.IsNil)
-       err = loadLanguageSettings(cfg, nil)
+       err = l.loadLanguageSettings(nil)
        c.Assert(err, qt.Not(qt.IsNil))
        c.Assert(err.Error(), qt.Contains, "cannot disable default language")
 }
 
        c := qt.New(t)
        // We need to use the OS fs for this.
-       cfg := viper.New()
+       cfg := config.New()
        fs := hugofs.NewFrom(hugofs.Os, cfg)
 
        workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugosym")
        b.AssertFileContent("public/section-not-bundle/single/index.html", "Section Single", "|<p>Single content.</p>")
 }
 
-func newTestBundleSources(t testing.TB) (*hugofs.Fs, *viper.Viper) {
+func newTestBundleSources(t testing.TB) (*hugofs.Fs, config.Provider) {
        cfg, fs := newTestCfgBasic()
        c := qt.New(t)
 
        return fs, cfg
 }
 
-func newTestBundleSourcesMultilingual(t *testing.T) (*hugofs.Fs, *viper.Viper) {
+func newTestBundleSourcesMultilingual(t *testing.T) (*hugofs.Fs, config.Provider) {
        cfg, fs := newTestCfgBasic()
 
        workDir := "/work"
        workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-bundler-home")
        c.Assert(err, qt.IsNil)
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("workingDir", workDir)
        fs := hugofs.NewFrom(hugofs.Os, cfg)
 
 
 
                section = s
 
-               maps.ToLower(pf.FrontMatter)
+               maps.PrepareParams(pf.FrontMatter)
                cascade1, ok := pf.FrontMatter["cascade"]
                hasCascade := n.p.bucket.cascade != nil && len(n.p.bucket.cascade) > 0
                if !ok {
 
 import (
        "testing"
 
-       "github.com/gohugoio/hugo/langs"
-
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/hugofs"
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/langs"
 )
 
 func TestNewPaths(t *testing.T) {
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        fs := hugofs.NewMem(v)
 
        v.Set("languages", map[string]interface{}{
 
        "path/filepath"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/common/hexec"
 
        jww "github.com/spf13/jwalterweatherman"
 
        "github.com/gohugoio/hugo/htesting"
 
-       "github.com/spf13/viper"
-
        qt "github.com/frankban/quicktest"
 
        "github.com/gohugoio/hugo/hugofs"
        var logBuf bytes.Buffer
        logger := loggers.NewBasicLoggerForWriter(jww.LevelInfo, &logBuf)
 
-       v := viper.New()
+       v := config.New()
        v.Set("workingDir", workDir)
        v.Set("disableKinds", []string{"taxonomy", "term", "page"})
        b := newTestSitesBuilder(t).WithLogger(logger)
 
        "math/rand"
        "os"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
 
        "path/filepath"
 
        "github.com/gohugoio/hugo/htesting"
 
-       "github.com/spf13/viper"
-
        qt "github.com/frankban/quicktest"
 
        "github.com/gohugoio/hugo/hugofs"
                        c.Assert(err, qt.IsNil)
                        defer clean()
 
-                       v := viper.New()
+                       v := config.New()
                        v.Set("workingDir", workDir)
                        b := newTestSitesBuilder(c).WithLogger(loggers.NewErrorLogger())
                        // Need to use OS fs for this.
                        c.Assert(err, qt.IsNil)
                        defer clean()
 
-                       v := viper.New()
+                       v := config.New()
                        v.Set("workingDir", workDir)
                        b := newTestSitesBuilder(c).WithLogger(loggers.NewErrorLogger())
                        // Need to use OS fs for this.
                        theme := "mytheme"
                        themesDir := filepath.Join(workDir, "themes")
                        themeDirs := filepath.Join(themesDir, theme)
-                       v := viper.New()
+                       v := config.New()
                        v.Set("workingDir", workDir)
                        v.Set("theme", theme)
                        b := newTestSitesBuilder(c).WithLogger(loggers.NewErrorLogger())
        c.Assert(err, qt.IsNil)
        defer clean1()
 
-       v := viper.New()
+       v := config.New()
        v.Set("workingDir", workDir)
        v.Set("theme", "mytheme")
        b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger())
 
        var logBuf bytes.Buffer
 
-       newTestBuilder := func(v *viper.Viper) *sitesBuilder {
+       newTestBuilder := func(v config.Provider) *sitesBuilder {
                v.Set("workingDir", workDir)
                v.Set("disableKinds", []string{"taxonomy", "term", "page"})
                logger := loggers.NewBasicLoggerForWriter(jww.LevelInfo, &logBuf)
                return b
        }
 
-       b := newTestBuilder(viper.New())
+       b := newTestBuilder(config.New())
 
        cssDir := filepath.Join(workDir, "assets", "css", "components")
        b.Assert(os.MkdirAll(cssDir, 0777), qt.IsNil)
        build := func(s string, shouldFail bool) error {
                b.Assert(os.RemoveAll(filepath.Join(workDir, "public")), qt.IsNil)
 
-               v := viper.New()
+               v := config.New()
                v.Set("build", map[string]interface{}{
                        "useResourceCacheWhen": s,
                })
 
 import (
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 )
 
 const robotTxtTemplate = `User-agent: Googlebot
 func TestRobotsTXTOutput(t *testing.T) {
        t.Parallel()
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("baseURL", "http://auth/bub/")
        cfg.Set("enableRobotsTXT", true)
 
 
        "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/markup/asciidocext"
        "github.com/gohugoio/hugo/markup/rst"
 
-       "github.com/spf13/viper"
-
        "github.com/gohugoio/hugo/parser/pageparser"
        "github.com/gohugoio/hugo/resources/page"
 
 func TestShortcodeEmoji(t *testing.T) {
        t.Parallel()
 
-       v := viper.New()
+       v := config.New()
        v.Set("enableEmoji", true)
 
        builder := newTestSitesBuilder(t).WithViper(v)
                t.Run(fmt.Sprintf("plainIDAnchors=%t", plainIDAnchors), func(t *testing.T) {
                        t.Parallel()
 
-                       v := viper.New()
+                       v := config.New()
                        v.Set("baseURL", "https://example.org")
                        v.Set("blackfriday", map[string]interface{}{
                                "plainIDAnchors": plainIDAnchors,
 
 
        "github.com/spf13/afero"
        "github.com/spf13/cast"
-       "github.com/spf13/viper"
 )
 
 // Site contains all the information relevant for constructing a static
        var relatedContentConfig related.Config
 
        if cfg.Language.IsSet("related") {
-               relatedContentConfig, err = related.DecodeConfig(cfg.Language.Get("related"))
+               relatedContentConfig, err = related.DecodeConfig(cfg.Language.GetParams("related"))
                if err != nil {
-                       return nil, err
+                       return nil, errors.Wrap(err, "failed to decode related config")
                }
        } else {
                relatedContentConfig = related.DefaultConfig
                return nil, err
        }
 
-       if err = applyDeps(cfg, s); err != nil {
+       var l configLoader
+       if err = l.applyDeps(cfg, s); err != nil {
                return nil, err
        }
 
 // Note: This is mainly used in single site tests.
 // TODO(bep) test refactor -- remove
 func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) {
-       v := viper.New()
-       if err := loadDefaultSettingsFor(v); err != nil {
+       l := configLoader{cfg: config.New()}
+       if err := l.applyConfigDefaults(); err != nil {
                return nil, err
        }
-       return newSiteForLang(langs.NewDefaultLanguage(v), withTemplate...)
+       return newSiteForLang(langs.NewDefaultLanguage(l.cfg), withTemplate...)
 }
 
 // NewEnglishSite creates a new site in English language.
 // Note: This is mainly used in single site tests.
 // TODO(bep) test refactor -- remove
 func NewEnglishSite(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) {
-       v := viper.New()
-       if err := loadDefaultSettingsFor(v); err != nil {
+       l := configLoader{cfg: config.New()}
+       if err := l.applyConfigDefaults(); err != nil {
                return nil, err
        }
-       return newSiteForLang(langs.NewLanguage("en", v), withTemplate...)
+       return newSiteForLang(langs.NewLanguage("en", l.cfg), withTemplate...)
 }
 
 // newSiteForLang creates a new site in the given language.
                                return vvv
                        }
                default:
-                       m := cast.ToStringMapBool(v)
+                       m := maps.ToStringMapBool(v)
                        uglyURLs = func(p page.Page) bool {
                                return m[p.Section()]
                        }
 
        "testing"
 
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/resources/page"
 
        "github.com/spf13/afero"
 
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/output"
-       "github.com/spf13/viper"
 )
 
 func TestSiteWithPageOutputs(t *testing.T) {
                        page.KindSection: []string{"JSON"},
                }
 
-               cfg := viper.New()
+               cfg := config.New()
                cfg.Set("outputs", outputsConfig)
 
                outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
        // Issue #4528
        t.Run("Mixed case", func(t *testing.T) {
                c := qt.New(t)
-               cfg := viper.New()
+               cfg := config.New()
 
                outputsConfig := map[string]interface{}{
                        // Note that we in Hugo 0.53.0 renamed this Kind to "taxonomy",
                page.KindHome: []string{"FOO", "JSON"},
        }
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("outputs", outputsConfig)
 
        _, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
                page.KindHome: []string{},
        }
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("outputs", outputsConfig)
 
        outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
                page.KindHome: []string{},
        }
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("outputs", outputsConfig)
 
        var (
 
        "testing"
 
        "github.com/gobuffalo/flect"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/publisher"
 
-       "github.com/spf13/viper"
-
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/resources/page"
        c := qt.New(t)
        for _, paramSet := range []bool{false, true} {
                c.Run(fmt.Sprintf("param-%t", paramSet), func(c *qt.C) {
-                       v := viper.New()
+                       v := config.New()
                        if paramSet {
                                v.Set("params", map[string]interface{}{
                                        "mainSections": []string{"a1", "a2"},
 
        "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/identity"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/tpl"
-
-       "github.com/spf13/viper"
 )
 
 func TestTemplateLookupOrder(t *testing.T) {
        var (
                fs  *hugofs.Fs
-               cfg *viper.Viper
+               cfg config.Provider
                th  testHelper
        )
 
 
 
        "github.com/fsnotify/fsnotify"
        "github.com/gohugoio/hugo/common/herrors"
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/resources/page"
 
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/tpl"
-       "github.com/spf13/viper"
 
        "github.com/gohugoio/hugo/resources/resource"
 
        // Default toml
        configFormat  string
        configFileSet bool
-       viperSet      bool
+       configSet      bool
 
        // Default is empty.
        // TODO(bep) revisit this and consider always setting it to something.
 }
 
 func newTestSitesBuilder(t testing.TB) *sitesBuilder {
-       v := viper.New()
+       v := config.New()
        fs := hugofs.NewMem(v)
 
        litterOptions := litter.Options{
 
        b.WithWorkingDir(workingDir)
 
-       return b.WithViper(d.Cfg.(*viper.Viper))
+       return b.WithViper(d.Cfg.(config.Provider))
 }
 
 func (s *sitesBuilder) Running() *sitesBuilder {
        return s.WithConfigFile(format, b.String())
 }
 
-func (s *sitesBuilder) WithViper(v *viper.Viper) *sitesBuilder {
+func (s *sitesBuilder) WithViper(v config.Provider) *sitesBuilder {
        s.T.Helper()
        if s.configFileSet {
                s.T.Fatal("WithViper: use Viper or config.toml, not both")
        }
        defer func() {
-               s.viperSet = true
+               s.configSet = true
        }()
 
        // Write to a config file to make sure the tests follow the same code path.
        var buff bytes.Buffer
-       m := v.AllSettings()
+       m := v.Get("").(maps.Params)
        s.Assert(parser.InterfaceToConfig(m, metadecoders.TOML, &buff), qt.IsNil)
        return s.WithConfigFile("toml", buff.String())
 }
 
 func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder {
        s.T.Helper()
-       if s.viperSet {
-               s.T.Fatal("WithConfigFile: use Viper or config.toml, not both")
+       if s.configSet {
+               s.T.Fatal("WithConfigFile: use config.Config or config.toml, not both")
        }
        s.configFileSet = true
        filename := s.absFilename("config." + format)
        return value
 }
 
-func loadTestConfig(fs afero.Fs, withConfig ...func(cfg config.Provider) error) (*viper.Viper, error) {
+func loadTestConfig(fs afero.Fs, withConfig ...func(cfg config.Provider) error) (config.Provider, error) {
        v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs}, withConfig...)
        return v, err
 }
 
-func newTestCfgBasic() (*viper.Viper, *hugofs.Fs) {
+func newTestCfgBasic() (config.Provider, *hugofs.Fs) {
        mm := afero.NewMemMapFs()
-       v := viper.New()
+       v := config.New()
        v.Set("defaultContentLanguageInSubdir", true)
 
        fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v)
        return v, fs
 }
 
-func newTestCfg(withConfig ...func(cfg config.Provider) error) (*viper.Viper, *hugofs.Fs) {
+func newTestCfg(withConfig ...func(cfg config.Provider) error) (config.Provider, *hugofs.Fs) {
        mm := afero.NewMemMapFs()
 
        v, err := loadTestConfig(mm, func(cfg config.Provider) error {
 
 
        var languages map[string]interface{}
 
-       languagesFromConfig := cfg.GetStringMap("languages")
+       languagesFromConfig := cfg.GetParams("languages")
        disableLanguages := cfg.GetStringSlice("disableLanguages")
 
        if len(disableLanguages) == 0 {
                languages = languagesFromConfig
        } else {
-               languages = make(map[string]interface{})
+               languages = make(maps.Params)
                for k, v := range languagesFromConfig {
                        for _, disabled := range disableLanguages {
                                if disabled == defaultLang {
                                }
 
                                if strings.EqualFold(k, disabled) {
-                                       v.(map[string]interface{})["disabled"] = true
+                                       v.(maps.Params)["disabled"] = true
                                        break
                                }
                        }
                        case "params":
                                m := maps.ToStringMap(v)
                                // Needed for case insensitive fetching of params values
-                               maps.ToLower(m)
+                               maps.PrepareParams(m)
                                for k, vv := range m {
                                        language.SetParam(k, vv)
                                }
 
        "github.com/gohugoio/hugo/langs"
        "github.com/gohugoio/hugo/resources/page"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 
        "github.com/gohugoio/hugo/deps"
 
        }
 }
 
-func getConfig() *viper.Viper {
-       v := viper.New()
-       v.SetDefault("defaultContentLanguage", "en")
+func getConfig() config.Provider {
+       v := config.New()
+       v.Set("defaultContentLanguage", "en")
        v.Set("contentDir", "content")
        v.Set("dataDir", "data")
        v.Set("i18nDir", "i18n")
 
 
        "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/config"
-       "github.com/spf13/cast"
 )
 
 // These are the settings that should only be looked up in the global Viper
        // absolute directory reference. It is what we get.
        ContentDir string
 
+       // Global config.
        Cfg config.Provider
 
+       // Language specific config.
+       LocalCfg config.Provider
+
+       // Composite config.
+       config.Provider
+
        // These are params declared in the [params] section of the language merged with the
        // site's params, the most specific (language) wins on duplicate keys.
        params    map[string]interface{}
        paramsMu  sync.Mutex
        paramsSet bool
-
-       // These are config values, i.e. the settings declared outside of the [params] section of the language.
-       // This is the map Hugo looks in when looking for configuration values (baseURL etc.).
-       // Values in this map can also be fetched from the params map above.
-       settings map[string]interface{}
 }
 
 func (l *Language) String() string {
        for k, v := range cfg.GetStringMap("params") {
                params[k] = v
        }
-       maps.ToLower(params)
+       maps.PrepareParams(params)
+
+       localCfg := config.New()
+       compositeConfig := config.NewCompositeConfig(cfg, localCfg)
 
-       l := &Language{Lang: lang, ContentDir: cfg.GetString("contentDir"), Cfg: cfg, params: params, settings: make(map[string]interface{})}
+       l := &Language{Lang: lang, ContentDir: cfg.GetString("contentDir"), Cfg: cfg, LocalCfg: localCfg, Provider: compositeConfig, params: params}
        return l
 }
 
        l.paramsMu.Lock()
        defer l.paramsMu.Unlock()
        if !l.paramsSet {
-               maps.ToLower(l.params)
+               maps.PrepareParams(l.params)
                l.paramsSet = true
        }
        return l.params
        l.params[k] = v
 }
 
-// GetBool returns the value associated with the key as a boolean.
-func (l *Language) GetBool(key string) bool { return cast.ToBool(l.Get(key)) }
-
-// GetString returns the value associated with the key as a string.
-func (l *Language) GetString(key string) string { return cast.ToString(l.Get(key)) }
-
-// GetInt returns the value associated with the key as an int.
-func (l *Language) GetInt(key string) int { return cast.ToInt(l.Get(key)) }
-
-// GetStringMap returns the value associated with the key as a map of interfaces.
-func (l *Language) GetStringMap(key string) map[string]interface{} {
-       return maps.ToStringMap(l.Get(key))
-}
-
-// GetStringMapString returns the value associated with the key as a map of strings.
-func (l *Language) GetStringMapString(key string) map[string]string {
-       return cast.ToStringMapString(l.Get(key))
-}
-
-// GetStringSlice returns the value associated with the key as a slice of strings.
-func (l *Language) GetStringSlice(key string) []string {
-       return cast.ToStringSlice(l.Get(key))
-}
-
-// Get returns a value associated with the key relying on specified language.
-// Get is case-insensitive for a key.
-//
-// Get returns an interface. For a specific value use one of the Get____ methods.
-func (l *Language) Get(key string) interface{} {
-       local := l.GetLocal(key)
-       if local != nil {
-               return local
-       }
-       return l.Cfg.Get(key)
-}
-
 // GetLocal gets a configuration value set on language level. It will
 // not fall back to any global value.
 // It will return nil if a value with the given key cannot be found.
        }
        key = strings.ToLower(key)
        if !globalOnlySettings[key] {
-               if v, ok := l.settings[key]; ok {
-                       return v
-               }
+               return l.LocalCfg.Get(key)
        }
        return nil
 }
 
-// Set sets the value for the key in the language's params.
-func (l *Language) Set(key string, value interface{}) {
-       if l == nil {
-               panic("language not set")
+func (l *Language) Set(k string, v interface{}) {
+       k = strings.ToLower(k)
+       if globalOnlySettings[k] {
+               return
        }
-       key = strings.ToLower(key)
-       l.settings[key] = value
+       l.Provider.Set(k, v)
+}
+
+// Merge is currently not supported for Language.
+func (l *Language) Merge(key string, value interface{}) {
+       panic("Not supported")
 }
 
 // IsSet checks whether the key is set in the language or the related config store.
 func (l *Language) IsSet(key string) bool {
-       key = strings.ToLower(key)
-
        key = strings.ToLower(key)
        if !globalOnlySettings[key] {
-               if _, ok := l.settings[key]; ok {
-                       return true
-               }
+               return l.Provider.IsSet(key)
        }
        return l.Cfg.IsSet(key)
 }
 
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        qt "github.com/frankban/quicktest"
-       "github.com/spf13/viper"
 )
 
 func TestGetGlobalOnlySetting(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        v.Set("defaultContentLanguageInSubdir", true)
        v.Set("contentDir", "content")
        v.Set("paginatePath", "page")
 func TestLanguageParams(t *testing.T) {
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        v.Set("p1", "p1cfg")
        v.Set("contentDir", "content")
 
 
        "testing"
 
        "github.com/gohugoio/hugo/common/loggers"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/markup/converter"
        "github.com/gohugoio/hugo/markup/markup_config"
        "github.com/gohugoio/hugo/markup/tableofcontents"
-       "github.com/spf13/viper"
 
        qt "github.com/frankban/quicktest"
 )
 
 func TestAsciidoctorDefaultArgs(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        mconf := markup_config.Default
 
        p, err := Provider.New(
 
 func TestAsciidoctorNonDefaultArgs(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        mconf := markup_config.Default
        mconf.AsciidocExt.Backend = "manpage"
        mconf.AsciidocExt.NoHeaderOrFooter = false
 
 func TestAsciidoctorDisallowedArgs(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        mconf := markup_config.Default
        mconf.AsciidocExt.Backend = "disallowed-backend"
        mconf.AsciidocExt.Extensions = []string{"./disallowed-extension"}
 
 func TestAsciidoctorArbitraryExtension(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        mconf := markup_config.Default
        mconf.AsciidocExt.Extensions = []string{"arbitrary-extension"}
        p, err := Provider.New(
 
 func TestAsciidoctorDisallowedExtension(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        for _, disallowedExtension := range []string{
                `foo-bar//`,
                `foo-bar\\ `,
 
 func TestAsciidoctorWorkingFolderCurrent(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        mconf := markup_config.Default
        mconf.AsciidocExt.WorkingFolderCurrent = true
        mconf.AsciidocExt.Trace = false
 
 func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        mconf := markup_config.Default
        mconf.AsciidocExt.NoHeaderOrFooter = true
        mconf.AsciidocExt.Extensions = []string{"asciidoctor-html5s", "asciidoctor-diagram"}
 
 func TestAsciidoctorAttributes(t *testing.T) {
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        mconf := markup_config.Default
        mconf.AsciidocExt.Attributes = map[string]string{"my-base-url": "https://gohugo.io/", "my-attribute-name": "my value"}
        mconf.AsciidocExt.Trace = false
 
 import (
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 
        "github.com/gohugoio/hugo/markup/converter"
 
 func TestConvert(t *testing.T) {
        c := qt.New(t)
        p, err := Provider.New(converter.ProviderConfig{
-               Cfg: viper.New(),
+               Cfg: config.New(),
        })
        c.Assert(err, qt.IsNil)
        conv, err := p.New(converter.DocumentContext{})
 func TestGetHTMLRendererAnchors(t *testing.T) {
        c := qt.New(t)
        p, err := Provider.New(converter.ProviderConfig{
-               Cfg: viper.New(),
+               Cfg: config.New(),
        })
        c.Assert(err, qt.IsNil)
        conv, err := p.New(converter.DocumentContext{
 
 import (
        "testing"
 
-       "github.com/spf13/viper"
-
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/config"
 )
 
 func TestConfig(t *testing.T) {
        c := qt.New(t)
 
        c.Run("applyLegacyConfig", func(c *qt.C) {
-               v := viper.New()
+               v := config.New()
                v.Set("pygmentsStyle", "hugo")
                v.Set("pygmentsUseClasses", false)
                v.Set("pygmentsCodeFences", false)
 
        "github.com/gohugoio/hugo/markup/tableofcontents"
        "github.com/gohugoio/hugo/parser"
        "github.com/mitchellh/mapstructure"
-       "github.com/spf13/cast"
 )
 
 type Config struct {
        if err != nil {
                return
        }
-       vm := cast.ToStringMap(v)
+       vm := maps.ToStringMap(v)
        // Changed from a bool in 0.81.0
        if vv, found := vm["attribute"]; found {
                if vvb, ok := vv.(bool); ok {
 
 import (
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 
        qt "github.com/frankban/quicktest"
 )
 
        c.Run("Decode", func(c *qt.C) {
                c.Parallel()
-               v := viper.New()
+               v := config.New()
 
                v.Set("markup", map[string]interface{}{
                        "goldmark": map[string]interface{}{
 
        c.Run("legacy", func(c *qt.C) {
                c.Parallel()
-               v := viper.New()
+               v := config.New()
 
                v.Set("blackfriday", map[string]interface{}{
                        "angledQuotes": true,
 
 import (
        "testing"
 
-       "github.com/spf13/viper"
-
-       "github.com/gohugoio/hugo/markup/converter"
-
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/config"
+       "github.com/gohugoio/hugo/markup/converter"
 )
 
 func TestConverterRegistry(t *testing.T) {
        c := qt.New(t)
 
-       r, err := NewConverterProvider(converter.ProviderConfig{Cfg: viper.New()})
+       r, err := NewConverterProvider(converter.ProviderConfig{Cfg: config.New()})
 
        c.Assert(err, qt.IsNil)
        c.Assert("goldmark", qt.Equals, r.GetMarkupConfig().DefaultMarkdownHandler)
 
 import (
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 
        "github.com/gohugoio/hugo/common/loggers"
 
 
 func TestConvert(t *testing.T) {
        c := qt.New(t)
-       p, err := Provider.New(converter.ProviderConfig{Cfg: viper.New(), Logger: loggers.NewErrorLogger()})
+       p, err := Provider.New(converter.ProviderConfig{Cfg: config.New(), Logger: loggers.NewErrorLogger()})
        c.Assert(err, qt.IsNil)
        conv, err := p.New(converter.DocumentContext{})
        c.Assert(err, qt.IsNil)
 
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/common/loggers"
-       "github.com/spf13/viper"
 
        "github.com/gohugoio/hugo/markup/converter"
 
        c := qt.New(t)
        p, err := Provider.New(converter.ProviderConfig{
                Logger: loggers.NewErrorLogger(),
-               Cfg:    viper.New(),
+               Cfg:    config.New(),
        })
        c.Assert(err, qt.IsNil)
        conv, err := p.New(converter.DocumentContext{})
 
                                return m, err
                        }
 
-                       vm := v.(map[string]interface{})
-                       maps.ToLower(vm)
+                       vm := maps.ToStringMap(v)
+                       maps.PrepareParams(vm)
                        _, delimiterSet := vm["delimiter"]
                        _, suffixSet := vm["suffix"]
 
 
 
        // Handle upstream renames.
        if td, found := m["tdewolff"]; found {
-               tdm := cast.ToStringMap(td)
+               tdm := maps.ToStringMap(td)
                for _, key := range []string{"css", "svg"} {
                        if v, found := tdm[key]; found {
-                               vm := cast.ToStringMap(v)
+                               vm := maps.ToStringMap(v)
                                if vv, found := vm["decimal"]; found {
                                        vvi := cast.ToInt(vv)
                                        if vvi > 0 {
 
 import (
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 
        qt "github.com/frankban/quicktest"
 )
 
 func TestConfig(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
 
        v.Set("minify", map[string]interface{}{
                "disablexml": true,
 
 func TestConfigLegacy(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
 
        // This was a bool < Hugo v0.58.
        v.Set("minify", true)
 
        "strings"
        "testing"
 
-       "github.com/gohugoio/hugo/media"
-
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/config"
+       "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/output"
-       "github.com/spf13/viper"
 )
 
 func TestNew(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
 
        var rawJS string
 
 func TestConfigureMinify(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        v.Set("minify", map[string]interface{}{
                "disablexml": true,
                "tdewolff": map[string]interface{}{
 
 func TestJSONRoundTrip(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
 
        for _, test := range []string{`{
 
 func TestBugs(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
 
        for _, test := range []struct {
 // Renamed to Precision in v2.7.0. Check that we support both.
 func TestDecodeConfigDecimalIsNowPrecision(t *testing.T) {
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        v.Set("minify", map[string]interface{}{
                "disablexml": true,
                "tdewolff": map[string]interface{}{
 
                if err != nil {
                        c.logger.Warnf("Failed to read module config for %q in %q: %s", tc.Path(), themeTOML, err)
                } else {
-                       maps.ToLower(themeCfg)
+                       maps.PrepareParams(themeCfg)
                }
        }
 
 
        "github.com/gohugoio/hugo/hugofs"
        "github.com/spf13/afero"
 
-       "github.com/spf13/cast"
+       "github.com/gohugoio/hugo/common/maps"
 
        "github.com/gohugoio/hugo/helpers"
 )
        var commentsm map[string]interface{}
        comments, found := b.originalPackageJSON["comments"]
        if found {
-               commentsm = cast.ToStringMap(comments)
+               commentsm = maps.ToStringMap(comments)
        } else {
                commentsm = make(map[string]interface{})
        }
        // These packages will be added by order of import (project, module1, module2...),
        // so that should at least give the project control over the situation.
        if devDeps, found := m[devDependenciesKey]; found {
-               mm := cast.ToStringMapString(devDeps)
+               mm := maps.ToStringMapString(devDeps)
                for k, v := range mm {
                        if _, added := b.devDependencies[k]; !added {
                                b.devDependencies[k] = v
        }
 
        if deps, found := m[dependenciesKey]; found {
-               mm := cast.ToStringMapString(deps)
+               mm := maps.ToStringMapString(deps)
                for k, v := range mm {
                        if _, added := b.dependencies[k]; !added {
                                b.dependencies[k] = v
 
                return err
        }
 
-       return decoder.Decode(input)
+       if err = decoder.Decode(input); err != nil {
+               return errors.Wrap(err, "failed to decode output format configuration")
+       }
+
+       return nil
 
 }
 
 
        "testing"
        "time"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/minifiers"
        "github.com/gohugoio/hugo/output"
 
        qt "github.com/frankban/quicktest"
-       "github.com/spf13/viper"
 )
 
 func TestClassCollector(t *testing.T) {
                                        if skipMinifyTest[test.name] {
                                                c.Skip("skip minify test")
                                        }
-                                       v := viper.New()
+                                       v := config.New()
                                        m, _ := minifiers.New(media.DefaultTypes, output.DefaultFormats, v)
                                        m.Minify(media.HTMLType, w, strings.NewReader(test.html))
 
 
        "strings"
        "time"
 
+       "github.com/gohugoio/hugo/common/maps"
+
        "github.com/gohugoio/hugo/common/types"
        "github.com/mitchellh/mapstructure"
 )
 }
 
 // DecodeConfig decodes a slice of map into Config.
-func DecodeConfig(in interface{}) (Config, error) {
-       if in == nil {
+func DecodeConfig(m maps.Params) (Config, error) {
+       if m == nil {
                return Config{}, errors.New("no related config provided")
        }
 
-       m, ok := in.(map[string]interface{})
-       if !ok {
-               return Config{}, fmt.Errorf("expected map[string]interface {} got %T", in)
-       }
-
        if len(m) == 0 {
                return Config{}, errors.New("empty related config provided")
        }
 
        "testing"
        "time"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/resources/resource"
-       "github.com/spf13/viper"
 
        qt "github.com/frankban/quicktest"
 )
 func TestFrontMatterNewConfig(t *testing.T) {
        c := qt.New(t)
 
-       cfg := viper.New()
+       cfg := config.New()
 
        cfg.Set("frontmatter", map[string]interface{}{
                "date":        []string{"publishDate", "LastMod"},
        c.Assert(fc.publishDate, qt.DeepEquals, []string{"date"})
 
        // Default
-       cfg = viper.New()
+       cfg = config.New()
        fc, err = newFrontmatterConfig(cfg)
        c.Assert(err, qt.IsNil)
        c.Assert(fc.date, qt.DeepEquals, []string{"date", "publishdate", "pubdate", "published", "lastmod", "modified"})
 
        for _, handlerID := range []string{":filename", ":fileModTime", ":git"} {
 
-               cfg := viper.New()
+               cfg := config.New()
 
                cfg.Set("frontmatter", map[string]interface{}{
                        "date": []string{handlerID, "date"},
 
        c := qt.New(t)
 
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("frontmatter", map[string]interface{}{
                "date":        []string{"mydate"},
                "lastmod":     []string{"publishdate"},
 
        c := qt.New(t)
 
-       cfg := viper.New()
+       cfg := config.New()
 
        cfg.Set("frontmatter", map[string]interface{}{
                "date":        []string{"mydate", ":default"},
 
        "html/template"
        "testing"
 
-       "github.com/spf13/viper"
+       "github.com/gohugoio/hugo/config"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/output"
 func TestPaginationURLFactory(t *testing.T) {
        t.Parallel()
        c := qt.New(t)
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("paginatePath", "zoo")
 
        for _, uglyURLs := range []bool{false, true} {
 
        "github.com/bep/gitmap"
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/resources/resource"
-       "github.com/spf13/viper"
+       
 
        "github.com/gohugoio/hugo/navigation"
 
 }
 
 func newTestPathSpec() *helpers.PathSpec {
-       return newTestPathSpecFor(viper.New())
+       return newTestPathSpecFor(config.New())
 }
 
 func newTestPathSpecFor(cfg config.Provider) *helpers.PathSpec {
 
                                if found {
                                        m := maps.ToStringMap(params)
                                        // Needed for case insensitive fetching of params values
-                                       maps.ToLower(m)
+                                       maps.PrepareParams(m)
                                        ma.updateParams(m)
                                }
                        }
 
        "path/filepath"
 
        "github.com/gohugoio/hugo/cache/filecache"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/output"
        "github.com/gohugoio/hugo/resources"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
 func NewTestResourceSpec() (*resources.Spec, error) {
-       cfg := viper.New()
+       cfg := config.New()
        cfg.Set("baseURL", "https://example.org")
        cfg.Set("publishDir", "public")
 
 
        "path/filepath"
        "strings"
 
-       "github.com/spf13/afero"
-
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/pkg/errors"
+       "github.com/spf13/afero"
 
        "github.com/evanw/esbuild/pkg/api"
 
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/media"
        "github.com/mitchellh/mapstructure"
-       "github.com/spf13/cast"
 )
 
 const (
 
        var defines map[string]string
        if opts.Defines != nil {
-               defines = cast.ToStringMapString(opts.Defines)
+               defines = maps.ToStringMapString(opts.Defines)
        }
 
        // By default we only need to specify outDir and no outFile
 
        "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/langs"
        "github.com/gohugoio/hugo/modules"
 
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/resources/resource"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
 type specDescriptor struct {
        fs      afero.Fs
 }
 
-func createTestCfg() *viper.Viper {
-       cfg := viper.New()
+func createTestCfg() config.Provider {
+       cfg := config.New()
        cfg.Set("resourceDir", "resources")
        cfg.Set("contentDir", "content")
        cfg.Set("dataDir", "data")
 
        "runtime"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/modules"
 
        "github.com/gohugoio/hugo/langs"
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/hugofs"
-
-       "github.com/spf13/viper"
 )
 
 func TestEmptySourceFilesystem(t *testing.T) {
        }
 }
 
-func newTestConfig() *viper.Viper {
-       v := viper.New()
+func newTestConfig() config.Provider {
+       v := config.New()
        v.Set("contentDir", "content")
        v.Set("dataDir", "data")
        v.Set("i18nDir", "i18n")
 
 
 import (
        "github.com/gohugoio/hugo/common/loggers"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/docshelper"
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/tpl/internal"
-       "github.com/spf13/viper"
 )
 
 // This file provides documentation support and is randomly put into this package.
 func init() {
        docsProvider := func() docshelper.DocProvider {
                d := &deps.Deps{
-                       Cfg:                 viper.New(),
+                       Cfg:                 config.New(),
                        Log:                 loggers.NewErrorLogger(),
                        BuildStartListeners: &deps.Listeners{},
                        Site:                page.NewDummyHugoSite(newTestConfig()),
        docshelper.AddDocProviderFunc(docsProvider)
 }
 
-func newTestConfig() *viper.Viper {
-       v := viper.New()
+func newTestConfig() config.Provider {
+       v := config.New()
        v.Set("contentDir", "content")
        return v
 }
 
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/langs"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
+       
 )
 
 type tstNoStringer struct{}
 }
 
 func newTestNs() *Namespace {
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        return New(newDeps(v))
 }
 
        "testing"
 
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/htesting/hqt"
        "github.com/gohugoio/hugo/langs"
        "github.com/gohugoio/hugo/tpl/internal"
-       "github.com/spf13/viper"
 )
 
 func TestInit(t *testing.T) {
        var found bool
        var ns *internal.TemplateFuncsNamespace
 
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        langs.LoadLanguageSettings(v, nil)
 
 
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/langs"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
+       
 )
 
 func TestScpGetLocal(t *testing.T) {
        t.Parallel()
-       v := viper.New()
+       v := config.New()
        fs := hugofs.NewMem(v)
        ps := helpers.FilePathSeparator
 
        c.Assert(err, qt.IsNil)
 
        for _, ignoreCache := range []bool{false} {
-               cfg := viper.New()
+               cfg := config.New()
                cfg.Set("ignoreCache", ignoreCache)
                cfg.Set("contentDir", "content")
 
 }
 
 func newTestNs() *Namespace {
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        return New(newDeps(v))
 }
 
        "errors"
        "html/template"
 
+       "github.com/gohugoio/hugo/common/maps"
        "github.com/spf13/cast"
 )
 
        case 2:
                var opts map[string]string
 
-               opts, err = cast.ToStringMapStringE(args[0])
+               opts, err = maps.ToStringMapStringE(args[0])
                if err != nil {
                        break
                }
 
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/htesting/hqt"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/tpl/internal"
-       "github.com/spf13/viper"
 )
 
 func TestInit(t *testing.T) {
        c := qt.New(t)
        var found bool
        var ns *internal.TemplateFuncsNamespace
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        s := page.NewDummyHugoSite(v)
 
 
        "testing"
 
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/hugofs"
        "github.com/spf13/afero"
        "github.com/spf13/cast"
-       "github.com/spf13/viper"
 )
 
 type tstNoStringer struct{}
        t.Parallel()
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        v.Set("workingDir", "/a/b")
 
        ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
 
        "path/filepath"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/hugofs"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
 )
 
 func TestReadFile(t *testing.T) {
 
        workingDir := "/home/hugo"
 
-       v := viper.New()
+       v := config.New()
        v.Set("workingDir", workingDir)
 
        // f := newTestFuncsterWithViper(v)
 
        workingDir := "/home/hugo"
 
-       v := viper.New()
+       v := config.New()
        v.Set("workingDir", workingDir)
 
        ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
        c := qt.New(t)
        workingDir := "/home/hugo"
 
-       v := viper.New()
+       v := config.New()
        v.Set("workingDir", workingDir)
 
        ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
 
        "testing"
 
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/deps"
-       "github.com/spf13/viper"
 )
 
-var ns = New(&deps.Deps{Cfg: viper.New()})
+var ns = New(&deps.Deps{Cfg: config.New()})
 
 type tstNoStringer struct{}
 
 
        }
 
        if m != nil {
-               maps.ToLower(m)
+               maps.PrepareParams(m)
                if t, found := m["transpiler"]; found {
                        switch t {
                        case transpilerDart, transpilerLibSass:
 
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/htesting/hqt"
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/tpl/internal"
-       "github.com/spf13/viper"
 )
 
 func TestInit(t *testing.T) {
 
        var found bool
        var ns *internal.TemplateFuncsNamespace
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        s := page.NewDummyHugoSite(v)
 
 
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/htesting/hqt"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/tpl/internal"
-       "github.com/spf13/viper"
 )
 
 func TestInit(t *testing.T) {
        var ns *internal.TemplateFuncsNamespace
 
        for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-               ns = nsf(&deps.Deps{Cfg: viper.New()})
+               ns = nsf(&deps.Deps{Cfg: config.New()})
                if ns.Name == name {
                        found = true
                        break
 
        "html/template"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/deps"
 
        qt "github.com/frankban/quicktest"
        "github.com/spf13/cast"
-       "github.com/spf13/viper"
 )
 
-var ns = New(&deps.Deps{Cfg: viper.New()})
+var ns = New(&deps.Deps{Cfg: config.New()})
 
 type tstNoStringer struct{}
 
 
        "github.com/gohugoio/hugo/tpl/internal"
        "github.com/gohugoio/hugo/tpl/partials"
        "github.com/spf13/afero"
-       "github.com/spf13/viper"
+       
 )
 
 var logger = loggers.NewErrorLogger()
 
 func newTestConfig() config.Provider {
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        v.Set("dataDir", "data")
        v.Set("i18nDir", "i18n")
 
 func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) {
        c := qt.New(b)
-       config := newDepsConfig(viper.New())
+       config := newDepsConfig(config.New())
        config.WithTemplate = func(templ tpl.TemplateManager) error {
                err := templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`)
                if err != nil {
 
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/htesting"
 
        qt "github.com/frankban/quicktest"
-       "github.com/spf13/viper"
 )
 
 func TestRemarshal(t *testing.T) {
        t.Parallel()
 
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
        c := qt.New(t)
 func TestRemarshalComments(t *testing.T) {
        t.Parallel()
 
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
 
        t.Parallel()
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
 
 func TestTestRemarshalMapInput(t *testing.T) {
        t.Parallel()
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
 
 
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/langs"
-       "github.com/spf13/viper"
+       
 )
 
 type tstNoStringer struct{}
        t.Parallel()
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        ns := New(newDeps(v))
 
        for _, test := range []struct {
        t.Parallel()
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
 
        t.Parallel()
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
 
        t.Parallel()
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
 
        t.Parallel()
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
 
 func TestMarkdownifyBlocksOfText(t *testing.T) {
        t.Parallel()
        c := qt.New(t)
-       v := viper.New()
+       v := config.New()
        v.Set("contentDir", "content")
        ns := New(newDeps(v))
 
        t.Parallel()
        c := qt.New(t)
 
-       v := viper.New()
+       v := config.New()
        ns := New(newDeps(v))
 
        for _, test := range []struct {
 
        "strings"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/common/hugio"
        "github.com/gohugoio/hugo/resources/resource"
 
        "github.com/gohugoio/hugo/media"
 
        qt "github.com/frankban/quicktest"
-       "github.com/spf13/viper"
 )
 
 const (
 }
 
 func TestUnmarshal(t *testing.T) {
-       v := viper.New()
+       v := config.New()
        ns := New(newDeps(v))
        c := qt.New(t)
 
 }
 
 func BenchmarkUnmarshalString(b *testing.B) {
-       v := viper.New()
+       v := config.New()
        ns := New(newDeps(v))
 
        const numJsons = 100
 }
 
 func BenchmarkUnmarshalResource(b *testing.B) {
-       v := viper.New()
+       v := config.New()
        ns := New(newDeps(v))
 
        const numJsons = 100
 
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/htesting/hqt"
        "github.com/gohugoio/hugo/tpl/internal"
-       "github.com/spf13/viper"
 )
 
 func TestInit(t *testing.T) {
        var ns *internal.TemplateFuncsNamespace
 
        for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
-               ns = nsf(&deps.Deps{Cfg: viper.New()})
+               ns = nsf(&deps.Deps{Cfg: config.New()})
                if ns.Name == name {
                        found = true
                        break
 
        "net/url"
        "testing"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/htesting/hqt"
 
        qt "github.com/frankban/quicktest"
        "github.com/gohugoio/hugo/deps"
-       "github.com/spf13/viper"
 )
 
-var ns = New(&deps.Deps{Cfg: viper.New()})
+var ns = New(&deps.Deps{Cfg: config.New()})
 
 type tstNoStringer struct{}