github.com/neohugo/neohugo@v0.123.8/hugolib/site.go (about)

     1  // Copyright 2024 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  	"mime"
    21  	"net/url"
    22  	"path/filepath"
    23  	"runtime"
    24  	"sort"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/bep/logg"
    30  	"github.com/neohugo/neohugo/common/htime"
    31  	"github.com/neohugo/neohugo/common/hugio"
    32  	"github.com/neohugo/neohugo/common/types"
    33  	"github.com/neohugo/neohugo/hugolib/doctree"
    34  	"golang.org/x/text/unicode/norm"
    35  
    36  	"github.com/neohugo/neohugo/common/paths"
    37  
    38  	"github.com/neohugo/neohugo/identity"
    39  
    40  	"github.com/neohugo/neohugo/markup/converter/hooks"
    41  
    42  	"github.com/neohugo/neohugo/markup/converter"
    43  
    44  	"github.com/neohugo/neohugo/common/text"
    45  
    46  	"github.com/neohugo/neohugo/publisher"
    47  
    48  	"github.com/neohugo/neohugo/langs"
    49  
    50  	"github.com/neohugo/neohugo/resources/kinds"
    51  	"github.com/neohugo/neohugo/resources/page"
    52  
    53  	"github.com/neohugo/neohugo/lazy"
    54  
    55  	"github.com/fsnotify/fsnotify"
    56  	bp "github.com/neohugo/neohugo/bufferpool"
    57  	"github.com/neohugo/neohugo/helpers"
    58  	"github.com/neohugo/neohugo/navigation"
    59  	"github.com/neohugo/neohugo/output"
    60  	"github.com/neohugo/neohugo/tpl"
    61  )
    62  
    63  func (s *Site) Taxonomies() page.TaxonomyList {
    64  	s.init.taxonomies.Do(context.Background()) // nolint
    65  	return s.taxonomies
    66  }
    67  
    68  type (
    69  	taxonomiesConfig       map[string]string
    70  	taxonomiesConfigValues struct {
    71  		views          []viewName
    72  		viewsByTreeKey map[string]viewName
    73  	}
    74  )
    75  
    76  func (t taxonomiesConfig) Values() taxonomiesConfigValues {
    77  	var views []viewName
    78  	for k, v := range t {
    79  		views = append(views, viewName{singular: k, plural: v, pluralTreeKey: cleanTreeKey(v)})
    80  	}
    81  	sort.Slice(views, func(i, j int) bool {
    82  		return views[i].plural < views[j].plural
    83  	})
    84  
    85  	viewsByTreeKey := make(map[string]viewName)
    86  	for _, v := range views {
    87  		viewsByTreeKey[v.pluralTreeKey] = v
    88  	}
    89  
    90  	return taxonomiesConfigValues{
    91  		views:          views,
    92  		viewsByTreeKey: viewsByTreeKey,
    93  	}
    94  }
    95  
    96  // Lazily loaded site dependencies.
    97  type siteInit struct {
    98  	prevNext          *lazy.Init
    99  	prevNextInSection *lazy.Init
   100  	menus             *lazy.Init
   101  	taxonomies        *lazy.Init
   102  }
   103  
   104  func (init *siteInit) Reset() {
   105  	init.prevNext.Reset()
   106  	init.prevNextInSection.Reset()
   107  	init.menus.Reset()
   108  	init.taxonomies.Reset()
   109  }
   110  
   111  func (s *Site) prepareInits() {
   112  	s.init = &siteInit{}
   113  
   114  	var init lazy.Init
   115  
   116  	s.init.prevNext = init.Branch(func(context.Context) (any, error) {
   117  		regularPages := s.RegularPages()
   118  		for i, p := range regularPages {
   119  			np, ok := p.(nextPrevProvider)
   120  			if !ok {
   121  				continue
   122  			}
   123  
   124  			pos := np.getNextPrev()
   125  			if pos == nil {
   126  				continue
   127  			}
   128  
   129  			pos.nextPage = nil
   130  			pos.prevPage = nil
   131  
   132  			if i > 0 {
   133  				pos.nextPage = regularPages[i-1]
   134  			}
   135  
   136  			if i < len(regularPages)-1 {
   137  				pos.prevPage = regularPages[i+1]
   138  			}
   139  		}
   140  		return nil, nil
   141  	})
   142  
   143  	s.init.prevNextInSection = init.Branch(func(context.Context) (any, error) {
   144  		setNextPrev := func(pas page.Pages) {
   145  			for i, p := range pas {
   146  				np, ok := p.(nextPrevInSectionProvider)
   147  				if !ok {
   148  					continue
   149  				}
   150  
   151  				pos := np.getNextPrevInSection()
   152  				if pos == nil {
   153  					continue
   154  				}
   155  
   156  				pos.nextPage = nil
   157  				pos.prevPage = nil
   158  
   159  				if i > 0 {
   160  					pos.nextPage = pas[i-1]
   161  				}
   162  
   163  				if i < len(pas)-1 {
   164  					pos.prevPage = pas[i+1]
   165  				}
   166  			}
   167  		}
   168  
   169  		sections := s.pageMap.getPagesInSection(
   170  			pageMapQueryPagesInSection{
   171  				pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
   172  					Path:    "",
   173  					KeyPart: "sectionorhome",
   174  					Include: pagePredicates.KindSection.Or(pagePredicates.KindHome),
   175  				},
   176  				IncludeSelf: true,
   177  				Recursive:   true,
   178  			},
   179  		)
   180  
   181  		for _, section := range sections {
   182  			setNextPrev(section.RegularPages())
   183  		}
   184  
   185  		return nil, nil
   186  	})
   187  
   188  	s.init.menus = init.Branch(func(context.Context) (any, error) {
   189  		err := s.assembleMenus()
   190  		return nil, err
   191  	})
   192  
   193  	s.init.taxonomies = init.Branch(func(ctx context.Context) (any, error) {
   194  		if err := s.pageMap.CreateSiteTaxonomies(ctx); err != nil {
   195  			return nil, err
   196  		}
   197  		return s.taxonomies, nil
   198  	})
   199  }
   200  
   201  type siteRenderingContext struct {
   202  	output.Format
   203  }
   204  
   205  func (s *Site) Menus() navigation.Menus {
   206  	//nolint
   207  	s.init.menus.Do(context.Background())
   208  	return s.menus
   209  }
   210  
   211  func (s *Site) initRenderFormats() {
   212  	formatSet := make(map[string]bool)
   213  	formats := output.Formats{}
   214  
   215  	w := &doctree.NodeShiftTreeWalker[contentNodeI]{
   216  		Tree: s.pageMap.treePages,
   217  		Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
   218  			if p, ok := n.(*pageState); ok {
   219  				for _, f := range p.m.configuredOutputFormats {
   220  					if !formatSet[f.Name] {
   221  						formats = append(formats, f)
   222  						formatSet[f.Name] = true
   223  					}
   224  				}
   225  			}
   226  			return false, nil
   227  		},
   228  	}
   229  
   230  	if err := w.Walk(context.TODO()); err != nil {
   231  		panic(err)
   232  	}
   233  
   234  	// Add the per kind configured output formats
   235  	for _, kind := range kinds.AllKindsInPages {
   236  		if siteFormats, found := s.conf.C.KindOutputFormats[kind]; found {
   237  			for _, f := range siteFormats {
   238  				if !formatSet[f.Name] {
   239  					formats = append(formats, f)
   240  					formatSet[f.Name] = true
   241  				}
   242  			}
   243  		}
   244  	}
   245  
   246  	sort.Sort(formats)
   247  	s.renderFormats = formats
   248  }
   249  
   250  func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
   251  	return s.relatedDocsHandler
   252  }
   253  
   254  func (s *Site) Language() *langs.Language {
   255  	return s.language
   256  }
   257  
   258  func (s *Site) Languages() langs.Languages {
   259  	return s.h.Configs.Languages
   260  }
   261  
   262  type siteRefLinker struct {
   263  	s *Site
   264  
   265  	errorLogger logg.LevelLogger
   266  	notFoundURL string
   267  }
   268  
   269  func newSiteRefLinker(s *Site) (siteRefLinker, error) {
   270  	logger := s.Log.Error()
   271  
   272  	notFoundURL := s.conf.RefLinksNotFoundURL
   273  	errLevel := s.conf.RefLinksErrorLevel
   274  	if strings.EqualFold(errLevel, "warning") {
   275  		logger = s.Log.Warn()
   276  	}
   277  	return siteRefLinker{s: s, errorLogger: logger, notFoundURL: notFoundURL}, nil
   278  }
   279  
   280  func (s siteRefLinker) logNotFound(ref, what string, p page.Page, position text.Position) {
   281  	if position.IsValid() {
   282  		s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q: %s: %s", s.s.Lang(), ref, position.String(), what)
   283  	} else if p == nil {
   284  		s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q: %s", s.s.Lang(), ref, what)
   285  	} else {
   286  		s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q from page %q: %s", s.s.Lang(), ref, p.Path(), what)
   287  	}
   288  }
   289  
   290  func (s *siteRefLinker) refLink(ref string, source any, relative bool, outputFormat string) (string, error) {
   291  	p, err := unwrapPage(source)
   292  	if err != nil {
   293  		return "", err
   294  	}
   295  
   296  	var refURL *url.URL
   297  
   298  	ref = filepath.ToSlash(ref)
   299  
   300  	refURL, err = url.Parse(ref)
   301  	if err != nil {
   302  		return s.notFoundURL, err
   303  	}
   304  
   305  	var target page.Page
   306  	var link string
   307  
   308  	if refURL.Path != "" {
   309  		var err error
   310  		target, err = s.s.getPageRef(p, refURL.Path)
   311  		var pos text.Position
   312  		if err != nil || target == nil {
   313  			if p, ok := source.(text.Positioner); ok {
   314  				pos = p.Position()
   315  			}
   316  		}
   317  
   318  		if err != nil {
   319  			s.logNotFound(refURL.Path, err.Error(), p, pos)
   320  			return s.notFoundURL, nil
   321  		}
   322  
   323  		if target == nil {
   324  			s.logNotFound(refURL.Path, "page not found", p, pos)
   325  			return s.notFoundURL, nil
   326  		}
   327  
   328  		var permalinker Permalinker = target
   329  
   330  		if outputFormat != "" {
   331  			o := target.OutputFormats().Get(outputFormat)
   332  
   333  			if o == nil {
   334  				s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), p, pos)
   335  				return s.notFoundURL, nil
   336  			}
   337  			permalinker = o
   338  		}
   339  
   340  		if relative {
   341  			link = permalinker.RelPermalink()
   342  		} else {
   343  			link = permalinker.Permalink()
   344  		}
   345  	}
   346  
   347  	if refURL.Fragment != "" {
   348  		_ = target
   349  		link = link + "#" + refURL.Fragment
   350  
   351  		if pctx, ok := target.(pageContext); ok {
   352  			if refURL.Path != "" {
   353  				if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok {
   354  					link = link + di.AnchorSuffix()
   355  				}
   356  			}
   357  		} else if pctx, ok := p.(pageContext); ok {
   358  			if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok {
   359  				link = link + di.AnchorSuffix()
   360  			}
   361  		}
   362  
   363  	}
   364  
   365  	return link, nil
   366  }
   367  
   368  func (s *Site) watching() bool {
   369  	return s.h != nil && s.h.Configs.Base.Internal.Watch
   370  }
   371  
   372  type whatChanged struct {
   373  	mu sync.Mutex
   374  
   375  	contentChanged bool
   376  	identitySet    identity.Identities
   377  }
   378  
   379  func (w *whatChanged) Add(ids ...identity.Identity) {
   380  	w.mu.Lock()
   381  	defer w.mu.Unlock()
   382  
   383  	for _, id := range ids {
   384  		w.identitySet[id] = true
   385  	}
   386  }
   387  
   388  func (w *whatChanged) Changes() []identity.Identity {
   389  	if w == nil || w.identitySet == nil {
   390  		return nil
   391  	}
   392  	return w.identitySet.AsSlice()
   393  }
   394  
   395  // RegisterMediaTypes will register the Site's media types in the mime
   396  // package, so it will behave correctly with Hugo's built-in server.
   397  func (s *Site) RegisterMediaTypes() {
   398  	for _, mt := range s.conf.MediaTypes.Config {
   399  		for _, suffix := range mt.Suffixes() {
   400  			_ = mime.AddExtensionType(mt.Delimiter+suffix, mt.Type)
   401  		}
   402  	}
   403  }
   404  
   405  func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event {
   406  	seen := make(map[fsnotify.Event]bool)
   407  
   408  	n := 0
   409  	for _, ev := range events {
   410  		// Avoid processing the same event twice.
   411  		if seen[ev] {
   412  			continue
   413  		}
   414  		seen[ev] = true
   415  
   416  		if h.SourceSpec.IgnoreFile(ev.Name) {
   417  			continue
   418  		}
   419  
   420  		if runtime.GOOS == "darwin" { // When a file system is HFS+, its filepath is in NFD form.
   421  			ev.Name = norm.NFC.String(ev.Name)
   422  		}
   423  
   424  		events[n] = ev
   425  		n++
   426  	}
   427  	return events[:n]
   428  }
   429  
   430  func (h *HugoSites) fileEventsTranslate(events []fsnotify.Event) []fsnotify.Event {
   431  	eventMap := make(map[string][]fsnotify.Event)
   432  
   433  	// We often get a Remove etc. followed by a Create, a Create followed by a Write.
   434  	// Remove the superfluous events to make the update logic simpler.
   435  	for _, ev := range events {
   436  		eventMap[ev.Name] = append(eventMap[ev.Name], ev)
   437  	}
   438  
   439  	n := 0
   440  	for _, ev := range events {
   441  		mapped := eventMap[ev.Name]
   442  
   443  		// Keep one
   444  		found := false
   445  		var kept fsnotify.Event
   446  		for i, ev2 := range mapped {
   447  			if i == 0 {
   448  				kept = ev2
   449  			}
   450  
   451  			if ev2.Op&fsnotify.Write == fsnotify.Write {
   452  				kept = ev2
   453  				found = true
   454  			}
   455  
   456  			if !found && ev2.Op&fsnotify.Create == fsnotify.Create {
   457  				kept = ev2
   458  			}
   459  		}
   460  
   461  		events[n] = kept
   462  		n++
   463  	}
   464  
   465  	return events
   466  }
   467  
   468  func (h *HugoSites) fileEventsContentPaths(p []pathChange) []pathChange {
   469  	var bundles []pathChange
   470  	var dirs []pathChange
   471  	var regular []pathChange
   472  
   473  	var others []pathChange
   474  	for _, p := range p {
   475  		if p.isDir {
   476  			dirs = append(dirs, p)
   477  		} else {
   478  			others = append(others, p)
   479  		}
   480  	}
   481  
   482  	// Remove all files below dir.
   483  	if len(dirs) > 0 {
   484  		n := 0
   485  		for _, d := range dirs {
   486  			dir := d.p.Path() + "/"
   487  			for _, o := range others {
   488  				if !strings.HasPrefix(o.p.Path(), dir) {
   489  					others[n] = o
   490  					n++
   491  				}
   492  			}
   493  
   494  		}
   495  		others = others[:n]
   496  	}
   497  
   498  	for _, p := range others {
   499  		if p.p.IsBundle() {
   500  			bundles = append(bundles, p)
   501  		} else {
   502  			regular = append(regular, p)
   503  		}
   504  	}
   505  
   506  	// Remove any files below leaf bundles.
   507  	// Remove any files in the same folder as branch bundles.
   508  	var keepers []pathChange
   509  
   510  	for _, o := range regular {
   511  		keep := true
   512  		for _, b := range bundles {
   513  			prefix := b.p.Base() + "/"
   514  			if b.p.IsLeafBundle() && strings.HasPrefix(o.p.Path(), prefix) {
   515  				keep = false
   516  				break
   517  			} else if b.p.IsBranchBundle() && o.p.Dir() == b.p.Dir() {
   518  				keep = false
   519  				break
   520  			}
   521  		}
   522  
   523  		if keep {
   524  			keepers = append(keepers, o)
   525  		}
   526  	}
   527  
   528  	keepers = append(dirs, keepers...)
   529  	keepers = append(bundles, keepers...)
   530  
   531  	return keepers
   532  }
   533  
   534  // HomeAbsURL is a convenience method giving the absolute URL to the home page.
   535  func (s *Site) HomeAbsURL() string {
   536  	base := ""
   537  	if len(s.conf.Languages) > 1 {
   538  		base = s.Language().Lang
   539  	}
   540  	return s.AbsURL(base, false)
   541  }
   542  
   543  // SitemapAbsURL is a convenience method giving the absolute URL to the sitemap.
   544  func (s *Site) SitemapAbsURL() string {
   545  	p := s.HomeAbsURL()
   546  	if !strings.HasSuffix(p, "/") {
   547  		p += "/"
   548  	}
   549  	p += s.conf.Sitemap.Filename
   550  	return p
   551  }
   552  
   553  func (s *Site) createNodeMenuEntryURL(in string) string {
   554  	if !strings.HasPrefix(in, "/") {
   555  		return in
   556  	}
   557  	// make it match the nodes
   558  	menuEntryURL := in
   559  	menuEntryURL = s.s.PathSpec.URLize(menuEntryURL)
   560  	if !s.conf.CanonifyURLs {
   561  		menuEntryURL = paths.AddContextRoot(s.s.PathSpec.Cfg.BaseURL().String(), menuEntryURL)
   562  	}
   563  	return menuEntryURL
   564  }
   565  
   566  func (s *Site) assembleMenus() error {
   567  	s.menus = make(navigation.Menus)
   568  
   569  	type twoD struct {
   570  		MenuName, EntryName string
   571  	}
   572  	flat := map[twoD]*navigation.MenuEntry{}
   573  	children := map[twoD]navigation.Menu{}
   574  
   575  	// add menu entries from config to flat hash
   576  	for name, menu := range s.conf.Menus.Config {
   577  		for _, me := range menu {
   578  			if types.IsNil(me.Page) && me.PageRef != "" {
   579  				// Try to resolve the page.
   580  				me.Page, _ = s.getPage(nil, me.PageRef)
   581  			}
   582  
   583  			// If page is still nill, we must make sure that we have a URL that considers baseURL etc.
   584  			if types.IsNil(me.Page) {
   585  				me.ConfiguredURL = s.createNodeMenuEntryURL(me.MenuConfig.URL)
   586  			}
   587  
   588  			flat[twoD{name, me.KeyName()}] = me
   589  		}
   590  	}
   591  
   592  	sectionPagesMenu := s.conf.SectionPagesMenu
   593  
   594  	if sectionPagesMenu != "" {
   595  		if err := s.pageMap.forEachPage(pagePredicates.ShouldListGlobal, func(p *pageState) (bool, error) {
   596  			if p.IsHome() || !p.m.shouldBeCheckedForMenuDefinitions() {
   597  				return false, nil
   598  			}
   599  			// The section pages menus are attached to the top level section.
   600  			id := p.Section()
   601  			if _, ok := flat[twoD{sectionPagesMenu, id}]; ok {
   602  				return false, nil
   603  			}
   604  			me := navigation.MenuEntry{
   605  				MenuConfig: navigation.MenuConfig{
   606  					Identifier: id,
   607  					Name:       p.LinkTitle(),
   608  					Weight:     p.Weight(),
   609  				},
   610  				Page: p,
   611  			}
   612  			navigation.SetPageValues(&me, p)
   613  			flat[twoD{sectionPagesMenu, me.KeyName()}] = &me
   614  			return false, nil
   615  		}); err != nil {
   616  			return err
   617  		}
   618  	}
   619  	// Add menu entries provided by pages
   620  	if err := s.pageMap.forEachPage(pagePredicates.ShouldListGlobal, func(p *pageState) (bool, error) {
   621  		for name, me := range p.pageMenus.menus() {
   622  			if _, ok := flat[twoD{name, me.KeyName()}]; ok {
   623  				err := p.wrapError(fmt.Errorf("duplicate menu entry with identifier %q in menu %q", me.KeyName(), name))
   624  				s.Log.Warnln(err)
   625  				continue
   626  			}
   627  			flat[twoD{name, me.KeyName()}] = me
   628  		}
   629  		return false, nil
   630  	}); err != nil {
   631  		return err
   632  	}
   633  
   634  	// Create Children Menus First
   635  	for _, e := range flat {
   636  		if e.Parent != "" {
   637  			children[twoD{e.Menu, e.Parent}] = children[twoD{e.Menu, e.Parent}].Add(e)
   638  		}
   639  	}
   640  
   641  	// Placing Children in Parents (in flat)
   642  	for p, childmenu := range children {
   643  		_, ok := flat[twoD{p.MenuName, p.EntryName}]
   644  		if !ok {
   645  			// if parent does not exist, create one without a URL
   646  			flat[twoD{p.MenuName, p.EntryName}] = &navigation.MenuEntry{
   647  				MenuConfig: navigation.MenuConfig{
   648  					Name: p.EntryName,
   649  				},
   650  			}
   651  		}
   652  		flat[twoD{p.MenuName, p.EntryName}].Children = childmenu
   653  	}
   654  
   655  	// Assembling Top Level of Tree
   656  	for menu, e := range flat {
   657  		if e.Parent == "" {
   658  			_, ok := s.menus[menu.MenuName]
   659  			if !ok {
   660  				s.menus[menu.MenuName] = navigation.Menu{}
   661  			}
   662  			s.menus[menu.MenuName] = s.menus[menu.MenuName].Add(e)
   663  		}
   664  	}
   665  
   666  	return nil
   667  }
   668  
   669  // get any language code to prefix the target file path with.
   670  func (s *Site) getLanguageTargetPathLang(alwaysInSubDir bool) string {
   671  	if s.h.Conf.IsMultihost() {
   672  		return s.Language().Lang
   673  	}
   674  
   675  	return s.getLanguagePermalinkLang(alwaysInSubDir)
   676  }
   677  
   678  // get any language code to prefix the relative permalink with.
   679  func (s *Site) getLanguagePermalinkLang(alwaysInSubDir bool) string {
   680  	if s.h.Conf.IsMultihost() {
   681  		return ""
   682  	}
   683  
   684  	if s.h.Conf.IsMultiLingual() && alwaysInSubDir {
   685  		return s.Language().Lang
   686  	}
   687  
   688  	return s.GetLanguagePrefix()
   689  }
   690  
   691  // Prepare site for a new full build.
   692  func (s *Site) resetBuildState(sourceChanged bool) {
   693  	s.relatedDocsHandler = s.relatedDocsHandler.Clone()
   694  	s.init.Reset()
   695  }
   696  
   697  func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
   698  	var errors []error
   699  	for e := range results {
   700  		errors = append(errors, e)
   701  	}
   702  
   703  	errs <- s.h.pickOneAndLogTheRest(errors)
   704  
   705  	close(errs)
   706  }
   707  
   708  // GetPage looks up a page of a given type for the given ref.
   709  // In Hugo <= 0.44 you had to add Page Kind (section, home) etc. as the first
   710  // argument and then either a unix styled path (with or without a leading slash))
   711  // or path elements separated.
   712  // When we now remove the Kind from this API, we need to make the transition as painless
   713  // as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }},
   714  // i.e. 2 arguments, so we test for that.
   715  func (s *Site) GetPage(ref ...string) (page.Page, error) {
   716  	p, err := s.s.getPageForRefs(ref...)
   717  
   718  	if p == nil {
   719  		// The nil struct has meaning in some situations, mostly to avoid breaking
   720  		// existing sites doing $nilpage.IsDescendant($p), which will always return
   721  		// false.
   722  		p = page.NilPage
   723  	}
   724  
   725  	return p, err
   726  }
   727  
   728  func (s *Site) absURLPath(targetPath string) string {
   729  	var path string
   730  	if s.conf.RelativeURLs {
   731  		path = helpers.GetDottedRelativePath(targetPath)
   732  	} else {
   733  		url := s.PathSpec.Cfg.BaseURL().String()
   734  		if !strings.HasSuffix(url, "/") {
   735  			url += "/"
   736  		}
   737  		path = url
   738  	}
   739  
   740  	return path
   741  }
   742  
   743  const (
   744  	pageDependencyScopeDefault int = iota
   745  	pageDependencyScopeGlobal
   746  )
   747  
   748  func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ tpl.Template) error {
   749  	s.h.buildCounters.pageRenderCounter.Add(1)
   750  	renderBuffer := bp.GetBuffer()
   751  	defer bp.PutBuffer(renderBuffer)
   752  
   753  	of := p.outputFormat()
   754  	p.incrRenderState()
   755  
   756  	ctx := tpl.Context.Page.Set(context.Background(), p)
   757  	ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
   758  
   759  	if err := s.renderForTemplate(ctx, p.Kind(), of.Name, d, renderBuffer, templ); err != nil {
   760  		return err
   761  	}
   762  
   763  	if renderBuffer.Len() == 0 {
   764  		return nil
   765  	}
   766  
   767  	isHTML := of.IsHTML
   768  	isRSS := of.Name == "rss"
   769  
   770  	pd := publisher.Descriptor{
   771  		Src:          renderBuffer,
   772  		TargetPath:   targetPath,
   773  		StatCounter:  statCounter,
   774  		OutputFormat: p.outputFormat(),
   775  	}
   776  
   777  	if isRSS {
   778  		// Always canonify URLs in RSS
   779  		pd.AbsURLPath = s.absURLPath(targetPath)
   780  	} else if isHTML {
   781  		if s.conf.RelativeURLs || s.conf.CanonifyURLs {
   782  			pd.AbsURLPath = s.absURLPath(targetPath)
   783  		}
   784  
   785  		if s.watching() && s.conf.Internal.Running && !s.conf.DisableLiveReload {
   786  			pd.LiveReloadBaseURL = s.Conf.BaseURLLiveReload().URL()
   787  		}
   788  
   789  		// For performance reasons we only inject the Hugo generator tag on the home page.
   790  		if p.IsHome() {
   791  			pd.AddHugoGeneratorTag = s.conf.DisableHugoGeneratorInject
   792  		}
   793  
   794  	}
   795  
   796  	return s.publisher.Publish(pd)
   797  }
   798  
   799  var infoOnMissingLayout = map[string]bool{
   800  	// The 404 layout is very much optional in Hugo, but we do look for it.
   801  	"404": true,
   802  }
   803  
   804  // hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
   805  // where ITEM is the thing being hooked.
   806  type hookRendererTemplate struct {
   807  	templateHandler tpl.TemplateHandler
   808  	templ           tpl.Template
   809  	resolvePosition func(ctx any) text.Position
   810  }
   811  
   812  func (hr hookRendererTemplate) RenderLink(cctx context.Context, w io.Writer, ctx hooks.LinkContext) error {
   813  	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
   814  }
   815  
   816  func (hr hookRendererTemplate) RenderHeading(cctx context.Context, w io.Writer, ctx hooks.HeadingContext) error {
   817  	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
   818  }
   819  
   820  func (hr hookRendererTemplate) RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
   821  	return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
   822  }
   823  
   824  func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
   825  	return hr.resolvePosition(ctx)
   826  }
   827  
   828  func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
   829  	return false
   830  }
   831  
   832  func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
   833  	if templ == nil {
   834  		s.logMissingLayout(name, "", "", outputFormat)
   835  		return nil
   836  	}
   837  
   838  	if ctx == nil {
   839  		panic("nil context")
   840  	}
   841  
   842  	if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil {
   843  		return fmt.Errorf("render of %q failed: %w", name, err)
   844  	}
   845  	return
   846  }
   847  
   848  func (s *Site) shouldBuild(p page.Page) bool {
   849  	if !s.conf.IsKindEnabled(p.Kind()) {
   850  		return false
   851  	}
   852  	return shouldBuild(s.Conf.BuildFuture(), s.Conf.BuildExpired(),
   853  		s.Conf.BuildDrafts(), p.Draft(), p.PublishDate(), p.ExpiryDate())
   854  }
   855  
   856  func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bool,
   857  	publishDate time.Time, expiryDate time.Time,
   858  ) bool {
   859  	if !(buildDrafts || !Draft) {
   860  		return false
   861  	}
   862  	hnow := htime.Now()
   863  	if !buildFuture && !publishDate.IsZero() && publishDate.After(hnow) {
   864  		return false
   865  	}
   866  	if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(hnow) {
   867  		return false
   868  	}
   869  	return true
   870  }
   871  
   872  func (s *Site) render(ctx *siteRenderContext) (err error) {
   873  	if err := page.Clear(); err != nil {
   874  		return err
   875  	}
   876  
   877  	if ctx.outIdx == 0 {
   878  		// Note that even if disableAliases is set, the aliases themselves are
   879  		// preserved on page. The motivation with this is to be able to generate
   880  		// 301 redirects in a .htaccess file and similar using a custom output format.
   881  		if !s.conf.DisableAliases {
   882  			// Aliases must be rendered before pages.
   883  			// Some sites, Hugo docs included, have faulty alias definitions that point
   884  			// to itself or another real page. These will be overwritten in the next
   885  			// step.
   886  			if err = s.renderAliases(); err != nil {
   887  				return
   888  			}
   889  		}
   890  	}
   891  
   892  	if err = s.renderPages(ctx); err != nil {
   893  		return
   894  	}
   895  
   896  	if !ctx.shouldRenderStandalonePage("") {
   897  		return
   898  	}
   899  
   900  	if err = s.renderMainLanguageRedirect(); err != nil {
   901  		return
   902  	}
   903  
   904  	return
   905  }