Repository: gohugoio/hugo (language: go)

Issue:
Add --date CLI flag
Some builds are run daily with expectations regarding dates. 

It would be interesting to be able to test drive the build on a with date so that Hugo time functions (`now` etc...) uses the time passed through the flag instead of the machine time.

This would allow users to make sure the expected behaviour on or after a certain date/time works as expected.

Parameter could be the same as what the [`time`](https://gohugo.io/functions/time/#readout) function is currently expecting.

```
hugo --date 2023-07-04
```

```
hugo --date 2016-05-28T10:30:00.00+10:00
```

I suspect this might be tricky, but if not, I'd regret not suggesting it.


Discussion / hints:
I'm not sure how to do this, but you would somehow need to set the Go clock. I don't think it's easy, but prove me wrong.

Fix patch:
diff --git a/cache/filecache/filecache.go b/cache/filecache/filecache.go
index 5a87382aeaf..3104d8ac6da 100644
--- a/cache/filecache/filecache.go
+++ b/cache/filecache/filecache.go
@@ -24,6 +24,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/hugio"
 
 	"github.com/gohugoio/hugo/helpers"
@@ -295,7 +296,7 @@ func (c *Cache) isExpired(modTime time.Time) bool {
 	if c.maxAge < 0 {
 		return false
 	}
-	return c.maxAge == 0 || time.Since(modTime) > c.maxAge
+	return c.maxAge == 0 || htime.Since(modTime) > c.maxAge
 }
 
 // For testing
diff --git a/commands/commandeer.go b/commands/commandeer.go
index 5b192c17250..b302cbfe0a3 100644
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -15,6 +15,7 @@ package commands
 
 import (
 	"errors"
+	"fmt"
 	"io/ioutil"
 	"net"
 	"os"
@@ -28,9 +29,11 @@ import (
 	"golang.org/x/sync/semaphore"
 
 	"github.com/gohugoio/hugo/common/herrors"
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/hugo"
 	"github.com/gohugoio/hugo/common/paths"
 
+	"github.com/spf13/cast"
 	jww "github.com/spf13/jwalterweatherman"
 
 	"github.com/gohugoio/hugo/common/loggers"
@@ -41,6 +44,7 @@ import (
 	"github.com/gohugoio/hugo/hugolib"
 	"github.com/spf13/afero"
 
+	"github.com/bep/clock"
 	"github.com/bep/debounce"
 	"github.com/bep/overlayfs"
 	"github.com/gohugoio/hugo/common/types"
@@ -94,13 +98,12 @@ type commandeer struct {
 
 	serverPorts []serverPortListener
 
-	languagesConfigured bool
-	languages           langs.Languages
-	doLiveReload        bool
-	renderStaticToDisk  bool
-	fastRenderMode      bool
-	showErrorInBrowser  bool
-	wasError            bool
+	languages          langs.Languages
+	doLiveReload       bool
+	renderStaticToDisk bool
+	fastRenderMode     bool
+	showErrorInBrowser bool
+	wasError           bool
 
 	configured bool
 	paused     bool
@@ -164,6 +167,21 @@ func (c *commandeer) initFs(fs *hugofs.Fs) error {
 	return nil
 }
 
+func (c *commandeer) initClock(loc *time.Location) error {
+	bt := c.Cfg.GetString("clock")
+	if bt == "" {
+		return nil
+	}
+
+	t, err := cast.StringToDateInDefaultLocation(bt, loc)
+	if err != nil {
+		return fmt.Errorf(`failed to parse "clock" flag: %s`, err)
+	}
+
+	htime.Clock = clock.Start(t)
+	return nil
+}
+
 func newCommandeer(mustHaveConfigFile, failOnInitErr, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, cfgInit func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
 	var rebuildDebouncer func(f func())
 	if running {
@@ -341,9 +359,15 @@ func (c *commandeer) loadConfig() error {
 
 	c.configFiles = configFiles
 
-	if l, ok := c.Cfg.Get("languagesSorted").(langs.Languages); ok {
-		c.languagesConfigured = true
-		c.languages = l
+	var ok bool
+	c.languages, ok = c.Cfg.Get("languagesSorted").(langs.Languages)
+	if !ok {
+		panic("languages not configured")
+	}
+
+	err = c.initClock(langs.GetLocation(c.languages[0]))
+	if err != nil {
+		return err
 	}
 
 	// Set some commonly used flags
diff --git a/commands/commands.go b/commands/commands.go
index 99b0866e558..d55a4e9aab3 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -18,6 +18,7 @@ import (
 	"os"
 	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/hugo"
 	"github.com/gohugoio/hugo/common/loggers"
 	hpaths "github.com/gohugoio/hugo/common/paths"
@@ -151,7 +152,7 @@ built with love by spf13 and friends in Go.
 
 Complete documentation is available at https://gohugo.io/.`,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			defer cc.timeTrack(time.Now(), "Total")
+			defer cc.timeTrack(htime.Now(), "Total")
 			cfgInit := func(c *commandeer) error {
 				if cc.buildWatch {
 					c.Set("disableLiveReload", true)
@@ -210,6 +211,7 @@ type hugoBuilderCommon struct {
 
 	buildWatch bool
 	poll       string
+	clock      string
 
 	gc bool
 
@@ -236,7 +238,7 @@ func (cc *hugoBuilderCommon) timeTrack(start time.Time, name string) {
 	if cc.quiet {
 		return
 	}
-	elapsed := time.Since(start)
+	elapsed := htime.Since(start)
 	fmt.Printf("%s in %v ms\n", name, int(1000*elapsed.Seconds()))
 }
 
@@ -279,6 +281,7 @@ func (cc *hugoBuilderCommon) handleCommonBuilderFlags(cmd *cobra.Command) {
 	cmd.PersistentFlags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
 	cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
 	cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
+	cmd.PersistentFlags().StringVar(&cc.clock, "clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00")
 }
 
 func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
diff --git a/commands/hugo.go b/commands/hugo.go
index 6bd0605db65..43ec7e5c708 100644
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -33,6 +33,7 @@ import (
 	"github.com/gohugoio/hugo/hugofs/files"
 	"github.com/gohugoio/hugo/tpl"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/types"
 
 	"github.com/gohugoio/hugo/hugofs"
@@ -187,6 +188,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
 		"buildDrafts",
 		"buildFuture",
 		"buildExpired",
+		"clock",
 		"uglyURLs",
 		"canonifyURLs",
 		"enableRobotsTXT",
@@ -679,7 +681,7 @@ func (c *commandeer) firstPathSpec() *helpers.PathSpec {
 }
 
 func (c *commandeer) timeTrack(start time.Time, name string) {
-	elapsed := time.Since(start)
+	elapsed := htime.Since(start)
 	c.logger.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
 }
 
@@ -790,7 +792,7 @@ func (c *commandeer) fullRebuild(changeType string) {
 			time.Sleep(2 * time.Second)
 		}()
 
-		defer c.timeTrack(time.Now(), "Rebuilt")
+		defer c.timeTrack(htime.Now(), "Rebuilt")
 
 		c.commandeerHugoState = newCommandeerHugoState()
 		err := c.loadConfig()
@@ -900,7 +902,7 @@ func (c *commandeer) printChangeDetected(typ string) {
 
 	c.logger.Println(msg)
 	const layout = "2006-01-02 15:04:05.000 -0700"
-	c.logger.Println(time.Now().Format(layout))
+	c.logger.Println(htime.Now().Format(layout))
 }
 
 const (
@@ -1135,7 +1137,7 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
 		c.changeDetector.PrepareNew()
 
 		func() {
-			defer c.timeTrack(time.Now(), "Total")
+			defer c.timeTrack(htime.Now(), "Total")
 			if err := c.rebuildSites(dynamicEvents); err != nil {
 				c.handleBuildErr(err, "Rebuild failed")
 			}
diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go
index 4f17b2e3985..232df519c12 100644
--- a/commands/import_jekyll.go
+++ b/commands/import_jekyll.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/gohugoio/hugo/parser/pageparser"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/hugio"
 
 	"github.com/gohugoio/hugo/parser/metadecoders"
@@ -362,12 +363,12 @@ func parseJekyllFilename(filename string) (time.Time, string, error) {
 	re := regexp.MustCompile(`(\d+-\d+-\d+)-(.+)\..*`)
 	r := re.FindAllStringSubmatch(filename, -1)
 	if len(r) == 0 {
-		return time.Now(), "", errors.New("filename not match")
+		return htime.Now(), "", errors.New("filename not match")
 	}
 
 	postDate, err := time.Parse("2006-1-2", r[0][1])
 	if err != nil {
-		return time.Now(), "", err
+		return htime.Now(), "", err
 	}
 
 	postName := r[0][2]
diff --git a/commands/list.go b/commands/list.go
index f4321d03115..4b62c91c53f 100644
--- a/commands/list.go
+++ b/commands/list.go
@@ -99,6 +99,10 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
 					return newSystemError("Error building sites", err)
 				}
 
+				if err != nil {
+					return newSystemError("Error building sites", err)
+				}
+
 				writer := csv.NewWriter(os.Stdout)
 				defer writer.Flush()
 
@@ -127,6 +131,10 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
 					return newSystemError("Error building sites", err)
 				}
 
+				if err != nil {
+					return newSystemError("Error building sites", err)
+				}
+
 				writer := csv.NewWriter(os.Stdout)
 				defer writer.Flush()
 
diff --git a/commands/new_theme.go b/commands/new_theme.go
index 9b1a4786c41..4e2357b5558 100644
--- a/commands/new_theme.go
+++ b/commands/new_theme.go
@@ -18,8 +18,8 @@ import (
 	"errors"
 	"path/filepath"
 	"strings"
-	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/spf13/cobra"
@@ -113,7 +113,7 @@ func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
 
 	by := []byte(`The MIT License (MIT)
 
-Copyright (c) ` + time.Now().Format("2006") + ` YOUR_NAME_HERE
+Copyright (c) ` + htime.Now().Format("2006") + ` YOUR_NAME_HERE
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of
 this software and associated documentation files (the "Software"), to deal in
diff --git a/commands/server.go b/commands/server.go
index 9857753368b..5fdde21c588 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -33,6 +33,7 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/paths"
 	"golang.org/x/sync/errgroup"
 
@@ -165,11 +166,6 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
 			c.Set("watch", true)
 		}
 
-		// TODO(bep) yes, we should fix.
-		if !c.languagesConfigured {
-			return nil
-		}
-
 		// We can only do this once.
 		serverCfgInit.Do(func() {
 			c.serverPorts = make([]serverPortListener, 1)
@@ -255,7 +251,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
 	}
 
 	err = func() error {
-		defer c.timeTrack(time.Now(), "Built")
+		defer c.timeTrack(htime.Now(), "Built")
 		err := c.serverBuild()
 		if err != nil {
 			cmd.PrintErrln("Error:", err.Error())
@@ -668,13 +664,13 @@ func memStats() error {
 		go func() {
 			var stats runtime.MemStats
 
-			start := time.Now().UnixNano()
+			start := htime.Now().UnixNano()
 
 			for {
 				runtime.ReadMemStats(&stats)
 				if fileMemStats != nil {
 					fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
-						(time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
+						(htime.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
 					time.Sleep(interval)
 				} else {
 					break
diff --git a/common/htime/time.go b/common/htime/time.go
index 552608b6f9f..052a45ed1dd 100644
--- a/common/htime/time.go
+++ b/common/htime/time.go
@@ -17,6 +17,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/bep/clock"
 	"github.com/spf13/cast"
 
 	toml "github.com/pelletier/go-toml/v2"
@@ -74,6 +75,8 @@ var (
 		"November",
 		"December",
 	}
+
+	Clock = clock.System()
 )
 
 func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
@@ -148,3 +151,13 @@ func ToTimeInDefaultLocationE(i any, location *time.Location) (tim time.Time, er
 	}
 	return cast.ToTimeInDefaultLocationE(i, location)
 }
+
+// Now returns time.Now() or time value based on the `clock` flag.
+// Use this function to fake time inside hugo.
+func Now() time.Time {
+	return Clock.Now()
+}
+
+func Since(t time.Time) time.Duration {
+	return Clock.Since(t)
+}
diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go
index 14c76ae4576..6b73c9f32c1 100644
--- a/common/loggers/loggers.go
+++ b/common/loggers/loggers.go
@@ -24,6 +24,7 @@ import (
 	"runtime"
 	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/terminal"
 
 	jww "github.com/spf13/jwalterweatherman"
@@ -176,7 +177,7 @@ func (l *logger) Out() io.Writer {
 // PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
 // if considerable time is spent.
 func (l *logger) PrintTimerIfDelayed(start time.Time, name string) {
-	elapsed := time.Since(start)
+	elapsed := htime.Since(start)
 	milli := int(1000 * elapsed.Seconds())
 	if milli < 500 {
 		return
@@ -185,7 +186,7 @@ func (l *logger) PrintTimerIfDelayed(start time.Time, name string) {
 }
 
 func (l *logger) PrintTimer(start time.Time, name string) {
-	elapsed := time.Since(start)
+	elapsed := htime.Since(start)
 	milli := int(1000 * elapsed.Seconds())
 	l.Printf("%s in %v ms", name, milli)
 }
diff --git a/go.mod b/go.mod
index 1a59f901db7..428b92d3a00 100644
--- a/go.mod
+++ b/go.mod
@@ -91,6 +91,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sso v1.4.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.7.0 // indirect
 	github.com/aws/smithy-go v1.8.0 // indirect
+	github.com/bep/clock v0.3.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
diff --git a/go.sum b/go.sum
index 6c4fd2d725e..0cf04e7f492 100644
--- a/go.sum
+++ b/go.sum
@@ -212,6 +212,16 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.7.0/go.mod h1:0qcSMCyASQPN2sk/1KQLQ2
 github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc=
 github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/bep/clock v0.1.0 h1:sdvJ08SsgwTY/ejA705YKBlktFfj2uVpmQSSQspZJ2c=
+github.com/bep/clock v0.1.0/go.mod h1:shVP9tZ3cXpbVj60SnlU1pMwKjFxECBRm9vZfpoA0Gs=
+github.com/bep/clock v0.2.0 h1:Uiv+P2wRVBy/g9Gybh645PgEOSC1BCXIQ7DOIc2bE64=
+github.com/bep/clock v0.2.0/go.mod h1:6Gz2lapnJ9vxpvPxQ2u6FcXFRoj4kkiqQ6pm0ERZlwk=
+github.com/bep/clock v0.2.1-0.20220507123307-c7e9c1bdae6f h1:dZlHVwWUCnS4P1Yk57qppK81fAUVV8DzDAo1lZA85hk=
+github.com/bep/clock v0.2.1-0.20220507123307-c7e9c1bdae6f/go.mod h1:6Gz2lapnJ9vxpvPxQ2u6FcXFRoj4kkiqQ6pm0ERZlwk=
+github.com/bep/clock v0.2.1-0.20220507124130-0a9d9c79927c h1:EPIuutNKs0tBieB1Z46GhNhiaYCs8bqSwwuo08xJHqs=
+github.com/bep/clock v0.2.1-0.20220507124130-0a9d9c79927c/go.mod h1:6Gz2lapnJ9vxpvPxQ2u6FcXFRoj4kkiqQ6pm0ERZlwk=
+github.com/bep/clock v0.3.0 h1:vfOA6+wVb6pPQEiXow9f/too92vNTLe9MuwO13PfI0M=
+github.com/bep/clock v0.3.0/go.mod h1:6Gz2lapnJ9vxpvPxQ2u6FcXFRoj4kkiqQ6pm0ERZlwk=
 github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo=
 github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
 github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840=
diff --git a/hugofs/fileinfo.go b/hugofs/fileinfo.go
index af37fce7e66..d923d632dfa 100644
--- a/hugofs/fileinfo.go
+++ b/hugofs/fileinfo.go
@@ -31,6 +31,7 @@ import (
 	"errors"
 
 	"github.com/gohugoio/hugo/common/hreflect"
+	"github.com/gohugoio/hugo/common/htime"
 
 	"github.com/spf13/afero"
 )
@@ -203,7 +204,7 @@ func newDirNameOnlyFileInfo(name string, meta *FileMeta, fileOpener func() (afer
 	m.IsOrdered = false
 
 	return NewFileMetaInfo(
-		&dirNameOnlyFileInfo{name: base, modTime: time.Now()},
+		&dirNameOnlyFileInfo{name: base, modTime: htime.Now()},
 		m,
 	)
 }
diff --git a/hugolib/content_factory.go b/hugolib/content_factory.go
index e6e82979f02..0a4d0aa0aa8 100644
--- a/hugolib/content_factory.go
+++ b/hugolib/content_factory.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/helpers"
 
 	"github.com/gohugoio/hugo/source"
@@ -70,7 +71,7 @@ func (f ContentFactory) ApplyArchetypeTemplate(w io.Writer, p page.Page, archety
 
 	d := &archetypeFileData{
 		Type: archetypeKind,
-		Date: time.Now().Format(time.RFC3339),
+		Date: htime.Now().Format(time.RFC3339),
 		Page: p,
 		File: p.File(),
 	}
diff --git a/hugolib/site.go b/hugolib/site.go
index cf7f0ff8249..cbfc4d836e0 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -30,6 +30,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/hugio"
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/modules"
@@ -1910,10 +1911,11 @@ func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bo
 	if !(buildDrafts || !Draft) {
 		return false
 	}
-	if !buildFuture && !publishDate.IsZero() && publishDate.After(time.Now()) {
+	hnow := htime.Now()
+	if !buildFuture && !publishDate.IsZero() && publishDate.After(hnow) {
 		return false
 	}
-	if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(time.Now()) {
+	if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(hnow) {
 		return false
 	}
 	return true
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 9c46fdf7ee3..471f48a0471 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -25,6 +25,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/compare"
 	"github.com/gohugoio/hugo/helpers"
@@ -129,7 +130,7 @@ func (s *Store) TrackValue(key string, value any, cached bool) {
 // MeasureSince adds a measurement for key to the metric store.
 func (s *Store) MeasureSince(key string, start time.Time) {
 	s.mu.Lock()
-	s.metrics[key] = append(s.metrics[key], time.Since(start))
+	s.metrics[key] = append(s.metrics[key], htime.Since(start))
 	s.mu.Unlock()
 }
 
diff --git a/modules/collect.go b/modules/collect.go
index ff83f9ecca8..4f5b1c36faa 100644
--- a/modules/collect.go
+++ b/modules/collect.go
@@ -23,6 +23,7 @@ import (
 	"time"
 
 	"github.com/bep/debounce"
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/loggers"
 
 	"github.com/spf13/cast"
@@ -505,7 +506,7 @@ func (c *collector) applyThemeConfig(tc *moduleAdapter) error {
 }
 
 func (c *collector) collect() {
-	defer c.logger.PrintTimerIfDelayed(time.Now(), "hugo: collected modules")
+	defer c.logger.PrintTimerIfDelayed(htime.Now(), "hugo: collected modules")
 	d := debounce.New(2 * time.Second)
 	d(func() {
 		c.logger.Println("hugo: downloading modules …")
diff --git a/resources/resource/dates.go b/resources/resource/dates.go
index b43f35c641a..6d19ca7b95f 100644
--- a/resources/resource/dates.go
+++ b/resources/resource/dates.go
@@ -13,7 +13,11 @@
 
 package resource
 
-import "time"
+import (
+	"time"
+
+	"github.com/gohugoio/hugo/common/htime"
+)
 
 var _ Dated = Dates{}
 
@@ -55,7 +59,8 @@ func IsFuture(d Dated) bool {
 	if d.PublishDate().IsZero() {
 		return false
 	}
-	return d.PublishDate().After(time.Now())
+
+	return d.PublishDate().After(htime.Now())
 }
 
 // IsExpired returns whether the argument is expired.
@@ -63,7 +68,7 @@ func IsExpired(d Dated) bool {
 	if d.ExpiryDate().IsZero() {
 		return false
 	}
-	return d.ExpiryDate().Before(time.Now())
+	return d.ExpiryDate().Before(htime.Now())
 }
 
 // IsZeroDates returns true if all of the dates are zero.
diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go
index 516e2c272fb..8fdc8527586 100644
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -35,6 +35,7 @@ import (
 )
 
 func init() {
+	// htime.Now cannot be used here
 	rand.Seed(time.Now().UTC().UnixNano())
 }
 
diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
index eb4ebfe321a..9ded32a7134 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -25,8 +25,8 @@ import (
 	"reflect"
 	"strings"
 	"sync"
-	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
 
 	"github.com/gohugoio/hugo/helpers"
@@ -222,7 +222,7 @@ func createKey(name string, variants ...any) (partialCacheKey, error) {
 var errUnHashable = errors.New("unhashable")
 
 func (ns *Namespace) getOrCreate(ctx context.Context, key partialCacheKey, context any) (result any, err error) {
-	start := time.Now()
+	start := htime.Now()
 	defer func() {
 		if r := recover(); r != nil {
 			err = r.(error)
diff --git a/tpl/time/time.go b/tpl/time/time.go
index 623b7206aa6..cd78b83aaa4 100644
--- a/tpl/time/time.go
+++ b/tpl/time/time.go
@@ -68,9 +68,9 @@ func (ns *Namespace) Format(layout string, v any) (string, error) {
 	return ns.timeFormatter.Format(t, layout), nil
 }
 
-// Now returns the current local time.
+// Now returns the current local time or `clock` time
 func (ns *Namespace) Now() _time.Time {
-	return _time.Now()
+	return htime.Now()
 }
 
 // ParseDuration parses the duration string s.
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index c092ff63816..d352f02e825 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -27,10 +27,10 @@ import (
 	"sort"
 	"strings"
 	"sync"
-	"time"
 	"unicode"
 	"unicode/utf8"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/types"
 
 	"github.com/gohugoio/hugo/helpers"
@@ -235,7 +235,7 @@ func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Templat
 		defer rlocker.RUnlock()
 	}
 	if t.Metrics != nil {
-		defer t.Metrics.MeasureSince(templ.Name(), time.Now())
+		defer t.Metrics.MeasureSince(templ.Name(), htime.Now())
 	}
 
 	if t.templateUsageTracker != nil {


Test patch:
diff --git a/commands/hugo_test.go b/commands/hugo_test.go
index bdad25cfcd2..1e132664275 100644
--- a/commands/hugo_test.go
+++ b/commands/hugo_test.go
@@ -21,7 +21,9 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/bep/clock"
 	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/spf13/afero"
 	"golang.org/x/tools/txtar"
@@ -95,6 +97,41 @@ Home.
 
 }
 
+// Issue #8787
+func TestHugoListCommandsWithClockFlag(t *testing.T) {
+	t.Cleanup(func() { htime.Clock = clock.System() })
+
+	c := qt.New(t)
+
+	files := `
+-- config.toml --
+baseURL = "https://example.org"
+title = "Hugo Commands"
+timeZone = "UTC"
+-- content/past.md --
+---
+title: "Past"
+date: 2000-11-06
+---
+-- content/future.md --
+---
+title: "Future"
+date: 2200-11-06
+---
+-- layouts/_default/single.html --
+Page: {{ .Title }}|
+
+`
+	s := newTestHugoCmdBuilder(c, files, []string{"list", "future"})
+	s.captureOut = true
+	s.Build()
+	p := filepath.Join("content", "future.md")
+	s.AssertStdout(p + ",2200-11-06T00:00:00Z")
+
+	s = newTestHugoCmdBuilder(c, files, []string{"list", "future", "--clock", "2300-11-06"}).Build()
+	s.AssertStdout("")
+}
+
 type testHugoCmdBuilder struct {
 	*qt.C
 
@@ -102,6 +139,9 @@ type testHugoCmdBuilder struct {
 	dir   string
 	files string
 	args  []string
+
+	captureOut bool
+	out        string
 }
 
 func newTestHugoCmdBuilder(c *qt.C, files string, args []string) *testHugoCmdBuilder {
@@ -127,8 +167,17 @@ func (s *testHugoCmdBuilder) Build() *testHugoCmdBuilder {
 	args := append(s.args, "-s="+s.dir, "--quiet")
 	cmd.SetArgs(args)
 
-	_, err := cmd.ExecuteC()
-	s.Assert(err, qt.IsNil)
+	if s.captureOut {
+		out, err := captureStdout(func() error {
+			_, err := cmd.ExecuteC()
+			return err
+		})
+		s.Assert(err, qt.IsNil)
+		s.out = out
+	} else {
+		_, err := cmd.ExecuteC()
+		s.Assert(err, qt.IsNil)
+	}
 
 	return s
 }
@@ -149,3 +198,9 @@ func (s *testHugoCmdBuilder) AssertFileContent(filename string, matches ...strin
 		}
 	}
 }
+
+func (s *testHugoCmdBuilder) AssertStdout(match string) {
+	s.Helper()
+	content := strings.TrimSpace(s.out)
+	s.Assert(content, qt.Contains, strings.TrimSpace(match))
+}
diff --git a/commands/list_test.go b/commands/list_test.go
index 56a3ee9b936..8b25355714e 100644
--- a/commands/list_test.go
+++ b/commands/list_test.go
@@ -34,9 +34,9 @@ func TestListAll(t *testing.T) {
 	hugoCmd := newCommandsBuilder().addAll().build()
 	cmd := hugoCmd.getCommand()
 
-	defer func() {
+	t.Cleanup(func() {
 		os.RemoveAll(dir)
-	}()
+	})
 
 	cmd.SetArgs([]string{"-s=" + dir, "list", "all"})
 
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
index 84e54306a2c..b931731313c 100644
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -22,12 +22,14 @@ import (
 	"testing"
 	"time"
 
+	"github.com/bep/clock"
 	"github.com/gohugoio/hugo/htesting"
 	"github.com/gohugoio/hugo/markup/asciidocext"
 	"github.com/gohugoio/hugo/markup/rst"
 
 	"github.com/gohugoio/hugo/config"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/loggers"
 
 	"github.com/gohugoio/hugo/hugofs"
@@ -1536,7 +1538,6 @@ Content.
 }
 
 func TestShouldBuild(t *testing.T) {
-	t.Parallel()
 	past := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
 	future := time.Date(2037, 11, 17, 20, 34, 58, 651387237, time.UTC)
 	zero := time.Time{}
@@ -1582,6 +1583,54 @@ func TestShouldBuild(t *testing.T) {
 	}
 }
 
+func TestShouldBuildWithClock(t *testing.T) {
+	htime.Clock = clock.Start(time.Date(2021, 11, 17, 20, 34, 58, 651387237, time.UTC))
+	t.Cleanup(func() { htime.Clock = clock.System() })
+	past := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
+	future := time.Date(2037, 11, 17, 20, 34, 58, 651387237, time.UTC)
+	zero := time.Time{}
+
+	publishSettings := []struct {
+		buildFuture  bool
+		buildExpired bool
+		buildDrafts  bool
+		draft        bool
+		publishDate  time.Time
+		expiryDate   time.Time
+		out          bool
+	}{
+		// publishDate and expiryDate
+		{false, false, false, false, zero, zero, true},
+		{false, false, false, false, zero, future, true},
+		{false, false, false, false, past, zero, true},
+		{false, false, false, false, past, future, true},
+		{false, false, false, false, past, past, false},
+		{false, false, false, false, future, future, false},
+		{false, false, false, false, future, past, false},
+
+		// buildFuture and buildExpired
+		{false, true, false, false, past, past, true},
+		{true, true, false, false, past, past, true},
+		{true, false, false, false, past, past, false},
+		{true, false, false, false, future, future, true},
+		{true, true, false, false, future, future, true},
+		{false, true, false, false, future, past, false},
+
+		// buildDrafts and draft
+		{true, true, false, true, past, future, false},
+		{true, true, true, true, past, future, true},
+		{true, true, true, true, past, future, true},
+	}
+
+	for _, ps := range publishSettings {
+		s := shouldBuild(ps.buildFuture, ps.buildExpired, ps.buildDrafts, ps.draft,
+			ps.publishDate, ps.expiryDate)
+		if s != ps.out {
+			t.Errorf("AssertShouldBuildWithClock unexpected output with params: %+v", ps)
+		}
+	}
+}
+
 // "dot" in path: #1885 and #2110
 // disablePathToLower regression: #3374
 func TestPathIssues(t *testing.T) {