hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit d392893cd73dc00c927f342778f6dca9628d328e
parent a886dd53b80322e1edf924f2ede4d4ea037c5baf
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Wed,  9 Jun 2021 10:58:18 +0200

Misc config loading fixes

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

Diffstat:
Mcache/filecache/filecache_config.go | 7++++++-
Mcache/filecache/filecache_config_test.go | 5++---
Mcommands/commandeer.go | 2--
Mcommands/commands_test.go | 9+++++----
Mcommands/config.go | 5+++--
Mcommands/new_site.go | 4++--
Mcommands/server_test.go | 4++--
Mcommon/maps/maps.go | 77++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcommon/maps/maps_test.go | 2+-
Mcommon/maps/params.go | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/maps/params_test.go | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mconfig/commonConfig_test.go | 4+---
Aconfig/compositeConfig.go | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig/compositeConfig_test.go | 40++++++++++++++++++++++++++++++++++++++++
Mconfig/configLoader.go | 23++---------------------
Mconfig/configProvider.go | 5+++++
Mconfig/configProvider_test.go | 3+--
Aconfig/defaultConfigProvider.go | 372+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig/defaultConfigProvider_test.go | 315+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig/docshelper.go | 45+++++++++++++++++++++++++++++++++++++++++++++
Mconfig/privacy/privacyConfig_test.go | 3+--
Mconfig/services/servicesConfig_test.go | 4++--
Mcreate/content_test.go | 5+++--
Mdeploy/deployConfig_test.go | 4++--
Mdocs/config.toml | 2+-
Mdocs/content/en/getting-started/configuration.md | 20++++++++++++++++++++
Mdocs/data/docs.json | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/layouts/shortcodes/code-toggle.html | 59+++++++++++++++++++++++++++++++++--------------------------
Mgo.mod | 2+-
Mgo.sum | 1+
Mhelpers/content_test.go | 5++---
Mhelpers/general_test.go | 4++--
Mhelpers/path_test.go | 4----
Mhelpers/testhelpers_test.go | 21+++++++--------------
Mhugofs/fs_test.go | 9+++++----
Mhugofs/rootmapping_fs_test.go | 4++--
Mhugolib/config.go | 680+++++++++++++++++++++++++++++++++++++------------------------------------------
Mhugolib/config_test.go | 372++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mhugolib/filesystems/basefs_test.go | 10+++++-----
Mhugolib/hugo_modules_test.go | 30+++++++++++++++---------------
Mhugolib/hugo_sites.go | 15+++++++++------
Mhugolib/hugo_sites_build_errors_test.go | 4+---
Mhugolib/image_test.go | 4++--
Mhugolib/js_test.go | 11+++++------
Mhugolib/minify_publisher_test.go | 4++--
Mhugolib/page__meta.go | 2+-
Mhugolib/page_test.go | 5++---
Mhugolib/pagebundler_test.go | 18+++++++++---------
Mhugolib/pages_capture.go | 2+-
Mhugolib/paths/paths_test.go | 7+++----
Mhugolib/resource_chain_babel_test.go | 6+++---
Mhugolib/resource_chain_test.go | 18+++++++++---------
Mhugolib/robotstxt_test.go | 4++--
Mhugolib/shortcode_test.go | 7+++----
Mhugolib/site.go | 22+++++++++++-----------
Mhugolib/site_output_test.go | 12++++++------
Mhugolib/site_test.go | 5++---
Mhugolib/template_test.go | 5++---
Mhugolib/testhelpers_test.go | 26+++++++++++++-------------
Mlangs/config.go | 8++++----
Mlangs/i18n/i18n_test.go | 7+++----
Mlangs/language.go | 84++++++++++++++++++++++++-------------------------------------------------------
Mlangs/language_test.go | 7++++---
Mmarkup/asciidocext/convert_test.go | 18+++++++++---------
Mmarkup/blackfriday/convert_test.go | 6+++---
Mmarkup/highlight/config_test.go | 5++---
Mmarkup/markup_config/config.go | 3+--
Mmarkup/markup_config/config_test.go | 6+++---
Mmarkup/markup_test.go | 8+++-----
Mmarkup/mmark/convert_test.go | 4++--
Mmarkup/org/convert_test.go | 5+++--
Mmedia/mediaType.go | 4++--
Mminifiers/config.go | 4++--
Mminifiers/config_test.go | 6+++---
Mminifiers/minifiers_test.go | 15+++++++--------
Mmodules/collect.go | 2+-
Mmodules/npm/package_builder.go | 8++++----
Moutput/outputFormat.go | 6+++++-
Mpublisher/htmlElementsCollector_test.go | 5+++--
Mrelated/inverted_index.go | 11++++-------
Mresources/page/pagemeta/page_frontmatter_test.go | 13+++++++------
Mresources/page/pagination_test.go | 4++--
Mresources/page/testhelpers_test.go | 4++--
Mresources/resource_metadata.go | 2+-
Mresources/resource_transformers/htesting/testhelpers.go | 4++--
Mresources/resource_transformers/js/options.go | 7+++----
Mresources/testhelpers_test.go | 6+++---
Msource/filesystem_test.go | 8++++----
Mtpl/cast/docshelper.go | 8++++----
Mtpl/collections/collections_test.go | 4++--
Mtpl/data/init_test.go | 4++--
Mtpl/data/resources_test.go | 8++++----
Mtpl/encoding/encoding.go | 3++-
Mtpl/hugo/init_test.go | 5+++--
Mtpl/images/images_test.go | 4++--
Mtpl/os/os_test.go | 9+++++----
Mtpl/path/path_test.go | 4++--
Mtpl/resources/resources.go | 2+-
Mtpl/site/init_test.go | 5+++--
Mtpl/strings/init_test.go | 5+++--
Mtpl/strings/strings_test.go | 4++--
Mtpl/tplimpl/template_funcs_test.go | 6+++---
Mtpl/transform/remarshal_test.go | 10+++++-----
Mtpl/transform/transform_test.go | 16++++++++--------
Mtpl/transform/unmarshal_test.go | 9+++++----
Mtpl/urls/init_test.go | 5+++--
Mtpl/urls/urls_test.go | 5+++--
107 files changed, 2115 insertions(+), 1016 deletions(-)
diff --git a/cache/filecache/filecache_config.go b/cache/filecache/filecache_config.go
@@ -19,6 +19,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/common/maps"
+
 	"github.com/gohugoio/hugo/config"
 
 	"github.com/gohugoio/hugo/helpers"
