shithub: hugo

Download patch

ref: fdfa4a5fe62232f65f1dd8d6fe0c500374228788
parent: 8cbe2bbfad6aa4de267921e24e166d4addf47040
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Wed Oct 21 07:17:48 EDT 2020

Allow getJSON errors to be ignored

This change is mostly motivated to get a more stable CI build (we're building the Hugo site there, with Instagram and Twitter shortcodes sometimes failing).

Fixes #7866

--- a/.travis.yml
+++ b/.travis.yml
@@ -72,6 +72,6 @@
         HUGO_TIMEOUT=30000 mage -v check;
     fi
   - mage -v hugo
-  - ./hugo -s docs/
-  - ./hugo --renderToMemory -s docs/
+  - HUGO_IGNOREERRORS=error-remote-getjson ./hugo -s docs/
+  - HUGO_IGNOREERRORS=error-remote-getjson ./hugo --renderToMemory -s docs/
   - df -h
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -60,7 +60,7 @@
 type commandeer struct {
 	*commandeerHugoState
 
-	logger       *loggers.Logger
+	logger       loggers.Logger
 	serverConfig *config.Server
 
 	// Currently only set when in "fast render mode". But it seems to
@@ -112,7 +112,7 @@
 }
 
 func (c *commandeer) errCount() int {
-	return int(c.logger.ErrorCounter.Count())
+	return int(c.logger.LogCounters().ErrorCounter.Count())
 }
 
 func (c *commandeer) getErrorWithContext() interface{} {
@@ -415,7 +415,7 @@
 	}
 	config.Set("cacheDir", cacheDir)
 
-	cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
+	cfg.Logger.Infoln("Using config file:", config.ConfigFileUsed())
 
 	return nil
 
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -322,16 +322,12 @@
 	_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
 }
 
-func checkErr(logger *loggers.Logger, err error, s ...string) {
+func checkErr(logger loggers.Logger, err error, s ...string) {
 	if err == nil {
 		return
 	}
-	if len(s) == 0 {
-		logger.CRITICAL.Println(err)
-		return
-	}
 	for _, message := range s {
-		logger.ERROR.Println(message)
+		logger.Errorln(message)
 	}
-	logger.ERROR.Println(err)
+	logger.Errorln(err)
 }
--- a/commands/convert.go
+++ b/commands/convert.go
@@ -123,7 +123,7 @@
 
 	site := h.Sites[0]
 
-	site.Log.FEEDBACK.Println("processing", len(site.AllPages()), "content files")
+	site.Log.Println("processing", len(site.AllPages()), "content files")
 	for _, p := range site.AllPages() {
 		if err := cc.convertAndSavePage(p, site, format); err != nil {
 			return err
@@ -147,12 +147,12 @@
 
 	errMsg := fmt.Errorf("Error processing file %q", p.Path())
 
-	site.Log.INFO.Println("Attempting to convert", p.File().Filename())
+	site.Log.Infoln("Attempting to convert", p.File().Filename())
 
 	f := p.File()
 	file, err := f.FileInfo().Meta().Open()
 	if err != nil {
-		site.Log.ERROR.Println(errMsg)
+		site.Log.Errorln(errMsg)
 		file.Close()
 		return nil
 	}
@@ -159,7 +159,7 @@
 
 	pf, err := pageparser.ParseFrontMatterAndContent(file)
 	if err != nil {
-		site.Log.ERROR.Println(errMsg)
+		site.Log.Errorln(errMsg)
 		file.Close()
 		return err
 	}
@@ -179,7 +179,7 @@
 	var newContent bytes.Buffer
 	err = parser.InterfaceToFrontMatter(pf.FrontMatter, targetFormat, &newContent)
 	if err != nil {
-		site.Log.ERROR.Println(errMsg)
+		site.Log.Errorln(errMsg)
 		return err
 	}
 
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -130,7 +130,7 @@
 
 }
 
-func (c *commandeer) createLogger(cfg config.Provider, running bool) (*loggers.Logger, error) {
+func (c *commandeer) createLogger(cfg config.Provider, running bool) (loggers.Logger, error) {
 	var (
 		logHandle       = ioutil.Discard
 		logThreshold    = jww.LevelWarn
@@ -374,12 +374,12 @@
 
 	f, err := os.Create(c.h.memprofile)
 	if err != nil {
-		c.logger.ERROR.Println("could not create memory profile: ", err)
+		c.logger.Errorf("could not create memory profile: ", err)
 	}
 	defer f.Close()
 	runtime.GC() // get up-to-date statistics
 	if err := pprof.WriteHeapProfile(f); err != nil {
-		c.logger.ERROR.Println("could not write memory profile: ", err)
+		c.logger.Errorf("could not write memory profile: ", err)
 	}
 }
 
@@ -518,7 +518,7 @@
 		if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
 			dupes := createCounter.ReportDuplicates()
 			if dupes != "" {
-				c.logger.WARN.Println("Duplicate target paths:", dupes)
+				c.logger.Warnln("Duplicate target paths:", dupes)
 			}
 		}
 	}
@@ -532,8 +532,8 @@
 		baseWatchDir := c.Cfg.GetString("workingDir")
 		rootWatchDirs := getRootWatchDirsStr(baseWatchDir, watchDirs)
 
-		c.logger.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
-		c.logger.FEEDBACK.Println("Press Ctrl+C to stop")
+		c.logger.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
+		c.logger.Println("Press Ctrl+C to stop")
 		watcher, err := c.newWatcher(watchDirs...)
 		checkErr(c.Logger, err)
 		defer watcher.Close()
@@ -590,7 +590,7 @@
 	staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static
 
 	if len(staticFilesystems) == 0 {
-		c.logger.INFO.Println("No static directories found to sync")
+		c.logger.Infoln("No static directories found to sync")
 		return langCount, nil
 	}
 
@@ -662,13 +662,13 @@
 	syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
 
 	if syncer.Delete {
-		c.logger.INFO.Println("removing all files from destination that don't exist in static dirs")
+		c.logger.Infoln("removing all files from destination that don't exist in static dirs")
 
 		syncer.DeleteFilter = func(f os.FileInfo) bool {
 			return f.IsDir() && strings.HasPrefix(f.Name(), ".")
 		}
 	}
-	c.logger.INFO.Println("syncing static files to", publishDir)
+	c.logger.Infoln("syncing static files to", publishDir)
 
 	// because we are using a baseFs (to get the union right).
 	// set sync src to root
@@ -689,7 +689,7 @@
 
 func (c *commandeer) timeTrack(start time.Time, name string) {
 	elapsed := time.Since(start)
-	c.logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
+	c.logger.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
 }
 
 // getDirList provides NewWatcher() with a list of directories to watch for changes.
@@ -698,7 +698,7 @@
 
 	walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
 		if err != nil {
-			c.logger.ERROR.Println("walker: ", err)
+			c.logger.Errorln("walker: ", err)
 			return nil
 		}
 
@@ -724,7 +724,7 @@
 
 		w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.logger, Info: fi, WalkFn: walkFn})
 		if err := w.Walk(); err != nil {
-			c.logger.ERROR.Println("walker: ", err)
+			c.logger.Errorln("walker: ", err)
 		}
 	}
 
@@ -740,8 +740,8 @@
 func (c *commandeer) handleBuildErr(err error, msg string) {
 	c.buildErr = err
 
-	c.logger.ERROR.Print(msg + ":\n\n")
-	c.logger.ERROR.Println(helpers.FirstUpper(err.Error()))
+	c.logger.Errorln(msg + ":\n")
+	c.logger.Errorln(helpers.FirstUpper(err.Error()))
 	if !c.h.quiet && c.h.verbose {
 		herrors.PrintStackTraceFromErr(err)
 	}
@@ -822,13 +822,13 @@
 		if !c.paused {
 			_, err := c.copyStatic()
 			if err != nil {
-				c.logger.ERROR.Println(err)
+				c.logger.Errorln(err)
 				return
 			}
 
 			err = c.buildSites()
 			if err != nil {
-				c.logger.ERROR.Println(err)
+				c.logger.Errorln(err)
 			} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
 				livereload.ForceRefresh()
 			}
@@ -862,7 +862,7 @@
 	// Identifies changes to config (config.toml) files.
 	configSet := make(map[string]bool)
 
-	c.logger.FEEDBACK.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
+	c.logger.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
 	for _, configFile := range c.configFiles {
 		watcher.Add(configFile)
 		configSet[configFile] = true
@@ -879,7 +879,7 @@
 				}
 			case err := <-watcher.Errors:
 				if err != nil {
-					c.logger.ERROR.Println("Error while watching:", err)
+					c.logger.Errorln("Error while watching:", err)
 				}
 			}
 		}
