hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

hugo_sites.go (26263B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package hugolib
   15 
   16 import (
   17 	"context"
   18 	"fmt"
   19 	"io"
   20 	"path/filepath"
   21 	"sort"
   22 	"strings"
   23 	"sync"
   24 	"sync/atomic"
   25 
   26 	"github.com/gohugoio/hugo/hugofs/glob"
   27 
   28 	"github.com/fsnotify/fsnotify"
   29 
   30 	"github.com/gohugoio/hugo/identity"
   31 
   32 	radix "github.com/armon/go-radix"
   33 
   34 	"github.com/gohugoio/hugo/output"
   35 	"github.com/gohugoio/hugo/parser/metadecoders"
   36 
   37 	"errors"
   38 
   39 	"github.com/gohugoio/hugo/common/para"
   40 	"github.com/gohugoio/hugo/hugofs"
   41 
   42 	"github.com/gohugoio/hugo/source"
   43 
   44 	"github.com/bep/gitmap"
   45 	"github.com/gohugoio/hugo/config"
   46 
   47 	"github.com/gohugoio/hugo/publisher"
   48 
   49 	"github.com/gohugoio/hugo/common/herrors"
   50 	"github.com/gohugoio/hugo/common/loggers"
   51 	"github.com/gohugoio/hugo/deps"
   52 	"github.com/gohugoio/hugo/helpers"
   53 	"github.com/gohugoio/hugo/langs"
   54 	"github.com/gohugoio/hugo/lazy"
   55 
   56 	"github.com/gohugoio/hugo/langs/i18n"
   57 	"github.com/gohugoio/hugo/resources/page"
   58 	"github.com/gohugoio/hugo/resources/page/pagemeta"
   59 	"github.com/gohugoio/hugo/tpl"
   60 	"github.com/gohugoio/hugo/tpl/tplimpl"
   61 )
   62 
   63 // HugoSites represents the sites to build. Each site represents a language.
   64 type HugoSites struct {
   65 	Sites []*Site
   66 
   67 	multilingual *Multilingual
   68 
   69 	// Multihost is set if multilingual and baseURL set on the language level.
   70 	multihost bool
   71 
   72 	// If this is running in the dev server.
   73 	running bool
   74 
   75 	// Render output formats for all sites.
   76 	renderFormats output.Formats
   77 
   78 	// The currently rendered Site.
   79 	currentSite *Site
   80 
   81 	*deps.Deps
   82 
   83 	gitInfo       *gitInfo
   84 	codeownerInfo *codeownerInfo
   85 
   86 	// As loaded from the /data dirs
   87 	data map[string]any
   88 
   89 	contentInit sync.Once
   90 	content     *pageMaps
   91 
   92 	// Keeps track of bundle directories and symlinks to enable partial rebuilding.
   93 	ContentChanges *contentChangeMap
   94 
   95 	// File change events with filename stored in this map will be skipped.
   96 	skipRebuildForFilenamesMu sync.Mutex
   97 	skipRebuildForFilenames   map[string]bool
   98 
   99 	init *hugoSitesInit
  100 
  101 	workers    *para.Workers
  102 	numWorkers int
  103 
  104 	*fatalErrorHandler
  105 	*testCounters
  106 }
  107 
  108 // ShouldSkipFileChangeEvent allows skipping filesystem event early before
  109 // the build is started.
  110 func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool {
  111 	h.skipRebuildForFilenamesMu.Lock()
  112 	defer h.skipRebuildForFilenamesMu.Unlock()
  113 	return h.skipRebuildForFilenames[ev.Name]
  114 }
  115 
  116 func (h *HugoSites) getContentMaps() *pageMaps {
  117 	h.contentInit.Do(func() {
  118 		h.content = newPageMaps(h)
  119 	})
  120 	return h.content
  121 }
  122 
  123 // Only used in tests.
  124 type testCounters struct {
  125 	contentRenderCounter uint64
  126 	pageRenderCounter    uint64
  127 }
  128 
  129 func (h *testCounters) IncrContentRender() {
  130 	if h == nil {
  131 		return
  132 	}
  133 	atomic.AddUint64(&h.contentRenderCounter, 1)
  134 }
  135 
  136 func (h *testCounters) IncrPageRender() {
  137 	if h == nil {
  138 		return
  139 	}
  140 	atomic.AddUint64(&h.pageRenderCounter, 1)
  141 }
  142 
  143 type fatalErrorHandler struct {
  144 	mu sync.Mutex
  145 
  146 	h *HugoSites
  147 
  148 	err error
  149 
  150 	done  bool
  151 	donec chan bool // will be closed when done
  152 }
  153 
  154 // FatalError error is used in some rare situations where it does not make sense to
  155 // continue processing, to abort as soon as possible and log the error.
  156 func (f *fatalErrorHandler) FatalError(err error) {
  157 	f.mu.Lock()
  158 	defer f.mu.Unlock()
  159 	if !f.done {
  160 		f.done = true
  161 		close(f.donec)
  162 	}
  163 	f.err = err
  164 }
  165 
  166 func (f *fatalErrorHandler) getErr() error {
  167 	f.mu.Lock()
  168 	defer f.mu.Unlock()
  169 	return f.err
  170 }
  171 
  172 func (f *fatalErrorHandler) Done() <-chan bool {
  173 	return f.donec
  174 }
  175 
  176 type hugoSitesInit struct {
  177 	// Loads the data from all of the /data folders.
  178 	data *lazy.Init
  179 
  180 	// Performs late initialization (before render) of the templates.
  181 	layouts *lazy.Init
  182 
  183 	// Loads the Git info and CODEOWNERS for all the pages if enabled.
  184 	gitInfo *lazy.Init
  185 
  186 	// Maps page translations.
  187 	translations *lazy.Init
  188 }
  189 
  190 func (h *hugoSitesInit) Reset() {
  191 	h.data.Reset()
  192 	h.layouts.Reset()
  193 	h.gitInfo.Reset()
  194 	h.translations.Reset()
  195 }
  196 
  197 func (h *HugoSites) Data() map[string]any {
  198 	if _, err := h.init.data.Do(); err != nil {
  199 		h.SendError(fmt.Errorf("failed to load data: %w", err))
  200 		return nil
  201 	}
  202 	return h.data
  203 }
  204 
  205 func (h *HugoSites) gitInfoForPage(p page.Page) (*gitmap.GitInfo, error) {
  206 	if _, err := h.init.gitInfo.Do(); err != nil {
  207 		return nil, err
  208 	}
  209 
  210 	if h.gitInfo == nil {
  211 		return nil, nil
  212 	}
  213 
  214 	return h.gitInfo.forPage(p), nil
  215 }
  216 
  217 func (h *HugoSites) codeownersForPage(p page.Page) ([]string, error) {
  218 	if _, err := h.init.gitInfo.Do(); err != nil {
  219 		return nil, err
  220 	}
  221 
  222 	if h.codeownerInfo == nil {
  223 		return nil, nil
  224 	}
  225 
  226 	return h.codeownerInfo.forPage(p), nil
  227 }
  228 
  229 func (h *HugoSites) siteInfos() page.Sites {
  230 	infos := make(page.Sites, len(h.Sites))
  231 	for i, site := range h.Sites {
  232 		infos[i] = site.Info
  233 	}
  234 	return infos
  235 }
  236 
  237 func (h *HugoSites) pickOneAndLogTheRest(errors []error) error {
  238 	if len(errors) == 0 {
  239 		return nil
  240 	}
  241 
  242 	var i int
  243 
  244 	for j, err := range errors {
  245 		// If this is in server mode, we want to return an error to the client
  246 		// with a file context, if possible.
  247 		if herrors.UnwrapFileError(err) != nil {
  248 			i = j
  249 			break
  250 		}
  251 	}
  252 
  253 	// Log the rest, but add a threshold to avoid flooding the log.
  254 	const errLogThreshold = 5
  255 
  256 	for j, err := range errors {
  257 		if j == i || err == nil {
  258 			continue
  259 		}
  260 
  261 		if j >= errLogThreshold {
  262 			break
  263 		}
  264 
  265 		h.Log.Errorln(err)
  266 	}
  267 
  268 	return errors[i]
  269 }
  270 
  271 func (h *HugoSites) IsMultihost() bool {
  272 	return h != nil && h.multihost
  273 }
  274 
  275 // TODO(bep) consolidate
  276 func (h *HugoSites) LanguageSet() map[string]int {
  277 	set := make(map[string]int)
  278 	for i, s := range h.Sites {
  279 		set[s.language.Lang] = i
  280 	}
  281 	return set
  282 }
  283 
  284 func (h *HugoSites) NumLogErrors() int {
  285 	if h == nil {
  286 		return 0
  287 	}
  288 	return int(h.Log.LogCounters().ErrorCounter.Count())
  289 }
  290 
  291 func (h *HugoSites) PrintProcessingStats(w io.Writer) {
  292 	stats := make([]*helpers.ProcessingStats, len(h.Sites))
  293 	for i := 0; i < len(h.Sites); i++ {
  294 		stats[i] = h.Sites[i].PathSpec.ProcessingStats
  295 	}
  296 	helpers.ProcessingStatsTable(w, stats...)
  297 }
  298 
  299 // GetContentPage finds a Page with content given the absolute filename.
  300 // Returns nil if none found.
  301 func (h *HugoSites) GetContentPage(filename string) page.Page {
  302 	var p page.Page
  303 
  304 	h.getContentMaps().walkBundles(func(b *contentNode) bool {
  305 		if b.p == nil || b.fi == nil {
  306 			return false
  307 		}
  308 
  309 		if b.fi.Meta().Filename == filename {
  310 			p = b.p
  311 			return true
  312 		}
  313 
  314 		return false
  315 	})
  316 
  317 	return p
  318 }
  319 
  320 // NewHugoSites creates a new collection of sites given the input sites, building
  321 // a language configuration based on those.
  322 func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) {
  323 	if cfg.Language != nil {
  324 		return nil, errors.New("Cannot provide Language in Cfg when sites are provided")
  325 	}
  326 
  327 	// Return error at the end. Make the caller decide if it's fatal or not.
  328 	var initErr error
  329 
  330 	langConfig, err := newMultiLingualFromSites(cfg.Cfg, sites...)
  331 	if err != nil {
  332 		return nil, fmt.Errorf("failed to create language config: %w", err)
  333 	}
  334 
  335 	var contentChangeTracker *contentChangeMap
  336 
  337 	numWorkers := config.GetNumWorkerMultiplier()
  338 	if numWorkers > len(sites) {
  339 		numWorkers = len(sites)
  340 	}
  341 	var workers *para.Workers
  342 	if numWorkers > 1 {
  343 		workers = para.New(numWorkers)
  344 	}
  345 
  346 	h := &HugoSites{
  347 		running:                 cfg.Running,
  348 		multilingual:            langConfig,
  349 		multihost:               cfg.Cfg.GetBool("multihost"),
  350 		Sites:                   sites,
  351 		workers:                 workers,
  352 		numWorkers:              numWorkers,
  353 		skipRebuildForFilenames: make(map[string]bool),
  354 		init: &hugoSitesInit{
  355 			data:         lazy.New(),
  356 			layouts:      lazy.New(),
  357 			gitInfo:      lazy.New(),
  358 			translations: lazy.New(),
  359 		},
  360 	}
  361 
  362 	h.fatalErrorHandler = &fatalErrorHandler{
  363 		h:     h,
  364 		donec: make(chan bool),
  365 	}
  366 
  367 	h.init.data.Add(func() (any, error) {
  368 		err := h.loadData(h.PathSpec.BaseFs.Data.Dirs)
  369 		if err != nil {
  370 			return nil, fmt.Errorf("failed to load data: %w", err)
  371 		}
  372 		return nil, nil
  373 	})
  374 
  375 	h.init.layouts.Add(func() (any, error) {
  376 		for _, s := range h.Sites {
  377 			if err := s.Tmpl().(tpl.TemplateManager).MarkReady(); err != nil {
  378 				return nil, err
  379 			}
  380 		}
  381 		return nil, nil
  382 	})
  383 
  384 	h.init.translations.Add(func() (any, error) {
  385 		if len(h.Sites) > 1 {
  386 			allTranslations := pagesToTranslationsMap(h.Sites)
  387 			assignTranslationsToPages(allTranslations, h.Sites)
  388 		}
  389 
  390 		return nil, nil
  391 	})
  392 
  393 	h.init.gitInfo.Add(func() (any, error) {
  394 		err := h.loadGitInfo()
  395 		if err != nil {
  396 			return nil, fmt.Errorf("failed to load Git info: %w", err)
  397 		}
  398 		return nil, nil
  399 	})
  400 
  401 	for _, s := range sites {
  402 		s.h = h
  403 	}
  404 
  405 	var l configLoader
  406 	if err := l.applyDeps(cfg, sites...); err != nil {
  407 		initErr = fmt.Errorf("add site dependencies: %w", err)
  408 	}
  409 
  410 	h.Deps = sites[0].Deps
  411 	if h.Deps == nil {
  412 		return nil, initErr
  413 	}
  414 
  415 	// Only needed in server mode.
  416 	// TODO(bep) clean up the running vs watching terms
  417 	if cfg.Running {
  418 		contentChangeTracker = &contentChangeMap{
  419 			pathSpec:      h.PathSpec,
  420 			symContent:    make(map[string]map[string]bool),
  421 			leafBundles:   radix.New(),
  422 			branchBundles: make(map[string]bool),
  423 		}
  424 		h.ContentChanges = contentChangeTracker
  425 	}
  426 
  427 	return h, initErr
  428 }
  429 
  430 func (h *HugoSites) loadGitInfo() error {
  431 	if h.Cfg.GetBool("enableGitInfo") {
  432 		gi, err := newGitInfo(h.Cfg)
  433 		if err != nil {
  434 			h.Log.Errorln("Failed to read Git log:", err)
  435 		} else {
  436 			h.gitInfo = gi
  437 		}
  438 
  439 		co, err := newCodeOwners(h.Cfg)
  440 		if err != nil {
  441 			h.Log.Errorln("Failed to read CODEOWNERS:", err)
  442 		} else {
  443 			h.codeownerInfo = co
  444 		}
  445 	}
  446 	return nil
  447 }
  448 
  449 func (l configLoader) applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
  450 	if cfg.TemplateProvider == nil {
  451 		cfg.TemplateProvider = tplimpl.DefaultTemplateProvider
  452 	}
  453 
  454 	if cfg.TranslationProvider == nil {
  455 		cfg.TranslationProvider = i18n.NewTranslationProvider()
  456 	}
  457 
  458 	var (
  459 		d   *deps.Deps
  460 		err error
  461 	)
  462 
  463 	for _, s := range sites {
  464 		if s.Deps != nil {
  465 			continue
  466 		}
  467 
  468 		onCreated := func(d *deps.Deps) error {
  469 			s.Deps = d
  470 
  471 			// Set up the main publishing chain.
  472 			pub, err := publisher.NewDestinationPublisher(
  473 				d.ResourceSpec,
  474 				s.outputFormatsConfig,
  475 				s.mediaTypesConfig,
  476 			)
  477 			if err != nil {
  478 				return err
  479 			}
  480 			s.publisher = pub
  481 
  482 			if err := s.initializeSiteInfo(); err != nil {
  483 				return err
  484 			}
  485 
  486 			d.Site = s.Info
  487 
  488 			siteConfig, err := l.loadSiteConfig(s.language)
  489 			if err != nil {
  490 				return fmt.Errorf("load site config: %w", err)
  491 			}
  492 			s.siteConfigConfig = siteConfig
  493 
  494 			pm := &pageMap{
  495 				contentMap: newContentMap(contentMapConfig{
  496 					lang:                 s.Lang(),
  497 					taxonomyConfig:       s.siteCfg.taxonomiesConfig.Values(),
  498 					taxonomyDisabled:     !s.isEnabled(page.KindTerm),
  499 					taxonomyTermDisabled: !s.isEnabled(page.KindTaxonomy),
  500 					pageDisabled:         !s.isEnabled(page.KindPage),
  501 				}),
  502 				s: s,
  503 			}
  504 
  505 			s.PageCollections = newPageCollections(pm)
  506 
  507 			s.siteRefLinker, err = newSiteRefLinker(s.language, s)
  508 			return err
  509 		}
  510 
  511 		cfg.Language = s.language
  512 		cfg.MediaTypes = s.mediaTypesConfig
  513 		cfg.OutputFormats = s.outputFormatsConfig
  514 
  515 		if d == nil {
  516 			cfg.WithTemplate = s.withSiteTemplates(cfg.WithTemplate)
  517 
  518 			var err error
  519 			d, err = deps.New(cfg)
  520 			if err != nil {
  521 				return fmt.Errorf("create deps: %w", err)
  522 			}
  523 
  524 			d.OutputFormatsConfig = s.outputFormatsConfig
  525 
  526 			if err := onCreated(d); err != nil {
  527 				return fmt.Errorf("on created: %w", err)
  528 			}
  529 
  530 			if err = d.LoadResources(); err != nil {
  531 				return fmt.Errorf("load resources: %w", err)
  532 			}
  533 
  534 		} else {
  535 			d, err = d.ForLanguage(cfg, onCreated)
  536 			if err != nil {
  537 				return err
  538 			}
  539 			d.OutputFormatsConfig = s.outputFormatsConfig
  540 		}
  541 	}
  542 
  543 	return nil
  544 }
  545 
  546 // NewHugoSites creates HugoSites from the given config.
  547 func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
  548 	if cfg.Logger == nil {
  549 		cfg.Logger = loggers.NewErrorLogger()
  550 	}
  551 	sites, err := createSitesFromConfig(cfg)
  552 	if err != nil {
  553 		return nil, fmt.Errorf("from config: %w", err)
  554 	}
  555 	return newHugoSites(cfg, sites...)
  556 }
  557 
  558 func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateManager) error) func(templ tpl.TemplateManager) error {
  559 	return func(templ tpl.TemplateManager) error {
  560 		for _, wt := range withTemplates {
  561 			if wt == nil {
  562 				continue
  563 			}
  564 			if err := wt(templ); err != nil {
  565 				return err
  566 			}
  567 		}
  568 
  569 		return nil
  570 	}
  571 }
  572 
  573 func createSitesFromConfig(cfg deps.DepsCfg) ([]*Site, error) {
  574 	var sites []*Site
  575 
  576 	languages := getLanguages(cfg.Cfg)
  577 
  578 	for _, lang := range languages {
  579 		if lang.Disabled {
  580 			continue
  581 		}
  582 		var s *Site
  583 		var err error
  584 		cfg.Language = lang
  585 		s, err = newSite(cfg)
  586 
  587 		if err != nil {
  588 			return nil, err
  589 		}
  590 
  591 		sites = append(sites, s)
  592 	}
  593 
  594 	return sites, nil
  595 }
  596 
  597 // Reset resets the sites and template caches etc., making it ready for a full rebuild.
  598 func (h *HugoSites) reset(config *BuildCfg) {
  599 	if config.ResetState {
  600 		for i, s := range h.Sites {
  601 			h.Sites[i] = s.reset()
  602 			if r, ok := s.Fs.PublishDir.(hugofs.Reseter); ok {
  603 				r.Reset()
  604 			}
  605 		}
  606 	}
  607 
  608 	h.fatalErrorHandler = &fatalErrorHandler{
  609 		h:     h,
  610 		donec: make(chan bool),
  611 	}
  612 
  613 	h.init.Reset()
  614 }
  615 
  616 // resetLogs resets the log counters etc. Used to do a new build on the same sites.
  617 func (h *HugoSites) resetLogs() {
  618 	h.Log.Reset()
  619 	loggers.GlobalErrorCounter.Reset()
  620 	for _, s := range h.Sites {
  621 		s.Deps.Log.Reset()
  622 		s.Deps.LogDistinct.Reset()
  623 	}
  624 }
  625 
  626 func (h *HugoSites) withSite(fn func(s *Site) error) error {
  627 	if h.workers == nil {
  628 		for _, s := range h.Sites {
  629 			if err := fn(s); err != nil {
  630 				return err
  631 			}
  632 		}
  633 		return nil
  634 	}
  635 
  636 	g, _ := h.workers.Start(context.Background())
  637 	for _, s := range h.Sites {
  638 		s := s
  639 		g.Run(func() error {
  640 			return fn(s)
  641 		})
  642 	}
  643 	return g.Wait()
  644 }
  645 
  646 func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error {
  647 	oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages)
  648 
  649 	l := configLoader{cfg: h.Cfg}
  650 	if err := l.loadLanguageSettings(oldLangs); err != nil {
  651 		return err
  652 	}
  653 
  654 	depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: l.cfg}
  655 
  656 	sites, err := createSitesFromConfig(depsCfg)
  657 	if err != nil {
  658 		return err
  659 	}
  660 
  661 	langConfig, err := newMultiLingualFromSites(depsCfg.Cfg, sites...)
  662 	if err != nil {
  663 		return err
  664 	}
  665 
  666 	h.Sites = sites
  667 
  668 	for _, s := range sites {
  669 		s.h = h
  670 	}
  671 
  672 	var cl configLoader
  673 	if err := cl.applyDeps(depsCfg, sites...); err != nil {
  674 		return err
  675 	}
  676 
  677 	h.Deps = sites[0].Deps
  678 
  679 	h.multilingual = langConfig
  680 	h.multihost = h.Deps.Cfg.GetBool("multihost")
  681 
  682 	return nil
  683 }
  684 
  685 func (h *HugoSites) toSiteInfos() []*SiteInfo {
  686 	infos := make([]*SiteInfo, len(h.Sites))
  687 	for i, s := range h.Sites {
  688 		infos[i] = s.Info
  689 	}
  690 	return infos
  691 }
  692 
  693 // BuildCfg holds build options used to, as an example, skip the render step.
  694 type BuildCfg struct {
  695 	// Reset site state before build. Use to force full rebuilds.
  696 	ResetState bool
  697 	// If set, we re-create the sites from the given configuration before a build.
  698 	// This is needed if new languages are added.
  699 	NewConfig config.Provider
  700 	// Skip rendering. Useful for testing.
  701 	SkipRender bool
  702 	// Use this to indicate what changed (for rebuilds).
  703 	whatChanged *whatChanged
  704 
  705 	// This is a partial re-render of some selected pages. This means
  706 	// we should skip most of the processing.
  707 	PartialReRender bool
  708 
  709 	// Set in server mode when the last build failed for some reason.
  710 	ErrRecovery bool
  711 
  712 	// Recently visited URLs. This is used for partial re-rendering.
  713 	RecentlyVisited map[string]bool
  714 
  715 	// Can be set to build only with a sub set of the content source.
  716 	ContentInclusionFilter *glob.FilenameFilter
  717 
  718 	// Set when the buildlock is already acquired (e.g. the archetype content builder).
  719 	NoBuildLock bool
  720 
  721 	testCounters *testCounters
  722 }
  723 
  724 // shouldRender is used in the Fast Render Mode to determine if we need to re-render
  725 // a Page: If it is recently visited (the home pages will always be in this set) or changed.
  726 // Note that a page does not have to have a content page / file.
  727 // For regular builds, this will allways return true.
  728 // TODO(bep) rename/work this.
  729 func (cfg *BuildCfg) shouldRender(p *pageState) bool {
  730 	if p == nil {
  731 		return false
  732 	}
  733 
  734 	if p.forceRender {
  735 		return true
  736 	}
  737 
  738 	if len(cfg.RecentlyVisited) == 0 {
  739 		return true
  740 	}
  741 
  742 	if cfg.RecentlyVisited[p.RelPermalink()] {
  743 		return true
  744 	}
  745 
  746 	if cfg.whatChanged != nil && !p.File().IsZero() {
  747 		return cfg.whatChanged.files[p.File().Filename()]
  748 	}
  749 
  750 	return false
  751 }
  752 
  753 func (h *HugoSites) renderCrossSitesSitemap() error {
  754 	if !h.multilingual.enabled() || h.IsMultihost() {
  755 		return nil
  756 	}
  757 
  758 	sitemapEnabled := false
  759 	for _, s := range h.Sites {
  760 		if s.isEnabled(kindSitemap) {
  761 			sitemapEnabled = true
  762 			break
  763 		}
  764 	}
  765 
  766 	if !sitemapEnabled {
  767 		return nil
  768 	}
  769 
  770 	s := h.Sites[0]
  771 
  772 	templ := s.lookupLayouts("sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml")
  773 
  774 	return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemapindex",
  775 		s.siteCfg.sitemap.Filename, h.toSiteInfos(), templ)
  776 }
  777 
  778 func (h *HugoSites) renderCrossSitesRobotsTXT() error {
  779 	if h.multihost {
  780 		return nil
  781 	}
  782 	if !h.Cfg.GetBool("enableRobotsTXT") {
  783 		return nil
  784 	}
  785 
  786 	s := h.Sites[0]
  787 
  788 	p, err := newPageStandalone(&pageMeta{
  789 		s:    s,
  790 		kind: kindRobotsTXT,
  791 		urlPaths: pagemeta.URLPath{
  792 			URL: "robots.txt",
  793 		},
  794 	},
  795 		output.RobotsTxtFormat)
  796 	if err != nil {
  797 		return err
  798 	}
  799 
  800 	if !p.render {
  801 		return nil
  802 	}
  803 
  804 	templ := s.lookupLayouts("robots.txt", "_default/robots.txt", "_internal/_default/robots.txt")
  805 
  806 	return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", "robots.txt", p, templ)
  807 }
  808 
  809 func (h *HugoSites) removePageByFilename(filename string) {
  810 	h.getContentMaps().withMaps(func(m *pageMap) error {
  811 		m.deleteBundleMatching(func(b *contentNode) bool {
  812 			if b.p == nil {
  813 				return false
  814 			}
  815 
  816 			if b.fi == nil {
  817 				return false
  818 			}
  819 
  820 			return b.fi.Meta().Filename == filename
  821 		})
  822 		return nil
  823 	})
  824 }
  825 
  826 func (h *HugoSites) createPageCollections() error {
  827 	allPages := newLazyPagesFactory(func() page.Pages {
  828 		var pages page.Pages
  829 		for _, s := range h.Sites {
  830 			pages = append(pages, s.Pages()...)
  831 		}
  832 
  833 		page.SortByDefault(pages)
  834 
  835 		return pages
  836 	})
  837 
  838 	allRegularPages := newLazyPagesFactory(func() page.Pages {
  839 		return h.findPagesByKindIn(page.KindPage, allPages.get())
  840 	})
  841 
  842 	for _, s := range h.Sites {
  843 		s.PageCollections.allPages = allPages
  844 		s.PageCollections.allRegularPages = allRegularPages
  845 	}
  846 
  847 	return nil
  848 }
  849 
  850 func (s *Site) preparePagesForRender(isRenderingSite bool, idx int) error {
  851 	var err error
  852 	s.pageMap.withEveryBundlePage(func(p *pageState) bool {
  853 		if err = p.initOutputFormat(isRenderingSite, idx); err != nil {
  854 			return true
  855 		}
  856 		return false
  857 	})
  858 	return nil
  859 }
  860 
  861 // Pages returns all pages for all sites.
  862 func (h *HugoSites) Pages() page.Pages {
  863 	return h.Sites[0].AllPages()
  864 }
  865 
  866 func (h *HugoSites) loadData(fis []hugofs.FileMetaInfo) (err error) {
  867 	spec := source.NewSourceSpec(h.PathSpec, nil, nil)
  868 
  869 	h.data = make(map[string]any)
  870 	for _, fi := range fis {
  871 		fileSystem := spec.NewFilesystemFromFileMetaInfo(fi)
  872 		files, err := fileSystem.Files()
  873 		if err != nil {
  874 			return err
  875 		}
  876 		for _, r := range files {
  877 			if err := h.handleDataFile(r); err != nil {
  878 				return err
  879 			}
  880 		}
  881 	}
  882 
  883 	return
  884 }
  885 
  886 func (h *HugoSites) handleDataFile(r source.File) error {
  887 	var current map[string]any
  888 
  889 	f, err := r.FileInfo().Meta().Open()
  890 	if err != nil {
  891 		return fmt.Errorf("data: failed to open %q: %w", r.LogicalName(), err)
  892 	}
  893 	defer f.Close()
  894 
  895 	// Crawl in data tree to insert data
  896 	current = h.data
  897 	keyParts := strings.Split(r.Dir(), helpers.FilePathSeparator)
  898 
  899 	for _, key := range keyParts {
  900 		if key != "" {
  901 			if _, ok := current[key]; !ok {
  902 				current[key] = make(map[string]any)
  903 			}
  904 			current = current[key].(map[string]any)
  905 		}
  906 	}
  907 
  908 	data, err := h.readData(r)
  909 	if err != nil {
  910 		return h.errWithFileContext(err, r)
  911 	}
  912 
  913 	if data == nil {
  914 		return nil
  915 	}
  916 
  917 	// filepath.Walk walks the files in lexical order, '/' comes before '.'
  918 	higherPrecedentData := current[r.BaseFileName()]
  919 
  920 	switch data.(type) {
  921 	case nil:
  922 	case map[string]any:
  923 
  924 		switch higherPrecedentData.(type) {
  925 		case nil:
  926 			current[r.BaseFileName()] = data
  927 		case map[string]any:
  928 			// merge maps: insert entries from data for keys that
  929 			// don't already exist in higherPrecedentData
  930 			higherPrecedentMap := higherPrecedentData.(map[string]any)
  931 			for key, value := range data.(map[string]any) {
  932 				if _, exists := higherPrecedentMap[key]; exists {
  933 					// this warning could happen if
  934 					// 1. A theme uses the same key; the main data folder wins
  935 					// 2. A sub folder uses the same key: the sub folder wins
  936 					// TODO(bep) figure out a way to detect 2) above and make that a WARN
  937 					h.Log.Infof("Data for key '%s' in path '%s' is overridden by higher precedence data already in the data tree", key, r.Path())
  938 				} else {
  939 					higherPrecedentMap[key] = value
  940 				}
  941 			}
  942 		default:
  943 			// can't merge: higherPrecedentData is not a map
  944 			h.Log.Warnf("The %T data from '%s' overridden by "+
  945 				"higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
  946 		}
  947 
  948 	case []any:
  949 		if higherPrecedentData == nil {
  950 			current[r.BaseFileName()] = data
  951 		} else {
  952 			// we don't merge array data
  953 			h.Log.Warnf("The %T data from '%s' overridden by "+
  954 				"higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
  955 		}
  956 
  957 	default:
  958 		h.Log.Errorf("unexpected data type %T in file %s", data, r.LogicalName())
  959 	}
  960 
  961 	return nil
  962 }
  963 
  964 func (h *HugoSites) errWithFileContext(err error, f source.File) error {
  965 	fim, ok := f.FileInfo().(hugofs.FileMetaInfo)
  966 	if !ok {
  967 		return err
  968 	}
  969 	realFilename := fim.Meta().Filename
  970 
  971 	return herrors.NewFileErrorFromFile(err, realFilename, h.SourceSpec.Fs.Source, nil)
  972 
  973 }
  974 
  975 func (h *HugoSites) readData(f source.File) (any, error) {
  976 	file, err := f.FileInfo().Meta().Open()
  977 	if err != nil {
  978 		return nil, fmt.Errorf("readData: failed to open data file: %w", err)
  979 	}
  980 	defer file.Close()
  981 	content := helpers.ReaderToBytes(file)
  982 
  983 	format := metadecoders.FormatFromString(f.Ext())
  984 	return metadecoders.Default.Unmarshal(content, format)
  985 }
  986 
  987 func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
  988 	return h.Sites[0].findPagesByKindIn(kind, inPages)
  989 }
  990 
  991 func (h *HugoSites) resetPageState() {
  992 	h.getContentMaps().walkBundles(func(n *contentNode) bool {
  993 		if n.p == nil {
  994 			return false
  995 		}
  996 		p := n.p
  997 		for _, po := range p.pageOutputs {
  998 			if po.cp == nil {
  999 				continue
 1000 			}
 1001 			po.cp.Reset()
 1002 		}
 1003 
 1004 		return false
 1005 	})
 1006 }
 1007 
 1008 func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
 1009 	h.getContentMaps().walkBundles(func(n *contentNode) bool {
 1010 		if n.p == nil {
 1011 			return false
 1012 		}
 1013 		p := n.p
 1014 	OUTPUTS:
 1015 		for _, po := range p.pageOutputs {
 1016 			if po.cp == nil {
 1017 				continue
 1018 			}
 1019 			for id := range idset {
 1020 				if po.cp.dependencyTracker.Search(id) != nil {
 1021 					po.cp.Reset()
 1022 					continue OUTPUTS
 1023 				}
 1024 			}
 1025 		}
 1026 
 1027 		if p.shortcodeState == nil {
 1028 			return false
 1029 		}
 1030 
 1031 		for _, s := range p.shortcodeState.shortcodes {
 1032 			for _, templ := range s.templs {
 1033 				sid := templ.(identity.Manager)
 1034 				for id := range idset {
 1035 					if sid.Search(id) != nil {
 1036 						for _, po := range p.pageOutputs {
 1037 							if po.cp != nil {
 1038 								po.cp.Reset()
 1039 							}
 1040 						}
 1041 						return false
 1042 					}
 1043 				}
 1044 			}
 1045 		}
 1046 		return false
 1047 	})
 1048 }
 1049 
 1050 // Used in partial reloading to determine if the change is in a bundle.
 1051 type contentChangeMap struct {
 1052 	mu sync.RWMutex
 1053 
 1054 	// Holds directories with leaf bundles.
 1055 	leafBundles *radix.Tree
 1056 
 1057 	// Holds directories with branch bundles.
 1058 	branchBundles map[string]bool
 1059 
 1060 	pathSpec *helpers.PathSpec
 1061 
 1062 	// Hugo supports symlinked content (both directories and files). This
 1063 	// can lead to situations where the same file can be referenced from several
 1064 	// locations in /content -- which is really cool, but also means we have to
 1065 	// go an extra mile to handle changes.
 1066 	// This map is only used in watch mode.
 1067 	// It maps either file to files or the real dir to a set of content directories
 1068 	// where it is in use.
 1069 	symContentMu sync.Mutex
 1070 	symContent   map[string]map[string]bool
 1071 }
 1072 
 1073 func (m *contentChangeMap) add(dirname string, tp bundleDirType) {
 1074 	m.mu.Lock()
 1075 	if !strings.HasSuffix(dirname, helpers.FilePathSeparator) {
 1076 		dirname += helpers.FilePathSeparator
 1077 	}
 1078 	switch tp {
 1079 	case bundleBranch:
 1080 		m.branchBundles[dirname] = true
 1081 	case bundleLeaf:
 1082 		m.leafBundles.Insert(dirname, true)
 1083 	default:
 1084 		m.mu.Unlock()
 1085 		panic("invalid bundle type")
 1086 	}
 1087 	m.mu.Unlock()
 1088 }
 1089 
 1090 func (m *contentChangeMap) resolveAndRemove(filename string) (string, bundleDirType) {
 1091 	m.mu.RLock()
 1092 	defer m.mu.RUnlock()
 1093 
 1094 	// Bundles share resources, so we need to start from the virtual root.
 1095 	relFilename := m.pathSpec.RelContentDir(filename)
 1096 	dir, name := filepath.Split(relFilename)
 1097 	if !strings.HasSuffix(dir, helpers.FilePathSeparator) {
 1098 		dir += helpers.FilePathSeparator
 1099 	}
 1100 
 1101 	if _, found := m.branchBundles[dir]; found {
 1102 		delete(m.branchBundles, dir)
 1103 		return dir, bundleBranch
 1104 	}
 1105 
 1106 	if key, _, found := m.leafBundles.LongestPrefix(dir); found {
 1107 		m.leafBundles.Delete(key)
 1108 		dir = string(key)
 1109 		return dir, bundleLeaf
 1110 	}
 1111 
 1112 	fileTp, isContent := classifyBundledFile(name)
 1113 	if isContent && fileTp != bundleNot {
 1114 		// A new bundle.
 1115 		return dir, fileTp
 1116 	}
 1117 
 1118 	return dir, bundleNot
 1119 }
 1120 
 1121 func (m *contentChangeMap) addSymbolicLinkMapping(fim hugofs.FileMetaInfo) {
 1122 	meta := fim.Meta()
 1123 	if !meta.IsSymlink {
 1124 		return
 1125 	}
 1126 	m.symContentMu.Lock()
 1127 
 1128 	from, to := meta.Filename, meta.OriginalFilename
 1129 	if fim.IsDir() {
 1130 		if !strings.HasSuffix(from, helpers.FilePathSeparator) {
 1131 			from += helpers.FilePathSeparator
 1132 		}
 1133 	}
 1134 
 1135 	mm, found := m.symContent[from]
 1136 
 1137 	if !found {
 1138 		mm = make(map[string]bool)
 1139 		m.symContent[from] = mm
 1140 	}
 1141 	mm[to] = true
 1142 	m.symContentMu.Unlock()
 1143 }
 1144 
 1145 func (m *contentChangeMap) GetSymbolicLinkMappings(dir string) []string {
 1146 	mm, found := m.symContent[dir]
 1147 	if !found {
 1148 		return nil
 1149 	}
 1150 	dirs := make([]string, len(mm))
 1151 	i := 0
 1152 	for dir := range mm {
 1153 		dirs[i] = dir
 1154 		i++
 1155 	}
 1156 
 1157 	sort.Strings(dirs)
 1158 
 1159 	return dirs
 1160 }