@@ -123,6 +125,9 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
 	_, isOsFs := fs.(*afero.OsFs)
 
 	for k, v := range m {
+		if _, ok := v.(maps.Params); !ok {
+			continue
+		}
 		cc := defaultCacheConfig
 
 		dc := &mapstructure.DecoderConfig{
@@ -137,7 +142,7 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
 		}
 
 		if err := decoder.Decode(v); err != nil {
-			return nil, err
+			return nil, errors.Wrap(err, "failed to decode filecache config")
 		}
 
 		if cc.Dir == "" {
diff --git a/cache/filecache/filecache_config_test.go b/cache/filecache/filecache_config_test.go
@@ -25,7 +25,6 @@ import (
 	"github.com/gohugoio/hugo/config"
 
 	qt "github.com/frankban/quicktest"
-	"github.com/spf13/viper"
 )
 
 func TestDecodeConfig(t *testing.T) {
@@ -178,8 +177,8 @@ dir = "/"
 	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")
diff --git a/commands/commandeer.go b/commands/commandeer.go
@@ -410,7 +410,5 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
 	}
 	config.Set("cacheDir", cacheDir)
 
-	cfg.Logger.Infoln("Using config file:", config.ConfigFileUsed())
-
 	return nil
 }
diff --git a/commands/commands_test.go b/commands/commands_test.go
@@ -20,6 +20,8 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/gohugoio/hugo/config"
+
 	"github.com/gohugoio/hugo/htesting"
 
 	"github.com/spf13/afero"
@@ -29,7 +31,6 @@ import (
 	"github.com/gohugoio/hugo/common/types"
 
 	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
 
 	qt "github.com/frankban/quicktest"
 )
@@ -166,7 +167,7 @@ func TestFlags(t *testing.T) {
 			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)
 			},