@@ -895,9 +895,9 @@
 	}
 	msg += " detected, rebuilding site."
 
-	c.logger.FEEDBACK.Println(msg)
+	c.logger.Println(msg)
 	const layout = "2006-01-02 15:04:05.000 -0700"
-	c.logger.FEEDBACK.Println(time.Now().Format(layout))
+	c.logger.Println(time.Now().Format(layout))
 }
 
 const (
@@ -979,7 +979,7 @@
 		return
 	}
 
-	c.logger.INFO.Println("Received System Events:", evs)
+	c.logger.Infoln("Received System Events:", evs)
 
 	staticEvents := []fsnotify.Event{}
 	dynamicEvents := []fsnotify.Event{}
@@ -1059,7 +1059,7 @@
 
 		walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
 			if f.IsDir() {
-				c.logger.FEEDBACK.Println("adding created directory to watchlist", path)
+				c.logger.Println("adding created directory to watchlist", path)
 				if err := watcher.Add(path); err != nil {
 					return err
 				}
@@ -1091,15 +1091,15 @@
 		c.printChangeDetected("Static files")
 
 		if c.Cfg.GetBool("forceSyncStatic") {
-			c.logger.FEEDBACK.Printf("Syncing all static files\n")
+			c.logger.Printf("Syncing all static files\n")
 			_, err := c.copyStatic()
 			if err != nil {
-				c.logger.ERROR.Println("Error copying static files to publish dir:", err)
+				c.logger.Errorln("Error copying static files to publish dir:", err)
 				return
 			}
 		} else {
 			if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
-				c.logger.ERROR.Println("Error syncing static files to publish dir:", err)
+				c.logger.Errorln("Error syncing static files to publish dir:", err)
 				return
 			}
 		}
--- a/commands/server.go
+++ b/commands/server.go
@@ -185,7 +185,7 @@
 						// port set explicitly by user -- he/she probably meant it!
 						err = newSystemErrorF("Server startup failed: %s", err)
 					}
-					c.logger.FEEDBACK.Println("port", sc.serverPort, "already in use, attempting to use an available port")
+					c.logger.Println("port", sc.serverPort, "already in use, attempting to use an available port")
 					sp, err := helpers.FindAvailablePort()
 					if err != nil {
 						err = newSystemError("Unable to find alternative port to use:", err)
@@ -350,7 +350,7 @@
 					w.WriteHeader(500)
 					r, err := f.errorTemplate(err)
 					if err != nil {
-						f.c.logger.ERROR.Println(err)
+						f.c.logger.Errorln(err)
 					}
 
 					port = 1313
@@ -508,7 +508,7 @@
 		go func() {
 			err = http.ListenAndServe(endpoint, mu)
 			if err != nil {
-				c.logger.ERROR.Printf("Error: %s\n", err.Error())
+				c.logger.Errorf("Error: %s\n", err.Error())
 				os.Exit(1)
 			}
 		}()
--- a/commands/static_syncer.go
+++ b/commands/static_syncer.go
@@ -107,10 +107,10 @@
 					logger.Println("Syncing", relPath, "to", publishDir)
 
 					if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
-						c.logger.ERROR.Println(err)
+						c.logger.Errorln(err)
 					}
 				} else {
-					c.logger.ERROR.Println(err)
+					c.logger.Errorln(err)
 				}
 
 				continue
@@ -119,7 +119,7 @@
 			// For all other event operations Hugo will sync static.
 			logger.Println("Syncing", relPath, "to", publishDir)
 			if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
-				c.logger.ERROR.Println(err)
+				c.logger.Errorln(err)
 			}
 		}
 
--- a/common/constants/constants.go
+++ b/common/constants/constants.go
@@ -18,4 +18,8 @@
 const (
 	ErrIDAmbigousDisableKindTaxonomy = "error-disable-taxonomy"
 	ErrIDAmbigousOutputKindTaxonomy  = "error-output-taxonomy"
+
+	// IDs for remote errors in tpl/data.
+	ErrRemoteGetJSON = "error-remote-getjson"
+	ErrRemoteGetCSV  = "error-remote-getcsv"
 )
--- a/common/loggers/ignorableLogger.go
+++ b/common/loggers/ignorableLogger.go
@@ -19,25 +19,31 @@
 )
 
 // IgnorableLogger is a logger that ignores certain log statements.
