URI: 
       Rework the Destination filesystem to make --renderStaticToDisk work - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit d070bdf10f14d233288f7318a4e9f7555f070a65
   DIR parent b08193971a821fc27e549a73120c15e5e5186775
  HTML Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Mon, 21 Mar 2022 09:35:15 +0100
       
       Rework the Destination filesystem to make --renderStaticToDisk work
       
       See #9626
       
       Diffstat:
         M cache/filecache/filecache_config_t… |       2 +-
         M cache/filecache/filecache_test.go   |       1 +
         M commands/commandeer.go              |      78 +++++++++++++++++++++++--------
         M commands/commands.go                |       9 ++++-----
         M commands/commands_test.go           |     124 ++++++++++++++-----------------
         M commands/hugo.go                    |      12 ++++--------
         M commands/hugo_test.go               |       6 ++----
         M commands/list_test.go               |       5 +----
         M commands/new_site.go                |       6 ++++--
         M commands/server.go                  |      18 ++++++++++++------
         M commands/server_test.go             |      12 ++++++++----
         M commands/static_syncer.go           |      21 ++++++---------------
         M common/paths/path.go                |       9 +++++++++
         M config/configProvider.go            |      28 +++++++++++++++++++---------
         M config/defaultConfigProvider.go     |       5 +++++
         M config/services/servicesConfig_tes… |       2 +-
         M go.mod                              |       3 ++-
         M go.sum                              |       4 ++++
         M helpers/content_test.go             |       4 ++--
         M helpers/general_test.go             |       5 ++---
         M helpers/path.go                     |      12 ++++++++++--
         M helpers/path_test.go                |      94 -------------------------------
         M helpers/testhelpers_test.go         |      15 +++------------
         M hugofs/createcounting_fs.go         |       8 ++++++++
         M hugofs/decorators.go                |       8 ++++++++
         M hugofs/filename_filter_fs.go        |       8 ++++++++
         M hugofs/filter_fs.go                 |       8 ++++++++
         M hugofs/fs.go                        |     142 ++++++++++++++++++++++++++-----
         M hugofs/fs_test.go                   |      48 ++++++++++++++++++-------------
         M hugofs/hashing_fs.go                |       9 ++++++++-
         M hugofs/language_composite_fs.go     |      13 ++++++++++---
         M hugofs/nosymlink_fs.go              |       8 ++++++++
         M hugofs/rootmapping_fs.go            |       8 ++++++++
         M hugofs/rootmapping_fs_test.go       |       5 ++---
         M hugofs/slice_fs.go                  |      15 ++++++++++++---
         M hugofs/stacktracer_fs.go            |      11 +++++++++--
         M hugolib/config.go                   |       3 +--
         M hugolib/filesystems/basefs.go       |      24 ++++++++----------------
         M hugolib/filesystems/basefs_test.go  |      17 +++++++----------
         M hugolib/hugo_modules_test.go        |      69 +++++++++++++++----------------
         M hugolib/hugo_sites.go               |       2 +-
         M hugolib/hugo_sites_build.go         |       6 +++---
         M hugolib/hugo_sites_build_test.go    |      22 +++++++++++-----------
         M hugolib/image_test.go               |       4 ++--
         M hugolib/integrationtest_builder.go  |      22 ++++++++++------------
         M hugolib/language_content_dir_test.… |      22 +++++++++++-----------
         M hugolib/minify_publisher_test.go    |       2 +-
         M hugolib/mount_filters_test.go       |       4 ++--
         M hugolib/page_test.go                |      16 ++++++----------
         M hugolib/pagebundler_test.go         |      75 ++++++++++++++++---------------
         M hugolib/paths/paths.go              |      18 ++++++------------
         M hugolib/paths/paths_test.go         |       2 +-
         M hugolib/resource_chain_test.go      |       2 +-
         M hugolib/robotstxt_test.go           |       2 +-
         M hugolib/rss_test.go                 |       2 +-
         M hugolib/shortcode_test.go           |       4 ++--
         M hugolib/site_output_test.go         |      10 +++++-----
         M hugolib/site_test.go                |       4 ++--
         M hugolib/site_url_test.go            |       4 ++--
         M hugolib/sitemap_test.go             |       2 +-
         M hugolib/testhelpers_test.go         |      31 +++++++++++++++----------------
         M langs/i18n/i18n_test.go             |      11 +----------
         M langs/language_test.go              |       7 +++----
         M markup/goldmark/codeblocks/integra… |       2 +-
         M minifiers/config_test.go            |       7 +++----
         M minifiers/minifiers_test.go         |      12 ++++++------
         M publisher/htmlElementsCollector_te… |       3 +--
         M resources/resource_transformers/ht… |       4 +---
         M resources/testhelpers_test.go       |       3 +--
         M resources/transform_test.go         |       4 ++--
         M source/filesystem_test.go           |      10 +---------
         M tpl/collections/collections_test.go |       4 +---
         M tpl/data/resources_test.go          |       9 +++------
         M tpl/images/images.go                |       2 +-
         M tpl/images/images_test.go           |       2 +-
       
       75 files changed, 650 insertions(+), 565 deletions(-)
       ---
   DIR diff --git a/cache/filecache/filecache_config_test.go b/cache/filecache/filecache_config_test.go
       @@ -184,7 +184,7 @@ dir = "/"
        }
        
        func newTestConfig() config.Provider {
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
                cfg.Set("contentDir", "content")
                cfg.Set("dataDir", "data")
   DIR diff --git a/cache/filecache/filecache_test.go b/cache/filecache/filecache_test.go
       @@ -342,6 +342,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
                cfg, err := config.FromConfigString(configStr, "toml")
                c.Assert(err, qt.IsNil)
                initConfig(fs, cfg)
       +        config.SetBaseTestDefaults(cfg)
                p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil)
                c.Assert(err, qt.IsNil)
                return p
   DIR diff --git a/commands/commandeer.go b/commands/commandeer.go
       @@ -30,6 +30,7 @@ import (
        
                "github.com/gohugoio/hugo/common/herrors"
                "github.com/gohugoio/hugo/common/hugo"
       +        "github.com/gohugoio/hugo/common/paths"
        
                jww "github.com/spf13/jwalterweatherman"
        
       @@ -42,6 +43,7 @@ import (
                "github.com/spf13/afero"
        
                "github.com/bep/debounce"
       +        "github.com/bep/overlayfs"
                "github.com/gohugoio/hugo/common/types"
                "github.com/gohugoio/hugo/deps"
                "github.com/gohugoio/hugo/helpers"
       @@ -73,8 +75,10 @@ type commandeer struct {
                // be fast enough that we could maybe just add it for all server modes.
                changeDetector *fileChangeDetector
        
       -        // We need to reuse this on server rebuilds.
       -        destinationFs afero.Fs
       +        // We need to reuse these on server rebuilds.
       +        // These 2 will be different if --renderStaticToDisk is set.
       +        publishDirFs       afero.Fs
       +        publishDirServerFs afero.Fs
        
                h    *hugoBuilderCommon
                ftch flagsToConfigHandler
       @@ -162,7 +166,8 @@ func (c *commandeer) Set(key string, value any) {
        }
        
        func (c *commandeer) initFs(fs *hugofs.Fs) error {
       -        c.destinationFs = fs.Destination
       +        c.publishDirFs = fs.PublishDir
       +        c.publishDirServerFs = fs.PublishDirServer
                c.DepsCfg.Fs = fs
        
                return nil
       @@ -378,28 +383,63 @@ func (c *commandeer) loadConfig() error {
                createMemFs := config.GetBool("renderToMemory")
                c.renderStaticToDisk = config.GetBool("renderStaticToDisk")
        
       -        if createMemFs && !c.renderStaticToDisk {
       +        if createMemFs {
                        // Rendering to memoryFS, publish to Root regardless of publishDir.
                        config.Set("publishDir", "/")
       +                config.Set("publishDirStatic", "/")
       +        } else if c.renderStaticToDisk {
       +                // Hybrid, render dynamic content to Root.
       +                config.Set("publishDirStatic", config.Get("publishDir"))
       +                config.Set("publishDir", "/")
       +
                }
        
                c.fsCreate.Do(func() {
                        fs := hugofs.NewFrom(sourceFs, config)
        
       -                if c.destinationFs != nil {
       +                if c.publishDirFs != nil {
                                // Need to reuse the destination on server rebuilds.
       -                        fs.Destination = c.destinationFs
       -                } else if createMemFs && c.renderStaticToDisk {
       -                        // Writes the dynamic output on memory,
       -                        // while serve others directly from publishDir
       +                        fs.PublishDir = c.publishDirFs
       +                        fs.PublishDirServer = c.publishDirServerFs
       +                } else {
                                publishDir := config.GetString("publishDir")
       -                        writableFs := afero.NewBasePathFs(afero.NewMemMapFs(), publishDir)
       -                        publicFs := afero.NewOsFs()
       -                        fs.Destination = afero.NewCopyOnWriteFs(afero.NewReadOnlyFs(publicFs), writableFs)
       -                        fs.DestinationStatic = publicFs
       -                } else if createMemFs {
       -                        // Hugo writes the output to memory instead of the disk.
       -                        fs.Destination = new(afero.MemMapFs)
       +                        publishDirStatic := config.GetString("publishDirStatic")
       +                        workingDir := config.GetString("workingDir")
       +                        absPublishDir := paths.AbsPathify(workingDir, publishDir)
       +                        absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
       +
       +                        if c.renderStaticToDisk {
       +                                // Writes the dynamic output oton memory,
       +                                // while serve others directly from /public on disk.
       +                                dynamicFs := afero.NewMemMapFs()
       +                                staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
       +
       +                                // Serve from both the static and dynamic fs,
       +                                // the first will take priority.
       +                                // THis is a read-only filesystem,
       +                                // we do all the writes to
       +                                // fs.Destination and fs.DestinationStatic.
       +                                fs.PublishDirServer = overlayfs.New(
       +                                        overlayfs.Options{
       +                                                Fss: []afero.Fs{
       +                                                        dynamicFs,
       +                                                        staticFs,
       +                                                },
       +                                        },
       +                                )
       +                                fs.PublishDir = dynamicFs
       +                                fs.PublishDirStatic = staticFs
       +                        } else if createMemFs {
       +                                // Hugo writes the output to memory instead of the disk.
       +                                fs.PublishDir = new(afero.MemMapFs)
       +                                fs.PublishDirServer = fs.PublishDir
       +                                fs.PublishDirStatic = fs.PublishDir
       +                        } else {
       +                                // Write everything to disk.
       +                                fs.PublishDir = afero.NewBasePathFs(afero.NewOsFs(), absPublishDir)
       +                                fs.PublishDirServer = fs.PublishDir
       +                                fs.PublishDirStatic = fs.PublishDir
       +                        }
                        }
        
                        if c.fastRenderMode {
       @@ -413,15 +453,15 @@ func (c *commandeer) loadConfig() error {
                                }
        
                                changeDetector.PrepareNew()
       -                        fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
       -                        fs.DestinationStatic = hugofs.NewHashingFs(fs.DestinationStatic, changeDetector)
       +                        fs.PublishDir = hugofs.NewHashingFs(fs.PublishDir, changeDetector)
       +                        fs.PublishDirStatic = hugofs.NewHashingFs(fs.PublishDirStatic, changeDetector)
                                c.changeDetector = changeDetector
                        }
        
                        if c.Cfg.GetBool("logPathWarnings") {
                                // Note that we only care about the "dynamic creates" here,
                                // so skip the static fs.
       -                        fs.Destination = hugofs.NewCreateCountingFs(fs.Destination)
       +                        fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
                        }
        
                        // To debug hard-to-find path issues.
   DIR diff --git a/commands/commands.go b/commands/commands.go
       @@ -18,10 +18,9 @@ import (
                "os"
                "time"
        
       -        "github.com/gohugoio/hugo/hugolib/paths"
       -
                "github.com/gohugoio/hugo/common/hugo"
                "github.com/gohugoio/hugo/common/loggers"
       +        hpaths "github.com/gohugoio/hugo/common/paths"
                "github.com/gohugoio/hugo/config"
                "github.com/gohugoio/hugo/helpers"
                "github.com/spf13/cobra"
       @@ -243,14 +242,14 @@ func (cc *hugoBuilderCommon) timeTrack(start time.Time, name string) {
        
        func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
                if cc.cfgDir != "" {
       -                return paths.AbsPathify(baseDir, cc.cfgDir)
       +                return hpaths.AbsPathify(baseDir, cc.cfgDir)
                }
        
                if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
       -                return paths.AbsPathify(baseDir, v)
       +                return hpaths.AbsPathify(baseDir, v)
                }
        
       -        return paths.AbsPathify(baseDir, "config")
       +        return hpaths.AbsPathify(baseDir, "config")
        }
        
        func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string {
   DIR diff --git a/commands/commands_test.go b/commands/commands_test.go
       @@ -22,8 +22,6 @@ import (
        
                "github.com/gohugoio/hugo/config"
        
       -        "github.com/gohugoio/hugo/htesting"
       -
                "github.com/spf13/afero"
        
                "github.com/gohugoio/hugo/hugofs"
       @@ -38,15 +36,13 @@ import (
        func TestExecute(t *testing.T) {
                c := qt.New(t)
        
       -        createSite := func(c *qt.C) (string, func()) {
       -                dir, clean, err := createSimpleTestSite(t, testSiteConfig{})
       -                c.Assert(err, qt.IsNil)
       -                return dir, clean
       +        createSite := func(c *qt.C) string {
       +                dir := createSimpleTestSite(t, testSiteConfig{})
       +                return dir
                }
        
                c.Run("hugo", func(c *qt.C) {
       -                dir, clean := createSite(c)
       -                defer clean()
       +                dir := createSite(c)
                        resp := Execute([]string{"-s=" + dir})
                        c.Assert(resp.Err, qt.IsNil)
                        result := resp.Result
       @@ -56,8 +52,7 @@ func TestExecute(t *testing.T) {
                })
        
                c.Run("hugo, set environment", func(c *qt.C) {
       -                dir, clean := createSite(c)
       -                defer clean()
       +                dir := createSite(c)
                        resp := Execute([]string{"-s=" + dir, "-e=staging"})
                        c.Assert(resp.Err, qt.IsNil)
                        result := resp.Result
       @@ -65,9 +60,8 @@ func TestExecute(t *testing.T) {
                })
        
                c.Run("convert toJSON", func(c *qt.C) {
       -                dir, clean := createSite(c)
       +                dir := createSite(c)
                        output := filepath.Join(dir, "myjson")
       -                defer clean()
                        resp := Execute([]string{"convert", "toJSON", "-s=" + dir, "-e=staging", "-o=" + output})
                        c.Assert(resp.Err, qt.IsNil)
                        converted := readFileFrom(c, filepath.Join(output, "content", "p1.md"))
       @@ -75,8 +69,7 @@ func TestExecute(t *testing.T) {
                })
        
                c.Run("config, set environment", func(c *qt.C) {
       -                dir, clean := createSite(c)
       -                defer clean()
       +                dir := createSite(c)
                        out, err := captureStdout(func() error {
                                resp := Execute([]string{"config", "-s=" + dir, "-e=staging"})
                                return resp.Err
       @@ -86,16 +79,14 @@ func TestExecute(t *testing.T) {
                })
        
                c.Run("deploy, environment set", func(c *qt.C) {
       -                dir, clean := createSite(c)
       -                defer clean()
       +                dir := createSite(c)
                        resp := Execute([]string{"deploy", "-s=" + dir, "-e=staging", "--target=mydeployment", "--dryRun"})
                        c.Assert(resp.Err, qt.Not(qt.IsNil))
                        c.Assert(resp.Err.Error(), qt.Contains, `no driver registered for "hugocloud"`)
                })
        
                c.Run("list", func(c *qt.C) {
       -                dir, clean := createSite(c)
       -                defer clean()
       +                dir := createSite(c)
                        out, err := captureStdout(func() error {
                                resp := Execute([]string{"list", "all", "-s=" + dir, "-e=staging"})
                                return resp.Err
       @@ -105,8 +96,7 @@ func TestExecute(t *testing.T) {
                })
        
                c.Run("new theme", func(c *qt.C) {
       -                dir, clean := createSite(c)
       -                defer clean()
       +                dir := createSite(c)
                        themesDir := filepath.Join(dir, "mythemes")
                        resp := Execute([]string{"new", "theme", "mytheme", "-s=" + dir, "-e=staging", "--themesDir=" + themesDir})
                        c.Assert(resp.Err, qt.IsNil)
       @@ -115,8 +105,7 @@ func TestExecute(t *testing.T) {
                })
        
                c.Run("new site", func(c *qt.C) {
       -                dir, clean := createSite(c)
       -                defer clean()
       +                dir := createSite(c)
                        siteDir := filepath.Join(dir, "mysite")
                        resp := Execute([]string{"new", "site", siteDir, "-e=staging"})
                        c.Assert(resp.Err, qt.IsNil)
       @@ -167,7 +156,7 @@ func TestFlags(t *testing.T) {
                                name: "ignoreVendorPaths",
                                args: []string{"server", "--ignoreVendorPaths=github.com/**"},
                                check: func(c *qt.C, cmd *serverCmd) {
       -                                cfg := config.New()
       +                                cfg := config.NewWithTestDefaults()
                                        cmd.flagsToConfig(cfg)
                                        c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**")
                                },
       @@ -208,7 +197,7 @@ func TestFlags(t *testing.T) {
                                        c.Assert(sc.serverPort, qt.Equals, 1366)
                                        c.Assert(sc.environment, qt.Equals, "testing")
        
       -                                cfg := config.New()
       +                                cfg := config.NewWithTestDefaults()
                                        sc.flagsToConfig(cfg)
                                        c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
                                        c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
       @@ -253,14 +242,8 @@ func TestFlags(t *testing.T) {
        func TestCommandsExecute(t *testing.T) {
                c := qt.New(t)
        
       -        dir, clean, err := createSimpleTestSite(t, testSiteConfig{})
       -        c.Assert(err, qt.IsNil)
       -
       -        dirOut, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-cli-out")
       -        c.Assert(err, qt.IsNil)
       -
       -        defer clean()
       -        defer clean2()
       +        dir := createSimpleTestSite(t, testSiteConfig{})
       +        dirOut := t.TempDir()
        
                sourceFlag := fmt.Sprintf("-s=%s", dir)
        
       @@ -297,29 +280,35 @@ func TestCommandsExecute(t *testing.T) {
                }
        
                for _, test := range tests {
       -                b := newCommandsBuilder().addAll().build()
       -                hugoCmd := b.getCommand()
       -                test.flags = append(test.flags, "--quiet")
       -                hugoCmd.SetArgs(append(test.commands, test.flags...))
       -
       -                // TODO(bep) capture output and add some simple asserts
       -                // TODO(bep) misspelled subcommands does not return an error. We should investigate this
       -                // but before that, check for "Error: unknown command".
       -
       -                _, err := hugoCmd.ExecuteC()
       -                if test.expectErrToContain != "" {
       -                        c.Assert(err, qt.Not(qt.IsNil))
       -                        c.Assert(err.Error(), qt.Contains, test.expectErrToContain)
       -                } else {
       -                        c.Assert(err, qt.IsNil)
       +                name := "hugo"
       +                if len(test.commands) > 0 {
       +                        name = test.commands[0]
                        }
       +                c.Run(name, func(c *qt.C) {
       +                        b := newCommandsBuilder().addAll().build()
       +                        hugoCmd := b.getCommand()
       +                        test.flags = append(test.flags, "--quiet")
       +                        hugoCmd.SetArgs(append(test.commands, test.flags...))
       +
       +                        // TODO(bep) capture output and add some simple asserts
       +                        // TODO(bep) misspelled subcommands does not return an error. We should investigate this
       +                        // but before that, check for "Error: unknown command".
       +
       +                        _, err := hugoCmd.ExecuteC()
       +                        if test.expectErrToContain != "" {
       +                                c.Assert(err, qt.Not(qt.IsNil))
       +                                c.Assert(err.Error(), qt.Contains, test.expectErrToContain)
       +                        } else {
       +                                c.Assert(err, qt.IsNil)
       +                        }
        
       -                // Assert that we have not left any development debug artifacts in
       -                // the code.
       -                if b.c != nil {
       -                        _, ok := b.c.destinationFs.(types.DevMarker)
       -                        c.Assert(ok, qt.Equals, false)
       -                }
       +                        // Assert that we have not left any development debug artifacts in
       +                        // the code.
       +                        if b.c != nil {
       +                                _, ok := b.c.publishDirFs.(types.DevMarker)
       +                                c.Assert(ok, qt.Equals, false)
       +                        }
       +                })
        
                }
        }
       @@ -329,11 +318,8 @@ type testSiteConfig struct {
                contentDir string
        }
        
       -func createSimpleTestSite(t testing.TB, cfg testSiteConfig) (string, func(), error) {
       -        d, clean, e := htesting.CreateTempDir(hugofs.Os, "hugo-cli")
       -        if e != nil {
       -                return "", nil, e
       -        }
       +func createSimpleTestSite(t testing.TB, cfg testSiteConfig) string {
       +        dir := t.TempDir()
        
                cfgStr := `
        
       @@ -352,23 +338,23 @@ title = "Hugo Commands"
                        contentDir = cfg.contentDir
                }
        
       -        os.MkdirAll(filepath.Join(d, "public"), 0777)
       +        os.MkdirAll(filepath.Join(dir, "public"), 0777)
        
                // Just the basic. These are for CLI tests, not site testing.
       -        writeFile(t, filepath.Join(d, "config.toml"), cfgStr)
       -        writeFile(t, filepath.Join(d, "config", "staging", "params.toml"), `myparam="paramstaging"`)
       -        writeFile(t, filepath.Join(d, "config", "staging", "deployment.toml"), `
       +        writeFile(t, filepath.Join(dir, "config.toml"), cfgStr)
       +        writeFile(t, filepath.Join(dir, "config", "staging", "params.toml"), `myparam="paramstaging"`)
       +        writeFile(t, filepath.Join(dir, "config", "staging", "deployment.toml"), `
        [[targets]]
        name = "mydeployment"
        URL = "hugocloud://hugotestbucket"
        `)
        
       -        writeFile(t, filepath.Join(d, "config", "testing", "params.toml"), `myparam="paramtesting"`)
       -        writeFile(t, filepath.Join(d, "config", "production", "params.toml"), `myparam="paramproduction"`)
       +        writeFile(t, filepath.Join(dir, "config", "testing", "params.toml"), `myparam="paramtesting"`)
       +        writeFile(t, filepath.Join(dir, "config", "production", "params.toml"), `myparam="paramproduction"`)
        
       -        writeFile(t, filepath.Join(d, "static", "myfile.txt"), `Hello World!`)
       +        writeFile(t, filepath.Join(dir, "static", "myfile.txt"), `Hello World!`)
        
       -        writeFile(t, filepath.Join(d, contentDir, "p1.md"), `
       +        writeFile(t, filepath.Join(dir, contentDir, "p1.md"), `
        ---
        title: "P1"
        weight: 1
       @@ -378,20 +364,20 @@ Content
        
        `)
        
       -        writeFile(t, filepath.Join(d, "layouts", "_default", "single.html"), `
       +        writeFile(t, filepath.Join(dir, "layouts", "_default", "single.html"), `
        
        Single: {{ .Title }}
        
        `)
        
       -        writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
       +        writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
        
        List: {{ .Title }}
        Environment: {{ hugo.Environment }}
        
        `)
        
       -        return d, clean, nil
       +        return dir
        }
        
        func writeFile(t testing.TB, filename, content string) {
   DIR diff --git a/commands/hugo.go b/commands/hugo.go
       @@ -508,7 +508,7 @@ func (c *commandeer) build() error {
                        c.hugo().PrintProcessingStats(os.Stdout)
                        fmt.Println()
        
       -                if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
       +                if createCounter, ok := c.publishDirFs.(hugofs.DuplicatesReporter); ok {
                                dupes := createCounter.ReportDuplicates()
                                if dupes != "" {
                                        c.logger.Warnln("Duplicate target paths:", dupes)
       @@ -634,11 +634,7 @@ func chmodFilter(dst, src os.FileInfo) bool {
        }
        
        func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
       -        publishDir := c.hugo().PathSpec.PublishDir
       -        // If root, remove the second '/'
       -        if publishDir == "//" {
       -                publishDir = helpers.FilePathSeparator
       -        }
       +        publishDir := helpers.FilePathSeparator
        
                if sourceFs.PublishFolder != "" {
                        publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
       @@ -651,9 +647,9 @@ func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint6
                syncer.NoChmod = c.Cfg.GetBool("noChmod")
                syncer.ChmodFilter = chmodFilter
                syncer.SrcFs = fs
       -        syncer.DestFs = c.Fs.Destination
       +        syncer.DestFs = c.Fs.PublishDir
                if c.renderStaticToDisk {
       -                syncer.DestFs = c.Fs.DestinationStatic
       +                syncer.DestFs = c.Fs.PublishDirStatic
                }
                // Now that we are using a unionFs for the static directories
                // We can effectively clean the publishDir on initial sync
   DIR diff --git a/commands/hugo_test.go b/commands/hugo_test.go
       @@ -36,12 +36,10 @@ title = "Hugo Commands"
        contentDir = "thisdoesnotexist"
        
        `
       -        dir, clean, err := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir})
       -        c.Assert(err, qt.IsNil)
       -        defer clean()
       +        dir := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir})
        
                cmd.SetArgs([]string{"-s=" + dir, "-c=" + contentDir})
        
       -        _, err = cmd.ExecuteC()
       +        _, err := cmd.ExecuteC()
                c.Assert(err, qt.IsNil)
        }
   DIR diff --git a/commands/list_test.go b/commands/list_test.go
       @@ -29,10 +29,7 @@ func captureStdout(f func() error) (string, error) {
        
        func TestListAll(t *testing.T) {
                c := qt.New(t)
       -        dir, clean, err := createSimpleTestSite(t, testSiteConfig{})
       -        defer clean()
       -
       -        c.Assert(err, qt.IsNil)
       +        dir := createSimpleTestSite(t, testSiteConfig{})
        
                hugoCmd := newCommandsBuilder().addAll().build()
                cmd := hugoCmd.getCommand()
   DIR diff --git a/commands/new_site.go b/commands/new_site.go
       @@ -122,8 +122,10 @@ func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error {
                }
        
                forceNew, _ := cmd.Flags().GetBool("force")
       -
       -        return n.doNewSite(hugofs.NewDefault(config.New()), createpath, forceNew)
       +        cfg := config.New()
       +        cfg.Set("workingDir", createpath)
       +        cfg.Set("publishDir", "public")
       +        return n.doNewSite(hugofs.NewDefault(cfg), createpath, forceNew)
        }
        
        func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
   DIR diff --git a/commands/server.go b/commands/server.go
       @@ -23,6 +23,7 @@ import (
                "net/url"
                "os"
                "os/signal"
       +        "path"
                "path/filepath"
                "regexp"
                "runtime"
       @@ -148,7 +149,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
                var serverCfgInit sync.Once
        
                cfgInit := func(c *commandeer) (rerr error) {
       -                c.Set("renderToMemory", !sc.renderToDisk)
       +                c.Set("renderToMemory", !(sc.renderToDisk || sc.renderStaticToDisk))
                        c.Set("renderStaticToDisk", sc.renderStaticToDisk)
                        if cmd.Flags().Changed("navigateToChanged") {
                                c.Set("navigateToChanged", sc.navigateToChanged)
       @@ -330,13 +331,18 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
                port := f.c.serverPorts[i].p
                listener := f.c.serverPorts[i].ln
        
       +        // For logging only.
       +        // TODO(bep) consolidate.
                publishDir := f.c.Cfg.GetString("publishDir")
       +        publishDirStatic := f.c.Cfg.GetString("publishDirStatic")
       +        workingDir := f.c.Cfg.GetString("workingDir")
        
                if root != "" {
                        publishDir = filepath.Join(publishDir, root)
       +                publishDirStatic = filepath.Join(publishDirStatic, root)
                }
       -
       -        absPublishDir := f.c.hugo().PathSpec.AbsPathify(publishDir)
       +        absPublishDir := paths.AbsPathify(workingDir, publishDir)
       +        absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
        
                jww.FEEDBACK.Printf("Environment: %q", f.c.hugo().Deps.Site.Hugo().Environment)
        
       @@ -344,14 +350,14 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
                        if f.s.renderToDisk {
                                jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
                        } else if f.s.renderStaticToDisk {
       -                        jww.FEEDBACK.Println("Serving pages from memory and static files from " + absPublishDir)
       +                        jww.FEEDBACK.Println("Serving pages from memory and static files from " + absPublishDirStatic)
                        } else {
                                jww.FEEDBACK.Println("Serving pages from memory")
                        }
                }
        
       -        httpFs := afero.NewHttpFs(f.c.destinationFs)
       -        fs := filesOnlyFs{httpFs.Dir(absPublishDir)}
       +        httpFs := afero.NewHttpFs(f.c.publishDirServerFs)
       +        fs := filesOnlyFs{httpFs.Dir(path.Join("/", root))}
        
                if i == 0 && f.c.fastRenderMode {
                        jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
   DIR diff --git a/commands/server_test.go b/commands/server_test.go
       @@ -77,6 +77,9 @@ func TestServerFlags(t *testing.T) {
                        {"--renderToDisk", func(c *qt.C, r serverTestResult) {
                                assertPublic(c, r, true)
                        }},
       +                {"--renderStaticToDisk", func(c *qt.C, r serverTestResult) {
       +                        assertPublic(c, r, true)
       +                }},
                } {
                        c.Run(test.flag, func(c *qt.C) {
                                config := `
       @@ -105,9 +108,7 @@ type serverTestResult struct {
        }
        
        func runServerTest(c *qt.C, getHome bool, config string, args ...string) (result serverTestResult) {
       -        dir, clean, err := createSimpleTestSite(c, testSiteConfig{configTOML: config})
       -        defer clean()
       -        c.Assert(err, qt.IsNil)
       +        dir := createSimpleTestSite(c, testSiteConfig{configTOML: config})
        
                sp, err := helpers.FindAvailablePort()
                c.Assert(err, qt.IsNil)
       @@ -141,12 +142,15 @@ func runServerTest(c *qt.C, getHome bool, config string, args ...string) (result
                        time.Sleep(567 * time.Millisecond)
                        resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port))
                        c.Check(err, qt.IsNil)
       +                c.Check(resp.StatusCode, qt.Equals, http.StatusOK)
                        if err == nil {
                                defer resp.Body.Close()
                                result.homeContent = helpers.ReaderToString(resp.Body)
                        }
                }
        
       +        time.Sleep(1 * time.Second)
       +
                select {
                case <-stop:
                case stop <- true:
       @@ -191,7 +195,7 @@ func TestFixURL(t *testing.T) {
                        t.Run(test.TestName, func(t *testing.T) {
                                b := newCommandsBuilder()
                                s := b.newServerCmd()
       -                        v := config.New()
       +                        v := config.NewWithTestDefaults()
                                baseURL := test.CLIBaseURL
                                v.Set("baseURL", test.CfgBaseURL)
                                s.serverAppend = test.AppendPort
   DIR diff --git a/commands/static_syncer.go b/commands/static_syncer.go
       @@ -40,11 +40,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
                c := s.c
        
                syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
       -                publishDir := c.hugo().PathSpec.PublishDir
       -                // If root, remove the second '/'
       -                if publishDir == "//" {
       -                        publishDir = helpers.FilePathSeparator
       -                }
       +                publishDir := helpers.FilePathSeparator
        
                        if sourceFs.PublishFolder != "" {
                                publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
       @@ -55,9 +51,9 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
                        syncer.NoChmod = c.Cfg.GetBool("noChmod")
                        syncer.ChmodFilter = chmodFilter
                        syncer.SrcFs = sourceFs.Fs
       -                syncer.DestFs = c.Fs.Destination
       +                syncer.DestFs = c.Fs.PublishDir
                        if c.renderStaticToDisk {
       -                        syncer.DestFs = c.Fs.DestinationStatic
       +                        syncer.DestFs = c.Fs.PublishDirStatic
                        }
        
                        // prevent spamming the log on changes
       @@ -101,19 +97,14 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
                                if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
                                        if _, err := sourceFs.Fs.Stat(relPath); os.IsNotExist(err) {
                                                // If file doesn't exist in any static dir, remove it
       -                                        toRemove := filepath.Join(publishDir, relPath)
       +                                        logger.Println("File no longer exists in static dir, removing", relPath)
       +                                        _ = c.Fs.PublishDirStatic.RemoveAll(relPath)
        
       -                                        logger.Println("File no longer exists in static dir, removing", toRemove)
       -                                        if c.renderStaticToDisk {
       -                                                _ = c.Fs.DestinationStatic.RemoveAll(toRemove)
       -                                        } else {
       -                                                _ = c.Fs.Destination.RemoveAll(toRemove)
       -                                        }
                                        } else if err == nil {
                                                // If file still exists, sync it
                                                logger.Println("Syncing", relPath, "to", publishDir)
        
       -                                        if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
       +                                        if err := syncer.Sync(relPath, relPath); err != nil {
                                                        c.logger.Errorln(err)
                                                }
                                        } else {
   DIR diff --git a/common/paths/path.go b/common/paths/path.go
       @@ -63,6 +63,15 @@ func (filepathBridge) Separator() string {
        
        var fpb filepathBridge
        
       +// AbsPathify creates an absolute path if given a working dir and a relative path.
       +// If already absolute, the path is just cleaned.
       +func AbsPathify(workingDir, inPath string) string {
       +        if filepath.IsAbs(inPath) {
       +                return filepath.Clean(inPath)
       +        }
       +        return filepath.Join(workingDir, inPath)
       +}
       +
        // MakeTitle converts the path given to a suitable title, trimming whitespace
        // and replacing hyphens with whitespace.
        func MakeTitle(inpath string) string {
   DIR diff --git a/config/configProvider.go b/config/configProvider.go
       @@ -45,13 +45,23 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string {
        }
        
        // SetBaseTestDefaults provides some common config defaults used in tests.
       -func SetBaseTestDefaults(cfg Provider) {
       -        cfg.Set("resourceDir", "resources")
       -        cfg.Set("contentDir", "content")
       -        cfg.Set("dataDir", "data")
       -        cfg.Set("i18nDir", "i18n")
       -        cfg.Set("layoutDir", "layouts")
       -        cfg.Set("assetDir", "assets")
       -        cfg.Set("archetypeDir", "archetypes")
       -        cfg.Set("publishDir", "public")
       +func SetBaseTestDefaults(cfg Provider) Provider {
       +        setIfNotSet(cfg, "baseURL", "https://example.org")
       +        setIfNotSet(cfg, "resourceDir", "resources")
       +        setIfNotSet(cfg, "contentDir", "content")
       +        setIfNotSet(cfg, "dataDir", "data")
       +        setIfNotSet(cfg, "i18nDir", "i18n")
       +        setIfNotSet(cfg, "layoutDir", "layouts")
       +        setIfNotSet(cfg, "assetDir", "assets")
       +        setIfNotSet(cfg, "archetypeDir", "archetypes")
       +        setIfNotSet(cfg, "publishDir", "public")
       +        setIfNotSet(cfg, "workingDir", "")
       +        setIfNotSet(cfg, "defaultContentLanguage", "en")
       +        return cfg
       +}
       +
       +func setIfNotSet(cfg Provider, key string, value any) {
       +        if !cfg.IsSet(key) {
       +                cfg.Set(key, value)
       +        }
        }
   DIR diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go
       @@ -75,6 +75,11 @@ func NewFrom(params maps.Params) Provider {
                }
        }
        
       +// NewWithTestDefaults is used in tests only.
       +func NewWithTestDefaults() Provider {
       +        return SetBaseTestDefaults(New())
       +}
       +
        // defaultConfigProvider is a Provider backed by a map where all keys are lower case.
        // All methods are thread safe.
        type defaultConfigProvider struct {
   DIR diff --git a/config/services/servicesConfig_test.go b/config/services/servicesConfig_test.go
       @@ -54,7 +54,7 @@ disableInlineCSS = true
        func TestUseSettingsFromRootIfSet(t *testing.T) {
                c := qt.New(t)
        
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("disqusShortname", "root_short")
                cfg.Set("googleAnalytics", "ga_root")
        
   DIR diff --git a/go.mod b/go.mod
       @@ -48,7 +48,7 @@ require (
                github.com/russross/blackfriday v1.6.0
                github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
                github.com/sanity-io/litter v1.5.4
       -        github.com/spf13/afero v1.8.1
       +        github.com/spf13/afero v1.8.2
                github.com/spf13/cast v1.4.1
                github.com/spf13/cobra v1.4.0
                github.com/spf13/fsync v0.9.0
       @@ -98,6 +98,7 @@ require (
                github.com/aws/aws-sdk-go-v2/service/sso v1.11.3 // indirect
                github.com/aws/aws-sdk-go-v2/service/sts v1.16.3 // indirect
                github.com/aws/smithy-go v1.11.2 // indirect
       +        github.com/bep/overlayfs v0.1.0 // indirect
                github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
                github.com/dlclark/regexp2 v1.4.0 // indirect
                github.com/go-openapi/jsonpointer v0.19.5 // indirect
   DIR diff --git a/go.sum b/go.sum
       @@ -186,6 +186,8 @@ github.com/bep/golibsass v1.0.0 h1:gNguBMSDi5yZEZzVZP70YpuFQE3qogJIGUlrVILTmOw=
        github.com/bep/golibsass v1.0.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
        github.com/bep/gowebp v0.1.0 h1:4/iQpfnxHyXs3x/aTxMMdOpLEQQhFmF6G7EieWPTQyo=
        github.com/bep/gowebp v0.1.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
       +github.com/bep/overlayfs v0.1.0 h1:1hOCrvS4E5Hf0qwxM7m+9oitqClD9mRjQ1d4pECsVcU=
       +github.com/bep/overlayfs v0.1.0/go.mod h1:NFjSmn3kCqG7KX2Lmz8qT8VhPPCwZap3UNogXawoQHM=
        github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
        github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
        github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg=
       @@ -564,6 +566,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
        github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
        github.com/spf13/afero v1.8.1 h1:izYHOT71f9iZ7iq37Uqjael60/vYC6vMtzedudZ0zEk=
        github.com/spf13/afero v1.8.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
       +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
       +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
        github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
        github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
        github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
   DIR diff --git a/helpers/content_test.go b/helpers/content_test.go
       @@ -19,10 +19,10 @@ import (
                "strings"
                "testing"
        
       -        "github.com/gohugoio/hugo/config"
                "github.com/spf13/afero"
        
                "github.com/gohugoio/hugo/common/loggers"
       +        "github.com/gohugoio/hugo/config"
        
                qt "github.com/frankban/quicktest"
        )
       @@ -102,7 +102,7 @@ func TestBytesToHTML(t *testing.T) {
        }
        
        func TestNewContentSpec(t *testing.T) {
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                c := qt.New(t)
        
                cfg.Set("summaryLength", 32)
   DIR diff --git a/helpers/general_test.go b/helpers/general_test.go
       @@ -20,9 +20,8 @@ import (
                "testing"
                "time"
        
       -        "github.com/gohugoio/hugo/config"
       -
                "github.com/gohugoio/hugo/common/loggers"
       +        "github.com/gohugoio/hugo/config"
        
                qt "github.com/frankban/quicktest"
                "github.com/spf13/afero"
       @@ -30,7 +29,7 @@ import (
        
        func TestResolveMarkup(t *testing.T) {
                c := qt.New(t)
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil)
                c.Assert(err, qt.IsNil)
        
   DIR diff --git a/helpers/path.go b/helpers/path.go
       @@ -459,9 +459,17 @@ func IsDir(path string, fs afero.Fs) (bool, error) {
                return afero.IsDir(fs, path)
        }
        
       -// IsEmpty checks if a given path is empty.
       +// IsEmpty checks if a given path is empty, meaning it doesn't contain any regular files.
        func IsEmpty(path string, fs afero.Fs) (bool, error) {
       -        return afero.IsEmpty(fs, path)
       +        var hasFile bool
       +        err := afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
       +                if info.IsDir() {
       +                        return nil
       +                }
       +                hasFile = true
       +                return filepath.SkipDir
       +        })
       +        return !hasFile, err
        }
        
        // Exists checks if a file or directory exists.
   DIR diff --git a/helpers/path_test.go b/helpers/path_test.go
       @@ -256,55 +256,6 @@ func TestIsDir(t *testing.T) {
                }
        }
        
       -func TestIsEmpty(t *testing.T) {
       -        zeroSizedFile, _ := createZeroSizedFileInTempDir()
       -        defer deleteFileInTempDir(zeroSizedFile)
       -        nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir()
       -        defer deleteFileInTempDir(nonZeroSizedFile)
       -        emptyDirectory, _ := createEmptyTempDir()
       -        defer deleteTempDir(emptyDirectory)
       -        nonEmptyZeroLengthFilesDirectory, _ := createTempDirWithZeroLengthFiles()
       -        defer deleteTempDir(nonEmptyZeroLengthFilesDirectory)
       -        nonEmptyNonZeroLengthFilesDirectory, _ := createTempDirWithNonZeroLengthFiles()
       -        defer deleteTempDir(nonEmptyNonZeroLengthFilesDirectory)
       -        nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt"
       -        nonExistentDir := os.TempDir() + "/this/directory/does/not/exist/"
       -
       -        fileDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentFile)
       -        dirDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentDir)
       -
       -        type test struct {
       -                input          string
       -                expectedResult bool
       -                expectedErr    error
       -        }
       -
       -        data := []test{
       -                {zeroSizedFile.Name(), true, nil},
       -                {nonZeroSizedFile.Name(), false, nil},
       -                {emptyDirectory, true, nil},
       -                {nonEmptyZeroLengthFilesDirectory, false, nil},
       -                {nonEmptyNonZeroLengthFilesDirectory, false, nil},
       -                {nonExistentFile, false, fileDoesNotExist},
       -                {nonExistentDir, false, dirDoesNotExist},
       -        }
       -        for i, d := range data {
       -                exists, err := IsEmpty(d.input, new(afero.OsFs))
       -                if d.expectedResult != exists {
       -                        t.Errorf("Test %d failed. Expected result %t got %t", i, d.expectedResult, exists)
       -                }
       -                if d.expectedErr != nil {
       -                        if d.expectedErr.Error() != err.Error() {
       -                                t.Errorf("Test %d failed. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err)
       -                        }
       -                } else {
       -                        if d.expectedErr != err {
       -                                t.Errorf("Test %d failed. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err)
       -                        }
       -                }
       -        }
       -}
       -
        func createZeroSizedFileInTempDir() (*os.File, error) {
                filePrefix := "_path_test_"
                f, e := ioutil.TempFile("", filePrefix) // dir is os.TempDir()
       @@ -346,51 +297,6 @@ func createEmptyTempDir() (string, error) {
                return d, nil
        }
        
       -func createTempDirWithZeroLengthFiles() (string, error) {
       -        d, dirErr := createEmptyTempDir()
       -        if dirErr != nil {
       -                return "", dirErr
       -        }
       -        filePrefix := "_path_test_"
       -        _, fileErr := ioutil.TempFile(d, filePrefix) // dir is os.TempDir()
       -        if fileErr != nil {
       -                // if there was an error no file was created.
       -                // but we need to remove the directory to clean-up
       -                deleteTempDir(d)
       -                return "", fileErr
       -        }
       -        // the dir now has one, zero length file in it
       -        return d, nil
       -}
       -
       -func createTempDirWithNonZeroLengthFiles() (string, error) {
       -        d, dirErr := createEmptyTempDir()
       -        if dirErr != nil {
       -                return "", dirErr
       -        }
       -        filePrefix := "_path_test_"
       -        f, fileErr := ioutil.TempFile(d, filePrefix) // dir is os.TempDir()
       -        if fileErr != nil {
       -                // if there was an error no file was created.
       -                // but we need to remove the directory to clean-up
       -                deleteTempDir(d)
       -                return "", fileErr
       -        }
       -        byteString := []byte("byteString")
       -
       -        fileErr = ioutil.WriteFile(f.Name(), byteString, 0644)
       -        if fileErr != nil {
       -                // delete the file
       -                deleteFileInTempDir(f)
       -                // also delete the directory
       -                deleteTempDir(d)
       -                return "", fileErr
       -        }
       -
       -        // the dir now has one, zero length file in it
       -        return d, nil
       -}
       -
        func deleteTempDir(d string) {
                _ = os.RemoveAll(d)
        }
   DIR diff --git a/helpers/testhelpers_test.go b/helpers/testhelpers_test.go
       @@ -17,9 +17,8 @@ func newTestPathSpec(fs *hugofs.Fs, v config.Provider) *PathSpec {
        }
        
        func newTestDefaultPathSpec(configKeyValues ...any) *PathSpec {
       -        v := config.New()
       -        fs := hugofs.NewMem(v)
                cfg := newTestCfg()
       +        fs := hugofs.NewMem(cfg)
        
                for i := 0; i < len(configKeyValues); i += 2 {
                        cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
       @@ -28,15 +27,7 @@ func newTestDefaultPathSpec(configKeyValues ...any) *PathSpec {
        }
        
        func newTestCfg() config.Provider {
       -        v := config.New()
       -        v.Set("contentDir", "content")
       -        v.Set("dataDir", "data")
       -        v.Set("i18nDir", "i18n")
       -        v.Set("layoutDir", "layouts")
       -        v.Set("assetDir", "assets")
       -        v.Set("resourceDir", "resources")
       -        v.Set("publishDir", "public")
       -        v.Set("archetypeDir", "archetypes")
       +        v := config.NewWithTestDefaults()
                langs.LoadLanguageSettings(v, nil)
                langs.LoadLanguageSettings(v, nil)
                mod, err := modules.CreateProjectModule(v)
       @@ -49,7 +40,7 @@ func newTestCfg() config.Provider {
        }
        
        func newTestContentSpec() *ContentSpec {
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                spec, err := NewContentSpec(v, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil)
                if err != nil {
                        panic(err)
   DIR diff --git a/hugofs/createcounting_fs.go b/hugofs/createcounting_fs.go
       @@ -33,10 +33,18 @@ type DuplicatesReporter interface {
                ReportDuplicates() string
        }
        
       +var (
       +        _ FilesystemUnwrapper = (*createCountingFs)(nil)
       +)
       +
        func NewCreateCountingFs(fs afero.Fs) afero.Fs {
                return &createCountingFs{Fs: fs, fileCount: make(map[string]int)}
        }
        
       +func (fs *createCountingFs) UnwrapFilesystem() afero.Fs {
       +        return fs.Fs
       +}
       +
        // ReportDuplicates reports filenames written more than once.
        func (c *createCountingFs) ReportDuplicates() string {
                c.mu.Lock()
   DIR diff --git a/hugofs/decorators.go b/hugofs/decorators.go
       @@ -23,6 +23,10 @@ import (
                "github.com/spf13/afero"
        )
        
       +var (
       +        _ FilesystemUnwrapper = (*baseFileDecoratorFs)(nil)
       +)
       +
        func decorateDirs(fs afero.Fs, meta *FileMeta) afero.Fs {
                ffs := &baseFileDecoratorFs{Fs: fs}
        
       @@ -151,6 +155,10 @@ type baseFileDecoratorFs struct {
                decorate func(fi os.FileInfo, filename string) (os.FileInfo, error)
        }
        
       +func (fs *baseFileDecoratorFs) UnwrapFilesystem() afero.Fs {
       +        return fs.Fs
       +}
       +
        func (fs *baseFileDecoratorFs) Stat(name string) (os.FileInfo, error) {
                fi, err := fs.Fs.Stat(name)
                if err != nil {
   DIR diff --git a/hugofs/filename_filter_fs.go b/hugofs/filename_filter_fs.go
       @@ -23,6 +23,10 @@ import (
                "github.com/spf13/afero"
        )
        
       +var (
       +        _ FilesystemUnwrapper = (*filenameFilterFs)(nil)
       +)
       +
        func newFilenameFilterFs(fs afero.Fs, base string, filter *glob.FilenameFilter) afero.Fs {
                return &filenameFilterFs{
                        fs:     fs,
       @@ -39,6 +43,10 @@ type filenameFilterFs struct {
                filter *glob.FilenameFilter
        }
        
       +func (fs *filenameFilterFs) UnwrapFilesystem() afero.Fs {
       +        return fs.fs
       +}
       +
        func (fs *filenameFilterFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
                fi, b, err := fs.fs.(afero.Lstater).LstatIfPossible(name)
                if err != nil {
   DIR diff --git a/hugofs/filter_fs.go b/hugofs/filter_fs.go
       @@ -121,6 +121,10 @@ func NewFilterFs(fs afero.Fs) (afero.Fs, error) {
                return ffs, nil
        }
        
       +var (
       +        _ FilesystemUnwrapper = (*FilterFs)(nil)
       +)
       +
        // FilterFs is an ordered composite filesystem.
        type FilterFs struct {
                fs afero.Fs
       @@ -141,6 +145,10 @@ func (fs *FilterFs) Chown(n string, uid, gid int) error {
                return syscall.EPERM
        }
        
       +func (fs *FilterFs) UnwrapFilesystem() afero.Fs {
       +        return fs.fs
       +}
       +
        func (fs *FilterFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
                fi, b, err := lstatIfPossible(fs.fs, name)
                if err != nil {
   DIR diff --git a/hugofs/fs.go b/hugofs/fs.go
       @@ -19,6 +19,8 @@ import (
                "os"
                "strings"
        
       +        "github.com/bep/overlayfs"
       +        "github.com/gohugoio/hugo/common/paths"
                "github.com/gohugoio/hugo/config"
                "github.com/spf13/afero"
        )
       @@ -26,32 +28,43 @@ import (
        // Os points to the (real) Os filesystem.
        var Os = &afero.OsFs{}
        
       -// Fs abstracts the file system to separate source and destination file systems
       -// and allows both to be mocked for testing.
       +// Fs holds the core filesystems used by Hugo.
        type Fs struct {
                // Source is Hugo's source file system.
       +        // Note that this will always be a "plain" Afero filesystem:
       +        // * afero.OsFs when running in production
       +        // * afero.MemMapFs for many of the tests.
                Source afero.Fs
        
       -        // Destination is Hugo's destination file system.
       -        Destination afero.Fs
       +        // PublishDir is where Hugo publishes its rendered content.
       +        // It's mounted inside publishDir (default /public).
       +        PublishDir afero.Fs
        
       -        // Destination used for `renderStaticToDisk`
       -        DestinationStatic afero.Fs
       +        // PublishDirStatic is the file system used for static files  when --renderStaticToDisk is set.
       +        // When this is set, PublishDir is set to write to memory.
       +        PublishDirStatic afero.Fs
       +
       +        // PublishDirServer is the file system used for serving the public directory with Hugo's development server.
       +        // This will typically be the same as PublishDir, but not if --renderStaticToDisk is set.
       +        PublishDirServer afero.Fs
        
                // Os is an OS file system.
                // NOTE: Field is currently unused.
                Os afero.Fs
        
       -        // WorkingDir is a read-only file system
       +        // WorkingDirReadOnly is a read-only file system
       +        // restricted to the project working dir.
       +        WorkingDirReadOnly afero.Fs
       +
       +        // WorkingDirWritable is a writable file system
                // restricted to the project working dir.
       -        // TODO(bep) get rid of this (se BaseFs)
       -        WorkingDir *afero.BasePathFs
       +        WorkingDirWritable afero.Fs
        }
        
        // NewDefault creates a new Fs with the OS file system
        // as source and destination file systems.
        func NewDefault(cfg config.Provider) *Fs {
       -        fs := &afero.OsFs{}
       +        fs := Os
                return newFs(fs, cfg)
        }
        
       @@ -71,23 +84,49 @@ func NewFrom(fs afero.Fs, cfg config.Provider) *Fs {
        }
        
        func newFs(base afero.Fs, cfg config.Provider) *Fs {
       +        workingDir := cfg.GetString("workingDir")
       +        publishDir := cfg.GetString("publishDir")
       +        if publishDir == "" {
       +                panic("publishDir is empty")
       +        }
       +
       +        // Sanity check
       +        if IsOsFs(base) && len(workingDir) < 2 {
       +                panic("workingDir is too short")
       +        }
       +
       +        absPublishDir := paths.AbsPathify(workingDir, publishDir)
       +
       +        // Make sure we always have the /public folder ready to use.
       +        if err := base.MkdirAll(absPublishDir, 0777); err != nil && !os.IsExist(err) {
       +                panic(err)
       +        }
       +
       +        pubFs := afero.NewBasePathFs(base, absPublishDir)
       +
                return &Fs{
       -                Source:            base,
       -                Destination:       base,
       -                DestinationStatic: base,
       -                Os:                &afero.OsFs{},
       -                WorkingDir:        getWorkingDirFs(base, cfg),
       +                Source:             base,
       +                PublishDir:         pubFs,
       +                PublishDirServer:   pubFs,
       +                PublishDirStatic:   pubFs,
       +                Os:                 &afero.OsFs{},
       +                WorkingDirReadOnly: getWorkingDirFsReadOnly(base, workingDir),
       +                WorkingDirWritable: getWorkingDirFsWritable(base, workingDir),
                }
        }
        
       -func getWorkingDirFs(base afero.Fs, cfg config.Provider) *afero.BasePathFs {
       -        workingDir := cfg.GetString("workingDir")
       -
       -        if workingDir != "" {
       -                return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir).(*afero.BasePathFs)
       +func getWorkingDirFsReadOnly(base afero.Fs, workingDir string) afero.Fs {
       +        if workingDir == "" {
       +                return afero.NewReadOnlyFs(base)
                }
       +        return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir)
       +}
        
       -        return nil
       +func getWorkingDirFsWritable(base afero.Fs, workingDir string) afero.Fs {
       +        if workingDir == "" {
       +                return base
       +        }
       +        return afero.NewBasePathFs(base, workingDir)
        }
        
        func isWrite(flag int) bool {
       @@ -117,3 +156,64 @@ func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) 
                })
                return counter, fs.RemoveAll(dir)
        }
       +
       +// HasOsFs returns whether fs is an OsFs or if it fs wraps an OsFs.
       +// TODO(bep) make this nore robust.
       +func IsOsFs(fs afero.Fs) bool {
       +        var isOsFs bool
       +        WalkFilesystems(fs, func(fs afero.Fs) bool {
       +                switch base := fs.(type) {
       +                case *afero.MemMapFs:
       +                        isOsFs = false
       +                case *afero.OsFs:
       +                        isOsFs = true
       +                case *afero.BasePathFs:
       +                        _, supportsLstat, _ := base.LstatIfPossible("asdfasdfasdf")
       +                        isOsFs = supportsLstat
       +                }
       +                return isOsFs
       +        })
       +        return isOsFs
       +}
       +
       +// FilesystemsUnwrapper returns the underlying filesystems.
       +type FilesystemsUnwrapper interface {
       +        UnwrapFilesystems() []afero.Fs
       +}
       +
       +// FilesystemsProvider returns the underlying filesystem.
       +type FilesystemUnwrapper interface {
       +        UnwrapFilesystem() afero.Fs
       +}
       +
       +// WalkFn is the walk func for WalkFilesystems.
       +type WalkFn func(fs afero.Fs) bool
       +
       +// WalkFilesystems walks fs recursively and calls fn.
       +// If fn returns true, walking is stopped.
       +func WalkFilesystems(fs afero.Fs, fn WalkFn) bool {
       +        if fn(fs) {
       +                return true
       +        }
       +
       +        if afs, ok := fs.(FilesystemUnwrapper); ok {
       +                if WalkFilesystems(afs.UnwrapFilesystem(), fn) {
       +                        return true
       +                }
       +
       +        } else if bfs, ok := fs.(FilesystemsUnwrapper); ok {
       +                for _, sf := range bfs.UnwrapFilesystems() {
       +                        if WalkFilesystems(sf, fn) {
       +                                return true
       +                        }
       +                }
       +        } else if cfs, ok := fs.(overlayfs.FilesystemIterator); ok {
       +                for i := 0; i < cfs.NumFilesystems(); i++ {
       +                        if WalkFilesystems(cfs.Filesystem(i), fn) {
       +                                return true
       +                        }
       +                }
       +        }
       +
       +        return false
       +}
   DIR diff --git a/hugofs/fs_test.go b/hugofs/fs_test.go
       @@ -23,38 +23,46 @@ import (
                "github.com/spf13/afero"
        )
        
       +func TestIsOsFs(t *testing.T) {
       +        c := qt.New(t)
       +
       +        c.Assert(IsOsFs(Os), qt.Equals, true)
       +        c.Assert(IsOsFs(&afero.MemMapFs{}), qt.Equals, false)
       +        c.Assert(IsOsFs(afero.NewBasePathFs(&afero.MemMapFs{}, "/public")), qt.Equals, false)
       +        c.Assert(IsOsFs(afero.NewBasePathFs(Os, t.TempDir())), qt.Equals, true)
       +
       +}
       +
        func TestNewDefault(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
       +        v.Set("workingDir", t.TempDir())
                f := NewDefault(v)
        
       -        c.Assert(f.Source, qt.Not(qt.IsNil))
       +        c.Assert(f.Source, qt.IsNotNil)
                c.Assert(f.Source, hqt.IsSameType, new(afero.OsFs))
       -        c.Assert(f.Os, qt.Not(qt.IsNil))
       -        c.Assert(f.WorkingDir, qt.IsNil)
       +        c.Assert(f.Os, qt.IsNotNil)
       +        c.Assert(f.WorkingDirReadOnly, qt.IsNotNil)
       +        c.Assert(f.WorkingDirReadOnly, hqt.IsSameType, new(afero.BasePathFs))
       +        c.Assert(IsOsFs(f.Source), qt.IsTrue)
       +        c.Assert(IsOsFs(f.WorkingDirReadOnly), qt.IsTrue)
       +        c.Assert(IsOsFs(f.PublishDir), qt.IsTrue)
       +        c.Assert(IsOsFs(f.Os), qt.IsTrue)
        }
        
        func TestNewMem(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                f := NewMem(v)
        
                c.Assert(f.Source, qt.Not(qt.IsNil))
                c.Assert(f.Source, hqt.IsSameType, new(afero.MemMapFs))
       -        c.Assert(f.Destination, qt.Not(qt.IsNil))
       -        c.Assert(f.Destination, hqt.IsSameType, new(afero.MemMapFs))
       +        c.Assert(f.PublishDir, qt.Not(qt.IsNil))
       +        c.Assert(f.PublishDir, hqt.IsSameType, new(afero.BasePathFs))
                c.Assert(f.Os, hqt.IsSameType, new(afero.OsFs))
       -        c.Assert(f.WorkingDir, qt.IsNil)
       -}
       -
       -func TestWorkingDir(t *testing.T) {
       -        c := qt.New(t)
       -        v := config.New()
       -
       -        v.Set("workingDir", "/a/b/")
       -
       -        f := NewMem(v)
       -
       -        c.Assert(f.WorkingDir, qt.Not(qt.IsNil))
       -        c.Assert(f.WorkingDir, hqt.IsSameType, new(afero.BasePathFs))
       +        c.Assert(f.WorkingDirReadOnly, qt.IsNotNil)
       +        c.Assert(IsOsFs(f.Source), qt.IsFalse)
       +        c.Assert(IsOsFs(f.WorkingDirReadOnly), qt.IsFalse)
       +        c.Assert(IsOsFs(f.PublishDir), qt.IsFalse)
       +        c.Assert(IsOsFs(f.Os), qt.IsTrue)
        }
   DIR diff --git a/hugofs/hashing_fs.go b/hugofs/hashing_fs.go
       @@ -22,7 +22,10 @@ import (
                "github.com/spf13/afero"
        )
        
       -var _ afero.Fs = (*md5HashingFs)(nil)
       +var (
       +        _ afero.Fs            = (*md5HashingFs)(nil)
       +        _ FilesystemUnwrapper = (*md5HashingFs)(nil)
       +)
        
        // FileHashReceiver will receive the filename an the content's MD5 sum on file close.
        type FileHashReceiver interface {
       @@ -45,6 +48,10 @@ func NewHashingFs(delegate afero.Fs, hashReceiver FileHashReceiver) afero.Fs {
                return &md5HashingFs{Fs: delegate, hashReceiver: hashReceiver}
        }
        
       +func (fs *md5HashingFs) UnwrapFilesystem() afero.Fs {
       +        return fs.Fs
       +}
       +
        func (fs *md5HashingFs) Create(name string) (afero.File, error) {
                f, err := fs.Fs.Create(name)
                if err == nil {
   DIR diff --git a/hugofs/language_composite_fs.go b/hugofs/language_composite_fs.go
       @@ -20,11 +20,14 @@ import (
        )
        
        var (
       -        _ afero.Fs      = (*languageCompositeFs)(nil)
       -        _ afero.Lstater = (*languageCompositeFs)(nil)
       +        _ afero.Fs             = (*languageCompositeFs)(nil)
       +        _ afero.Lstater        = (*languageCompositeFs)(nil)
       +        _ FilesystemsUnwrapper = (*languageCompositeFs)(nil)
        )
        
        type languageCompositeFs struct {
       +        base    afero.Fs
       +        overlay afero.Fs
                *afero.CopyOnWriteFs
        }
        
       @@ -33,7 +36,11 @@ type languageCompositeFs struct {
        // to the target filesystem. This information is available in Readdir, Stat etc. via the
        // special LanguageFileInfo FileInfo implementation.
        func NewLanguageCompositeFs(base, overlay afero.Fs) afero.Fs {
       -        return &languageCompositeFs{afero.NewCopyOnWriteFs(base, overlay).(*afero.CopyOnWriteFs)}
       +        return &languageCompositeFs{base, overlay, afero.NewCopyOnWriteFs(base, overlay).(*afero.CopyOnWriteFs)}
       +}
       +
       +func (fs *languageCompositeFs) UnwrapFilesystems() []afero.Fs {
       +        return []afero.Fs{fs.base, fs.overlay}
        }
        
        // Open takes the full path to the file in the target filesystem. If it is a directory, it gets merged
   DIR diff --git a/hugofs/nosymlink_fs.go b/hugofs/nosymlink_fs.go
       @@ -30,6 +30,10 @@ func NewNoSymlinkFs(fs afero.Fs, logger loggers.Logger, allowFiles bool) afero.F
                return &noSymlinkFs{Fs: fs, logger: logger, allowFiles: allowFiles}
        }
        
       +var (
       +        _ FilesystemUnwrapper = (*noSymlinkFs)(nil)
       +)
       +
        // noSymlinkFs is a filesystem that prevents symlinking.
        type noSymlinkFs struct {
                allowFiles bool // block dirs only
       @@ -67,6 +71,10 @@ func (f *noSymlinkFile) Readdirnames(count int) ([]string, error) {
                return fileInfosToNames(dirs), nil
        }
        
       +func (fs *noSymlinkFs) UnwrapFilesystem() afero.Fs {
       +        return fs.Fs
       +}
       +
        func (fs *noSymlinkFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
                return fs.stat(name)
        }
   DIR diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go
       @@ -151,6 +151,10 @@ func (r RootMapping) trimFrom(name string) string {
                return strings.TrimPrefix(name, r.From)
        }
        
       +var (
       +        _ FilesystemUnwrapper = (*RootMappingFs)(nil)
       +)
       +
        // A RootMappingFs maps several roots into one. Note that the root of this filesystem
        // is directories only, and they will be returned in Readdir and Readdirnames
        // in the order given.
       @@ -200,6 +204,10 @@ func (fs *RootMappingFs) Dirs(base string) ([]FileMetaInfo, error) {
                return fss, nil
        }
        
       +func (fs *RootMappingFs) UnwrapFilesystem() afero.Fs {
       +        return fs.Fs
       +}
       +
        // Filter creates a copy of this filesystem with only mappings matching a filter.
        func (fs RootMappingFs) Filter(f func(m RootMapping) bool) *RootMappingFs {
                rootMapToReal := radix.New()
   DIR diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go
       @@ -20,9 +20,8 @@ import (
                "sort"
                "testing"
        
       -        "github.com/gohugoio/hugo/hugofs/glob"
       -
                "github.com/gohugoio/hugo/config"
       +        "github.com/gohugoio/hugo/hugofs/glob"
        
                qt "github.com/frankban/quicktest"
                "github.com/gohugoio/hugo/htesting"
       @@ -31,7 +30,7 @@ import (
        
        func TestLanguageRootMapping(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("contentDir", "content")
        
                fs := NewBaseFileDecorator(afero.NewMemMapFs())
   DIR diff --git a/hugofs/slice_fs.go b/hugofs/slice_fs.go
       @@ -24,9 +24,10 @@ import (
        )
        
        var (
       -        _ afero.Fs      = (*SliceFs)(nil)
       -        _ afero.Lstater = (*SliceFs)(nil)
       -        _ afero.File    = (*sliceDir)(nil)
       +        _ afero.Fs             = (*SliceFs)(nil)
       +        _ afero.Lstater        = (*SliceFs)(nil)
       +        _ FilesystemsUnwrapper = (*SliceFs)(nil)
       +        _ afero.File           = (*sliceDir)(nil)
        )
        
        func NewSliceFs(dirs ...FileMetaInfo) (afero.Fs, error) {
       @@ -52,6 +53,14 @@ type SliceFs struct {
                dirs []FileMetaInfo
        }
        
       +func (fs *SliceFs) UnwrapFilesystems() []afero.Fs {
       +        var fss []afero.Fs
       +        for _, dir := range fs.dirs {
       +                fss = append(fss, dir.Meta().Fs)
       +        }
       +        return fss
       +}
       +
        func (fs *SliceFs) Chmod(n string, m os.FileMode) error {
                return syscall.EPERM
        }
   DIR diff --git a/hugofs/stacktracer_fs.go b/hugofs/stacktracer_fs.go
       @@ -24,8 +24,11 @@ import (
                "github.com/spf13/afero"
        )
        
       -// Make sure we don't accidentally use this in the real Hugo.
       -var _ types.DevMarker = (*stacktracerFs)(nil)
       +var (
       +        //  Make sure we don't accidentally use this in the real Hugo.
       +        _ types.DevMarker     = (*stacktracerFs)(nil)
       +        _ FilesystemUnwrapper = (*stacktracerFs)(nil)
       +)
        
        // NewStacktracerFs wraps the given fs printing stack traces for file creates
        // matching the given regexp pattern.
       @@ -45,6 +48,10 @@ type stacktracerFs struct {
        func (fs *stacktracerFs) DevOnly() {
        }
        
       +func (fs *stacktracerFs) UnwrapFilesystem() afero.Fs {
       +        return fs.Fs
       +}
       +
        func (fs *stacktracerFs) onCreate(filename string) {
                if fs.re.MatchString(filename) {
                        trace := make([]byte, 1500)
   DIR diff --git a/hugolib/config.go b/hugolib/config.go
       @@ -35,7 +35,6 @@ import (
        
                "github.com/gohugoio/hugo/common/herrors"
                "github.com/gohugoio/hugo/common/hugo"
       -        "github.com/gohugoio/hugo/hugolib/paths"
                "github.com/gohugoio/hugo/langs"
                "github.com/gohugoio/hugo/modules"
                "github.com/pkg/errors"
       @@ -359,7 +358,7 @@ func (l configLoader) collectModules(modConfig modules.Config, v1 config.Provide
                        workingDir = v1.GetString("workingDir")
                }
        
       -        themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
       +        themesDir := cpaths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
        
                var ignoreVendor glob.Glob
                if s := v1.GetString("ignoreVendorPaths"); s != "" {
   DIR diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go
       @@ -38,8 +38,8 @@ import (
        
                "github.com/gohugoio/hugo/modules"
        
       +        hpaths "github.com/gohugoio/hugo/common/paths"
                "github.com/gohugoio/hugo/hugofs"
       -
                "github.com/gohugoio/hugo/hugolib/paths"
                "github.com/spf13/afero"
        )
       @@ -68,12 +68,12 @@ type BaseFs struct {
                // This usually maps to /my-project/public.
                PublishFs afero.Fs
        
       -        // A read-only filesystem starting from the project workDir.
       -        WorkDir afero.Fs
       -
                // The filesystem used for renderStaticToDisk.
                PublishFsStatic afero.Fs
        
       +        // A read-only filesystem starting from the project workDir.
       +        WorkDir afero.Fs
       +
                theBigFs *filesystemsCollector
        
                // Locks.
       @@ -434,21 +434,13 @@ func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) err
                        logger = loggers.NewWarningLogger()
                }
        
       -        // Make sure we always have the /public folder ready to use.
       -        if err := fs.Destination.MkdirAll(p.AbsPublishDir, 0777); err != nil && !os.IsExist(err) {
       -                return nil, err
       -        }
       -
       -        publishFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Destination, p.AbsPublishDir))
       +        publishFs := hugofs.NewBaseFileDecorator(fs.PublishDir)
                sourceFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Source, p.WorkingDir))
       -        publishFsStatic := afero.NewBasePathFs(fs.Source, p.AbsPublishDir)
       -
       -        // Same as sourceFs, but no decoration. This is what's used by os.ReadDir etc.
       -        workDir := afero.NewBasePathFs(afero.NewReadOnlyFs(fs.Source), p.WorkingDir)
       +        publishFsStatic := fs.PublishDirStatic
        
                b := &BaseFs{
                        SourceFs:        sourceFs,
       -                WorkDir:         workDir,
       +                WorkDir:         fs.WorkingDirReadOnly,
                        PublishFs:       publishFs,
                        PublishFsStatic: publishFsStatic,
                        buildMu:         lockedfile.MutexAt(filepath.Join(p.WorkingDir, lockFileBuild)),
       @@ -638,7 +630,7 @@ func (b *sourceFilesystemsBuilder) createModFs(
                        if filepath.IsAbs(path) {
                                return "", path
                        }
       -                return md.dir, paths.AbsPathify(md.dir, path)
       +                return md.dir, hpaths.AbsPathify(md.dir, path)
                }
        
                for i, mount := range md.Mounts() {
   DIR diff --git a/hugolib/filesystems/basefs_test.go b/hugolib/filesystems/basefs_test.go
       @@ -75,7 +75,7 @@ func initConfig(fs afero.Fs, cfg config.Provider) error {
        
        func TestNewBaseFs(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
        
                fs := hugofs.NewMem(v)
        
       @@ -181,7 +181,7 @@ theme = ["atheme"]
        }
        
        func createConfig() config.Provider {
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("contentDir", "mycontent")
                v.Set("i18nDir", "myi18n")
                v.Set("staticDir", "mystatic")
       @@ -219,22 +219,19 @@ func TestNewBaseFsEmpty(t *testing.T) {
        func TestRealDirs(t *testing.T) {
                c := qt.New(t)
                v := createConfig()
       +        root, themesDir := t.TempDir(), t.TempDir()
       +        v.Set("workingDir", root)
       +        v.Set("themesDir", themesDir)
       +        v.Set("theme", "mytheme")
       +
                fs := hugofs.NewDefault(v)
                sfs := fs.Source
        
       -        root, err := afero.TempDir(sfs, "", "realdir")
       -        c.Assert(err, qt.IsNil)
       -        themesDir, err := afero.TempDir(sfs, "", "themesDir")
       -        c.Assert(err, qt.IsNil)
                defer func() {
                        os.RemoveAll(root)
                        os.RemoveAll(themesDir)
                }()
        
       -        v.Set("workingDir", root)
       -        v.Set("themesDir", themesDir)
       -        v.Set("theme", "mytheme")
       -
                c.Assert(sfs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf1"), 0755), qt.IsNil)
                c.Assert(sfs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf2"), 0755), qt.IsNil)
                c.Assert(sfs.MkdirAll(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf2"), 0755), qt.IsNil)
   DIR diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go
       @@ -59,13 +59,14 @@ path="github.com/gohugoio/hugoTestModule2"
                        return fmt.Sprintf(tomlConfig, workingDir, moduleOpts)
                }
        
       -        newTestBuilder := func(t testing.TB, moduleOpts string) (*sitesBuilder, func()) {
       +        newTestBuilder := func(t testing.TB, moduleOpts string) *sitesBuilder {
                        b := newTestSitesBuilder(t)
       -                tempDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-variants")
       -                b.Assert(err, qt.IsNil)
       +                tempDir := t.TempDir()
                        workingDir := filepath.Join(tempDir, "myhugosite")
                        b.Assert(os.MkdirAll(workingDir, 0777), qt.IsNil)
       -                b.Fs = hugofs.NewDefault(config.New())
       +                cfg := config.NewWithTestDefaults()
       +                cfg.Set("workingDir", workingDir)
       +                b.Fs = hugofs.NewDefault(cfg)
                        b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts))
                        b.WithTemplates(
                                "index.html", `
       @@ -92,22 +93,18 @@ github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877 h1:WLM2bQ
        github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877/go.mod h1:CBFZS3khIAXKxReMwq0le8sEl/D8hcXmixlOHVv+Gd0=
        `)
        
       -                return b, clean
       +                return b
                }
        
                t.Run("Target in subfolder", func(t *testing.T) {
       -                b, clean := newTestBuilder(t, "ignoreImports=true")
       -                defer clean()
       -
       +                b := newTestBuilder(t, "ignoreImports=true")
                        b.Build(BuildCfg{})
        
                        b.AssertFileContent("public/p1/index.html", `<p>Page|https://bep.is|Title: |Text: A link|END</p>`)
                })
        
                t.Run("Ignore config", func(t *testing.T) {
       -                b, clean := newTestBuilder(t, "ignoreConfig=true")
       -                defer clean()
       -
       +                b := newTestBuilder(t, "ignoreConfig=true")
                        b.Build(BuildCfg{})
        
                        b.AssertFileContent("public/index.html", `
       @@ -117,9 +114,7 @@ JS imported in module: |
                })
        
                t.Run("Ignore imports", func(t *testing.T) {
       -                b, clean := newTestBuilder(t, "ignoreImports=true")
       -                defer clean()
       -
       +                b := newTestBuilder(t, "ignoreImports=true")
                        b.Build(BuildCfg{})
        
                        b.AssertFileContent("public/index.html", `
       @@ -129,8 +124,7 @@ JS imported in module: |
                })
        
                t.Run("Create package.json", func(t *testing.T) {
       -                b, clean := newTestBuilder(t, "")
       -                defer clean()
       +                b := newTestBuilder(t, "")
        
                        b.WithSourceFile("package.json", `{
                        "name": "mypack",
       @@ -205,8 +199,7 @@ JS imported in module: |
                })
        
                t.Run("Create package.json, no default", func(t *testing.T) {
       -                b, clean := newTestBuilder(t, "")
       -                defer clean()
       +                b := newTestBuilder(t, "")
        
                        const origPackageJSON = `{
                        "name": "mypack",
       @@ -268,8 +261,7 @@ JS imported in module: |
                })
        
                t.Run("Create package.json, no default, no package.json", func(t *testing.T) {
       -                b, clean := newTestBuilder(t, "")
       -                defer clean()
       +                b := newTestBuilder(t, "")
        
                        b.Build(BuildCfg{})
                        b.Assert(npm.Pack(b.H.BaseFs.SourceFs, b.H.BaseFs.Assets.Dirs), qt.IsNil)
       @@ -333,12 +325,13 @@ func TestHugoModulesMatrix(t *testing.T) {
                for _, m := range testmods[:2] {
                        c := qt.New(t)
        
       -                v := config.New()
       -
                        workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-test")
                        c.Assert(err, qt.IsNil)
                        defer clean()
        
       +                v := config.NewWithTestDefaults()
       +                v.Set("workingDir", workingDir)
       +
                        configTemplate := `
        baseURL = "https://example.com"
        title = "My Modular Site"
       @@ -670,13 +663,14 @@ func TestModulesSymlinks(t *testing.T) {
                }()
        
                c := qt.New(t)
       +        workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mod-sym")
       +        c.Assert(err, qt.IsNil)
       +
                // We need to use the OS fs for this.
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
       +        cfg.Set("workingDir", workingDir)
                fs := hugofs.NewFrom(hugofs.Os, cfg)
        
       -        workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mod-sym")
       -        c.Assert(err, qt.IsNil)
       -
                defer clean()
        
                const homeTemplate = `
       @@ -694,9 +688,9 @@ Data: {{ .Site.Data }}
                }
        
                // Create project dirs and files.
       -        createDirsAndFiles(workDir)
       +        createDirsAndFiles(workingDir)
                // Create one module inside the default themes folder.
       -        themeDir := filepath.Join(workDir, "themes", "mymod")
       +        themeDir := filepath.Join(workingDir, "themes", "mymod")
                createDirsAndFiles(themeDir)
        
                createSymlinks := func(baseDir, id string) {
       @@ -711,7 +705,7 @@ Data: {{ .Site.Data }}
                        }
                }
        
       -        createSymlinks(workDir, "project")
       +        createSymlinks(workingDir, "project")
                createSymlinks(themeDir, "mod")
        
                config := `
       @@ -729,12 +723,12 @@ weight = 2
        
        `
        
       -        b := newTestSitesBuilder(t).WithNothingAdded().WithWorkingDir(workDir)
       +        b := newTestSitesBuilder(t).WithNothingAdded().WithWorkingDir(workingDir)
                b.WithLogger(loggers.NewErrorLogger())
                b.Fs = fs
        
                b.WithConfigFile("toml", config)
       -        c.Assert(os.Chdir(workDir), qt.IsNil)
       +        c.Assert(os.Chdir(workingDir), qt.IsNil)
        
                b.Build(BuildCfg{})
        
       @@ -846,7 +840,10 @@ workingDir = %q
        
                b := newTestSitesBuilder(t).Running()
        
       -        b.Fs = hugofs.NewDefault(config.New())
       +        cfg := config.NewWithTestDefaults()
       +        cfg.Set("workingDir", workingDir)
       +
       +        b.Fs = hugofs.NewDefault(cfg)
        
                b.WithWorkingDir(workingDir).WithConfigFile("toml", tomlConfig)
                b.WithTemplatesAdded("index.html", `
       @@ -968,7 +965,9 @@ workingDir = %q
        
                        b := newTestSitesBuilder(c).Running()
        
       -                b.Fs = hugofs.NewDefault(config.New())
       +                cfg := config.NewWithTestDefaults()
       +                cfg.Set("workingDir", workingDir)
       +                b.Fs = hugofs.NewDefault(cfg)
        
                        os.MkdirAll(filepath.Join(workingDir, "content", "blog"), 0777)
        
       @@ -1067,7 +1066,7 @@ func TestSiteWithGoModButNoModules(t *testing.T) {
                workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-no-mod")
                c.Assert(err, qt.IsNil)
        
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("workingDir", workDir)
                fs := hugofs.NewFrom(hugofs.Os, cfg)
        
       @@ -1093,7 +1092,7 @@ func TestModuleAbsMount(t *testing.T) {
                absContentDir, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-content")
                c.Assert(err, qt.IsNil)
        
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("workingDir", workDir)
                fs := hugofs.NewFrom(hugofs.Os, cfg)
        
   DIR diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
       @@ -597,7 +597,7 @@ func (h *HugoSites) reset(config *BuildCfg) {
                if config.ResetState {
                        for i, s := range h.Sites {
                                h.Sites[i] = s.reset()
       -                        if r, ok := s.Fs.Destination.(hugofs.Reseter); ok {
       +                        if r, ok := s.Fs.PublishDir.(hugofs.Reseter); ok {
                                        r.Reset()
                                }
                        }
   DIR diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
       @@ -496,9 +496,9 @@ func (h *HugoSites) writeBuildStats() error {
                        return err
                }
        
       -        // Write to the destination, too, if a mem fs is in play.
       -        if h.Fs.Source != hugofs.Os {
       -                if err := afero.WriteFile(h.Fs.Destination, filename, js, 0666); err != nil {
       +        // Write to the destination as well if it's a in-memory fs.
       +        if !hugofs.IsOsFs(h.Fs.Source) {
       +                if err := afero.WriteFile(h.Fs.WorkingDirWritable, filename, js, 0666); err != nil {
                                return err
                        }
                }
   DIR diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go
       @@ -489,7 +489,7 @@ func TestMultiSitesRebuild(t *testing.T) {
                                        c.Assert(enSite.RegularPages()[0].Title(), qt.Equals, "new_en_2")
                                        c.Assert(enSite.RegularPages()[1].Title(), qt.Equals, "new_en_1")
        
       -                                rendered := readDestination(t, fs, "public/en/new1/index.html")
       +                                rendered := readWorkingDir(t, fs, "public/en/new1/index.html")
                                        c.Assert(strings.Contains(rendered, "new_en_1"), qt.Equals, true)
                                },
                        },
       @@ -503,7 +503,7 @@ func TestMultiSitesRebuild(t *testing.T) {
                                []fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc1.en.md"), Op: fsnotify.Write}},
                                func(t *testing.T) {
                                        c.Assert(len(enSite.RegularPages()), qt.Equals, 6)
       -                                doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html")
       +                                doc1 := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html")
                                        c.Assert(strings.Contains(doc1, "CHANGED"), qt.Equals, true)
                                },
                        },
       @@ -521,7 +521,7 @@ func TestMultiSitesRebuild(t *testing.T) {
                                func(t *testing.T) {
                                        c.Assert(len(enSite.RegularPages()), qt.Equals, 6, qt.Commentf("Rename"))
                                        c.Assert(enSite.RegularPages()[1].Title(), qt.Equals, "new_en_1")
       -                                rendered := readDestination(t, fs, "public/en/new1renamed/index.html")
       +                                rendered := readWorkingDir(t, fs, "public/en/new1renamed/index.html")
                                        c.Assert(rendered, qt.Contains, "new_en_1")
                                },
                        },
       @@ -538,7 +538,7 @@ func TestMultiSitesRebuild(t *testing.T) {
                                        c.Assert(len(enSite.RegularPages()), qt.Equals, 6)
                                        c.Assert(len(enSite.AllPages()), qt.Equals, 34)
                                        c.Assert(len(frSite.RegularPages()), qt.Equals, 5)
       -                                doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html")
       +                                doc1 := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html")
                                        c.Assert(strings.Contains(doc1, "Template Changed"), qt.Equals, true)
                                },
                        },
       @@ -555,9 +555,9 @@ func TestMultiSitesRebuild(t *testing.T) {
                                        c.Assert(len(enSite.RegularPages()), qt.Equals, 6)
                                        c.Assert(len(enSite.AllPages()), qt.Equals, 34)
                                        c.Assert(len(frSite.RegularPages()), qt.Equals, 5)
       -                                docEn := readDestination(t, fs, "public/en/sect/doc1-slug/index.html")
       +                                docEn := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html")
                                        c.Assert(strings.Contains(docEn, "Hello"), qt.Equals, true)
       -                                docFr := readDestination(t, fs, "public/fr/sect/doc1/index.html")
       +                                docFr := readWorkingDir(t, fs, "public/fr/sect/doc1/index.html")
                                        c.Assert(strings.Contains(docFr, "Salut"), qt.Equals, true)
        
                                        homeEn := enSite.getPage(page.KindHome)
       @@ -700,7 +700,7 @@ END
        
        func checkContent(s *sitesBuilder, filename string, matches ...string) {
                s.T.Helper()
       -        content := readDestination(s.T, s.Fs, filename)
       +        content := readWorkingDir(s.T, s.Fs, filename)
                for _, match := range matches {
                        if !strings.Contains(content, match) {
                                s.Fatalf("No match for\n%q\nin content for %s\n%q\nDiff:\n%s", match, filename, content, htesting.DiffStrings(content, match))
       @@ -1170,13 +1170,13 @@ func writeToFs(t testing.TB, fs afero.Fs, filename, content string) {
                }
        }
        
       -func readDestination(t testing.TB, fs *hugofs.Fs, filename string) string {
       +func readWorkingDir(t testing.TB, fs *hugofs.Fs, filename string) string {
                t.Helper()
       -        return readFileFromFs(t, fs.Destination, filename)
       +        return readFileFromFs(t, fs.WorkingDirReadOnly, filename)
        }
        
       -func destinationExists(fs *hugofs.Fs, filename string) bool {
       -        b, err := helpers.Exists(filename, fs.Destination)
       +func workingDirExists(fs *hugofs.Fs, filename string) bool {
       +        b, err := helpers.Exists(filename, fs.WorkingDirReadOnly)
                if err != nil {
                        panic(err)
                }
   DIR diff --git a/hugolib/image_test.go b/hugolib/image_test.go
       @@ -38,7 +38,7 @@ func TestImageOps(t *testing.T) {
                defer clean()
        
                newBuilder := func(timeout any) *sitesBuilder {
       -                v := config.New()
       +                v := config.NewWithTestDefaults()
                        v.Set("workingDir", workDir)
                        v.Set("baseURL", "https://example.org")
                        v.Set("timeout", timeout)
       @@ -141,7 +141,7 @@ IMG SHORTCODE: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_129x239_r
        
                assertImages := func() {
                        b.Helper()
       -                b.AssertFileContent(filepath.Join(workDir, "public/index.html"), imgExpect)
       +                b.AssertFileContent("public/index.html", imgExpect)
                        b.AssertImage(350, 219, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_350x0_resize_q75_box.a86fe88d894e5db613f6aa8a80538fefc25b20fa24ba0d782c057adcef616f56.jpg")
                        b.AssertImage(129, 239, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_129x239_resize_q75_box.jpg")
                }
   DIR diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
       @@ -47,6 +47,8 @@ func NewIntegrationTestBuilder(conf IntegrationTestConfig) *IntegrationTestBuild
                        if doClean {
                                c.Cleanup(clean)
                        }
       +        } else if conf.WorkingDir == "" {
       +                conf.WorkingDir = helpers.FilePathSeparator
                }
        
                return &IntegrationTestBuilder{
       @@ -157,7 +159,7 @@ func (s *IntegrationTestBuilder) AssertDestinationExists(filename string, b bool
        }
        
        func (s *IntegrationTestBuilder) destinationExists(filename string) bool {
       -        b, err := helpers.Exists(filename, s.fs.Destination)
       +        b, err := helpers.Exists(filename, s.fs.PublishDir)
                if err != nil {
                        panic(err)
                }
       @@ -258,11 +260,7 @@ func (s *IntegrationTestBuilder) RenameFile(old, new string) *IntegrationTestBui
        
        func (s *IntegrationTestBuilder) FileContent(filename string) string {
                s.Helper()
       -        filename = filepath.FromSlash(filename)
       -        if !strings.HasPrefix(filename, s.Cfg.WorkingDir) {
       -                filename = filepath.Join(s.Cfg.WorkingDir, filename)
       -        }
       -        return s.readDestination(s, s.fs, filename)
       +        return s.readWorkingDir(s, s.fs, filepath.FromSlash(filename))
        }
        
        func (s *IntegrationTestBuilder) initBuilder() {
       @@ -280,8 +278,6 @@ func (s *IntegrationTestBuilder) initBuilder() {
        
                        logger := loggers.NewBasicLoggerForWriter(s.Cfg.LogLevel, &s.logBuff)
        
       -                fs := hugofs.NewFrom(afs, config.New())
       -
                        for _, f := range s.data.Files {
                                filename := filepath.Join(s.Cfg.WorkingDir, f.Name)
                                s.Assert(afs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil)
       @@ -301,10 +297,12 @@ func (s *IntegrationTestBuilder) initBuilder() {
                                },
                        )
        
       -                s.Assert(err, qt.IsNil)
       -
                        cfg.Set("workingDir", s.Cfg.WorkingDir)
        
       +                fs := hugofs.NewFrom(afs, cfg)
       +
       +                s.Assert(err, qt.IsNil)
       +
                        depsCfg := deps.DepsCfg{Cfg: cfg, Fs: fs, Running: s.Cfg.Running, Logger: logger}
                        sites, err := NewHugoSites(depsCfg)
                        s.Assert(err, qt.IsNil)
       @@ -400,9 +398,9 @@ func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
                return events
        }
        
       -func (s *IntegrationTestBuilder) readDestination(t testing.TB, fs *hugofs.Fs, filename string) string {
       +func (s *IntegrationTestBuilder) readWorkingDir(t testing.TB, fs *hugofs.Fs, filename string) string {
                t.Helper()
       -        return s.readFileFromFs(t, fs.Destination, filename)
       +        return s.readFileFromFs(t, fs.WorkingDirReadOnly, filename)
        }
        
        func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filename string) string {
   DIR diff --git a/hugolib/language_content_dir_test.go b/hugolib/language_content_dir_test.go
       @@ -224,8 +224,8 @@ Content.
                nnSite := b.H.Sites[1]
                svSite := b.H.Sites[2]
        
       -        b.AssertFileContent("/my/project/public/en/mystatic/file1.yaml", "en")
       -        b.AssertFileContent("/my/project/public/nn/mystatic/file1.yaml", "nn")
       +        b.AssertFileContent("public/en/mystatic/file1.yaml", "en")
       +        b.AssertFileContent("public/nn/mystatic/file1.yaml", "nn")
        
                // dumpPages(nnSite.RegularPages()...)
        
       @@ -300,16 +300,16 @@ Content.
                c.Assert(len(bundleSv.Resources()), qt.Equals, 4)
                c.Assert(len(bundleEn.Resources()), qt.Equals, 4)
        
       -        b.AssertFileContent("/my/project/public/en/sect/mybundle/index.html", "image/png: /en/sect/mybundle/logo.png")
       -        b.AssertFileContent("/my/project/public/nn/sect/mybundle/index.html", "image/png: /nn/sect/mybundle/logo.png")
       -        b.AssertFileContent("/my/project/public/sv/sect/mybundle/index.html", "image/png: /sv/sect/mybundle/logo.png")
       +        b.AssertFileContent("public/en/sect/mybundle/index.html", "image/png: /en/sect/mybundle/logo.png")
       +        b.AssertFileContent("public/nn/sect/mybundle/index.html", "image/png: /nn/sect/mybundle/logo.png")
       +        b.AssertFileContent("public/sv/sect/mybundle/index.html", "image/png: /sv/sect/mybundle/logo.png")
        
       -        b.AssertFileContent("/my/project/public/sv/sect/mybundle/featured.png", "PNG Data for sv")
       -        b.AssertFileContent("/my/project/public/nn/sect/mybundle/featured.png", "PNG Data for nn")
       -        b.AssertFileContent("/my/project/public/en/sect/mybundle/featured.png", "PNG Data for en")
       -        b.AssertFileContent("/my/project/public/en/sect/mybundle/logo.png", "PNG Data")
       -        b.AssertFileContent("/my/project/public/sv/sect/mybundle/logo.png", "PNG Data")
       -        b.AssertFileContent("/my/project/public/nn/sect/mybundle/logo.png", "PNG Data")
       +        b.AssertFileContent("public/sv/sect/mybundle/featured.png", "PNG Data for sv")
       +        b.AssertFileContent("public/nn/sect/mybundle/featured.png", "PNG Data for nn")
       +        b.AssertFileContent("public/en/sect/mybundle/featured.png", "PNG Data for en")
       +        b.AssertFileContent("public/en/sect/mybundle/logo.png", "PNG Data")
       +        b.AssertFileContent("public/sv/sect/mybundle/logo.png", "PNG Data")
       +        b.AssertFileContent("public/nn/sect/mybundle/logo.png", "PNG Data")
        
                nnSect := nnSite.getPage(page.KindSection, "sect")
                c.Assert(nnSect, qt.Not(qt.IsNil))
   DIR diff --git a/hugolib/minify_publisher_test.go b/hugolib/minify_publisher_test.go
       @@ -22,7 +22,7 @@ import (
        func TestMinifyPublisher(t *testing.T) {
                t.Parallel()
        
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("minify", true)
                v.Set("baseURL", "https://example.org/")
        
   DIR diff --git a/hugolib/mount_filters_test.go b/hugolib/mount_filters_test.go
       @@ -101,13 +101,13 @@ Resources: {{ resources.Match "**.js" }}
        
                assertExists := func(name string, shouldExist bool) {
                        b.Helper()
       -                b.Assert(b.CheckExists(filepath.Join(workingDir, name)), qt.Equals, shouldExist)
       +                b.Assert(b.CheckExists(name), qt.Equals, shouldExist)
                }
        
                assertExists("public/a/b/p1/index.html", true)
                assertExists("public/a/c/p2/index.html", false)
        
       -        b.AssertFileContent(filepath.Join(workingDir, "public", "index.html"), `
       +        b.AssertFileContent(filepath.Join("public", "index.html"), `
        Data: map[mydata:map[b:map[b1:bval]]]:END        
        Template: false
        Resource1: js/include.js:END
   DIR diff --git a/hugolib/page_test.go b/hugolib/page_test.go
       @@ -23,7 +23,6 @@ import (
                "time"
        
                "github.com/gohugoio/hugo/htesting"
       -
                "github.com/gohugoio/hugo/markup/asciidocext"
                "github.com/gohugoio/hugo/markup/rst"
        
       @@ -35,7 +34,6 @@ import (
        
                "github.com/gohugoio/hugo/resources/page"
                "github.com/gohugoio/hugo/resources/resource"
       -        "github.com/spf13/afero"
                "github.com/spf13/jwalterweatherman"
        
                qt "github.com/frankban/quicktest"
       @@ -1031,14 +1029,14 @@ func TestPageWithLastmodFromGitInfo(t *testing.T) {
                }
                c := qt.New(t)
        
       -        // We need to use the OS fs for this.
       -        cfg := config.New()
       -        fs := hugofs.NewFrom(hugofs.Os, cfg)
       -        fs.Destination = &afero.MemMapFs{}
       -
                wd, err := os.Getwd()
                c.Assert(err, qt.IsNil)
        
       +        // We need to use the OS fs for this.
       +        cfg := config.NewWithTestDefaults()
       +        cfg.Set("workingDir", filepath.Join(wd, "testsite"))
       +        fs := hugofs.NewFrom(hugofs.Os, cfg)
       +
                cfg.Set("frontmatter", map[string]any{
                        "lastmod": []string{":git", "lastmod"},
                })
       @@ -1060,8 +1058,6 @@ func TestPageWithLastmodFromGitInfo(t *testing.T) {
                cfg.Set("languages", langConfig)
                cfg.Set("enableGitInfo", true)
        
       -        cfg.Set("workingDir", filepath.Join(wd, "testsite"))
       -
                b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded()
        
                b.Build(BuildCfg{SkipRender: true})
       @@ -1314,7 +1310,7 @@ func TestChompBOM(t *testing.T) {
        
        func TestPageWithEmoji(t *testing.T) {
                for _, enableEmoji := range []bool{true, false} {
       -                v := config.New()
       +                v := config.NewWithTestDefaults()
                        v.Set("enableEmoji", enableEmoji)
        
                        b := newTestSitesBuilder(t).WithViper(v)
   DIR diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go
       @@ -127,22 +127,22 @@ func TestPageBundlerSiteRegular(t *testing.T) {
        
                                                        // Check both output formats
                                                        rel, filename := relFilename("/a/1/", "index.html")
       -                                                b.AssertFileContent(filepath.Join("/work/public", filename),
       +                                                b.AssertFileContent(filepath.Join("public", filename),
                                                                "TheContent",
                                                                "Single RelPermalink: "+rel,
                                                        )
        
                                                        rel, filename = relFilename("/cpath/a/1/", "cindex.html")
        
       -                                                b.AssertFileContent(filepath.Join("/work/public", filename),
       +                                                b.AssertFileContent(filepath.Join("public", filename),
                                                                "TheContent",
                                                                "Single RelPermalink: "+rel,
                                                        )
        
       -                                                b.AssertFileContent(filepath.FromSlash("/work/public/images/hugo-logo.png"), "content")
       +                                                b.AssertFileContent(filepath.FromSlash("public/images/hugo-logo.png"), "content")
        
                                                        // This should be just copied to destination.
       -                                                b.AssertFileContent(filepath.FromSlash("/work/public/assets/pic1.png"), "content")
       +                                                b.AssertFileContent(filepath.FromSlash("public/assets/pic1.png"), "content")
        
                                                        leafBundle1 := s.getPage(page.KindPage, "b/my-bundle/index.md")
                                                        c.Assert(leafBundle1, qt.Not(qt.IsNil))
       @@ -159,8 +159,8 @@ func TestPageBundlerSiteRegular(t *testing.T) {
                                                        c.Assert(rootBundle, qt.Not(qt.IsNil))
                                                        c.Assert(rootBundle.Parent().IsHome(), qt.Equals, true)
                                                        if !ugly {
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single RelPermalink: "+relURLBase+"/root/")
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/cpath/root/cindex.html"), "Single RelPermalink: "+relURLBase+"/cpath/root/")
       +                                                        b.AssertFileContent(filepath.FromSlash("public/root/index.html"), "Single RelPermalink: "+relURLBase+"/root/")
       +                                                        b.AssertFileContent(filepath.FromSlash("public/cpath/root/cindex.html"), "Single RelPermalink: "+relURLBase+"/cpath/root/")
                                                        }
        
                                                        leafBundle2 := s.getPage(page.KindPage, "a/b/index.md")
       @@ -202,17 +202,17 @@ func TestPageBundlerSiteRegular(t *testing.T) {
                                                        }
        
                                                        if ugly {
       -                                                        b.AssertFileContent("/work/public/2017/pageslug.html",
       +                                                        b.AssertFileContent("public/2017/pageslug.html",
                                                                        relPermalinker("Single RelPermalink: %s/2017/pageslug.html"),
                                                                        permalinker("Single Permalink: %s/2017/pageslug.html"),
                                                                        relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
                                                                        permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"))
                                                        } else {
       -                                                        b.AssertFileContent("/work/public/2017/pageslug/index.html",
       +                                                        b.AssertFileContent("public/2017/pageslug/index.html",
                                                                        relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
                                                                        permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"))
        
       -                                                        b.AssertFileContent("/work/public/cpath/2017/pageslug/cindex.html",
       +                                                        b.AssertFileContent("public/cpath/2017/pageslug/cindex.html",
                                                                        relPermalinker("Single RelPermalink: %s/cpath/2017/pageslug/"),
                                                                        relPermalinker("Short Sunset RelPermalink: %s/cpath/2017/pageslug/sunset2.jpg"),
                                                                        relPermalinker("Sunset RelPermalink: %s/cpath/2017/pageslug/sunset1.jpg"),
       @@ -220,15 +220,15 @@ func TestPageBundlerSiteRegular(t *testing.T) {
                                                                )
                                                        }
        
       -                                                b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/c/logo.png"), "content")
       -                                                b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/c/logo.png"), "content")
       -                                                c.Assert(b.CheckExists("/work/public/cpath/cpath/2017/pageslug/c/logo.png"), qt.Equals, false)
       +                                                b.AssertFileContent(filepath.FromSlash("public/2017/pageslug/c/logo.png"), "content")
       +                                                b.AssertFileContent(filepath.FromSlash("public/cpath/2017/pageslug/c/logo.png"), "content")
       +                                                c.Assert(b.CheckExists("public/cpath/cpath/2017/pageslug/c/logo.png"), qt.Equals, false)
        
                                                        // Custom media type defined in site config.
                                                        c.Assert(len(leafBundle1.Resources().ByType("bepsays")), qt.Equals, 1)
        
                                                        if ugly {
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug.html"),
       +                                                        b.AssertFileContent(filepath.FromSlash("public/2017/pageslug.html"),
                                                                        "TheContent",
                                                                        relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
                                                                        permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"),
       @@ -247,18 +247,18 @@ func TestPageBundlerSiteRegular(t *testing.T) {
        
                                                                // https://github.com/gohugoio/hugo/issues/5882
                                                                b.AssertFileContent(
       -                                                                filepath.FromSlash("/work/public/2017/pageslug.html"), "0: Page RelPermalink: |")
       +                                                                filepath.FromSlash("public/2017/pageslug.html"), "0: Page RelPermalink: |")
        
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug.html"), "TheContent")
       +                                                        b.AssertFileContent(filepath.FromSlash("public/cpath/2017/pageslug.html"), "TheContent")
        
                                                                // 은행
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/c/은행/logo-은행.png"), "은행 PNG")
       +                                                        b.AssertFileContent(filepath.FromSlash("public/c/은행/logo-은행.png"), "은행 PNG")
        
                                                        } else {
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "TheContent")
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/cindex.html"), "TheContent")
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "Single Title")
       -                                                        b.AssertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single Title")
       +                                                        b.AssertFileContent(filepath.FromSlash("public/2017/pageslug/index.html"), "TheContent")
       +                                                        b.AssertFileContent(filepath.FromSlash("public/cpath/2017/pageslug/cindex.html"), "TheContent")
       +                                                        b.AssertFileContent(filepath.FromSlash("public/2017/pageslug/index.html"), "Single Title")
       +                                                        b.AssertFileContent(filepath.FromSlash("public/root/index.html"), "Single Title")
        
                                                        }
                                                })
       @@ -397,23 +397,24 @@ func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) {
                }()
        
                c := qt.New(t)
       -        // We need to use the OS fs for this.
       -        cfg := config.New()
       -        fs := hugofs.NewFrom(hugofs.Os, cfg)
        
       -        workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugosym")
       +        // We need to use the OS fs for this.
       +        workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugosym")
                c.Assert(err, qt.IsNil)
       +        cfg := config.NewWithTestDefaults()
       +        cfg.Set("workingDir", workingDir)
       +        fs := hugofs.NewFrom(hugofs.Os, cfg)
        
                contentDirName := "content"
        
       -        contentDir := filepath.Join(workDir, contentDirName)
       +        contentDir := filepath.Join(workingDir, contentDirName)
                c.Assert(os.MkdirAll(filepath.Join(contentDir, "a"), 0777), qt.IsNil)
        
                for i := 1; i <= 3; i++ {
       -                c.Assert(os.MkdirAll(filepath.Join(workDir, fmt.Sprintf("symcontent%d", i)), 0777), qt.IsNil)
       +                c.Assert(os.MkdirAll(filepath.Join(workingDir, fmt.Sprintf("symcontent%d", i)), 0777), qt.IsNil)
                }
        
       -        c.Assert(os.MkdirAll(filepath.Join(workDir, "symcontent2", "a1"), 0777), qt.IsNil)
       +        c.Assert(os.MkdirAll(filepath.Join(workingDir, "symcontent2", "a1"), 0777), qt.IsNil)
        
                // Symlinked sections inside content.
                os.Chdir(contentDir)
       @@ -431,11 +432,11 @@ func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) {
                // Create a circular symlink. Will print some warnings.
                c.Assert(os.Symlink(filepath.Join("..", contentDirName), filepath.FromSlash("circus")), qt.IsNil)
        
       -        c.Assert(os.Chdir(workDir), qt.IsNil)
       +        c.Assert(os.Chdir(workingDir), qt.IsNil)
        
                defer clean()
        
       -        cfg.Set("workingDir", workDir)
       +        cfg.Set("workingDir", workingDir)
                cfg.Set("contentDir", contentDirName)
                cfg.Set("baseURL", "https://example.com")
        
       @@ -488,9 +489,9 @@ TheContent.
                c.Assert(len(a1Bundle.Resources()), qt.Equals, 2)
                c.Assert(len(a1Bundle.Resources().ByType(pageResourceType)), qt.Equals, 1)
        
       -        b.AssertFileContent(filepath.FromSlash(workDir+"/public/a/page/index.html"), "TheContent")
       -        b.AssertFileContent(filepath.FromSlash(workDir+"/public/symbolic1/s1/index.html"), "TheContent")
       -        b.AssertFileContent(filepath.FromSlash(workDir+"/public/symbolic2/a1/index.html"), "TheContent")
       +        b.AssertFileContent(filepath.FromSlash("public/a/page/index.html"), "TheContent")
       +        b.AssertFileContent(filepath.FromSlash("public/symbolic1/s1/index.html"), "TheContent")
       +        b.AssertFileContent(filepath.FromSlash("public/symbolic2/a1/index.html"), "TheContent")
        }
        
        func TestPageBundlerHeadless(t *testing.T) {
       @@ -563,12 +564,12 @@ HEADLESS {{< myShort >}}
        
                th := newTestHelper(s.Cfg, s.Fs, t)
        
       -        th.assertFileContent(filepath.FromSlash(workDir+"/public/s1/index.html"), "TheContent")
       -        th.assertFileContent(filepath.FromSlash(workDir+"/public/s1/l1.png"), "PNG")
       +        th.assertFileContent(filepath.FromSlash("public/s1/index.html"), "TheContent")
       +        th.assertFileContent(filepath.FromSlash("public/s1/l1.png"), "PNG")
        
       -        th.assertFileNotExist(workDir + "/public/s2/index.html")
       +        th.assertFileNotExist("public/s2/index.html")
                // But the bundled resources needs to be published
       -        th.assertFileContent(filepath.FromSlash(workDir+"/public/s2/l1.png"), "PNG")
       +        th.assertFileContent(filepath.FromSlash("public/s2/l1.png"), "PNG")
        
                // No headless bundles here, please.
                // https://github.com/gohugoio/hugo/issues/6492
       @@ -1321,7 +1322,7 @@ func TestPageBundlerHome(t *testing.T) {
                workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-bundler-home")
                c.Assert(err, qt.IsNil)
        
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("workingDir", workDir)
                fs := hugofs.NewFrom(hugofs.Os, cfg)
        
   DIR diff --git a/hugolib/paths/paths.go b/hugolib/paths/paths.go
       @@ -18,6 +18,8 @@ import (
                "path/filepath"
                "strings"
        
       +        hpaths "github.com/gohugoio/hugo/common/paths"
       +
                "github.com/gohugoio/hugo/config"
                "github.com/gohugoio/hugo/langs"
                "github.com/gohugoio/hugo/modules"
       @@ -51,6 +53,7 @@ type Paths struct {
                // pagination path handling
                PaginatePath string
        
       +        // TODO1 check usage
                PublishDir string
        
                // When in multihost mode, this returns a list of base paths below PublishDir
       @@ -123,7 +126,7 @@ func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) {
                        languages = langs.Languages{&langs.Language{Lang: "en", Cfg: cfg, ContentDir: contentDir}}
                }
        
       -        absPublishDir := AbsPathify(workingDir, publishDir)
       +        absPublishDir := hpaths.AbsPathify(workingDir, publishDir)
                if !strings.HasSuffix(absPublishDir, FilePathSeparator) {
                        absPublishDir += FilePathSeparator
                }
       @@ -131,7 +134,7 @@ func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) {
                if absPublishDir == "//" {
                        absPublishDir = FilePathSeparator
                }
       -        absResourcesDir := AbsPathify(workingDir, resourceDir)
       +        absResourcesDir := hpaths.AbsPathify(workingDir, resourceDir)
                if !strings.HasSuffix(absResourcesDir, FilePathSeparator) {
                        absResourcesDir += FilePathSeparator
                }
       @@ -254,7 +257,7 @@ func (p *Paths) GetLangSubDir(lang string) string {
        // AbsPathify creates an absolute path if given a relative path. If already
        // absolute, the path is just cleaned.
        func (p *Paths) AbsPathify(inPath string) string {
       -        return AbsPathify(p.WorkingDir, inPath)
       +        return hpaths.AbsPathify(p.WorkingDir, inPath)
        }
        
        // RelPathify trims any WorkingDir prefix from the given filename. If
       @@ -267,12 +270,3 @@ func (p *Paths) RelPathify(filename string) string {
        
                return strings.TrimPrefix(strings.TrimPrefix(filename, p.WorkingDir), FilePathSeparator)
        }
       -
       -// AbsPathify creates an absolute path if given a working dir and a relative path.
       -// If already absolute, the path is just cleaned.
       -func AbsPathify(workingDir, inPath string) string {
       -        if filepath.IsAbs(inPath) {
       -                return filepath.Clean(inPath)
       -        }
       -        return filepath.Join(workingDir, inPath)
       -}
   DIR diff --git a/hugolib/paths/paths_test.go b/hugolib/paths/paths_test.go
       @@ -25,7 +25,7 @@ import (
        func TestNewPaths(t *testing.T) {
                c := qt.New(t)
        
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                fs := hugofs.NewMem(v)
        
                v.Set("languages", map[string]any{
   DIR diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go
       @@ -137,7 +137,7 @@ Edited content.
        
        `)
        
       -                b.Assert(b.Fs.Destination.Remove("public"), qt.IsNil)
       +                b.Assert(b.Fs.WorkingDirWritable.Remove("public"), qt.IsNil)
                        b.H.ResourceSpec.ClearCaches()
        
                }
   DIR diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go
       @@ -28,7 +28,7 @@ const robotTxtTemplate = `User-agent: Googlebot
        func TestRobotsTXTOutput(t *testing.T) {
                t.Parallel()
        
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("baseURL", "http://auth/bub/")
                cfg.Set("enableRobotsTXT", true)
        
   DIR diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go
       @@ -50,7 +50,7 @@ func TestRSSOutput(t *testing.T) {
                th.assertFileContent(filepath.Join("public", "categories", "hugo", rssURI), "<?xml", "rss version", "hugo on RSSTest")
        
                // RSS Item Limit
       -        content := readDestination(t, fs, filepath.Join("public", rssURI))
       +        content := readWorkingDir(t, fs, filepath.Join("public", rssURI))
                c := strings.Count(content, "<item>")
                if c != rssLimit {
                        t.Errorf("incorrect RSS item count: expected %d, got %d", rssLimit, c)
   DIR diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
       @@ -1212,7 +1212,7 @@ title: "Hugo Rocks!"
        func TestShortcodeEmoji(t *testing.T) {
                t.Parallel()
        
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("enableEmoji", true)
        
                builder := newTestSitesBuilder(t).WithViper(v)
       @@ -1277,7 +1277,7 @@ func TestShortcodeRef(t *testing.T) {
                        t.Run(fmt.Sprintf("plainIDAnchors=%t", plainIDAnchors), func(t *testing.T) {
                                t.Parallel()
        
       -                        v := config.New()
       +                        v := config.NewWithTestDefaults()
                                v.Set("baseURL", "https://example.org")
                                v.Set("blackfriday", map[string]any{
                                        "plainIDAnchors": plainIDAnchors,
   DIR diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go
       @@ -363,7 +363,7 @@ func TestCreateSiteOutputFormats(t *testing.T) {
                                page.KindSection: []string{"JSON"},
                        }
        
       -                cfg := config.New()
       +                cfg := config.NewWithTestDefaults()
                        cfg.Set("outputs", outputsConfig)
        
                        outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
       @@ -388,7 +388,7 @@ func TestCreateSiteOutputFormats(t *testing.T) {
                // Issue #4528
                t.Run("Mixed case", func(t *testing.T) {
                        c := qt.New(t)
       -                cfg := config.New()
       +                cfg := config.NewWithTestDefaults()
        
                        outputsConfig := map[string]any{
                                // Note that we in Hugo 0.53.0 renamed this Kind to "taxonomy",
       @@ -410,7 +410,7 @@ func TestCreateSiteOutputFormatsInvalidConfig(t *testing.T) {
                        page.KindHome: []string{"FOO", "JSON"},
                }
        
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("outputs", outputsConfig)
        
                _, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
       @@ -424,7 +424,7 @@ func TestCreateSiteOutputFormatsEmptyConfig(t *testing.T) {
                        page.KindHome: []string{},
                }
        
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("outputs", outputsConfig)
        
                outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
       @@ -439,7 +439,7 @@ func TestCreateSiteOutputFormatsCustomFormats(t *testing.T) {
                        page.KindHome: []string{},
                }
        
       -        cfg := config.New()
       +        cfg := config.NewWithTestDefaults()
                cfg.Set("outputs", outputsConfig)
        
                var (
   DIR diff --git a/hugolib/site_test.go b/hugolib/site_test.go
       @@ -336,7 +336,7 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) {
                }
        
                for _, test := range tests {
       -                content := readDestination(t, fs, test.doc)
       +                content := readWorkingDir(t, fs, test.doc)
        
                        if content != test.expected {
                                t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content)
       @@ -362,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 := config.New()
       +                        v := config.NewWithTestDefaults()
                                if paramSet {
                                        v.Set("params", map[string]any{
                                                "mainSections": []string{"a1", "a2"},
   DIR diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go
       @@ -76,7 +76,7 @@ func TestPageCount(t *testing.T) {
                writeSourcesToSource(t, "", fs, urlFakeSource...)
                s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
        
       -        _, err := s.Fs.Destination.Open("public/blue")
       +        _, err := s.Fs.WorkingDirReadOnly.Open("public/blue")
                if err != nil {
                        t.Errorf("No indexed rendered.")
                }
       @@ -87,7 +87,7 @@ func TestPageCount(t *testing.T) {
                        "public/sd3/index.html",
                        "public/sd4.html",
                } {
       -                if _, err := s.Fs.Destination.Open(filepath.FromSlash(pth)); err != nil {
       +                if _, err := s.Fs.WorkingDirReadOnly.Open(filepath.FromSlash(pth)); err != nil {
                                t.Errorf("No alias rendered: %s", pth)
                        }
                }
   DIR diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go
       @@ -80,7 +80,7 @@ func doTestSitemapOutput(t *testing.T, internal bool) {
                        "<loc>http://auth/bub/categories/hugo/</loc>",
                )
        
       -        content := readDestination(th, th.Fs, outputSitemap)
       +        content := readWorkingDir(th, th.Fs, outputSitemap)
                c.Assert(content, qt.Not(qt.Contains), "404")
                c.Assert(content, qt.Not(qt.Contains), "<loc></loc>")
        }
   DIR diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go
       @@ -114,7 +114,7 @@ type filenameContent struct {
        }
        
        func newTestSitesBuilder(t testing.TB) *sitesBuilder {
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                fs := hugofs.NewMem(v)
        
                litterOptions := litter.Options{
       @@ -475,6 +475,9 @@ func (s *sitesBuilder) CreateSites() *sitesBuilder {
                        s.Fatalf("Failed to create sites: %s", err)
                }
        
       +        s.Assert(s.Fs.PublishDir, qt.IsNotNil)
       +        s.Assert(s.Fs.WorkingDirReadOnly, qt.IsNotNil)
       +
                return s
        }
        
       @@ -536,7 +539,7 @@ func (s *sitesBuilder) CreateSitesE() error {
                        return errors.Wrap(err, "failed to load config")
                }
        
       -        s.Fs.Destination = hugofs.NewCreateCountingFs(s.Fs.Destination)
       +        s.Fs.PublishDir = hugofs.NewCreateCountingFs(s.Fs.PublishDir)
        
                depsCfg := s.depsCfg
                depsCfg.Fs = s.Fs
       @@ -759,8 +762,7 @@ func (s *sitesBuilder) AssertFileDoesNotExist(filename string) {
        }
        
        func (s *sitesBuilder) AssertImage(width, height int, filename string) {
       -        filename = filepath.Join(s.workingDir, filename)
       -        f, err := s.Fs.Destination.Open(filename)
       +        f, err := s.Fs.WorkingDirReadOnly.Open(filename)
                s.Assert(err, qt.IsNil)
                defer f.Close()
                cfg, err := jpeg.DecodeConfig(f)
       @@ -771,17 +773,14 @@ func (s *sitesBuilder) AssertImage(width, height int, filename string) {
        
        func (s *sitesBuilder) AssertNoDuplicateWrites() {
                s.Helper()
       -        d := s.Fs.Destination.(hugofs.DuplicatesReporter)
       +        d := s.Fs.PublishDir.(hugofs.DuplicatesReporter)
                s.Assert(d.ReportDuplicates(), qt.Equals, "")
        }
        
        func (s *sitesBuilder) FileContent(filename string) string {
       -        s.T.Helper()
       +        s.Helper()
                filename = filepath.FromSlash(filename)
       -        if !strings.HasPrefix(filename, s.workingDir) {
       -                filename = filepath.Join(s.workingDir, filename)
       -        }
       -        return readDestination(s.T, s.Fs, filename)
       +        return readWorkingDir(s.T, s.Fs, filename)
        }
        
        func (s *sitesBuilder) AssertObject(expected string, object any) {
       @@ -797,7 +796,7 @@ func (s *sitesBuilder) AssertObject(expected string, object any) {
        }
        
        func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) {
       -        content := readDestination(s.T, s.Fs, filename)
       +        content := readWorkingDir(s.T, s.Fs, filename)
                for _, match := range matches {
                        r := regexp.MustCompile("(?s)" + match)
                        if !r.MatchString(content) {
       @@ -807,7 +806,7 @@ func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) {
        }
        
        func (s *sitesBuilder) CheckExists(filename string) bool {
       -        return destinationExists(s.Fs, filepath.Clean(filename))
       +        return workingDirExists(s.Fs, filepath.Clean(filename))
        }
        
        func (s *sitesBuilder) GetPage(ref string) page.Page {
       @@ -848,7 +847,7 @@ type testHelper struct {
        func (th testHelper) assertFileContent(filename string, matches ...string) {
                th.Helper()
                filename = th.replaceDefaultContentLanguageValue(filename)
       -        content := readDestination(th, th.Fs, filename)
       +        content := readWorkingDir(th, th.Fs, filename)
                for _, match := range matches {
                        match = th.replaceDefaultContentLanguageValue(match)
                        th.Assert(strings.Contains(content, match), qt.Equals, true, qt.Commentf(match+" not in: \n"+content))
       @@ -857,7 +856,7 @@ func (th testHelper) assertFileContent(filename string, matches ...string) {
        
        func (th testHelper) assertFileContentRegexp(filename string, matches ...string) {
                filename = th.replaceDefaultContentLanguageValue(filename)
       -        content := readDestination(th, th.Fs, filename)
       +        content := readWorkingDir(th, th.Fs, filename)
                for _, match := range matches {
                        match = th.replaceDefaultContentLanguageValue(match)
                        r := regexp.MustCompile(match)
       @@ -870,7 +869,7 @@ func (th testHelper) assertFileContentRegexp(filename string, matches ...string)
        }
        
        func (th testHelper) assertFileNotExist(filename string) {
       -        exists, err := helpers.Exists(filename, th.Fs.Destination)
       +        exists, err := helpers.Exists(filename, th.Fs.PublishDir)
                th.Assert(err, qt.IsNil)
                th.Assert(exists, qt.Equals, false)
        }
       @@ -892,7 +891,7 @@ func loadTestConfig(fs afero.Fs, withConfig ...func(cfg config.Provider) error) 
        
        func newTestCfgBasic() (config.Provider, *hugofs.Fs) {
                mm := afero.NewMemMapFs()
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("defaultContentLanguageInSubdir", true)
        
                fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v)
   DIR diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
       @@ -500,16 +500,7 @@ func newDepsConfig(tp *TranslationProvider, cfg config.Provider, fs *hugofs.Fs) 
        }
        
        func getConfig() config.Provider {
       -        v := config.New()
       -        v.Set("defaultContentLanguage", "en")
       -        v.Set("contentDir", "content")
       -        v.Set("dataDir", "data")
       -        v.Set("i18nDir", "i18n")
       -        v.Set("layoutDir", "layouts")
       -        v.Set("archetypeDir", "archetypes")
       -        v.Set("assetDir", "assets")
       -        v.Set("resourceDir", "resources")
       -        v.Set("publishDir", "public")
       +        v := config.NewWithTestDefaults()
                langs.LoadLanguageSettings(v, nil)
                mod, err := modules.CreateProjectModule(v)
                if err != nil {
   DIR diff --git a/langs/language_test.go b/langs/language_test.go
       @@ -16,14 +16,13 @@ package langs
        import (
                "testing"
        
       -        "github.com/gohugoio/hugo/config"
       -
                qt "github.com/frankban/quicktest"
       +        "github.com/gohugoio/hugo/config"
        )
        
        func TestGetGlobalOnlySetting(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("defaultContentLanguageInSubdir", true)
                v.Set("contentDir", "content")
                v.Set("paginatePath", "page")
       @@ -38,7 +37,7 @@ func TestGetGlobalOnlySetting(t *testing.T) {
        func TestLanguageParams(t *testing.T) {
                c := qt.New(t)
        
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("p1", "p1cfg")
                v.Set("contentDir", "content")
        
   DIR diff --git a/markup/goldmark/codeblocks/integration_test.go b/markup/goldmark/codeblocks/integration_test.go
       @@ -262,7 +262,7 @@ Position: {{ .Position | safeHTML }}
                        },
                ).Build()
        
       -        b.AssertFileContent("public/p1/index.html", filepath.FromSlash("Position: \"content/p1.md:7:1\""))
       +        b.AssertFileContent("public/p1/index.html", filepath.FromSlash("Position: \"/content/p1.md:7:1\""))
        }
        
        // Issue 9571
   DIR diff --git a/minifiers/config_test.go b/minifiers/config_test.go
       @@ -16,14 +16,13 @@ package minifiers
        import (
                "testing"
        
       -        "github.com/gohugoio/hugo/config"
       -
                qt "github.com/frankban/quicktest"
       +        "github.com/gohugoio/hugo/config"
        )
        
        func TestConfig(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
        
                v.Set("minify", map[string]any{
                        "disablexml": true,
       @@ -53,7 +52,7 @@ func TestConfig(t *testing.T) {
        
        func TestConfigLegacy(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
        
                // This was a bool < Hugo v0.58.
                v.Set("minify", true)
   DIR diff --git a/minifiers/minifiers_test.go b/minifiers/minifiers_test.go
       @@ -28,7 +28,7 @@ import (
        
        func TestNew(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
        
                var rawJS string
       @@ -76,7 +76,7 @@ func TestNew(t *testing.T) {
        
        func TestConfigureMinify(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("minify", map[string]any{
                        "disablexml": true,
                        "tdewolff": map[string]any{
       @@ -110,7 +110,7 @@ func TestConfigureMinify(t *testing.T) {
        
        func TestJSONRoundTrip(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
        
                for _, test := range []string{`{
       @@ -148,7 +148,7 @@ func TestJSONRoundTrip(t *testing.T) {
        
        func TestBugs(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
        
                for _, test := range []struct {
       @@ -171,7 +171,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 := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("minify", map[string]any{
                        "disablexml": true,
                        "tdewolff": map[string]any{
       @@ -194,7 +194,7 @@ func TestDecodeConfigDecimalIsNowPrecision(t *testing.T) {
        // Issue 9456
        func TestDecodeConfigKeepWhitespace(t *testing.T) {
                c := qt.New(t)
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("minify", map[string]any{
                        "tdewolff": map[string]any{
                                "html": map[string]any{
   DIR diff --git a/publisher/htmlElementsCollector_test.go b/publisher/htmlElementsCollector_test.go
       @@ -23,7 +23,6 @@ import (
                "time"
        
                "github.com/gohugoio/hugo/config"
       -
                "github.com/gohugoio/hugo/media"
                "github.com/gohugoio/hugo/minifiers"
                "github.com/gohugoio/hugo/output"
       @@ -139,7 +138,7 @@ func TestClassCollector(t *testing.T) {
                                                if skipMinifyTest[test.name] {
                                                        c.Skip("skip minify test")
                                                }
       -                                        v := config.New()
       +                                        v := config.NewWithTestDefaults()
                                                m, _ := minifiers.New(media.DefaultTypes, output.DefaultFormats, v)
                                                m.Minify(media.HTMLType, w, strings.NewReader(test.html))
        
   DIR diff --git a/resources/resource_transformers/htesting/testhelpers.go b/resources/resource_transformers/htesting/testhelpers.go
       @@ -27,9 +27,7 @@ import (
        )
        
        func NewTestResourceSpec() (*resources.Spec, error) {
       -        cfg := config.New()
       -        cfg.Set("baseURL", "https://example.org")
       -        cfg.Set("publishDir", "public")
       +        cfg := config.NewWithTestDefaults()
        
                imagingCfg := map[string]any{
                        "resampleFilter": "linear",
   DIR diff --git a/resources/testhelpers_test.go b/resources/testhelpers_test.go
       @@ -79,7 +79,7 @@ func newTestResourceSpec(desc specDescriptor) *Spec {
                cfg.Set("imaging", imagingCfg)
        
                fs := hugofs.NewFrom(afs, cfg)
       -        fs.Destination = hugofs.NewCreateCountingFs(fs.Destination)
       +        fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
        
                s, err := helpers.NewPathSpec(fs, cfg, nil)
                c.Assert(err, qt.IsNil)
       @@ -118,7 +118,6 @@ func newTestResourceOsFs(c *qt.C) (*Spec, string) {
                cfg.Set("workingDir", workDir)
        
                fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(hugofs.Os), cfg)
       -        fs.Destination = &afero.MemMapFs{}
        
                s, err := helpers.NewPathSpec(fs, cfg, nil)
                c.Assert(err, qt.IsNil)
   DIR diff --git a/resources/transform_test.go b/resources/transform_test.go
       @@ -70,13 +70,13 @@ func TestTransform(t *testing.T) {
                // Verify that we publish the same file once only.
                assertNoDuplicateWrites := func(c *qt.C, spec *Spec) {
                        c.Helper()
       -                d := spec.Fs.Destination.(hugofs.DuplicatesReporter)
       +                d := spec.Fs.PublishDir.(hugofs.DuplicatesReporter)
                        c.Assert(d.ReportDuplicates(), qt.Equals, "")
                }
        
                assertShouldExist := func(c *qt.C, spec *Spec, filename string, should bool) {
                        c.Helper()
       -                exists, _ := helpers.Exists(filepath.FromSlash(filename), spec.Fs.Destination)
       +                exists, _ := helpers.Exists(filepath.FromSlash(filename), spec.Fs.WorkingDirReadOnly)
                        c.Assert(exists, qt.Equals, should)
                }
        
   DIR diff --git a/source/filesystem_test.go b/source/filesystem_test.go
       @@ -77,15 +77,7 @@ func TestUnicodeNorm(t *testing.T) {
        }
        
        func newTestConfig() config.Provider {
       -        v := config.New()
       -        v.Set("contentDir", "content")
       -        v.Set("dataDir", "data")
       -        v.Set("i18nDir", "i18n")
       -        v.Set("layoutDir", "layouts")
       -        v.Set("archetypeDir", "archetypes")
       -        v.Set("resourceDir", "resources")
       -        v.Set("publishDir", "public")
       -        v.Set("assetDir", "assets")
       +        v := config.NewWithTestDefaults()
                _, err := langs.LoadLanguageSettings(v, nil)
                if err != nil {
                        panic(err)
   DIR diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go
       @@ -985,7 +985,5 @@ func newDeps(cfg config.Provider) *deps.Deps {
        }
        
        func newTestNs() *Namespace {
       -        v := config.New()
       -        v.Set("contentDir", "content")
       -        return New(newDeps(v))
       +        return New(newDeps(config.NewWithTestDefaults()))
        }
   DIR diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go
       @@ -40,7 +40,7 @@ import (
        
        func TestScpGetLocal(t *testing.T) {
                t.Parallel()
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                fs := hugofs.NewMem(v)
                ps := helpers.FilePathSeparator
        
       @@ -145,9 +145,8 @@ func TestScpGetRemoteParallel(t *testing.T) {
                c.Assert(err, qt.IsNil)
        
                for _, ignoreCache := range []bool{false} {
       -                cfg := config.New()
       +                cfg := config.NewWithTestDefaults()
                        cfg.Set("ignoreCache", ignoreCache)
       -                cfg.Set("contentDir", "content")
        
                        ns := New(newDeps(cfg))
                        ns.client = cl
       @@ -227,7 +226,5 @@ func newDeps(cfg config.Provider) *deps.Deps {
        }
        
        func newTestNs() *Namespace {
       -        v := config.New()
       -        v.Set("contentDir", "content")
       -        return New(newDeps(v))
       +        return New(newDeps(config.NewWithTestDefaults()))
        }
   DIR diff --git a/tpl/images/images.go b/tpl/images/images.go
       @@ -74,7 +74,7 @@ func (ns *Namespace) Config(path any) (image.Config, error) {
                        return config, nil
                }
        
       -        f, err := ns.deps.Fs.WorkingDir.Open(filename)
       +        f, err := ns.deps.Fs.WorkingDirReadOnly.Open(filename)
                if err != nil {
                        return image.Config{}, err
                }
   DIR diff --git a/tpl/images/images_test.go b/tpl/images/images_test.go
       @@ -82,7 +82,7 @@ func TestNSConfig(t *testing.T) {
                t.Parallel()
                c := qt.New(t)
        
       -        v := config.New()
       +        v := config.NewWithTestDefaults()
                v.Set("workingDir", "/a/b")
        
                ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})