@@ -176,7 +177,7 @@ func TestFlags(t *testing.T) {
 			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/**")
 			},
@@ -216,7 +217,7 @@ func TestFlags(t *testing.T) {
 				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")
diff --git a/commands/config.go b/commands/config.go
@@ -22,13 +22,14 @@ import (
 	"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)
@@ -81,7 +82,7 @@ func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
 		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.
diff --git a/commands/new_site.go b/commands/new_site.go
@@ -19,6 +19,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/parser/metadecoders"
 
 	_errors "github.com/pkg/errors"
@@ -29,7 +30,6 @@ import (
 	"github.com/gohugoio/hugo/parser"
 	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
-	"github.com/spf13/viper"
 )
 
 var _ cmder = (*newSiteCmd)(nil)
@@ -123,7 +123,7 @@ func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error {
 
 	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) {
diff --git a/commands/server_test.go b/commands/server_test.go
@@ -22,10 +22,10 @@ import (
 	"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) {
@@ -101,7 +101,7 @@ func TestFixURL(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
diff --git a/common/maps/maps.go b/common/maps/maps.go
@@ -18,53 +18,65 @@ import (
 	"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{}:
@@ -127,9 +139,8 @@ func (KeyRenamer) keyPath(k1, k2 string) string {
 	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{}) {
diff --git a/common/maps/maps_test.go b/common/maps/maps_test.go
@@ -67,7 +67,7 @@ func TestToLower(t *testing.T) {
 	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)
 			}
diff --git a/common/maps/params.go b/common/maps/params.go
@@ -14,6 +14,7 @@
 package maps
 
 import (
+	"fmt"
 	"strings"
 
 	"github.com/spf13/cast"
@@ -29,6 +30,95 @@ func (p Params) Get(indices ...string) interface{} {
 	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
@@ -108,3 +198,61 @@ func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interf
 
 	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
+		}
+	}
+}
diff --git a/common/maps/params_test.go b/common/maps/params_test.go
@@ -69,3 +69,90 @@ func TestGetNestedParamFnNestedNewKey(t *testing.T) {
 	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",
+	})
+
+}
diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go
@@ -21,14 +21,12 @@ import (
 	"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",
 	})
diff --git a/config/compositeConfig.go b/config/compositeConfig.go
@@ -0,0 +1,113 @@
+// 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")
+}
diff --git a/config/compositeConfig_test.go b/config/compositeConfig_test.go
@@ -0,0 +1,40 @@
+// 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)
+	})
+}
diff --git a/config/configLoader.go b/config/configLoader.go
@@ -20,7 +20,6 @@ import (
 	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/parser/metadecoders"
 	"github.com/spf13/afero"
-	"github.com/spf13/viper"
 )
 
 var (
@@ -43,15 +42,11 @@ func IsValidConfigFilename(filename string) bool {
 
 // 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.
@@ -60,15 +55,7 @@ func FromFile(fs afero.Fs, filename string) (Provider, error) {
 	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
@@ -116,9 +103,3 @@ func init() {
 func RenameKeys(m map[string]interface{}) {
 	keyAliases.Rename(m)
 }
-
-func newViper() *viper.Viper {
-	v := viper.New()
-
-	return v
-}
diff --git a/config/configProvider.go b/config/configProvider.go
@@ -14,6 +14,7 @@
 package config
 
 import (
+	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/common/types"
 )
 
@@ -22,11 +23,15 @@ type Provider interface {
 	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
 }
 
diff --git a/config/configProvider_test.go b/config/configProvider_test.go
@@ -17,12 +17,11 @@ import (
 	"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"}
diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go
@@ -0,0 +1,372 @@
+// 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
+}
diff --git a/config/defaultConfigProvider_test.go b/config/defaultConfigProvider_test.go
@@ -0,0 +1,315 @@
+// 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)
+		}
+	})
+}
diff --git a/config/docshelper.go b/config/docshelper.go
@@ -0,0 +1,45 @@
+// 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)
+}
diff --git a/config/privacy/privacyConfig_test.go b/config/privacy/privacyConfig_test.go
@@ -18,7 +18,6 @@ import (
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/config"
-	"github.com/spf13/viper"
 )
 
 func TestDecodeConfigFromTOML(t *testing.T) {
@@ -94,7 +93,7 @@ PrivacyENhanced = true
 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)
diff --git a/config/services/servicesConfig_test.go b/config/services/servicesConfig_test.go
@@ -18,7 +18,7 @@ import (
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/config"
-	"github.com/spf13/viper"
+	
 )
 
 func TestDecodeConfigFromTOML(t *testing.T) {
@@ -55,7 +55,7 @@ disableInlineCSS = true
 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")
 
diff --git a/create/content_test.go b/create/content_test.go
@@ -20,6 +20,8 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/gohugoio/hugo/config"
+
 	"github.com/gohugoio/hugo/deps"
 
 	"github.com/gohugoio/hugo/hugolib"
@@ -30,7 +32,6 @@ import (
 	"github.com/gohugoio/hugo/create"
 	"github.com/gohugoio/hugo/helpers"
 	"github.com/spf13/afero"
-	"github.com/spf13/viper"
 )
 
 func TestNewContent(t *testing.T) {
@@ -245,7 +246,7 @@ func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string {
 	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"
diff --git a/deploy/deployConfig_test.go b/deploy/deployConfig_test.go
@@ -21,7 +21,7 @@ import (
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/config"
-	"github.com/spf13/viper"
+	
 )
 
 func TestDecodeConfigFromTOML(t *testing.T) {
@@ -164,7 +164,7 @@ Pattern = "["  # invalid regular expression
 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)
diff --git a/docs/config.toml b/docs/config.toml
@@ -7,7 +7,7 @@ footnotereturnlinkcontents = "↩"
 languageCode = "en-us"
 title = "Hugo"
 
- ignoreErrors = ["error-remote-getjson"]
+ ignoreErrors = ["error-remote-getjson", "err-missing-instagram-accesstoken"]
 
 
 googleAnalytics = "UA-7131036-4"
diff --git a/docs/content/en/getting-started/configuration.md b/docs/content/en/getting-started/configuration.md
@@ -80,6 +80,26 @@ Considering the structure above, when running `hugo --environment staging`, Hugo
 {{% 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
diff --git a/docs/data/docs.json b/docs/data/docs.json
@@ -1587,6 +1587,65 @@
         "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,
diff --git a/docs/layouts/shortcodes/code-toggle.html b/docs/layouts/shortcodes/code-toggle.html
@@ -1,34 +1,41 @@
 {{ $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>&nbsp;
-		{{ 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>
+      &nbsp;
+    {{ 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>
diff --git a/go.mod b/go.mod
@@ -55,7 +55,7 @@ require (
 	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
diff --git a/go.sum b/go.sum
@@ -642,6 +642,7 @@ github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0
 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=
diff --git a/helpers/content_test.go b/helpers/content_test.go
@@ -19,12 +19,11 @@ import (
 	"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"
 )
 
@@ -103,7 +102,7 @@ func TestBytesToHTML(t *testing.T) {
 }
 
 func TestNewContentSpec(t *testing.T) {
-	cfg := viper.New()
+	cfg := config.New()
 	c := qt.New(t)
 
 	cfg.Set("summaryLength", 32)
diff --git a/helpers/general_test.go b/helpers/general_test.go
@@ -19,7 +19,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/spf13/viper"
+	"github.com/gohugoio/hugo/config"
 
 	"github.com/gohugoio/hugo/common/loggers"
 
@@ -29,7 +29,7 @@ import (
 
 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)
 
diff --git a/helpers/path_test.go b/helpers/path_test.go
@@ -31,7 +31,6 @@ import (
 
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/spf13/afero"
-	"github.com/spf13/viper"
 )
 
 func TestMakePath(t *testing.T) {
@@ -490,8 +489,6 @@ func TestExists(t *testing.T) {
 }
 
 func TestAbsPathify(t *testing.T) {
-	defer viper.Reset()
-
 	type test struct {
 		inPath, workingDir, expected string
 	}
@@ -511,7 +508,6 @@ func TestAbsPathify(t *testing.T) {
 	}
 
 	for i, d := range data {
-		viper.Reset()
 		// todo see comment in AbsPathify
 		ps := newTestDefaultPathSpec("workingDir", d.workingDir)
 
diff --git a/helpers/testhelpers_test.go b/helpers/testhelpers_test.go
@@ -2,24 +2,24 @@ package helpers
 
 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])
@@ -27,15 +27,8 @@ func newTestDefaultPathSpec(configKeyValues ...interface{}) *PathSpec {
 	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")
@@ -56,7 +49,7 @@ func newTestCfg() *viper.Viper {
 }
 
 func newTestContentSpec() *ContentSpec {
-	v := viper.New()
+	v := config.New()
 	spec, err := NewContentSpec(v, loggers.NewErrorLogger(), afero.NewMemMapFs())
 	if err != nil {
 		panic(err)
diff --git a/hugofs/fs_test.go b/hugofs/fs_test.go
@@ -16,15 +16,16 @@ package hugofs
 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))
@@ -35,7 +36,7 @@ func TestNewDefault(t *testing.T) {
 
 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))
@@ -48,7 +49,7 @@ func TestNewMem(t *testing.T) {
 
 func TestWorkingDir(t *testing.T) {
 	c := qt.New(t)
-	v := viper.New()
+	v := config.New()
 
 	v.Set("workingDir", "/a/b/")
 
diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go
@@ -20,7 +20,7 @@ import (
 	"sort"
 	"testing"
 
-	"github.com/spf13/viper"
+	"github.com/gohugoio/hugo/config"
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/htesting"
@@ -29,7 +29,7 @@ import (
 
 func TestLanguageRootMapping(t *testing.T) {
 	c := qt.New(t)
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 
 	fs := NewBaseFileDecorator(afero.NewMemMapFs())
diff --git a/hugolib/config.go b/hugolib/config.go
@@ -43,34 +43,136 @@ import (
 	"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.).
@@ -98,6 +200,13 @@ type ConfigSourceDescriptor struct {
 	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"}
@@ -105,185 +214,219 @@ func (d ConfigSourceDescriptor) configFilenames() []string {
 	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) {
@@ -318,24 +461,13 @@ func (l configLoader) loadConfig(configName string, v *viper.Viper) (string, err
 		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
 
@@ -421,9 +553,8 @@ func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) 
 			// 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
 		})
@@ -436,8 +567,13 @@ func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) 
 	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
 	}
@@ -445,211 +581,29 @@ func (l configLoader) loadModulesConfig(v1 *viper.Viper) (modules.Config, error)
 	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
 }
diff --git a/hugolib/config_test.go b/hugolib/config_test.go
@@ -17,11 +17,15 @@ import (
 	"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) {
@@ -77,12 +81,7 @@ func TestLoadConfigFromTheme(t *testing.T) {
 
 	c := qt.New(t)
 
-	mainConfigBasic := `
-theme = "test-theme"
-baseURL = "https://example.com/"
-
-`
-	mainConfig := `
+	mainConfigTemplate := `
 theme = "test-theme"
 baseURL = "https://example.com/"
 
@@ -90,9 +89,12 @@ 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"]
@@ -130,7 +132,14 @@ expiryDate = ["date"]
 [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"]
@@ -176,190 +185,137 @@ name = "menu-theme"
 
 `
 
-	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) {
@@ -490,7 +446,12 @@ intSlice = [5,7,9]
 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"
@@ -504,9 +465,16 @@ quality = 75
 
 	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"
 
@@ -530,14 +498,16 @@ 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)
@@ -551,9 +521,23 @@ theme_param="themevalue2"
 	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)
+
 }
diff --git a/hugolib/filesystems/basefs_test.go b/hugolib/filesystems/basefs_test.go
@@ -33,7 +33,7 @@ import (
 	"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 {
@@ -76,7 +76,7 @@ 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)
 
@@ -181,8 +181,8 @@ theme = ["atheme"]
 	}
 }
 
-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")
@@ -453,7 +453,7 @@ func countFilesAndGetFilenames(fs afero.Fs, dirname string) (int, []string, erro
 	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)
diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/modules/npm"
 
 	"github.com/gohugoio/hugo/common/loggers"
@@ -37,7 +38,6 @@ import (
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/testmodBuilder/mods"
-	"github.com/spf13/viper"
 )
 
 func TestHugoModulesVariants(t *testing.T) {
@@ -45,7 +45,7 @@ func TestHugoModulesVariants(t *testing.T) {
 		t.Skip("skip (relative) long running modules test when running locally")
 	}
 
-	config := `
+	tomlConfig := `
 baseURL="https://example.org"
 workingDir = %q
 
@@ -56,7 +56,7 @@ path="github.com/gohugoio/hugoTestModule2"
 `
 
 	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()) {
@@ -65,7 +65,7 @@ path="github.com/gohugoio/hugoTestModule2"
 		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", `
@@ -333,7 +333,7 @@ func TestHugoModulesMatrix(t *testing.T) {
 	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)
@@ -671,7 +671,7 @@ func TestModulesSymlinks(t *testing.T) {
 
 	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")
@@ -839,13 +839,13 @@ workingDir = %q
 
 `
 
-	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 }}
@@ -960,16 +960,16 @@ workingDir = %q
 %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,
@@ -1064,7 +1064,7 @@ func TestSiteWithGoModButNoModules(t *testing.T) {
 	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)
 
@@ -1090,7 +1090,7 @@ func TestModuleAbsMount(t *testing.T) {
 	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)
 
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
@@ -374,7 +374,8 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) {
 		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")
 	}
 
@@ -407,7 +408,7 @@ func (h *HugoSites) loadGitInfo() error {
 	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
 	}
@@ -446,7 +447,7 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
 
 			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")
 			}
@@ -607,11 +608,12 @@ func (h *HugoSites) withSite(fn func(s *Site) error) error {
 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 {
@@ -629,7 +631,8 @@ func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error {
 		s.h = h
 	}
 
-	if err := applyDeps(depsCfg, sites...); err != nil {
+	var cl configLoader
+	if err := cl.applyDeps(depsCfg, sites...); err != nil {
 		return err
 	}
 
diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go
@@ -5,9 +5,7 @@ import (
 	"path/filepath"
 	"strings"
 	"testing"
-	"time"
 
-	"github.com/fortytw2/leaktest"
 	"github.com/gohugoio/hugo/htesting"
 
 	qt "github.com/frankban/quicktest"
@@ -318,7 +316,7 @@ Some content.
 // 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)
diff --git a/hugolib/image_test.go b/hugolib/image_test.go
@@ -21,11 +21,11 @@ import (
 	"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,
@@ -38,7 +38,7 @@ func TestImageOps(t *testing.T) {
 	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)
diff --git a/hugolib/js_test.go b/hugolib/js_test.go
@@ -21,11 +21,10 @@ import (
 	"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"
@@ -88,7 +87,7 @@ document.body.textContent = greeter(user);`
 	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())
@@ -162,7 +161,7 @@ func TestJSBuild(t *testing.T) {
 	c.Assert(err, qt.IsNil)
 	defer clean()
 
-	config := fmt.Sprintf(`
+	tomlConfig := fmt.Sprintf(`
 baseURL = "https://example.org"
 workingDir = %q
 
@@ -177,8 +176,8 @@ path="github.com/gohugoio/hugoTestProjectJSModImports"
 `, 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
diff --git a/hugolib/minify_publisher_test.go b/hugolib/minify_publisher_test.go
@@ -16,13 +16,13 @@ package hugolib
 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/")
 
diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
@@ -336,7 +336,7 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron
 
 	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 {
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
@@ -37,7 +37,6 @@ import (
 	"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"
@@ -786,7 +785,7 @@ func TestPageWithLastmodFromGitInfo(t *testing.T) {
 	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{}
 
@@ -1066,7 +1065,7 @@ func TestChompBOM(t *testing.T) {
 
 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)
diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go
@@ -23,6 +23,8 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/gohugoio/hugo/config"
+
 	"github.com/gohugoio/hugo/hugofs/files"
 
 	"github.com/gohugoio/hugo/helpers"
@@ -35,7 +37,6 @@ import (
 	"github.com/gohugoio/hugo/htesting"
 
 	"github.com/gohugoio/hugo/deps"
-	"github.com/spf13/viper"
 
 	qt "github.com/frankban/quicktest"
 )
@@ -352,12 +353,11 @@ func TestMultilingualDisableDefaultLanguage(t *testing.T) {
 
 	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")
 }
@@ -397,7 +397,7 @@ func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) {
 
 	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")
@@ -696,7 +696,7 @@ Single content.
 	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)
 
@@ -863,7 +863,7 @@ Content for 은행.
 	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"
@@ -1319,7 +1319,7 @@ func TestPageBundlerHome(t *testing.T) {
 	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)
 
diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go
@@ -130,7 +130,7 @@ func (c *pagesCollector) isCascadingEdit(dir contentDirKey) (bool, string) {
 
 		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 {
diff --git a/hugolib/paths/paths_test.go b/hugolib/paths/paths_test.go
@@ -16,17 +16,16 @@ package paths
 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{}{
diff --git a/hugolib/resource_chain_babel_test.go b/hugolib/resource_chain_babel_test.go
@@ -19,14 +19,14 @@ import (
 	"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"
@@ -91,7 +91,7 @@ class Car2 {
 	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)
diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go
@@ -20,6 +20,8 @@ import (
 	"math/rand"
 	"os"
 
+	"github.com/gohugoio/hugo/config"
+
 	"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
 
 	"path/filepath"
@@ -35,8 +37,6 @@ import (
 
 	"github.com/gohugoio/hugo/htesting"
 
-	"github.com/spf13/viper"
-
 	qt "github.com/frankban/quicktest"
 
 	"github.com/gohugoio/hugo/hugofs"
@@ -65,7 +65,7 @@ func TestSCSSWithIncludePaths(t *testing.T) {
 			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.
@@ -130,7 +130,7 @@ func TestSCSSWithRegularCSSImport(t *testing.T) {
 			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.
@@ -230,7 +230,7 @@ func TestSCSSWithThemeOverrides(t *testing.T) {
 			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())
@@ -345,7 +345,7 @@ func TestSCSSWithIncludePathsSass(t *testing.T) {
 	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())
@@ -974,7 +974,7 @@ h1 {
 
 	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)
@@ -997,7 +997,7 @@ Styles Content: Len: {{ len $styles.Content }}|
 		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)
@@ -1049,7 +1049,7 @@ Styles Content: Len: 770878|
 	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,
 		})
diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go
@@ -16,7 +16,7 @@ package hugolib
 import (
 	"testing"
 
-	"github.com/spf13/viper"
+	"github.com/gohugoio/hugo/config"
 )
 
 const robotTxtTemplate = `User-agent: Googlebot
@@ -28,7 +28,7 @@ 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)
 
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
@@ -20,11 +20,10 @@ import (
 	"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"
 
@@ -1214,7 +1213,7 @@ title: "Hugo Rocks!"
 func TestShortcodeEmoji(t *testing.T) {
 	t.Parallel()
 
-	v := viper.New()
+	v := config.New()
 	v.Set("enableEmoji", true)
 
 	builder := newTestSitesBuilder(t).WithViper(v)
@@ -1279,7 +1278,7 @@ func TestShortcodeRef(t *testing.T) {
 		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,
diff --git a/hugolib/site.go b/hugolib/site.go
@@ -77,7 +77,6 @@ import (
 
 	"github.com/spf13/afero"
 	"github.com/spf13/cast"
-	"github.com/spf13/viper"
 )
 
 // Site contains all the information relevant for constructing a static
@@ -501,9 +500,9 @@ But this also means that your site configuration may not do what you expect. If 
 	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
@@ -574,7 +573,8 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) {
 		return nil, err
 	}
 
-	if err = applyDeps(cfg, s); err != nil {
+	var l configLoader
+	if err = l.applyDeps(cfg, s); err != nil {
 		return nil, err
 	}
 
@@ -586,11 +586,11 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) {
 // 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.
@@ -598,11 +598,11 @@ func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateManager) error) (
 // 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.
@@ -1314,7 +1314,7 @@ func (s *Site) initializeSiteInfo() error {
 				return vvv
 			}
 		default:
-			m := cast.ToStringMapBool(v)
+			m := maps.ToStringMapBool(v)
 			uglyURLs = func(p page.Page) bool {
 				return m[p.Section()]
 			}
diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go
@@ -19,13 +19,13 @@ import (
 	"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) {
@@ -333,7 +333,7 @@ func TestCreateSiteOutputFormats(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)
@@ -358,7 +358,7 @@ func TestCreateSiteOutputFormats(t *testing.T) {
 	// 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",
@@ -380,7 +380,7 @@ func TestCreateSiteOutputFormatsInvalidConfig(t *testing.T) {
 		page.KindHome: []string{"FOO", "JSON"},
 	}
 
-	cfg := viper.New()
+	cfg := config.New()
 	cfg.Set("outputs", outputsConfig)
 
 	_, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
@@ -394,7 +394,7 @@ func TestCreateSiteOutputFormatsEmptyConfig(t *testing.T) {
 		page.KindHome: []string{},
 	}
 
-	cfg := viper.New()
+	cfg := config.New()
 	cfg.Set("outputs", outputsConfig)
 
 	outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
@@ -409,7 +409,7 @@ func TestCreateSiteOutputFormatsCustomFormats(t *testing.T) {
 		page.KindHome: []string{},
 	}
 
-	cfg := viper.New()
+	cfg := config.New()
 	cfg.Set("outputs", outputsConfig)
 
 	var (
diff --git a/hugolib/site_test.go b/hugolib/site_test.go
@@ -23,10 +23,9 @@ import (
 	"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"
@@ -363,7 +362,7 @@ func TestMainSections(t *testing.T) {
 	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"},
diff --git a/hugolib/template_test.go b/hugolib/template_test.go
@@ -19,20 +19,19 @@ import (
 	"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
 	)
 
diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go
@@ -30,6 +30,7 @@ import (
 
 	"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"
@@ -39,7 +40,6 @@ import (
 
 	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/tpl"
-	"github.com/spf13/viper"
 
 	"github.com/gohugoio/hugo/resources/resource"
 
@@ -83,7 +83,7 @@ type sitesBuilder struct {
 	// Default toml
 	configFormat  string
 	configFileSet bool
-	viperSet      bool
+	configSet      bool
 
 	// Default is empty.
 	// TODO(bep) revisit this and consider always setting it to something.
@@ -111,7 +111,7 @@ type filenameContent struct {
 }
 
 func newTestSitesBuilder(t testing.TB) *sitesBuilder {
-	v := viper.New()
+	v := config.New()
 	fs := hugofs.NewMem(v)
 
 	litterOptions := litter.Options{
@@ -140,7 +140,7 @@ func newTestSitesBuilderFromDepsCfg(t testing.TB, d deps.DepsCfg) *sitesBuilder 
 
 	b.WithWorkingDir(workingDir)
 
-	return b.WithViper(d.Cfg.(*viper.Viper))
+	return b.WithViper(d.Cfg.(config.Provider))
 }
 
 func (s *sitesBuilder) Running() *sitesBuilder {
@@ -186,26 +186,26 @@ func (s *sitesBuilder) WithConfigTemplate(data interface{}, format, configTempla
 	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)
@@ -845,14 +845,14 @@ func (th testHelper) replaceDefaultContentLanguageValue(value string) string {
 	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)
@@ -860,7 +860,7 @@ func newTestCfgBasic() (*viper.Viper, *hugofs.Fs) {
 	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 {
diff --git a/langs/config.go b/langs/config.go
@@ -43,13 +43,13 @@ func LoadLanguageSettings(cfg config.Provider, oldLangs Languages) (c LanguagesC
 
 	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 {
@@ -57,7 +57,7 @@ func LoadLanguageSettings(cfg config.Provider, oldLangs Languages) (c LanguagesC
 				}
 
 				if strings.EqualFold(k, disabled) {
-					v.(map[string]interface{})["disabled"] = true
+					v.(maps.Params)["disabled"] = true
 					break
 				}
 			}
@@ -193,7 +193,7 @@ func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (Languages
 			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)
 				}
diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
@@ -28,7 +28,6 @@ import (
 	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/spf13/afero"
-	"github.com/spf13/viper"
 
 	"github.com/gohugoio/hugo/deps"
 
@@ -500,9 +499,9 @@ func newDepsConfig(tp *TranslationProvider, cfg config.Provider, fs *hugofs.Fs) 
 	}
 }
 
-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")
diff --git a/langs/language.go b/langs/language.go
@@ -20,7 +20,6 @@ import (
 
 	"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
@@ -55,18 +54,20 @@ type Language struct {
 	// 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 {
@@ -81,9 +82,12 @@ func NewLanguage(lang string, cfg config.Provider) *Language {
 	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
 }
 
@@ -133,7 +137,7 @@ func (l *Language) Params() maps.Params {
 	l.paramsMu.Lock()
 	defer l.paramsMu.Unlock()
 	if !l.paramsSet {
-		maps.ToLower(l.params)
+		maps.PrepareParams(l.params)
 		l.paramsSet = true
 	}
 	return l.params
@@ -183,42 +187,6 @@ func (l *Language) SetParam(k string, v interface{}) {
 	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.
@@ -228,31 +196,29 @@ func (l *Language) GetLocal(key string) interface{} {
 	}
 	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)
 }
diff --git a/langs/language_test.go b/langs/language_test.go
@@ -16,13 +16,14 @@ package langs
 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")
@@ -37,7 +38,7 @@ func TestGetGlobalOnlySetting(t *testing.T) {
 func TestLanguageParams(t *testing.T) {
 	c := qt.New(t)
 
-	v := viper.New()
+	v := config.New()
 	v.Set("p1", "p1cfg")
 	v.Set("contentDir", "content")
 
diff --git a/markup/asciidocext/convert_test.go b/markup/asciidocext/convert_test.go
@@ -22,17 +22,17 @@ import (
 	"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(
@@ -57,7 +57,7 @@ func TestAsciidoctorDefaultArgs(t *testing.T) {
 
 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
@@ -88,7 +88,7 @@ func TestAsciidoctorNonDefaultArgs(t *testing.T) {
 
 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"}
@@ -117,7 +117,7 @@ func TestAsciidoctorDisallowedArgs(t *testing.T) {
 
 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(
@@ -142,7 +142,7 @@ func TestAsciidoctorArbitraryExtension(t *testing.T) {
 
 func TestAsciidoctorDisallowedExtension(t *testing.T) {
 	c := qt.New(t)
-	cfg := viper.New()
+	cfg := config.New()
 	for _, disallowedExtension := range []string{
 		`foo-bar//`,
 		`foo-bar\\ `,
@@ -177,7 +177,7 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) {
 
 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
@@ -208,7 +208,7 @@ func TestAsciidoctorWorkingFolderCurrent(t *testing.T) {
 
 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"}
@@ -247,7 +247,7 @@ func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) {
 
 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
diff --git a/markup/blackfriday/convert_test.go b/markup/blackfriday/convert_test.go
@@ -16,7 +16,7 @@ package blackfriday
 import (
 	"testing"
 
-	"github.com/spf13/viper"
+	"github.com/gohugoio/hugo/config"
 
 	"github.com/gohugoio/hugo/markup/converter"
 
@@ -140,7 +140,7 @@ func TestGetAllFlags(t *testing.T) {
 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{})
@@ -153,7 +153,7 @@ func TestConvert(t *testing.T) {
 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{
diff --git a/markup/highlight/config_test.go b/markup/highlight/config_test.go
@@ -17,16 +17,15 @@ package highlight
 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)
diff --git a/markup/markup_config/config.go b/markup/markup_config/config.go
@@ -24,7 +24,6 @@ import (
 	"github.com/gohugoio/hugo/markup/tableofcontents"
 	"github.com/gohugoio/hugo/parser"
 	"github.com/mitchellh/mapstructure"
-	"github.com/spf13/cast"
 )
 
 type Config struct {
@@ -73,7 +72,7 @@ func normalizeConfig(m map[string]interface{}) {
 	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 {
diff --git a/markup/markup_config/config_test.go b/markup/markup_config/config_test.go
@@ -16,7 +16,7 @@ package markup_config
 import (
 	"testing"
 
-	"github.com/spf13/viper"
+	"github.com/gohugoio/hugo/config"
 
 	qt "github.com/frankban/quicktest"
 )
@@ -26,7 +26,7 @@ func TestConfig(t *testing.T) {
 
 	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{}{
@@ -55,7 +55,7 @@ func TestConfig(t *testing.T) {
 
 	c.Run("legacy", func(c *qt.C) {
 		c.Parallel()
-		v := viper.New()
+		v := config.New()
 
 		v.Set("blackfriday", map[string]interface{}{
 			"angledQuotes": true,
diff --git a/markup/markup_test.go b/markup/markup_test.go
@@ -16,17 +16,15 @@ package markup
 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)
diff --git a/markup/mmark/convert_test.go b/markup/mmark/convert_test.go
@@ -16,7 +16,7 @@ package mmark
 import (
 	"testing"
 
-	"github.com/spf13/viper"
+	"github.com/gohugoio/hugo/config"
 
 	"github.com/gohugoio/hugo/common/loggers"
 
@@ -62,7 +62,7 @@ func TestGetMmarkExtensions(t *testing.T) {
 
 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)
diff --git a/markup/org/convert_test.go b/markup/org/convert_test.go
@@ -16,8 +16,9 @@ package org
 import (
 	"testing"
 
+	"github.com/gohugoio/hugo/config"
+
 	"github.com/gohugoio/hugo/common/loggers"
-	"github.com/spf13/viper"
 
 	"github.com/gohugoio/hugo/markup/converter"
 
@@ -28,7 +29,7 @@ func TestConvert(t *testing.T) {
 	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{})
diff --git a/media/mediaType.go b/media/mediaType.go
@@ -385,8 +385,8 @@ func DecodeTypes(mms ...map[string]interface{}) (Types, error) {
 				return m, err
 			}
 
-			vm := v.(map[string]interface{})
-			maps.ToLower(vm)
+			vm := maps.ToStringMap(v)
+			maps.PrepareParams(vm)
 			_, delimiterSet := vm["delimiter"]
 			_, suffixSet := vm["suffix"]
 
diff --git a/minifiers/config.go b/minifiers/config.go
@@ -99,10 +99,10 @@ func decodeConfig(cfg config.Provider) (conf minifyConfig, err error) {
 
 	// 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 {
diff --git a/minifiers/config_test.go b/minifiers/config_test.go
@@ -16,14 +16,14 @@ package minifiers
 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,
@@ -53,7 +53,7 @@ func TestConfig(t *testing.T) {
 
 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)
diff --git a/minifiers/minifiers_test.go b/minifiers/minifiers_test.go
@@ -19,16 +19,15 @@ import (
 	"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
@@ -76,7 +75,7 @@ func TestNew(t *testing.T) {
 
 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{}{
@@ -110,7 +109,7 @@ func TestConfigureMinify(t *testing.T) {
 
 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{`{
@@ -148,7 +147,7 @@ func TestJSONRoundTrip(t *testing.T) {
 
 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 {
@@ -171,7 +170,7 @@ func TestBugs(t *testing.T) {
 // 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{}{
diff --git a/modules/collect.go b/modules/collect.go
@@ -424,7 +424,7 @@ func (c *collector) applyThemeConfig(tc *moduleAdapter) error {
 		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)
 		}
 	}
 
diff --git a/modules/npm/package_builder.go b/modules/npm/package_builder.go
@@ -29,7 +29,7 @@ import (
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/spf13/afero"
 
-	"github.com/spf13/cast"
+	"github.com/gohugoio/hugo/common/maps"
 
 	"github.com/gohugoio/hugo/helpers"
 )
@@ -122,7 +122,7 @@ func Pack(fs afero.Fs, fis []hugofs.FileMetaInfo) error {
 	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{})
 	}
@@ -205,7 +205,7 @@ func (b *packageBuilder) addm(source string, m 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
@@ -215,7 +215,7 @@ func (b *packageBuilder) addm(source string, m map[string]interface{}) {
 	}
 
 	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
diff --git a/output/outputFormat.go b/output/outputFormat.go
@@ -368,7 +368,11 @@ func decode(mediaTypes media.Types, input interface{}, output *Format) error {
 		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
 
 }
 
diff --git a/publisher/htmlElementsCollector_test.go b/publisher/htmlElementsCollector_test.go
@@ -22,12 +22,13 @@ import (
 	"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) {
@@ -138,7 +139,7 @@ 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))
 
diff --git a/related/inverted_index.go b/related/inverted_index.go
@@ -22,6 +22,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/common/maps"
+
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/mitchellh/mapstructure"
 )
@@ -404,16 +406,11 @@ func norm(num, min, max int) int {
 }
 
 // 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")
 	}
diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go
@@ -18,8 +18,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/gohugoio/hugo/config"
+
 	"github.com/gohugoio/hugo/resources/resource"
-	"github.com/spf13/viper"
 
 	qt "github.com/frankban/quicktest"
 )
@@ -72,7 +73,7 @@ func newTestFd() *FrontMatterDescriptor {
 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"},
@@ -89,7 +90,7 @@ func TestFrontMatterNewConfig(t *testing.T) {
 	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"})
@@ -117,7 +118,7 @@ func TestFrontMatterDatesHandlers(t *testing.T) {
 
 	for _, handlerID := range []string{":filename", ":fileModTime", ":git"} {
 
-		cfg := viper.New()
+		cfg := config.New()
 
 		cfg.Set("frontmatter", map[string]interface{}{
 			"date": []string{handlerID, "date"},
@@ -157,7 +158,7 @@ func TestFrontMatterDatesCustomConfig(t *testing.T) {
 
 	c := qt.New(t)
 
-	cfg := viper.New()
+	cfg := config.New()
 	cfg.Set("frontmatter", map[string]interface{}{
 		"date":        []string{"mydate"},
 		"lastmod":     []string{"publishdate"},
@@ -204,7 +205,7 @@ func TestFrontMatterDatesDefaultKeyword(t *testing.T) {
 
 	c := qt.New(t)
 
-	cfg := viper.New()
+	cfg := config.New()
 
 	cfg.Set("frontmatter", map[string]interface{}{
 		"date":        []string{"mydate", ":default"},
diff --git a/resources/page/pagination_test.go b/resources/page/pagination_test.go
@@ -18,7 +18,7 @@ import (
 	"html/template"
 	"testing"
 
-	"github.com/spf13/viper"
+	"github.com/gohugoio/hugo/config"
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/output"
@@ -196,7 +196,7 @@ func doTestPagerNoPages(t *testing.T, paginator *Paginator) {
 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} {
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
@@ -29,7 +29,7 @@ import (
 	"github.com/bep/gitmap"
 	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/resources/resource"
-	"github.com/spf13/viper"
+	
 
 	"github.com/gohugoio/hugo/navigation"
 
@@ -69,7 +69,7 @@ func newTestPageWithFile(filename string) *testPage {
 }
 
 func newTestPathSpec() *helpers.PathSpec {
-	return newTestPathSpecFor(viper.New())
+	return newTestPathSpecFor(config.New())
 }
 
 func newTestPathSpecFor(cfg config.Provider) *helpers.PathSpec {
diff --git a/resources/resource_metadata.go b/resources/resource_metadata.go
@@ -130,7 +130,7 @@ func AssignMetadata(metadata []map[string]interface{}, resources ...resource.Res
 				if found {
 					m := maps.ToStringMap(params)
 					// Needed for case insensitive fetching of params values
-					maps.ToLower(m)
+					maps.PrepareParams(m)
 					ma.updateParams(m)
 				}
 			}
diff --git a/resources/resource_transformers/htesting/testhelpers.go b/resources/resource_transformers/htesting/testhelpers.go
@@ -17,17 +17,17 @@ import (
 	"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")
 
diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go
@@ -20,9 +20,9 @@ import (
 	"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"
 
@@ -30,7 +30,6 @@ import (
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/media"
 	"github.com/mitchellh/mapstructure"
-	"github.com/spf13/cast"
 )
 
 const (
@@ -348,7 +347,7 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
 
 	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
diff --git a/resources/testhelpers_test.go b/resources/testhelpers_test.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/modules"
 
@@ -22,7 +23,6 @@ import (
 	"github.com/gohugoio/hugo/resources/page"
 	"github.com/gohugoio/hugo/resources/resource"
 	"github.com/spf13/afero"
-	"github.com/spf13/viper"
 )
 
 type specDescriptor struct {
@@ -31,8 +31,8 @@ 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")
diff --git a/source/filesystem_test.go b/source/filesystem_test.go
@@ -19,6 +19,8 @@ import (
 	"runtime"
 	"testing"
 
+	"github.com/gohugoio/hugo/config"
+
 	"github.com/gohugoio/hugo/modules"
 
 	"github.com/gohugoio/hugo/langs"
@@ -28,8 +30,6 @@ import (
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/hugofs"
-
-	"github.com/spf13/viper"
 )
 
 func TestEmptySourceFilesystem(t *testing.T) {
@@ -76,8 +76,8 @@ func TestUnicodeNorm(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")
diff --git a/tpl/cast/docshelper.go b/tpl/cast/docshelper.go
@@ -15,18 +15,18 @@ package cast
 
 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()),
@@ -46,8 +46,8 @@ func init() {
 	docshelper.AddDocProviderFunc(docsProvider)
 }
 
-func newTestConfig() *viper.Viper {
-	v := viper.New()
+func newTestConfig() config.Provider {
+	v := config.New()
 	v.Set("contentDir", "content")
 	return v
 }
diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go
@@ -32,7 +32,7 @@ import (
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/langs"
 	"github.com/spf13/afero"
-	"github.com/spf13/viper"
+	
 )
 
 type tstNoStringer struct{}
@@ -986,7 +986,7 @@ func newDeps(cfg config.Provider) *deps.Deps {
 }
 
 func newTestNs() *Namespace {
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 	return New(newDeps(v))
 }
diff --git a/tpl/data/init_test.go b/tpl/data/init_test.go
@@ -17,10 +17,10 @@ import (
 	"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) {
@@ -28,7 +28,7 @@ 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)
 
diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go
@@ -34,12 +34,12 @@ import (
 	"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
 
@@ -144,7 +144,7 @@ func TestScpGetRemoteParallel(t *testing.T) {
 	c.Assert(err, qt.IsNil)
 
 	for _, ignoreCache := range []bool{false} {
-		cfg := viper.New()
+		cfg := config.New()
 		cfg.Set("ignoreCache", ignoreCache)
 		cfg.Set("contentDir", "content")
 
@@ -223,7 +223,7 @@ func newDeps(cfg config.Provider) *deps.Deps {
 }
 
 func newTestNs() *Namespace {
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 	return New(newDeps(v))
 }
diff --git a/tpl/encoding/encoding.go b/tpl/encoding/encoding.go
@@ -20,6 +20,7 @@ import (
 	"errors"
 	"html/template"
 
+	"github.com/gohugoio/hugo/common/maps"
 	"github.com/spf13/cast"
 )
 
@@ -71,7 +72,7 @@ func (ns *Namespace) Jsonify(args ...interface{}) (template.HTML, error) {
 	case 2:
 		var opts map[string]string
 
-		opts, err = cast.ToStringMapStringE(args[0])
+		opts, err = maps.ToStringMapStringE(args[0])
 		if err != nil {
 			break
 		}
diff --git a/tpl/hugo/init_test.go b/tpl/hugo/init_test.go
@@ -16,20 +16,21 @@ package hugo
 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)
 
diff --git a/tpl/images/images_test.go b/tpl/images/images_test.go
@@ -22,11 +22,11 @@ import (
 	"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{}
@@ -82,7 +82,7 @@ func TestNSConfig(t *testing.T) {
 	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)})
diff --git a/tpl/os/os_test.go b/tpl/os/os_test.go
@@ -17,11 +17,12 @@ import (
 	"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) {
@@ -30,7 +31,7 @@ func TestReadFile(t *testing.T) {
 
 	workingDir := "/home/hugo"
 
-	v := viper.New()
+	v := config.New()
 	v.Set("workingDir", workingDir)
 
 	// f := newTestFuncsterWithViper(v)
@@ -68,7 +69,7 @@ func TestFileExists(t *testing.T) {
 
 	workingDir := "/home/hugo"
 
-	v := viper.New()
+	v := config.New()
 	v.Set("workingDir", workingDir)
 
 	ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
@@ -103,7 +104,7 @@ func TestStat(t *testing.T) {
 	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)})
diff --git a/tpl/path/path_test.go b/tpl/path/path_test.go
@@ -18,11 +18,11 @@ import (
 	"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{}
 
diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go
@@ -282,7 +282,7 @@ func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) {
 	}
 
 	if m != nil {
-		maps.ToLower(m)
+		maps.PrepareParams(m)
 		if t, found := m["transpiler"]; found {
 			switch t {
 			case transpilerDart, transpilerLibSass:
diff --git a/tpl/site/init_test.go b/tpl/site/init_test.go
@@ -16,12 +16,13 @@ package site
 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) {
@@ -29,7 +30,7 @@ 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)
 
diff --git a/tpl/strings/init_test.go b/tpl/strings/init_test.go
@@ -16,12 +16,13 @@ package strings
 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) {
@@ -30,7 +31,7 @@ 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
diff --git a/tpl/strings/strings_test.go b/tpl/strings/strings_test.go
@@ -17,14 +17,14 @@ import (
 	"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{}
 
diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go
@@ -37,13 +37,13 @@ import (
 	"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")
@@ -206,7 +206,7 @@ func BenchmarkPartialCached(b *testing.B) {
 
 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 {
diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go
@@ -16,16 +16,16 @@ package transform
 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)
@@ -112,7 +112,7 @@ title: Test Metadata
 func TestRemarshalComments(t *testing.T) {
 	t.Parallel()
 
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 	ns := New(newDeps(v))
 
@@ -158,7 +158,7 @@ func TestTestRemarshalError(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 	ns := New(newDeps(v))
 
@@ -172,7 +172,7 @@ func TestTestRemarshalError(t *testing.T) {
 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))
 
diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go
@@ -26,7 +26,7 @@ import (
 	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/langs"
-	"github.com/spf13/viper"
+	
 )
 
 type tstNoStringer struct{}
@@ -35,7 +35,7 @@ func TestEmojify(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	v := viper.New()
+	v := config.New()
 	ns := New(newDeps(v))
 
 	for _, test := range []struct {
@@ -64,7 +64,7 @@ func TestHighlight(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 	ns := New(newDeps(v))
 
@@ -96,7 +96,7 @@ func TestHTMLEscape(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 	ns := New(newDeps(v))
 
@@ -126,7 +126,7 @@ func TestHTMLUnescape(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 	ns := New(newDeps(v))
 
@@ -156,7 +156,7 @@ func TestMarkdownify(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	v := viper.New()
+	v := config.New()
 	v.Set("contentDir", "content")
 	ns := New(newDeps(v))
 
@@ -185,7 +185,7 @@ func TestMarkdownify(t *testing.T) {
 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))
 
@@ -211,7 +211,7 @@ func TestPlainify(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	v := viper.New()
+	v := config.New()
 	ns := New(newDeps(v))
 
 	for _, test := range []struct {
diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go
@@ -19,13 +19,14 @@ import (
 	"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 (
@@ -79,7 +80,7 @@ func (t testContentResource) Key() string {
 }
 
 func TestUnmarshal(t *testing.T) {
-	v := viper.New()
+	v := config.New()
 	ns := New(newDeps(v))
 	c := qt.New(t)
 
@@ -173,7 +174,7 @@ a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment"
 }
 
 func BenchmarkUnmarshalString(b *testing.B) {
-	v := viper.New()
+	v := config.New()
 	ns := New(newDeps(v))
 
 	const numJsons = 100
@@ -196,7 +197,7 @@ func BenchmarkUnmarshalString(b *testing.B) {
 }
 
 func BenchmarkUnmarshalResource(b *testing.B) {
-	v := viper.New()
+	v := config.New()
 	ns := New(newDeps(v))
 
 	const numJsons = 100
diff --git a/tpl/urls/init_test.go b/tpl/urls/init_test.go
@@ -16,11 +16,12 @@ package urls
 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) {
@@ -29,7 +30,7 @@ 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
diff --git a/tpl/urls/urls_test.go b/tpl/urls/urls_test.go
@@ -17,14 +17,15 @@ import (
 	"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{}