-type IgnorableLogger struct {
-	logger     *Logger
+type IgnorableLogger interface {
+	Logger
+	Errorsf(statementID, format string, v ...interface{})
+}
+
+type ignorableLogger struct {
+	Logger
 	statements map[string]bool
 }
 
 // NewIgnorableLogger wraps the given logger and ignores the log statement IDs given.
-func NewIgnorableLogger(logger *Logger, statements ...string) IgnorableLogger {
+func NewIgnorableLogger(logger Logger, statements ...string) IgnorableLogger {
 	statementsSet := make(map[string]bool)
 	for _, s := range statements {
 		statementsSet[strings.ToLower(s)] = true
 
 	}
-	return IgnorableLogger{
-		logger:     logger,
+	return ignorableLogger{
+		Logger:     logger,
 		statements: statementsSet,
 	}
 }
 
-func (l IgnorableLogger) Errorf(statementID, format string, v ...interface{}) {
+// Errorsf logs statementID as an ERROR if not configured as ignoreable.
+func (l ignorableLogger) Errorsf(statementID, format string, v ...interface{}) {
 	if l.statements[statementID] {
 		// Ignore.
 		return
@@ -48,5 +54,5 @@
 
 	format += ignoreMsg
 
-	l.logger.ERROR.Printf(format, v...)
+	l.Errorf(format, v...)
 }
--- a/common/loggers/loggers.go
+++ b/common/loggers/loggers.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2020 The Hugo Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -56,39 +56,122 @@
 	return len(p), nil
 }
 
-// Logger wraps a *loggers.Logger and some other related logging state.
-type Logger struct {
+type Logger interface {
+	Printf(format string, v ...interface{})
+	Println(v ...interface{})
+	PrintTimerIfDelayed(start time.Time, name string)
+	Debug() *log.Logger
+	Info() *log.Logger
+	Infof(format string, v ...interface{})
+	Infoln(v ...interface{})
+	Warn() *log.Logger
+	Warnf(format string, v ...interface{})
+	Warnln(v ...interface{})
+	Error() *log.Logger
+	Errorf(format string, v ...interface{})
+	Errorln(v ...interface{})
+	Errors() string
+
+	Out() io.Writer
+
+	Reset()
+
+	// Used in tests.
+	LogCounters() *LogCounters
+}
+
+type LogCounters struct {
+	ErrorCounter *jww.Counter
+	WarnCounter  *jww.Counter
+}
+
+type logger struct {
 	*jww.Notepad
 
 	// The writer that represents stdout.
 	// Will be ioutil.Discard when in quiet mode.
-	Out io.Writer
+	out io.Writer
 
-	ErrorCounter *jww.Counter
-	WarnCounter  *jww.Counter
+	logCounters *LogCounters
 
 	// This is only set in server mode.
 	errors *bytes.Buffer
 }
 
+func (l *logger) Printf(format string, v ...interface{}) {
+	l.FEEDBACK.Printf(format, v...)
+}
+
+func (l *logger) Println(v ...interface{}) {
+	l.FEEDBACK.Println(v...)
+}
+
+func (l *logger) Debug() *log.Logger {
+	return l.DEBUG
+}
+
+func (l *logger) Infof(format string, v ...interface{}) {
+	l.INFO.Printf(format, v...)
+}
+
+func (l *logger) Infoln(v ...interface{}) {
+	l.INFO.Println(v...)
+}
+
+func (l *logger) Info() *log.Logger {
+	return l.INFO
+}
+
+func (l *logger) Warnf(format string, v ...interface{}) {
+	l.WARN.Printf(format, v...)
+}
+
+func (l *logger) Warnln(v ...interface{}) {
+	l.WARN.Println(v...)
+}
+
+func (l *logger) Warn() *log.Logger {
+	return l.WARN
+}
+
+func (l *logger) Errorf(format string, v ...interface{}) {
+	l.ERROR.Printf(format, v...)
+}
+
+func (l *logger) Errorln(v ...interface{}) {
+	l.ERROR.Println(v...)
+}
+
+func (l *logger) Error() *log.Logger {
+	return l.ERROR
+}
+
+func (l *logger) LogCounters() *LogCounters {
+	return l.logCounters
+}
+
+func (l *logger) Out() io.Writer {
+	return l.out
+}
+
 // PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
 // if considerable time is spent.
-func (l *Logger) PrintTimerIfDelayed(start time.Time, name string) {
+func (l *logger) PrintTimerIfDelayed(start time.Time, name string) {
 	elapsed := time.Since(start)
 	milli := int(1000 * elapsed.Seconds())
 	if milli < 500 {
 		return
 	}
-	l.FEEDBACK.Printf("%s in %v ms", name, milli)
+	l.Printf("%s in %v ms", name, milli)
 }
 
-func (l *Logger) PrintTimer(start time.Time, name string) {
+func (l *logger) PrintTimer(start time.Time, name string) {
 	elapsed := time.Since(start)
 	milli := int(1000 * elapsed.Seconds())
-	l.FEEDBACK.Printf("%s in %v ms", name, milli)
+	l.Printf("%s in %v ms", name, milli)
 }
 
-func (l *Logger) Errors() string {
+func (l *logger) Errors() string {
 	if l.errors == nil {
 		return ""
 	}
@@ -96,8 +179,8 @@
 }
 
 // Reset resets the logger's internal state.
-func (l *Logger) Reset() {
-	l.ErrorCounter.Reset()
+func (l *logger) Reset() {
+	l.logCounters.ErrorCounter.Reset()
 	if l.errors != nil {
 		l.errors.Reset()
 	}
@@ -104,37 +187,37 @@
 }
 
 //  NewLogger creates a new Logger for the given thresholds
-func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
+func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) Logger {
 	return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors)
 }
 
 // NewDebugLogger is a convenience function to create a debug logger.
-func NewDebugLogger() *Logger {
+func NewDebugLogger() Logger {
 	return NewBasicLogger(jww.LevelDebug)
 }
 
 // NewWarningLogger is a convenience function to create a warning logger.
-func NewWarningLogger() *Logger {
+func NewWarningLogger() Logger {
 	return NewBasicLogger(jww.LevelWarn)
 }
 
 // NewInfoLogger is a convenience function to create a info logger.
-func NewInfoLogger() *Logger {
+func NewInfoLogger() Logger {
 	return NewBasicLogger(jww.LevelInfo)
 }
 
 // NewErrorLogger is a convenience function to create an error logger.
-func NewErrorLogger() *Logger {
+func NewErrorLogger() Logger {
 	return NewBasicLogger(jww.LevelError)
 }
 
 // NewBasicLogger creates a new basic logger writing to Stdout.
-func NewBasicLogger(t jww.Threshold) *Logger {
+func NewBasicLogger(t jww.Threshold) Logger {
 	return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false)
 }
 
 // NewBasicLoggerForWriter creates a new basic logger writing to w.
-func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) *Logger {
+func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger {
 	return newLogger(t, jww.LevelError, w, ioutil.Discard, false)
 }
 
@@ -219,7 +302,7 @@
 	return new(fatalLogWriter)
 }
 
-func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
+func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *logger {
 	errorCounter := &jww.Counter{}
 	warnCounter := &jww.Counter{}
 	outHandle, logHandle = getLogWriters(outHandle, logHandle)
@@ -239,11 +322,13 @@
 		listeners = append(listeners, errorCapture)
 	}
 
-	return &Logger{
-		Notepad:      jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
-		Out:          outHandle,
-		ErrorCounter: errorCounter,
-		WarnCounter:  warnCounter,
-		errors:       errorBuff,
+	return &logger{
+		Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
+		out:     outHandle,
+		logCounters: &LogCounters{
+			ErrorCounter: errorCounter,
+			WarnCounter:  warnCounter,
+		},
+		errors: errorBuff,
 	}
 }
--- a/common/loggers/loggers_test.go
+++ b/common/loggers/loggers_test.go
@@ -26,11 +26,11 @@
 	c := qt.New(t)
 	l := NewWarningLogger()
 
-	l.ERROR.Println("One error")
-	l.ERROR.Println("Two error")
-	l.WARN.Println("A warning")
+	l.Errorln("One error")
+	l.Errorln("Two error")
+	l.Warnln("A warning")
 
-	c.Assert(l.ErrorCounter.Count(), qt.Equals, uint64(2))
+	c.Assert(l.LogCounters().ErrorCounter.Count(), qt.Equals, uint64(2))
 
 }
 
--- a/deps/deps.go
+++ b/deps/deps.go
@@ -21,6 +21,7 @@
 	"github.com/gohugoio/hugo/resources"
 	"github.com/gohugoio/hugo/source"
 	"github.com/gohugoio/hugo/tpl"
+	"github.com/spf13/cast"
 	jww "github.com/spf13/jwalterweatherman"
 )
 
@@ -30,7 +31,7 @@
 type Deps struct {
 
 	// The logger to use.
-	Log *loggers.Logger `json:"-"`
+	Log loggers.Logger `json:"-"`
 
 	// Used to log errors that may repeat itself many times.
 	DistinctErrorLog *helpers.DistinctLogger
@@ -260,12 +261,15 @@
 		timeoutms = 3000
 	}
 
-	distinctErrorLogger := helpers.NewDistinctLogger(logger.ERROR)
-	distinctWarnLogger := helpers.NewDistinctLogger(logger.WARN)
+	ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors"))
+	ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...)
 
+	distinctErrorLogger := helpers.NewDistinctLogger(logger.Error())
+	distinctWarnLogger := helpers.NewDistinctLogger(logger.Warn())
+
 	d := &Deps{
 		Fs:                      fs,
-		Log:                     logger,
+		Log:                     ignorableLogger,
 		DistinctErrorLog:        distinctErrorLogger,
 		DistinctWarningLog:      distinctWarnLogger,
 		templateProvider:        cfg.TemplateProvider,
@@ -350,7 +354,7 @@
 type DepsCfg struct {
 
 	// The Logger to use.
-	Logger *loggers.Logger
+	Logger loggers.Logger
 
 	// The file systems to use
 	Fs *hugofs.Fs
--- a/helpers/content.go
+++ b/helpers/content.go
@@ -65,7 +65,7 @@
 
 // NewContentSpec returns a ContentSpec initialized
 // with the appropriate fields from the given config.Provider.
-func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero.Fs) (*ContentSpec, error) {
+func NewContentSpec(cfg config.Provider, logger loggers.Logger, contentFs afero.Fs) (*ContentSpec, error) {
 
 	spec := &ContentSpec{
 		summaryLength: cfg.GetInt("summaryLength"),
--- a/helpers/pathspec.go
+++ b/helpers/pathspec.go
@@ -38,13 +38,13 @@
 }
 
 // NewPathSpec creats a new PathSpec from the given filesystems and language.
-func NewPathSpec(fs *hugofs.Fs, cfg config.Provider, logger *loggers.Logger) (*PathSpec, error) {
+func NewPathSpec(fs *hugofs.Fs, cfg config.Provider, logger loggers.Logger) (*PathSpec, error) {
 	return NewPathSpecWithBaseBaseFsProvided(fs, cfg, logger, nil)
 }
 
 // NewPathSpecWithBaseBaseFsProvided creats a new PathSpec from the given filesystems and language.
 // If an existing BaseFs is provided, parts of that is reused.
-func NewPathSpecWithBaseBaseFsProvided(fs *hugofs.Fs, cfg config.Provider, logger *loggers.Logger, baseBaseFs *filesystems.BaseFs) (*PathSpec, error) {
+func NewPathSpecWithBaseBaseFsProvided(fs *hugofs.Fs, cfg config.Provider, logger loggers.Logger, baseBaseFs *filesystems.BaseFs) (*PathSpec, error) {
 
 	p, err := paths.New(fs, cfg)
 	if err != nil {
--- a/hugofs/nosymlink_fs.go
+++ b/hugofs/nosymlink_fs.go
@@ -28,7 +28,7 @@
 )
 
 // NewNoSymlinkFs creates a new filesystem that prevents symlinks.
-func NewNoSymlinkFs(fs afero.Fs, logger *loggers.Logger, allowFiles bool) afero.Fs {
+func NewNoSymlinkFs(fs afero.Fs, logger loggers.Logger, allowFiles bool) afero.Fs {
 	return &noSymlinkFs{Fs: fs, logger: logger, allowFiles: allowFiles}
 }
 
@@ -35,7 +35,7 @@
 // noSymlinkFs is a filesystem that prevents symlinking.
 type noSymlinkFs struct {
 	allowFiles bool // block dirs only
-	logger     *loggers.Logger
+	logger     loggers.Logger
 	afero.Fs
 }
 
--- a/hugofs/nosymlink_test.go
+++ b/hugofs/nosymlink_test.go
@@ -68,7 +68,7 @@
 
 	for _, bfs := range []afero.Fs{NewBaseFileDecorator(Os), Os} {
 		for _, allowFiles := range []bool{false, true} {
-			logger.WarnCounter.Reset()
+			logger.LogCounters().WarnCounter.Reset()
 			fs := NewNoSymlinkFs(bfs, logger, allowFiles)
 			ls := fs.(afero.Lstater)
 			symlinkedDir := filepath.Join(workDir, "symlinkdedir")
@@ -139,7 +139,7 @@
 			_, err = f.Readdir(-1)
 			c.Assert(err, qt.IsNil)
 			f.Close()
-			c.Assert(logger.WarnCounter.Count(), qt.Equals, uint64(1))
+			c.Assert(logger.LogCounters().WarnCounter.Count(), qt.Equals, uint64(1))
 
 		}
 	}
--- a/hugofs/walk.go
+++ b/hugofs/walk.go
@@ -37,7 +37,7 @@
 	root     string
 	basePath string
 
-	logger *loggers.Logger
+	logger loggers.Logger
 
 	// May be pre-set
 	fi         FileMetaInfo
@@ -59,7 +59,7 @@
 	Root     string
 	BasePath string
 
-	Logger *loggers.Logger
+	Logger loggers.Logger
 
 	// One or both of these may be pre-set.
 	Info       FileMetaInfo
@@ -158,7 +158,7 @@
 		// The file may be removed in process.
 		// This may be a ERROR situation, but it is not possible
 		// to determine as a general case.
-		w.logger.WARN.Printf("File %q not found, skipping.", filename)
+		w.logger.Warnf("File %q not found, skipping.", filename)
 		return true
 	}
 
@@ -165,8 +165,8 @@
 	return false
 }
 
-func logUnsupportedSymlink(filename string, logger *loggers.Logger) {
-	logger.WARN.Printf("Unsupported symlink found in %q, skipping.", filename)
+func logUnsupportedSymlink(filename string, logger loggers.Logger) {
+	logger.Warnf("Unsupported symlink found in %q, skipping.", filename)
 }
 
 // walk recursively descends path, calling walkFn.
--- a/hugolib/alias.go
+++ b/hugolib/alias.go
@@ -33,11 +33,11 @@
 
 type aliasHandler struct {
 	t         tpl.TemplateHandler
-	log       *loggers.Logger
+	log       loggers.Logger
 	allowRoot bool
 }
 
-func newAliasHandler(t tpl.TemplateHandler, l *loggers.Logger, allowRoot bool) aliasHandler {
+func newAliasHandler(t tpl.TemplateHandler, l loggers.Logger, allowRoot bool) aliasHandler {
 	return aliasHandler{t, l, allowRoot}
 }
 
@@ -80,7 +80,7 @@
 func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
 	handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot)
 
-	s.Log.DEBUG.Println("creating alias:", path, "redirecting to", permalink)
+	s.Log.Debug().Println("creating alias:", path, "redirecting to", permalink)
 
 	targetPath, err := handler.targetPathAlias(path)
 	if err != nil {
@@ -153,12 +153,12 @@
 	if len(msgs) > 0 {
 		if runtime.GOOS == "windows" {
 			for _, m := range msgs {
-				a.log.ERROR.Println(m)
+				a.log.Errorln(m)
 			}
 			return "", fmt.Errorf("cannot create \"%s\": Windows filename restriction", originalAlias)
 		}
 		for _, m := range msgs {
-			a.log.INFO.Println(m)
+			a.log.Infoln(m)
 		}
 	}
 
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -74,7 +74,7 @@
 // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
 type ConfigSourceDescriptor struct {
 	Fs     afero.Fs
-	Logger *loggers.Logger
+	Logger loggers.Logger
 
 	// Path to the config file to use, e.g. /my/project/config.toml
 	Filename string
--- a/hugolib/content_map_page.go
+++ b/hugolib/content_map_page.go
@@ -581,7 +581,7 @@
 		w := getParamToLower(b.p, viewName.plural+"_weight")
 		weight, err := cast.ToIntE(w)
 		if err != nil {
-			m.s.Log.ERROR.Printf("Unable to convert taxonomy weight %#v to int for %q", w, b.p.Path())
+			m.s.Log.Errorf("Unable to convert taxonomy weight %#v to int for %q", w, b.p.Path())
 			// weight will equal zero, so let the flow continue
 		}
 
--- a/hugolib/filesystems/basefs.go
+++ b/hugolib/filesystems/basefs.go
@@ -359,7 +359,7 @@
 }
 
 // NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase
-func NewBase(p *paths.Paths, logger *loggers.Logger, options ...func(*BaseFs) error) (*BaseFs, error) {
+func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) error) (*BaseFs, error) {
 	fs := p.Fs
 	if logger == nil {
 		logger = loggers.NewWarningLogger()
@@ -396,7 +396,7 @@
 }
 
 type sourceFilesystemsBuilder struct {
-	logger   *loggers.Logger
+	logger   loggers.Logger
 	p        *paths.Paths
 	sourceFs afero.Fs
 	result   *SourceFilesystems
@@ -403,7 +403,7 @@
 	theBigFs *filesystemsCollector
 }
 
-func newSourceFilesystemsBuilder(p *paths.Paths, logger *loggers.Logger, b *BaseFs) *sourceFilesystemsBuilder {
+func newSourceFilesystemsBuilder(p *paths.Paths, logger loggers.Logger, b *BaseFs) *sourceFilesystemsBuilder {
 	sourceFs := hugofs.NewBaseFileDecorator(p.Fs.Source)
 	return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}}
 }
--- a/hugolib/hugo_modules_test.go
+++ b/hugolib/hugo_modules_test.go
@@ -649,7 +649,7 @@
 
 	c := qt.New(t)
 
-	c.Assert(logger.WarnCounter.Count(), qt.Equals, uint64(3))
+	c.Assert(logger.LogCounters().WarnCounter.Count(), qt.Equals, uint64(3))
 
 }
 
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -223,7 +223,7 @@
 			break
 		}
 
-		h.Log.ERROR.Println(err)
+		h.Log.Errorln(err)
 	}
 
 	return errors[i]
@@ -246,7 +246,7 @@
 	if h == nil {
 		return 0
 	}
-	return int(h.Log.ErrorCounter.Count())
+	return int(h.Log.LogCounters().ErrorCounter.Count())
 }
 
 func (h *HugoSites) PrintProcessingStats(w io.Writer) {
@@ -386,7 +386,7 @@
 	if h.Cfg.GetBool("enableGitInfo") {
 		gi, err := newGitInfo(h.Cfg)
 		if err != nil {
-			h.Log.ERROR.Println("Failed to read Git log:", err)
+			h.Log.Errorln("Failed to read Git log:", err)
 		} else {
 			h.gitInfo = gi
 		}
@@ -570,7 +570,7 @@
 	h.Log.Reset()
 	loggers.GlobalErrorCounter.Reset()
 	for _, s := range h.Sites {
-		s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.ERROR)
+		s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.Error())
 	}
 }
 
@@ -879,7 +879,7 @@
 					// 1. A theme uses the same key; the main data folder wins
 					// 2. A sub folder uses the same key: the sub folder wins
 					// TODO(bep) figure out a way to detect 2) above and make that a WARN
-					h.Log.INFO.Printf("Data for key '%s' in path '%s' is overridden by higher precedence data already in the data tree", key, r.Path())
+					h.Log.Infof("Data for key '%s' in path '%s' is overridden by higher precedence data already in the data tree", key, r.Path())
 				} else {
 					higherPrecedentMap[key] = value
 				}
@@ -886,7 +886,7 @@
 			}
 		default:
 			// can't merge: higherPrecedentData is not a map
-			h.Log.WARN.Printf("The %T data from '%s' overridden by "+
+			h.Log.Warnf("The %T data from '%s' overridden by "+
 				"higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
 		}
 
@@ -895,12 +895,12 @@
 			current[r.BaseFileName()] = data
 		} else {
 			// we don't merge array data
-			h.Log.WARN.Printf("The %T data from '%s' overridden by "+
+			h.Log.Warnf("The %T data from '%s' overridden by "+
 				"higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
 		}
 
 	default:
-		h.Log.ERROR.Printf("unexpected data type %T in file %s", data, r.LogicalName())
+		h.Log.Errorf("unexpected data type %T in file %s", data, r.LogicalName())
 	}
 
 	return nil
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -162,9 +162,8 @@
 		var b bytes.Buffer
 		h.Metrics.WriteMetrics(&b)
 
-		h.Log.FEEDBACK.Printf("\nTemplate Metrics:\n\n")
-		h.Log.FEEDBACK.Print(b.String())
-		h.Log.FEEDBACK.Println()
+		h.Log.Printf("\nTemplate Metrics:\n\n")
+		h.Log.Println(b.String())
 	}
 
 	select {
@@ -183,7 +182,7 @@
 		return err
 	}
 
-	errorCount := h.Log.ErrorCounter.Count()
+	errorCount := h.Log.LogCounters().ErrorCounter.Count()
 	if errorCount > 0 {
 		return fmt.Errorf("logged %d error(s)", errorCount)
 	}
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -536,7 +536,7 @@
 					// mode when the same resource is member of different page bundles.
 					toBeDeleted = append(toBeDeleted, i)
 				} else {
-					p.s.Log.ERROR.Printf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err)
+					p.s.Log.Errorf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err)
 				}
 			} else {
 				p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files)
@@ -718,7 +718,7 @@
 	})
 
 	if err != nil {
-		p.s.Log.ERROR.Println("Failed to create content converter:", err)
+		p.s.Log.Errorln("Failed to create content converter:", err)
 	}
 	return p.m.contentConverter
 }
