github.com/gohugoio/hugo@v0.88.1/hugolib/hugo_sites.go (about)

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