--- a/hugolib/page__menus.go
+++ b/hugolib/page__menus.go
@@ -66,7 +66,7 @@
 		var err error
 		p.pm, err = navigation.PageMenusFromPage(p.p)
 		if err != nil {
-			p.p.s.Log.ERROR.Println(p.p.wrapError(err))
+			p.p.s.Log.Errorln(p.p.wrapError(err))
 		}
 
 	})
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -431,7 +431,7 @@
 	// more easily tested without the Page, but the coupling is strong.
 	err := pm.s.frontmatterHandler.HandleDates(descriptor)
 	if err != nil {
-		p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
+		p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
 	}
 
 	pm.buildConfig, err = pagemeta.DecodeBuildConfig(frontmatter["_build"])
@@ -486,7 +486,7 @@
 					// We added support for page relative URLs in Hugo 0.55 and
 					// this may get its language path added twice.
 					// TODO(bep) eventually remove this.
-					p.s.Log.WARN.Printf(`Front matter in %q with the url %q with no leading / has what looks like the language prefix added. In Hugo 0.55 we added support for page relative URLs in front matter, no language prefix needed. Check the URL and consider to either add a leading / or remove the language prefix.`, p.pathOrTitle(), url)
+					p.s.Log.Warnf(`Front matter in %q with the url %q with no leading / has what looks like the language prefix added. In Hugo 0.55 we added support for page relative URLs in front matter, no language prefix needed. Check the URL and consider to either add a leading / or remove the language prefix.`, p.pathOrTitle(), url)
 
 				}
 			}
@@ -515,7 +515,7 @@
 				outFormats, err := p.s.outputFormatsConfig.GetByNames(o...)
 
 				if err != nil {
-					p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err)
+					p.s.Log.Errorf("Failed to resolve output formats: %s", err)
 				} else {
 					pm.configuredOutputFormats = outFormats
 					pm.params[loki] = outFormats
@@ -634,7 +634,7 @@
 
 	if draft != nil && published != nil {
 		pm.draft = *draft
-		p.m.s.Log.WARN.Printf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
+		p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
 	} else if draft != nil {
 		pm.draft = *draft
 	} else if published != nil {
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -91,7 +91,7 @@
 			// See https://github.com/gohugoio/hugo/issues/6210
 			if r := recover(); r != nil {
 				err = fmt.Errorf("%s", r)
-				p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
+				p.s.Log.Errorf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
 			}
 		}()
 
@@ -177,7 +177,7 @@
 			} else {
 				summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
 				if err != nil {
-					cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
+					cp.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
 				} else {
 					cp.workContent = content
 					cp.summary = helpers.BytesToHTML(summary)
--- a/hugolib/pages_capture.go
+++ b/hugolib/pages_capture.go
@@ -41,7 +41,7 @@
 func newPagesCollector(
 	sp *source.SourceSpec,
 	contentMap *pageMaps,
-	logger *loggers.Logger,
+	logger loggers.Logger,
 	contentTracker *contentChangeMap,
 	proc pagesCollectorProcessorProvider, filenames ...string) *pagesCollector {
 
@@ -83,7 +83,7 @@
 type pagesCollector struct {
 	sp     *source.SourceSpec
 	fs     afero.Fs
-	logger *loggers.Logger
+	logger loggers.Logger
 
 	contentMap *pageMaps
 
@@ -441,7 +441,7 @@
 			// The branch variant will win because of sort order, but log
 			// a warning about it.
 			if thisBtype > bundleNot && btype > bundleNot && thisBtype != btype {
-				c.logger.WARN.Printf("Content directory %q have both index.* and _index.* files, pick one.", dir.Meta().Filename())
+				c.logger.Warnf("Content directory %q have both index.* and _index.* files, pick one.", dir.Meta().Filename())
 				// Reclassify it so it will be handled as a content file inside the
 				// section, which is in line with the <= 0.55 behaviour.
 				meta["classifier"] = files.ContentClassContent
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -324,7 +324,7 @@
 		var found, more bool
 		tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
 		if !found {
-			s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
+			s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
 			return "", false, nil
 		}
 		hasVariants = hasVariants || more
@@ -349,7 +349,7 @@
 				hasVariants = hasVariants || more
 				inner += s
 			default:
-				s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
+				s.Log.Errorf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
 					sc.name, p.File().Path(), reflect.TypeOf(innerData))
 				return "", false, nil
 			}
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -425,7 +425,7 @@
 		delete(disabledKinds, "taxonomyTerm")
 	} else if disabledKinds[page.KindTaxonomy] && !disabledKinds[page.KindTerm] {
 		// This is a potentially ambigous situation. It may be correct.
-		ignorableLogger.Errorf(constants.ErrIDAmbigousDisableKindTaxonomy, `You have the value 'taxonomy' in the disabledKinds list. In Hugo 0.73.0 we fixed these to be what most people expect (taxonomy and term).
+		ignorableLogger.Errorsf(constants.ErrIDAmbigousDisableKindTaxonomy, `You have the value 'taxonomy' in the disabledKinds list. In Hugo 0.73.0 we fixed these to be what most people expect (taxonomy and term).
 But this also means that your site configuration may not do what you expect. If it is correct, you can suppress this message by following the instructions below.`)
 
 	}
@@ -485,7 +485,7 @@
 			delete(siteOutputs, "taxonomyTerm")
 		} else if hasTaxonomy && !hasTerm {
 			// This is a potentially ambigous situation. It may be correct.
-			ignorableLogger.Errorf(constants.ErrIDAmbigousOutputKindTaxonomy, `You have configured output formats for 'taxonomy' in your site configuration. In Hugo 0.73.0 we fixed these to be what most people expect (taxonomy and term).
+			ignorableLogger.Errorsf(constants.ErrIDAmbigousOutputKindTaxonomy, `You have configured output formats for 'taxonomy' in your site configuration. In Hugo 0.73.0 we fixed these to be what most people expect (taxonomy and term).
 But this also means that your site configuration may not do what you expect. If it is correct, you can suppress this message by following the instructions below.`)
 		}
 		if !hasTaxonomy && hasTaxonomyTerm {
@@ -806,12 +806,12 @@
 }
 
 func newSiteRefLinker(cfg config.Provider, s *Site) (siteRefLinker, error) {
-	logger := s.Log.ERROR
+	logger := s.Log.Error()
 
 	notFoundURL := cfg.GetString("refLinksNotFoundURL")
 	errLevel := cfg.GetString("refLinksErrorLevel")
 	if strings.EqualFold(errLevel, "warning") {
-		logger = s.Log.WARN
+		logger = s.Log.Warn()
 	}
 	return siteRefLinker{s: s, errorLogger: logger, notFoundURL: notFoundURL}, nil
 }
@@ -1006,7 +1006,7 @@
 
 	changeIdentities := make(identity.Identities)
 
-	s.Log.DEBUG.Printf("Rebuild for events %q", events)
+	s.Log.Debug().Printf("Rebuild for events %q", events)
 
 	h := s.h
 
@@ -1377,17 +1377,17 @@
 		for name, menu := range menus {
 			m, err := cast.ToSliceE(menu)
 			if err != nil {
-				s.Log.ERROR.Printf("unable to process menus in site config\n")
-				s.Log.ERROR.Println(err)
+				s.Log.Errorf("unable to process menus in site config\n")
+				s.Log.Errorln(err)
 			} else {
 				for _, entry := range m {
-					s.Log.DEBUG.Printf("found menu: %q, in site config\n", name)
+					s.Log.Debug().Printf("found menu: %q, in site config\n", name)
 
 					menuEntry := navigation.MenuEntry{Menu: name}
 					ime, err := maps.ToStringMapE(entry)
 					if err != nil {
-						s.Log.ERROR.Printf("unable to process menus in site config\n")
-						s.Log.ERROR.Println(err)
+						s.Log.Errorf("unable to process menus in site config\n")
+						s.Log.Errorln(err)
 					}
 
 					menuEntry.MarshallMap(ime)
@@ -1471,7 +1471,7 @@
 		for name, me := range p.pageMenus.menus() {
 			if _, ok := flat[twoD{name, me.KeyName()}]; ok {
 				err := p.wrapError(errors.Errorf("duplicate menu entry with identifier %q in menu %q", me.KeyName(), name))
-				s.Log.WARN.Println(err)
+				s.Log.Warnln(err)
 				continue
 			}
 			flat[twoD{name, me.KeyName()}] = me
@@ -1643,7 +1643,7 @@
 }
 
 func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath string, d interface{}, templ tpl.Template) error {
-	s.Log.DEBUG.Printf("Render XML for %q to %q", name, targetPath)
+	s.Log.Debug().Printf("Render XML for %q to %q", name, targetPath)
 	renderBuffer := bp.GetBuffer()
 	defer bp.PutBuffer(renderBuffer)
 
@@ -1665,7 +1665,7 @@
 }
 
 func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, templ tpl.Template) error {
-	s.Log.DEBUG.Printf("Render %s to %q", name, targetPath)
+	s.Log.Debug().Printf("Render %s to %q", name, targetPath)
 	renderBuffer := bp.GetBuffer()
 	defer bp.PutBuffer(renderBuffer)
 
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -149,9 +149,9 @@
 }
 
 func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) {
-	log := s.Log.WARN
+	log := s.Log.Warn()
 	if name != "" && infoOnMissingLayout[name] {
-		log = s.Log.INFO
+		log = s.Log.Info()
 	}
 
 	errMsg := "You should create a template file which matches Hugo Layouts Lookup Rules for this combination."
@@ -396,13 +396,13 @@
 		mainLang := s.h.multilingual.DefaultLang
 		if s.Info.defaultContentLanguageInSubdir {
 			mainLangURL := s.PathSpec.AbsURL(mainLang.Lang+"/", false)
-			s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
+			s.Log.Debug().Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
 			if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil {
 				return err
 			}
 		} else {
 			mainLangURL := s.PathSpec.AbsURL("", false)
-			s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
+			s.Log.Debug().Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
 			if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, html, nil); err != nil {
 				return err
 			}
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -67,7 +67,7 @@
 
 	*qt.C
 
-	logger *loggers.Logger
+	logger loggers.Logger
 	rnd    *rand.Rand
 	dumper litter.Options
 
@@ -154,7 +154,7 @@
 	return s
 }
 
-func (s *sitesBuilder) WithLogger(logger *loggers.Logger) *sitesBuilder {
+func (s *sitesBuilder) WithLogger(logger loggers.Logger) *sitesBuilder {
 	s.logger = logger
 	return s
 }
--- a/langs/i18n/i18n.go
+++ b/langs/i18n/i18n.go
@@ -35,11 +35,11 @@
 type Translator struct {
 	translateFuncs map[string]translateFunc
 	cfg            config.Provider
-	logger         *loggers.Logger
+	logger         loggers.Logger
 }
 
 // NewTranslator creates a new Translator for the given language bundle and configuration.
-func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger *loggers.Logger) Translator {
+func NewTranslator(b *i18n.Bundle, cfg config.Provider, logger loggers.Logger) Translator {
 	t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]translateFunc)}
 	t.initFuncs(b)
 	return t
@@ -51,12 +51,12 @@
 	if f, ok := t.translateFuncs[lang]; ok {
 		return f
 	}
-	t.logger.INFO.Printf("Translation func for language %v not found, use default.", lang)
+	t.logger.Infof("Translation func for language %v not found, use default.", lang)
 	if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok {
 		return f
 	}
 
-	t.logger.INFO.Println("i18n not initialized; if you need string translations, check that you have a bundle in /i18n that matches the site language or the default language.")
+	t.logger.Infoln("i18n not initialized; if you need string translations, check that you have a bundle in /i18n that matches the site language or the default language.")
 	return func(translationID string, args interface{}) string {
 		return ""
 	}
@@ -98,7 +98,7 @@
 			}
 
 			if _, ok := err.(*i18n.MessageNotFoundErr); !ok {
-				t.logger.WARN.Printf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err)
+				t.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err)
 			}
 
 			if t.cfg.GetBool("logI18nWarnings") {
--- a/markup/asciidocext/convert.go
+++ b/markup/asciidocext/convert.go
@@ -82,7 +82,7 @@
 func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte {
 	path := getAsciidoctorExecPath()
 	if path == "" {
-		a.cfg.Logger.ERROR.Println("asciidoctor not found in $PATH: Please install.\n",
+		a.cfg.Logger.Errorln("asciidoctor not found in $PATH: Please install.\n",
 			"                 Leaving AsciiDoc content unrendered.")
 		return src
 	}
@@ -90,7 +90,7 @@
 	args := a.parseArgs(ctx)
 	args = append(args, "-")
 
-	a.cfg.Logger.INFO.Println("Rendering", ctx.DocumentName, "with", path, "using asciidoctor args", args, "...")
+	a.cfg.Logger.Infoln("Rendering", ctx.DocumentName, "with", path, "using asciidoctor args", args, "...")
 
 	return internal.ExternallyRenderContent(a.cfg, ctx, src, path, args)
 }
@@ -103,7 +103,7 @@
 
 	for _, extension := range cfg.Extensions {
 		if !asciidocext_config.AllowedExtensions[extension] {
-			a.cfg.Logger.ERROR.Println("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored.")
+			a.cfg.Logger.Errorln("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored.")
 			continue
 		}
 
@@ -112,7 +112,7 @@
 
 	for attributeKey, attributeValue := range cfg.Attributes {
 		if asciidocext_config.DisallowedAttributes[attributeKey] {
-			a.cfg.Logger.ERROR.Println("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.")
+			a.cfg.Logger.Errorln("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.")
 			continue
 		}
 
@@ -125,7 +125,7 @@
 		destinationDir := a.cfg.Cfg.GetString("destination")
 
 		if destinationDir == "" {
-			a.cfg.Logger.ERROR.Println("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set")
+			a.cfg.Logger.Errorln("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set")
 		}
 		if !filepath.IsAbs(destinationDir) && sourceDir != "" {
 			destinationDir = filepath.Join(sourceDir, destinationDir)
@@ -144,7 +144,7 @@
 			if ok {
 				postDir = filepath.Base(page.RelPermalink())
 			} else {
-				a.cfg.Logger.ERROR.Println("unable to cast interface to pageSubset")
+				a.cfg.Logger.Errorln("unable to cast interface to pageSubset")
 			}
 
 			outDir, err = filepath.Abs(filepath.Join(destinationDir, filepath.Dir(ctx.DocumentName), postDir))
@@ -151,7 +151,7 @@
 		}
 
 		if err != nil {
-			a.cfg.Logger.ERROR.Println("asciidoctor outDir: ", err)
+			a.cfg.Logger.Errorln("asciidoctor outDir: ", err)
 		}
 
 		args = append(args, "--base-dir", contentDir, "-a", "outdir="+outDir)
@@ -160,7 +160,7 @@
 	if cfg.NoHeaderOrFooter {
 		args = append(args, "--no-header-footer")
 	} else {
-		a.cfg.Logger.WARN.Println("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering")
+		a.cfg.Logger.Warnln("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering")
 	}
 
 	if cfg.SectionNumbers {
@@ -187,7 +187,7 @@
 		if allowedValues[value] {
 			args = append(args, option, value)
 		} else {
-			a.cfg.Logger.ERROR.Println("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.")
+			a.cfg.Logger.Errorln("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.")
 		}
 	}
 	return args
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -31,7 +31,7 @@
 
 	Cfg       config.Provider // Site config
 	ContentFs afero.Fs
-	Logger    *loggers.Logger
+	Logger    loggers.Logger
 	Highlight func(code, lang, optsStr string) (string, error)
 }
 
--- a/markup/internal/external.go
+++ b/markup/internal/external.go
@@ -25,11 +25,11 @@
 	for _, item := range strings.Split(cmderr.String(), "\n") {
 		item := strings.TrimSpace(item)
 		if item != "" {
-			logger.ERROR.Printf("%s: %s", ctx.DocumentName, item)
+			logger.Errorf("%s: %s", ctx.DocumentName, item)
 		}
 	}
 	if err != nil {
-		logger.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err)
+		logger.Errorf("%s rendering %s: %v", path, ctx.DocumentName, err)
 	}
 
 	return normalizeExternalHelperLineFeeds(out.Bytes())
--- a/markup/org/convert.go
+++ b/markup/org/convert.go
@@ -47,7 +47,7 @@
 func (c *orgConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
 	logger := c.cfg.Logger
 	config := org.New()
-	config.Log = logger.WARN
+	config.Log = logger.Warn()
 	config.ReadFile = func(filename string) ([]byte, error) {
 		return afero.ReadFile(c.cfg.ContentFs, filename)
 	}
@@ -55,7 +55,7 @@
 	writer.HighlightCodeBlock = func(source, lang string, inline bool) string {
 		highlightedSource, err := c.cfg.Highlight(source, lang, "")
 		if err != nil {
-			logger.ERROR.Printf("Could not highlight source as lang %s. Using raw source.", lang)
+			logger.Errorf("Could not highlight source as lang %s. Using raw source.", lang)
 			return source
 		}
 		return highlightedSource
@@ -63,7 +63,7 @@
 
 	html, err := config.Parse(bytes.NewReader(ctx.Src), c.ctx.DocumentName).Write(writer)
 	if err != nil {
-		logger.ERROR.Printf("Could not render org: %s. Using unrendered content.", err)
+		logger.Errorf("Could not render org: %s. Using unrendered content.", err)
 		return converter.Bytes(ctx.Src), nil
 	}
 	return converter.Bytes([]byte(html)), nil
--- a/markup/pandoc/convert.go
+++ b/markup/pandoc/convert.go
@@ -57,7 +57,7 @@
 	logger := c.cfg.Logger
 	path := getPandocExecPath()
 	if path == "" {
-		logger.ERROR.Println("pandoc not found in $PATH: Please install.\n",
+		logger.Println("pandoc not found in $PATH: Please install.\n",
 			"                 Leaving pandoc content unrendered.")
 		return src
 	}
--- a/markup/rst/convert.go
+++ b/markup/rst/convert.go
@@ -60,11 +60,11 @@
 	path := getRstExecPath()
 
 	if path == "" {
-		logger.ERROR.Println("rst2html / rst2html.py not found in $PATH: Please install.\n",
+		logger.Println("rst2html / rst2html.py not found in $PATH: Please install.\n",
 			"                 Leaving reStructuredText content unrendered.")
 		return src
 	}
-	logger.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
+	logger.Println("Rendering", ctx.DocumentName, "with", path, "...")
 	var result []byte
 	// certain *nix based OSs wrap executables in scripted launchers
 	// invoking binaries on these OSs via python interpreter causes SyntaxError
--- a/modules/client.go
+++ b/modules/client.go
@@ -119,7 +119,7 @@
 // Client contains most of the API provided by this package.
 type Client struct {
 	fs     afero.Fs
-	logger *loggers.Logger
+	logger loggers.Logger
 
 	noVendor glob.Glob
 
@@ -329,7 +329,7 @@
 }
 
 func (c *Client) get(args ...string) error {
-	if err := c.runGo(context.Background(), c.logger.Out, append([]string{"get"}, args...)...); err != nil {
+	if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil {
 		errors.Wrapf(err, "failed to get %q", args)
 	}
 	return nil
@@ -339,7 +339,7 @@
 // If path is empty, Go will try to guess.
 // If this succeeds, this project will be marked as Go Module.
 func (c *Client) Init(path string) error {
-	err := c.runGo(context.Background(), c.logger.Out, "mod", "init", path)
+	err := c.runGo(context.Background(), c.logger.Out(), "mod", "init", path)
 	if err != nil {
 		return errors.Wrap(err, "failed to init modules")
 	}
@@ -403,7 +403,7 @@
 		}
 		_, err = hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m.Dir)
 		if err == nil {
-			c.logger.FEEDBACK.Printf("hugo: cleaned module cache for %q", m.Path)
+			c.logger.Printf("hugo: cleaned module cache for %q", m.Path)
 		}
 	}
 	return err
@@ -560,7 +560,7 @@
 
 		if strings.Contains(stderr.String(), "invalid version: unknown revision") {
 			// See https://github.com/gohugoio/hugo/issues/6825
-			c.logger.FEEDBACK.Println(`hugo: you need to manually edit go.mod to resolve the unknown revision.`)
+			c.logger.Println(`hugo: you need to manually edit go.mod to resolve the unknown revision.`)
 		}
 
 		_, ok := err.(*exec.ExitError)
@@ -616,7 +616,7 @@
 // ClientConfig configures the module Client.
 type ClientConfig struct {
 	Fs     afero.Fs
-	Logger *loggers.Logger
+	Logger loggers.Logger
 
 	// If set, it will be run before we do any duplicate checks for modules
 	// etc.
--- a/modules/collect.go
+++ b/modules/collect.go
@@ -124,11 +124,11 @@
 	GoModulesFilename string
 }
 
-func (m *ModulesConfig) setActiveMods(logger *loggers.Logger) error {
+func (m *ModulesConfig) setActiveMods(logger loggers.Logger) error {
 	var activeMods Modules
 	for _, mod := range m.AllModules {
 		if !mod.Config().HugoVersion.IsValid() {
-			logger.WARN.Printf(`Module %q is not compatible with this Hugo version; run "hugo mod graph" for more information.`, mod.Path())
+			logger.Warnf(`Module %q is not compatible with this Hugo version; run "hugo mod graph" for more information.`, mod.Path())
 		}
 		if !mod.Disabled() {
 			activeMods = append(activeMods, mod)
@@ -140,7 +140,7 @@
 	return nil
 }
 
-func (m *ModulesConfig) finalize(logger *loggers.Logger) error {
+func (m *ModulesConfig) finalize(logger loggers.Logger) error {
 	for _, mod := range m.AllModules {
 		m := mod.(*moduleAdapter)
 		m.mounts = filterUnwantedMounts(m.mounts)
@@ -422,7 +422,7 @@
 		}
 		themeCfg, err = metadecoders.Default.UnmarshalToMap(data, metadecoders.TOML)
 		if err != nil {
-			c.logger.WARN.Printf("Failed to read module config for %q in %q: %s", tc.Path(), themeTOML, err)
+			c.logger.Warnf("Failed to read module config for %q in %q: %s", tc.Path(), themeTOML, err)
 		} else {
 			maps.ToLower(themeCfg)
 		}
@@ -480,7 +480,7 @@
 	defer c.logger.PrintTimerIfDelayed(time.Now(), "hugo: collected modules")
 	d := debounce.New(2 * time.Second)
 	d(func() {
-		c.logger.FEEDBACK.Println("hugo: downloading modules …")
+		c.logger.Println("hugo: downloading modules …")
 	})
 	defer d(func() {})
 
--- a/resources/page/pagemeta/page_frontmatter.go
+++ b/resources/page/pagemeta/page_frontmatter.go
@@ -38,7 +38,7 @@
 	// A map of all date keys configured, including any custom.
 	allDateKeys map[string]bool
 
-	logger *loggers.Logger
+	logger loggers.Logger
 }
 
 // FrontMatterDescriptor describes how to handle front matter for a given Page.
@@ -148,7 +148,7 @@
 			// First successful handler wins.
 			success, err := h(d)
 			if err != nil {
-				f.logger.ERROR.Println(err)
+				f.logger.Errorln(err)
 			} else if success {
 				return true, nil
 			}
@@ -262,7 +262,7 @@
 
 // NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration.
 // If no logger is provided, one will be created.
-func NewFrontmatterHandler(logger *loggers.Logger, cfg config.Provider) (FrontMatterHandler, error) {
+func NewFrontmatterHandler(logger loggers.Logger, cfg config.Provider) (FrontMatterHandler, error) {
 
 	if logger == nil {
 		logger = loggers.NewErrorLogger()
--- a/resources/resource_spec.go
+++ b/resources/resource_spec.go
@@ -47,7 +47,7 @@
 	s *helpers.PathSpec,
 	fileCaches filecache.Caches,
 	incr identity.Incrementer,
-	logger *loggers.Logger,
+	logger loggers.Logger,
 	errorHandler herrors.ErrorSender,
 	outputFormats output.Formats,
 	mimeTypes media.Types) (*Spec, error) {
@@ -105,7 +105,7 @@
 	MediaTypes    media.Types
 	OutputFormats output.Formats
 
-	Logger      *loggers.Logger
+	Logger      loggers.Logger
 	ErrorSender herrors.ErrorSender
 
 	TextTemplates tpl.TemplateParseFinder
--- a/resources/resource_transformers/babel/babel.go
+++ b/resources/resource_transformers/babel/babel.go
@@ -121,7 +121,7 @@
 	logger := t.rs.Logger
 
 	var errBuf bytes.Buffer
-	infoW := loggers.LoggerToWriterWithPrefix(logger.INFO, "babel")
+	infoW := loggers.LoggerToWriterWithPrefix(logger.Info(), "babel")
 
 	if t.options.Config != "" {
 		configFile = t.options.Config
@@ -143,7 +143,7 @@
 	var cmdArgs []string
 
 	if configFile != "" {
-		logger.INFO.Println("babel: use config file", configFile)
+		logger.Infoln("babel: use config file", configFile)
 		cmdArgs = []string{"--config-file", configFile}
 	}
 
--- a/resources/resource_transformers/postcss/postcss.go
+++ b/resources/resource_transformers/postcss/postcss.go
@@ -181,7 +181,7 @@
 	var cmdArgs []string
 
 	if configFile != "" {
-		logger.INFO.Println("postcss: use config file", configFile)
+		logger.Infoln("postcss: use config file", configFile)
 		cmdArgs = []string{"--config", configFile}
 	}
 
@@ -192,7 +192,7 @@
 	cmd := exec.Command(binary, cmdArgs...)
 
 	var errBuf bytes.Buffer
-	infoW := loggers.LoggerToWriterWithPrefix(logger.INFO, "postcss")
+	infoW := loggers.LoggerToWriterWithPrefix(logger.Info(), "postcss")
 
 	cmd.Stdout = ctx.To
 	cmd.Stderr = io.MultiWriter(infoW, &errBuf)
@@ -245,10 +245,10 @@
 	contentSeen map[string]bool
 	linemap     map[int]fileOffset
 	fs          afero.Fs
-	logger      *loggers.Logger
+	logger      loggers.Logger
 }
 
-func newImportResolver(r io.Reader, inPath string, fs afero.Fs, logger *loggers.Logger) *importResolver {
+func newImportResolver(r io.Reader, inPath string, fs afero.Fs, logger loggers.Logger) *importResolver {
 	return &importResolver{
 		r:      r,
 		inPath: inPath,
@@ -296,7 +296,7 @@
 			importContent, hash := imp.contentHash(filename)
 			if importContent == nil {
 				trackLine(i, offset, "ERROR")
-				imp.logger.WARN.Printf("postcss: Failed to resolve CSS @import in %q for path %q", inPath, filename)
+				imp.logger.Warnf("postcss: Failed to resolve CSS @import in %q for path %q", inPath, filename)
 				continue
 			}
 
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -290,7 +290,7 @@
 		r.publisherErr = r.target.Publish()
 
 		if r.publisherErr != nil {
-			r.spec.Logger.ERROR.Printf("Failed to publish Resource: %s", r.publisherErr)
+			r.spec.Logger.Errorf("Failed to publish Resource: %s", r.publisherErr)
 		}
 	})
 
@@ -546,7 +546,7 @@
 			if r.spec.ErrorSender != nil {
 				r.spec.ErrorSender.SendError(r.transformationsErr)
 			} else {
-				r.spec.Logger.ERROR.Printf("Transformation failed: %s", r.transformationsErr)
+				r.spec.Logger.Errorf("Transformation failed: %s", r.transformationsErr)
 			}
 		}
 	})
--- a/tpl/data/data.go
+++ b/tpl/data/data.go
@@ -23,6 +23,9 @@
 	"net/http"
 	"strings"
 
+	"github.com/gohugoio/hugo/common/constants"
+	"github.com/gohugoio/hugo/common/loggers"
+
 	"github.com/spf13/cast"
 
 	"github.com/gohugoio/hugo/cache/filecache"
@@ -85,7 +88,7 @@
 
 	err = ns.getResource(cache, unmarshal, req)
 	if err != nil {
-		ns.deps.Log.ERROR.Printf("Failed to get CSV resource %q: %s", url, err)
+		ns.deps.Log.(loggers.IgnorableLogger).Errorsf(constants.ErrRemoteGetCSV, "Failed to get CSV resource %q: %s", url, err)
 		return nil, nil
 	}
 
@@ -117,7 +120,7 @@
 
 	err = ns.getResource(cache, unmarshal, req)
 	if err != nil {
-		ns.deps.Log.ERROR.Printf("Failed to get JSON resource %q: %s", url, err)
+		ns.deps.Log.(loggers.IgnorableLogger).Errorsf(constants.ErrRemoteGetJSON, "Failed to get JSON resource %q: %s", url, err)
 		return nil, nil
 	}
 
--- a/tpl/data/data_test.go
+++ b/tpl/data/data_test.go
@@ -108,7 +108,7 @@
 		got, err := ns.GetCSV(test.sep, test.url)
 
 		if _, ok := test.expect.(bool); ok {
-			c.Assert(int(ns.deps.Log.ErrorCounter.Count()), qt.Equals, 1)
+			c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 1)
 			//c.Assert(err, msg, qt.Not(qt.IsNil))
 			c.Assert(got, qt.IsNil)
 			continue
@@ -115,7 +115,7 @@
 		}
 
 		c.Assert(err, qt.IsNil, msg)
-		c.Assert(int(ns.deps.Log.ErrorCounter.Count()), qt.Equals, 0)
+		c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 0)
 		c.Assert(got, qt.Not(qt.IsNil), msg)
 		c.Assert(got, qt.DeepEquals, test.expect, msg)
 
@@ -198,12 +198,12 @@
 		got, _ := ns.GetJSON(test.url)
 
 		if _, ok := test.expect.(bool); ok {
-			c.Assert(int(ns.deps.Log.ErrorCounter.Count()), qt.Equals, 1)
+			c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 1)
 			//c.Assert(err, msg, qt.Not(qt.IsNil))
 			continue
 		}
 
-		c.Assert(int(ns.deps.Log.ErrorCounter.Count()), qt.Equals, 0, msg)
+		c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 0, msg)
 		c.Assert(got, qt.Not(qt.IsNil), msg)
 		c.Assert(got, qt.DeepEquals, test.expect)
 	}
--- a/tpl/data/resources.go
+++ b/tpl/data/resources.go
@@ -45,7 +45,7 @@
 		var err error
 		handled = true
 		for i := 0; i <= resRetries; i++ {
-			ns.deps.Log.INFO.Printf("Downloading: %s ...", url)
+			ns.deps.Log.Infof("Downloading: %s ...", url)
 			var res *http.Response
 			res, err = ns.client.Do(req)
 			if err != nil {
@@ -75,8 +75,8 @@
 				return nil, err
 			}
 
-			ns.deps.Log.INFO.Printf("Cannot read remote resource %s: %s", url, err)
-			ns.deps.Log.INFO.Printf("Retry #%d for %s and sleeping for %s", i+1, url, resSleep)
+			ns.deps.Log.Infof("Cannot read remote resource %s: %s", url, err)
+			ns.deps.Log.Infof("Retry #%d for %s and sleeping for %s", i+1, url, resSleep)
 			time.Sleep(resSleep)
 		}
 
--- a/tpl/data/resources_test.go
+++ b/tpl/data/resources_test.go
@@ -195,13 +195,13 @@
 	}
 	cfg.Set("allModules", modules.Modules{mod})
 
-	cs, err := helpers.NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs())
+	logger := loggers.NewIgnorableLogger(loggers.NewErrorLogger(), "none")
+	cs, err := helpers.NewContentSpec(cfg, logger, afero.NewMemMapFs())
 	if err != nil {
 		panic(err)
 	}
 
 	fs := hugofs.NewMem(cfg)
-	logger := loggers.NewErrorLogger()
 
 	p, err := helpers.NewPathSpec(fs, cfg, nil)
 	if err != nil {
@@ -219,7 +219,7 @@
 		FileCaches:       fileCaches,
 		ContentSpec:      cs,
 		Log:              logger,
-		DistinctErrorLog: helpers.NewDistinctLogger(logger.ERROR),
+		DistinctErrorLog: helpers.NewDistinctLogger(logger.Error()),
 	}
 }
 
--- a/tpl/fmt/fmt.go
+++ b/tpl/fmt/fmt.go
@@ -24,8 +24,8 @@
 // New returns a new instance of the fmt-namespaced template functions.
 func New(d *deps.Deps) *Namespace {
 	ns := &Namespace{
-		errorLogger: helpers.NewDistinctLogger(d.Log.ERROR),
-		warnLogger:  helpers.NewDistinctLogger(d.Log.WARN),
+		errorLogger: helpers.NewDistinctLogger(d.Log.Error()),
+		warnLogger:  helpers.NewDistinctLogger(d.Log.Warn()),
 	}
 
 	d.BuildStartListeners.Add(func() {