site.go (49214B)
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 "fmt"
18 "html/template"
19 "io"
20 "log"
21 "mime"
22 "net/url"
23 "os"
24 "path"
25 "path/filepath"
26 "regexp"
27 "runtime"
28 "sort"
29 "strconv"
30 "strings"
31 "time"
32
33 "github.com/gohugoio/hugo/common/htime"
34 "github.com/gohugoio/hugo/common/hugio"
35 "github.com/gohugoio/hugo/common/types"
36 "github.com/gohugoio/hugo/modules"
37 "golang.org/x/text/unicode/norm"
38
39 "github.com/gohugoio/hugo/common/paths"
40
41 "github.com/gohugoio/hugo/common/constants"
42
43 "github.com/gohugoio/hugo/common/loggers"
44
45 "github.com/gohugoio/hugo/resources"
46
47 "github.com/gohugoio/hugo/identity"
48
49 "github.com/gohugoio/hugo/markup/converter/hooks"
50
51 "github.com/gohugoio/hugo/resources/resource"
52
53 "github.com/gohugoio/hugo/markup/converter"
54
55 "github.com/gohugoio/hugo/hugofs/files"
56
57 "github.com/gohugoio/hugo/common/maps"
58
59 "github.com/gohugoio/hugo/common/text"
60
61 "github.com/gohugoio/hugo/common/hugo"
62 "github.com/gohugoio/hugo/publisher"
63
64 "github.com/gohugoio/hugo/langs"
65
66 "github.com/gohugoio/hugo/resources/page"
67
68 "github.com/gohugoio/hugo/config"
69 "github.com/gohugoio/hugo/lazy"
70
71 "github.com/gohugoio/hugo/media"
72
73 "github.com/fsnotify/fsnotify"
74 bp "github.com/gohugoio/hugo/bufferpool"
75 "github.com/gohugoio/hugo/deps"
76 "github.com/gohugoio/hugo/helpers"
77 "github.com/gohugoio/hugo/navigation"
78 "github.com/gohugoio/hugo/output"
79 "github.com/gohugoio/hugo/related"
80 "github.com/gohugoio/hugo/resources/page/pagemeta"
81 "github.com/gohugoio/hugo/source"
82 "github.com/gohugoio/hugo/tpl"
83
84 "github.com/spf13/afero"
85 "github.com/spf13/cast"
86 )
87
88 // Site contains all the information relevant for constructing a static
89 // site. The basic flow of information is as follows:
90 //
91 // 1. A list of Files is parsed and then converted into Pages.
92 //
93 // 2. Pages contain sections (based on the file they were generated from),
94 // aliases and slugs (included in a pages frontmatter) which are the
95 // various targets that will get generated. There will be canonical
96 // listing. The canonical path can be overruled based on a pattern.
97 //
98 // 3. Taxonomies are created via configuration and will present some aspect of
99 // the final page and typically a perm url.
100 //
101 // 4. All Pages are passed through a template based on their desired
102 // layout based on numerous different elements.
103 //
104 // 5. The entire collection of files is written to disk.
105 type Site struct {
106
107 // The owning container. When multiple languages, there will be multiple
108 // sites .
109 h *HugoSites
110
111 *PageCollections
112
113 taxonomies TaxonomyList
114
115 Sections Taxonomy
116 Info *SiteInfo
117
118 language *langs.Language
119 siteBucket *pagesMapBucket
120
121 siteCfg siteConfigHolder
122
123 disabledKinds map[string]bool
124
125 // Output formats defined in site config per Page Kind, or some defaults
126 // if not set.
127 // Output formats defined in Page front matter will override these.
128 outputFormats map[string]output.Formats
129
130 // All the output formats and media types available for this site.
131 // These values will be merged from the Hugo defaults, the site config and,
132 // finally, the language settings.
133 outputFormatsConfig output.Formats
134 mediaTypesConfig media.Types
135
136 siteConfigConfig SiteConfig
137
138 // How to handle page front matter.
139 frontmatterHandler pagemeta.FrontMatterHandler
140
141 // We render each site for all the relevant output formats in serial with
142 // this rendering context pointing to the current one.
143 rc *siteRenderingContext
144
145 // The output formats that we need to render this site in. This slice
146 // will be fixed once set.
147 // This will be the union of Site.Pages' outputFormats.
148 // This slice will be sorted.
149 renderFormats output.Formats
150
151 // Logger etc.
152 *deps.Deps `json:"-"`
153
154 // The func used to title case titles.
155 titleFunc func(s string) string
156
157 relatedDocsHandler *page.RelatedDocsHandler
158 siteRefLinker
159
160 publisher publisher.Publisher
161
162 menus navigation.Menus
163
164 // Shortcut to the home page. Note that this may be nil if
165 // home page, for some odd reason, is disabled.
166 home *pageState
167
168 // The last modification date of this site.
169 lastmod time.Time
170
171 // Lazily loaded site dependencies
172 init *siteInit
173 }
174
175 func (s *Site) Taxonomies() TaxonomyList {
176 s.init.taxonomies.Do()
177 return s.taxonomies
178 }
179
180 type taxonomiesConfig map[string]string
181
182 func (t taxonomiesConfig) Values() []viewName {
183 var vals []viewName
184 for k, v := range t {
185 vals = append(vals, viewName{singular: k, plural: v})
186 }
187 sort.Slice(vals, func(i, j int) bool {
188 return vals[i].plural < vals[j].plural
189 })
190
191 return vals
192 }
193
194 type siteConfigHolder struct {
195 sitemap config.Sitemap
196 taxonomiesConfig taxonomiesConfig
197 timeout time.Duration
198 hasCJKLanguage bool
199 enableEmoji bool
200 }
201
202 // Lazily loaded site dependencies.
203 type siteInit struct {
204 prevNext *lazy.Init
205 prevNextInSection *lazy.Init
206 menus *lazy.Init
207 taxonomies *lazy.Init
208 }
209
210 func (init *siteInit) Reset() {
211 init.prevNext.Reset()
212 init.prevNextInSection.Reset()
213 init.menus.Reset()
214 init.taxonomies.Reset()
215 }
216
217 func (s *Site) initInit(init *lazy.Init, pctx pageContext) bool {
218 _, err := init.Do()
219 if err != nil {
220 s.h.FatalError(pctx.wrapError(err))
221 }
222 return err == nil
223 }
224
225 func (s *Site) prepareInits() {
226 s.init = &siteInit{}
227
228 var init lazy.Init
229
230 s.init.prevNext = init.Branch(func() (any, error) {
231 regularPages := s.RegularPages()
232 for i, p := range regularPages {
233 np, ok := p.(nextPrevProvider)
234 if !ok {
235 continue
236 }
237
238 pos := np.getNextPrev()
239 if pos == nil {
240 continue
241 }
242
243 pos.nextPage = nil
244 pos.prevPage = nil
245
246 if i > 0 {
247 pos.nextPage = regularPages[i-1]
248 }
249
250 if i < len(regularPages)-1 {
251 pos.prevPage = regularPages[i+1]
252 }
253 }
254 return nil, nil
255 })
256
257 s.init.prevNextInSection = init.Branch(func() (any, error) {
258 var sections page.Pages
259 s.home.treeRef.m.collectSectionsRecursiveIncludingSelf(pageMapQuery{Prefix: s.home.treeRef.key}, func(n *contentNode) {
260 sections = append(sections, n.p)
261 })
262
263 setNextPrev := func(pas page.Pages) {
264 for i, p := range pas {
265 np, ok := p.(nextPrevInSectionProvider)
266 if !ok {
267 continue
268 }
269
270 pos := np.getNextPrevInSection()
271 if pos == nil {
272 continue
273 }
274
275 pos.nextPage = nil
276 pos.prevPage = nil
277
278 if i > 0 {
279 pos.nextPage = pas[i-1]
280 }
281
282 if i < len(pas)-1 {
283 pos.prevPage = pas[i+1]
284 }
285 }
286 }
287
288 for _, sect := range sections {
289 treeRef := sect.(treeRefProvider).getTreeRef()
290
291 var pas page.Pages
292 treeRef.m.collectPages(pageMapQuery{Prefix: treeRef.key + cmBranchSeparator}, func(c *contentNode) {
293 pas = append(pas, c.p)
294 })
295 page.SortByDefault(pas)
296
297 setNextPrev(pas)
298 }
299
300 // The root section only goes one level down.
301 treeRef := s.home.getTreeRef()
302
303 var pas page.Pages
304 treeRef.m.collectPages(pageMapQuery{Prefix: treeRef.key + cmBranchSeparator}, func(c *contentNode) {
305 pas = append(pas, c.p)
306 })
307 page.SortByDefault(pas)
308
309 setNextPrev(pas)
310
311 return nil, nil
312 })
313
314 s.init.menus = init.Branch(func() (any, error) {
315 s.assembleMenus()
316 return nil, nil
317 })
318
319 s.init.taxonomies = init.Branch(func() (any, error) {
320 err := s.pageMap.assembleTaxonomies()
321 return nil, err
322 })
323 }
324
325 type siteRenderingContext struct {
326 output.Format
327 }
328
329 func (s *Site) Menus() navigation.Menus {
330 s.init.menus.Do()
331 return s.menus
332 }
333
334 func (s *Site) initRenderFormats() {
335 formatSet := make(map[string]bool)
336 formats := output.Formats{}
337 s.pageMap.pageTrees.WalkRenderable(func(s string, n *contentNode) bool {
338 for _, f := range n.p.m.configuredOutputFormats {
339 if !formatSet[f.Name] {
340 formats = append(formats, f)
341 formatSet[f.Name] = true
342 }
343 }
344 return false
345 })
346
347 // Add the per kind configured output formats
348 for _, kind := range allKindsInPages {
349 if siteFormats, found := s.outputFormats[kind]; found {
350 for _, f := range siteFormats {
351 if !formatSet[f.Name] {
352 formats = append(formats, f)
353 formatSet[f.Name] = true
354 }
355 }
356 }
357 }
358
359 sort.Sort(formats)
360 s.renderFormats = formats
361 }
362
363 func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
364 return s.relatedDocsHandler
365 }
366
367 func (s *Site) Language() *langs.Language {
368 return s.language
369 }
370
371 func (s *Site) isEnabled(kind string) bool {
372 if kind == kindUnknown {
373 panic("Unknown kind")
374 }
375 return !s.disabledKinds[kind]
376 }
377
378 // reset returns a new Site prepared for rebuild.
379 func (s *Site) reset() *Site {
380 return &Site{
381 Deps: s.Deps,
382 disabledKinds: s.disabledKinds,
383 titleFunc: s.titleFunc,
384 relatedDocsHandler: s.relatedDocsHandler.Clone(),
385 siteRefLinker: s.siteRefLinker,
386 outputFormats: s.outputFormats,
387 rc: s.rc,
388 outputFormatsConfig: s.outputFormatsConfig,
389 frontmatterHandler: s.frontmatterHandler,
390 mediaTypesConfig: s.mediaTypesConfig,
391 language: s.language,
392 siteBucket: s.siteBucket,
393 h: s.h,
394 publisher: s.publisher,
395 siteConfigConfig: s.siteConfigConfig,
396 init: s.init,
397 PageCollections: s.PageCollections,
398 siteCfg: s.siteCfg,
399 }
400 }
401
402 // newSite creates a new site with the given configuration.
403 func newSite(cfg deps.DepsCfg) (*Site, error) {
404 if cfg.Language == nil {
405 cfg.Language = langs.NewDefaultLanguage(cfg.Cfg)
406 }
407 if cfg.Logger == nil {
408 panic("logger must be set")
409 }
410
411 ignoreErrors := cast.ToStringSlice(cfg.Language.Get("ignoreErrors"))
412 ignorableLogger := loggers.NewIgnorableLogger(cfg.Logger, ignoreErrors...)
413
414 disabledKinds := make(map[string]bool)
415 for _, disabled := range cast.ToStringSlice(cfg.Language.Get("disableKinds")) {
416 disabledKinds[disabled] = true
417 }
418
419 if disabledKinds["taxonomyTerm"] {
420 // Correct from the value it had before Hugo 0.73.0.
421 if disabledKinds[page.KindTaxonomy] {
422 disabledKinds[page.KindTerm] = true
423 } else {
424 disabledKinds[page.KindTaxonomy] = true
425 }
426
427 delete(disabledKinds, "taxonomyTerm")
428 } else if disabledKinds[page.KindTaxonomy] && !disabledKinds[page.KindTerm] {
429 // This is a potentially ambigous situation. It may be correct.
430 ignorableLogger.Errorsf(constants.ErrIDAmbigousDisableKindTaxonomy, `You have the value 'taxonomy' in the disabledKinds list. In Hugo 0.73.0 we fixed these to be what most people expect (taxonomy and term).
431 But this also means that your site configuration may not do what you expect. If it is correct, you can suppress this message by following the instructions below.`)
432 }
433
434 var (
435 mediaTypesConfig []map[string]any
436 outputFormatsConfig []map[string]any
437
438 siteOutputFormatsConfig output.Formats
439 siteMediaTypesConfig media.Types
440 err error
441 )
442
443 // Add language last, if set, so it gets precedence.
444 for _, cfg := range []config.Provider{cfg.Cfg, cfg.Language} {
445 if cfg.IsSet("mediaTypes") {
446 mediaTypesConfig = append(mediaTypesConfig, cfg.GetStringMap("mediaTypes"))
447 }
448 if cfg.IsSet("outputFormats") {
449 outputFormatsConfig = append(outputFormatsConfig, cfg.GetStringMap("outputFormats"))
450 }
451 }
452
453 siteMediaTypesConfig, err = media.DecodeTypes(mediaTypesConfig...)
454 if err != nil {
455 return nil, err
456 }
457
458 siteOutputFormatsConfig, err = output.DecodeFormats(siteMediaTypesConfig, outputFormatsConfig...)
459 if err != nil {
460 return nil, err
461 }
462
463 rssDisabled := disabledKinds[kindRSS]
464 if rssDisabled {
465 // Legacy
466 tmp := siteOutputFormatsConfig[:0]
467 for _, x := range siteOutputFormatsConfig {
468 if !strings.EqualFold(x.Name, "rss") {
469 tmp = append(tmp, x)
470 }
471 }
472 siteOutputFormatsConfig = tmp
473 }
474
475 var siteOutputs map[string]any
476 if cfg.Language.IsSet("outputs") {
477 siteOutputs = cfg.Language.GetStringMap("outputs")
478
479 // Check and correct taxonomy kinds vs pre Hugo 0.73.0.
480 v1, hasTaxonomyTerm := siteOutputs["taxonomyterm"]
481 v2, hasTaxonomy := siteOutputs[page.KindTaxonomy]
482 _, hasTerm := siteOutputs[page.KindTerm]
483 if hasTaxonomy && hasTaxonomyTerm {
484 siteOutputs[page.KindTaxonomy] = v1
485 siteOutputs[page.KindTerm] = v2
486 delete(siteOutputs, "taxonomyTerm")
487 } else if hasTaxonomy && !hasTerm {
488 // This is a potentially ambigous situation. It may be correct.
489 ignorableLogger.Errorsf(constants.ErrIDAmbigousOutputKindTaxonomy, `You have configured output formats for 'taxonomy' in your site configuration. In Hugo 0.73.0 we fixed these to be what most people expect (taxonomy and term).
490 But this also means that your site configuration may not do what you expect. If it is correct, you can suppress this message by following the instructions below.`)
491 }
492 if !hasTaxonomy && hasTaxonomyTerm {
493 siteOutputs[page.KindTaxonomy] = v1
494 delete(siteOutputs, "taxonomyterm")
495 }
496 }
497
498 outputFormats, err := createSiteOutputFormats(siteOutputFormatsConfig, siteOutputs, rssDisabled)
499 if err != nil {
500 return nil, err
501 }
502
503 taxonomies := cfg.Language.GetStringMapString("taxonomies")
504
505 var relatedContentConfig related.Config
506
507 if cfg.Language.IsSet("related") {
508 relatedContentConfig, err = related.DecodeConfig(cfg.Language.GetParams("related"))
509 if err != nil {
510 return nil, fmt.Errorf("failed to decode related config: %w", err)
511 }
512 } else {
513 relatedContentConfig = related.DefaultConfig
514 if _, found := taxonomies["tag"]; found {
515 relatedContentConfig.Add(related.IndexConfig{Name: "tags", Weight: 80})
516 }
517 }
518
519 titleFunc := helpers.GetTitleFunc(cfg.Language.GetString("titleCaseStyle"))
520
521 frontMatterHandler, err := pagemeta.NewFrontmatterHandler(cfg.Logger, cfg.Cfg)
522 if err != nil {
523 return nil, err
524 }
525
526 timeout := 30 * time.Second
527 if cfg.Language.IsSet("timeout") {
528 v := cfg.Language.Get("timeout")
529 d, err := types.ToDurationE(v)
530 if err == nil {
531 timeout = d
532 }
533 }
534
535 siteConfig := siteConfigHolder{
536 sitemap: config.DecodeSitemap(config.Sitemap{Priority: -1, Filename: "sitemap.xml"}, cfg.Language.GetStringMap("sitemap")),
537 taxonomiesConfig: taxonomies,
538 timeout: timeout,
539 hasCJKLanguage: cfg.Language.GetBool("hasCJKLanguage"),
540 enableEmoji: cfg.Language.Cfg.GetBool("enableEmoji"),
541 }
542
543 var siteBucket *pagesMapBucket
544 if cfg.Language.IsSet("cascade") {
545 var err error
546 cascade, err := page.DecodeCascade(cfg.Language.Get("cascade"))
547 if err != nil {
548 return nil, fmt.Errorf("failed to decode cascade config: %s", err)
549 }
550
551 siteBucket = &pagesMapBucket{
552 cascade: cascade,
553 }
554
555 }
556
557 s := &Site{
558 language: cfg.Language,
559 siteBucket: siteBucket,
560 disabledKinds: disabledKinds,
561
562 outputFormats: outputFormats,
563 outputFormatsConfig: siteOutputFormatsConfig,
564 mediaTypesConfig: siteMediaTypesConfig,
565
566 siteCfg: siteConfig,
567
568 titleFunc: titleFunc,
569
570 rc: &siteRenderingContext{output.HTMLFormat},
571
572 frontmatterHandler: frontMatterHandler,
573 relatedDocsHandler: page.NewRelatedDocsHandler(relatedContentConfig),
574 }
575
576 s.prepareInits()
577
578 return s, nil
579 }
580
581 // NewSite creates a new site with the given dependency configuration.
582 // The site will have a template system loaded and ready to use.
583 // Note: This is mainly used in single site tests.
584 func NewSite(cfg deps.DepsCfg) (*Site, error) {
585 s, err := newSite(cfg)
586 if err != nil {
587 return nil, err
588 }
589
590 var l configLoader
591 if err = l.applyDeps(cfg, s); err != nil {
592 return nil, err
593 }
594
595 return s, nil
596 }
597
598 // NewSiteDefaultLang creates a new site in the default language.
599 // The site will have a template system loaded and ready to use.
600 // Note: This is mainly used in single site tests.
601 // TODO(bep) test refactor -- remove
602 func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) {
603 l := configLoader{cfg: config.New()}
604 if err := l.applyConfigDefaults(); err != nil {
605 return nil, err
606 }
607 return newSiteForLang(langs.NewDefaultLanguage(l.cfg), withTemplate...)
608 }
609
610 // NewEnglishSite creates a new site in English language.
611 // The site will have a template system loaded and ready to use.
612 // Note: This is mainly used in single site tests.
613 // TODO(bep) test refactor -- remove
614 func NewEnglishSite(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) {
615 l := configLoader{cfg: config.New()}
616 if err := l.applyConfigDefaults(); err != nil {
617 return nil, err
618 }
619 return newSiteForLang(langs.NewLanguage("en", l.cfg), withTemplate...)
620 }
621
622 // newSiteForLang creates a new site in the given language.
623 func newSiteForLang(lang *langs.Language, withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) {
624 withTemplates := func(templ tpl.TemplateManager) error {
625 for _, wt := range withTemplate {
626 if err := wt(templ); err != nil {
627 return err
628 }
629 }
630 return nil
631 }
632
633 cfg := deps.DepsCfg{WithTemplate: withTemplates, Cfg: lang}
634
635 return NewSiteForCfg(cfg)
636 }
637
638 // NewSiteForCfg creates a new site for the given configuration.
639 // The site will have a template system loaded and ready to use.
640 // Note: This is mainly used in single site tests.
641 func NewSiteForCfg(cfg deps.DepsCfg) (*Site, error) {
642 h, err := NewHugoSites(cfg)
643 if err != nil {
644 return nil, err
645 }
646 return h.Sites[0], nil
647 }
648
649 type SiteInfo struct {
650 Authors page.AuthorList
651 Social SiteSocial
652
653 hugoInfo hugo.Info
654 title string
655 RSSLink string
656 Author map[string]any
657 LanguageCode string
658 Copyright string
659
660 permalinks map[string]string
661
662 LanguagePrefix string
663 Languages langs.Languages
664
665 BuildDrafts bool
666
667 canonifyURLs bool
668 relativeURLs bool
669 uglyURLs func(p page.Page) bool
670
671 owner *HugoSites
672 s *Site
673 language *langs.Language
674 defaultContentLanguageInSubdir bool
675 sectionPagesMenu string
676 }
677
678 func (s *SiteInfo) Pages() page.Pages {
679 return s.s.Pages()
680 }
681
682 func (s *SiteInfo) RegularPages() page.Pages {
683 return s.s.RegularPages()
684 }
685
686 func (s *SiteInfo) AllPages() page.Pages {
687 return s.s.AllPages()
688 }
689
690 func (s *SiteInfo) AllRegularPages() page.Pages {
691 return s.s.AllRegularPages()
692 }
693
694 func (s *SiteInfo) LastChange() time.Time {
695 return s.s.lastmod
696 }
697
698 func (s *SiteInfo) Title() string {
699 return s.title
700 }
701
702 func (s *SiteInfo) Site() page.Site {
703 return s
704 }
705
706 func (s *SiteInfo) Menus() navigation.Menus {
707 return s.s.Menus()
708 }
709
710 // TODO(bep) type
711 func (s *SiteInfo) Taxonomies() any {
712 return s.s.Taxonomies()
713 }
714
715 func (s *SiteInfo) Params() maps.Params {
716 return s.s.Language().Params()
717 }
718
719 func (s *SiteInfo) Data() map[string]any {
720 return s.s.h.Data()
721 }
722
723 func (s *SiteInfo) Language() *langs.Language {
724 return s.language
725 }
726
727 func (s *SiteInfo) Config() SiteConfig {
728 return s.s.siteConfigConfig
729 }
730
731 func (s *SiteInfo) Hugo() hugo.Info {
732 return s.hugoInfo
733 }
734
735 // Sites is a convenience method to get all the Hugo sites/languages configured.
736 func (s *SiteInfo) Sites() page.Sites {
737 return s.s.h.siteInfos()
738 }
739
740 // Current returns the currently rendered Site.
741 // If that isn't set yet, which is the situation before we start rendering,
742 // if will return the Site itself.
743 func (s *SiteInfo) Current() page.Site {
744 if s.s.h.currentSite == nil {
745 return s
746 }
747 return s.s.h.currentSite.Info
748 }
749
750 func (s *SiteInfo) String() string {
751 return fmt.Sprintf("Site(%q)", s.title)
752 }
753
754 func (s *SiteInfo) BaseURL() template.URL {
755 return template.URL(s.s.PathSpec.BaseURL.String())
756 }
757
758 // ServerPort returns the port part of the BaseURL, 0 if none found.
759 func (s *SiteInfo) ServerPort() int {
760 ps := s.s.PathSpec.BaseURL.URL().Port()
761 if ps == "" {
762 return 0
763 }
764 p, err := strconv.Atoi(ps)
765 if err != nil {
766 return 0
767 }
768 return p
769 }
770
771 // GoogleAnalytics is kept here for historic reasons.
772 func (s *SiteInfo) GoogleAnalytics() string {
773 return s.Config().Services.GoogleAnalytics.ID
774 }
775
776 // DisqusShortname is kept here for historic reasons.
777 func (s *SiteInfo) DisqusShortname() string {
778 return s.Config().Services.Disqus.Shortname
779 }
780
781 // SiteSocial is a place to put social details on a site level. These are the
782 // standard keys that themes will expect to have available, but can be
783 // expanded to any others on a per site basis
784 // github
785 // facebook
786 // facebook_admin
787 // twitter
788 // twitter_domain
789 // pinterest
790 // instagram
791 // youtube
792 // linkedin
793 type SiteSocial map[string]string
794
795 // Param is a convenience method to do lookups in SiteInfo's Params map.
796 //
797 // This method is also implemented on Page.
798 func (s *SiteInfo) Param(key any) (any, error) {
799 return resource.Param(s, nil, key)
800 }
801
802 func (s *SiteInfo) IsMultiLingual() bool {
803 return len(s.Languages) > 1
804 }
805
806 func (s *SiteInfo) IsServer() bool {
807 return s.owner.running
808 }
809
810 type siteRefLinker struct {
811 s *Site
812
813 errorLogger *log.Logger
814 notFoundURL string
815 }
816
817 func newSiteRefLinker(cfg config.Provider, s *Site) (siteRefLinker, error) {
818 logger := s.Log.Error()
819
820 notFoundURL := cfg.GetString("refLinksNotFoundURL")
821 errLevel := cfg.GetString("refLinksErrorLevel")
822 if strings.EqualFold(errLevel, "warning") {
823 logger = s.Log.Warn()
824 }
825 return siteRefLinker{s: s, errorLogger: logger, notFoundURL: notFoundURL}, nil
826 }
827
828 func (s siteRefLinker) logNotFound(ref, what string, p page.Page, position text.Position) {
829 if position.IsValid() {
830 s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q: %s: %s", s.s.Lang(), ref, position.String(), what)
831 } else if p == nil {
832 s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q: %s", s.s.Lang(), ref, what)
833 } else {
834 s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q from page %q: %s", s.s.Lang(), ref, p.Pathc(), what)
835 }
836 }
837
838 func (s *siteRefLinker) refLink(ref string, source any, relative bool, outputFormat string) (string, error) {
839 p, err := unwrapPage(source)
840 if err != nil {
841 return "", err
842 }
843
844 var refURL *url.URL
845
846 ref = filepath.ToSlash(ref)
847
848 refURL, err = url.Parse(ref)
849
850 if err != nil {
851 return s.notFoundURL, err
852 }
853
854 var target page.Page
855 var link string
856
857 if refURL.Path != "" {
858 var err error
859 target, err = s.s.getPageRef(p, refURL.Path)
860 var pos text.Position
861 if err != nil || target == nil {
862 if p, ok := source.(text.Positioner); ok {
863 pos = p.Position()
864 }
865 }
866
867 if err != nil {
868 s.logNotFound(refURL.Path, err.Error(), p, pos)
869 return s.notFoundURL, nil
870 }
871
872 if target == nil {
873 s.logNotFound(refURL.Path, "page not found", p, pos)
874 return s.notFoundURL, nil
875 }
876
877 var permalinker Permalinker = target
878
879 if outputFormat != "" {
880 o := target.OutputFormats().Get(outputFormat)
881
882 if o == nil {
883 s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), p, pos)
884 return s.notFoundURL, nil
885 }
886 permalinker = o
887 }
888
889 if relative {
890 link = permalinker.RelPermalink()
891 } else {
892 link = permalinker.Permalink()
893 }
894 }
895
896 if refURL.Fragment != "" {
897 _ = target
898 link = link + "#" + refURL.Fragment
899
900 if pctx, ok := target.(pageContext); ok {
901 if refURL.Path != "" {
902 if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok {
903 link = link + di.AnchorSuffix()
904 }
905 }
906 } else if pctx, ok := p.(pageContext); ok {
907 if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok {
908 link = link + di.AnchorSuffix()
909 }
910 }
911
912 }
913
914 return link, nil
915 }
916
917 func (s *Site) running() bool {
918 return s.h != nil && s.h.running
919 }
920
921 func (s *Site) multilingual() *Multilingual {
922 return s.h.multilingual
923 }
924
925 type whatChanged struct {
926 source bool
927 files map[string]bool
928 }
929
930 // RegisterMediaTypes will register the Site's media types in the mime
931 // package, so it will behave correctly with Hugo's built-in server.
932 func (s *Site) RegisterMediaTypes() {
933 for _, mt := range s.mediaTypesConfig {
934 for _, suffix := range mt.Suffixes() {
935 _ = mime.AddExtensionType(mt.Delimiter+suffix, mt.Type()+"; charset=utf-8")
936 }
937 }
938 }
939
940 func (s *Site) filterFileEvents(events []fsnotify.Event) []fsnotify.Event {
941 var filtered []fsnotify.Event
942 seen := make(map[fsnotify.Event]bool)
943
944 for _, ev := range events {
945 // Avoid processing the same event twice.
946 if seen[ev] {
947 continue
948 }
949 seen[ev] = true
950
951 if s.SourceSpec.IgnoreFile(ev.Name) {
952 continue
953 }
954
955 // Throw away any directories
956 isRegular, err := s.SourceSpec.IsRegularSourceFile(ev.Name)
957 if err != nil && os.IsNotExist(err) && (ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename) {
958 // Force keep of event
959 isRegular = true
960 }
961 if !isRegular {
962 continue
963 }
964
965 if runtime.GOOS == "darwin" { // When a file system is HFS+, its filepath is in NFD form.
966 ev.Name = norm.NFC.String(ev.Name)
967 }
968
969 filtered = append(filtered, ev)
970 }
971
972 return filtered
973 }
974
975 func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event {
976 var filtered []fsnotify.Event
977
978 eventMap := make(map[string][]fsnotify.Event)
979
980 // We often get a Remove etc. followed by a Create, a Create followed by a Write.
981 // Remove the superfluous events to mage the update logic simpler.
982 for _, ev := range events {
983 eventMap[ev.Name] = append(eventMap[ev.Name], ev)
984 }
985
986 for _, ev := range events {
987 mapped := eventMap[ev.Name]
988
989 // Keep one
990 found := false
991 var kept fsnotify.Event
992 for i, ev2 := range mapped {
993 if i == 0 {
994 kept = ev2
995 }
996
997 if ev2.Op&fsnotify.Write == fsnotify.Write {
998 kept = ev2
999 found = true
1000 }
1001
1002 if !found && ev2.Op&fsnotify.Create == fsnotify.Create {
1003 kept = ev2
1004 }
1005 }
1006
1007 filtered = append(filtered, kept)
1008 }
1009
1010 return filtered
1011 }
1012
1013 var (
1014 // These are only used for cache busting, so false positives are fine.
1015 // We also deliberately do not match for file suffixes to also catch
1016 // directory names.
1017 // TODO(bep) consider this when completing the relevant PR rewrite on this.
1018 cssFileRe = regexp.MustCompile("(css|sass|scss)")
1019 cssConfigRe = regexp.MustCompile(`(postcss|tailwind)\.config\.js`)
1020 jsFileRe = regexp.MustCompile("(js|ts|jsx|tsx)")
1021 )
1022
1023 // reBuild partially rebuilds a site given the filesystem events.
1024 // It returns whatever the content source was changed.
1025 // TODO(bep) clean up/rewrite this method.
1026 func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
1027 events = s.filterFileEvents(events)
1028 events = s.translateFileEvents(events)
1029
1030 changeIdentities := make(identity.Identities)
1031
1032 s.Log.Debugf("Rebuild for events %q", events)
1033
1034 h := s.h
1035
1036 // First we need to determine what changed
1037
1038 var (
1039 sourceChanged = []fsnotify.Event{}
1040 sourceReallyChanged = []fsnotify.Event{}
1041 contentFilesChanged []string
1042
1043 tmplChanged bool
1044 tmplAdded bool
1045 dataChanged bool
1046 i18nChanged bool
1047
1048 sourceFilesChanged = make(map[string]bool)
1049
1050 // prevent spamming the log on changes
1051 logger = helpers.NewDistinctErrorLogger()
1052 )
1053
1054 var cachePartitions []string
1055 // Special case
1056 // TODO(bep) I have a ongoing branch where I have redone the cache. Consider this there.
1057 var (
1058 evictCSSRe *regexp.Regexp
1059 evictJSRe *regexp.Regexp
1060 )
1061
1062 for _, ev := range events {
1063 if assetsFilename, _ := s.BaseFs.Assets.MakePathRelative(ev.Name); assetsFilename != "" {
1064 cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...)
1065 if evictCSSRe == nil {
1066 if cssFileRe.MatchString(assetsFilename) || cssConfigRe.MatchString(assetsFilename) {
1067 evictCSSRe = cssFileRe
1068 }
1069 }
1070 if evictJSRe == nil && jsFileRe.MatchString(assetsFilename) {
1071 evictJSRe = jsFileRe
1072 }
1073 }
1074
1075 id, found := s.eventToIdentity(ev)
1076 if found {
1077 changeIdentities[id] = id
1078
1079 switch id.Type {
1080 case files.ComponentFolderContent:
1081 logger.Println("Source changed", ev)
1082 sourceChanged = append(sourceChanged, ev)
1083 case files.ComponentFolderLayouts:
1084 tmplChanged = true
1085 if !s.Tmpl().HasTemplate(id.Path) {
1086 tmplAdded = true
1087 }
1088 if tmplAdded {
1089 logger.Println("Template added", ev)
1090 } else {
1091 logger.Println("Template changed", ev)
1092 }
1093
1094 case files.ComponentFolderData:
1095 logger.Println("Data changed", ev)
1096 dataChanged = true
1097 case files.ComponentFolderI18n:
1098 logger.Println("i18n changed", ev)
1099 i18nChanged = true
1100
1101 }
1102 }
1103 }
1104
1105 changed := &whatChanged{
1106 source: len(sourceChanged) > 0,
1107 files: sourceFilesChanged,
1108 }
1109
1110 config.whatChanged = changed
1111
1112 if err := init(config); err != nil {
1113 return err
1114 }
1115
1116 // These in memory resource caches will be rebuilt on demand.
1117 for _, s := range s.h.Sites {
1118 s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
1119 if evictCSSRe != nil {
1120 s.ResourceSpec.ResourceCache.DeleteMatches(evictCSSRe)
1121 }
1122 if evictJSRe != nil {
1123 s.ResourceSpec.ResourceCache.DeleteMatches(evictJSRe)
1124 }
1125 }
1126
1127 if tmplChanged || i18nChanged {
1128 sites := s.h.Sites
1129 first := sites[0]
1130
1131 s.h.init.Reset()
1132
1133 // TOD(bep) globals clean
1134 if err := first.Deps.LoadResources(); err != nil {
1135 return err
1136 }
1137
1138 for i := 1; i < len(sites); i++ {
1139 site := sites[i]
1140 var err error
1141 depsCfg := deps.DepsCfg{
1142 Language: site.language,
1143 MediaTypes: site.mediaTypesConfig,
1144 OutputFormats: site.outputFormatsConfig,
1145 }
1146 site.Deps, err = first.Deps.ForLanguage(depsCfg, func(d *deps.Deps) error {
1147 d.Site = site.Info
1148 return nil
1149 })
1150 if err != nil {
1151 return err
1152 }
1153 }
1154 }
1155
1156 if dataChanged {
1157 s.h.init.data.Reset()
1158 }
1159
1160 for _, ev := range sourceChanged {
1161 removed := false
1162
1163 if ev.Op&fsnotify.Remove == fsnotify.Remove {
1164 removed = true
1165 }
1166
1167 // Some editors (Vim) sometimes issue only a Rename operation when writing an existing file
1168 // Sometimes a rename operation means that file has been renamed other times it means
1169 // it's been updated
1170 if ev.Op&fsnotify.Rename == fsnotify.Rename {
1171 // If the file is still on disk, it's only been updated, if it's not, it's been moved
1172 if ex, err := afero.Exists(s.Fs.Source, ev.Name); !ex || err != nil {
1173 removed = true
1174 }
1175 }
1176
1177 if removed && files.IsContentFile(ev.Name) {
1178 h.removePageByFilename(ev.Name)
1179 }
1180
1181 sourceReallyChanged = append(sourceReallyChanged, ev)
1182 sourceFilesChanged[ev.Name] = true
1183 }
1184
1185 if config.ErrRecovery || tmplAdded || dataChanged {
1186 h.resetPageState()
1187 } else {
1188 h.resetPageStateFromEvents(changeIdentities)
1189 }
1190
1191 if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
1192 var filenamesChanged []string
1193 for _, e := range sourceReallyChanged {
1194 filenamesChanged = append(filenamesChanged, e.Name)
1195 }
1196 if len(contentFilesChanged) > 0 {
1197 filenamesChanged = append(filenamesChanged, contentFilesChanged...)
1198 }
1199
1200 filenamesChanged = helpers.UniqueStringsReuse(filenamesChanged)
1201
1202 if err := s.readAndProcessContent(*config, filenamesChanged...); err != nil {
1203 return err
1204 }
1205
1206 }
1207
1208 return nil
1209 }
1210
1211 func (s *Site) process(config BuildCfg) (err error) {
1212 if err = s.initialize(); err != nil {
1213 err = fmt.Errorf("initialize: %w", err)
1214 return
1215 }
1216 if err = s.readAndProcessContent(config); err != nil {
1217 err = fmt.Errorf("readAndProcessContent: %w", err)
1218 return
1219 }
1220 return err
1221 }
1222
1223 func (s *Site) render(ctx *siteRenderContext) (err error) {
1224 if err := page.Clear(); err != nil {
1225 return err
1226 }
1227
1228 if ctx.outIdx == 0 {
1229 // Note that even if disableAliases is set, the aliases themselves are
1230 // preserved on page. The motivation with this is to be able to generate
1231 // 301 redirects in a .htacess file and similar using a custom output format.
1232 if !s.Cfg.GetBool("disableAliases") {
1233 // Aliases must be rendered before pages.
1234 // Some sites, Hugo docs included, have faulty alias definitions that point
1235 // to itself or another real page. These will be overwritten in the next
1236 // step.
1237 if err = s.renderAliases(); err != nil {
1238 return
1239 }
1240 }
1241 }
1242
1243 if err = s.renderPages(ctx); err != nil {
1244 return
1245 }
1246
1247 if ctx.outIdx == 0 {
1248 if err = s.renderSitemap(); err != nil {
1249 return
1250 }
1251
1252 if ctx.multihost {
1253 if err = s.renderRobotsTXT(); err != nil {
1254 return
1255 }
1256 }
1257
1258 if err = s.render404(); err != nil {
1259 return
1260 }
1261 }
1262
1263 if !ctx.renderSingletonPages() {
1264 return
1265 }
1266
1267 if err = s.renderMainLanguageRedirect(); err != nil {
1268 return
1269 }
1270
1271 return
1272 }
1273
1274 func (s *Site) Initialise() (err error) {
1275 return s.initialize()
1276 }
1277
1278 func (s *Site) initialize() (err error) {
1279 return s.initializeSiteInfo()
1280 }
1281
1282 // HomeAbsURL is a convenience method giving the absolute URL to the home page.
1283 func (s *SiteInfo) HomeAbsURL() string {
1284 base := ""
1285 if s.IsMultiLingual() {
1286 base = s.Language().Lang
1287 }
1288 return s.owner.AbsURL(base, false)
1289 }
1290
1291 // SitemapAbsURL is a convenience method giving the absolute URL to the sitemap.
1292 func (s *SiteInfo) SitemapAbsURL() string {
1293 p := s.HomeAbsURL()
1294 if !strings.HasSuffix(p, "/") {
1295 p += "/"
1296 }
1297 p += s.s.siteCfg.sitemap.Filename
1298 return p
1299 }
1300
1301 func (s *Site) initializeSiteInfo() error {
1302 var (
1303 lang = s.language
1304 languages langs.Languages
1305 )
1306
1307 if s.h != nil && s.h.multilingual != nil {
1308 languages = s.h.multilingual.Languages
1309 }
1310
1311 permalinks := s.Cfg.GetStringMapString("permalinks")
1312
1313 defaultContentInSubDir := s.Cfg.GetBool("defaultContentLanguageInSubdir")
1314 defaultContentLanguage := s.Cfg.GetString("defaultContentLanguage")
1315
1316 languagePrefix := ""
1317 if s.multilingualEnabled() && (defaultContentInSubDir || lang.Lang != defaultContentLanguage) {
1318 languagePrefix = "/" + lang.Lang
1319 }
1320
1321 uglyURLs := func(p page.Page) bool {
1322 return false
1323 }
1324
1325 v := s.Cfg.Get("uglyURLs")
1326 if v != nil {
1327 switch vv := v.(type) {
1328 case bool:
1329 uglyURLs = func(p page.Page) bool {
1330 return vv
1331 }
1332 case string:
1333 // Is what be get from CLI (--uglyURLs)
1334 vvv := cast.ToBool(vv)
1335 uglyURLs = func(p page.Page) bool {
1336 return vvv
1337 }
1338 default:
1339 m := maps.ToStringMapBool(v)
1340 uglyURLs = func(p page.Page) bool {
1341 return m[p.Section()]
1342 }
1343 }
1344 }
1345
1346 // Assemble dependencies to be used in hugo.Deps.
1347 // TODO(bep) another reminder: We need to clean up this Site vs HugoSites construct.
1348 var deps []*hugo.Dependency
1349 var depFromMod func(m modules.Module) *hugo.Dependency
1350 depFromMod = func(m modules.Module) *hugo.Dependency {
1351 dep := &hugo.Dependency{
1352 Path: m.Path(),
1353 Version: m.Version(),
1354 Time: m.Time(),
1355 Vendor: m.Vendor(),
1356 }
1357
1358 // These are pointers, but this all came from JSON so there's no recursive navigation,
1359 // so just create new values.
1360 if m.Replace() != nil {
1361 dep.Replace = depFromMod(m.Replace())
1362 }
1363 if m.Owner() != nil {
1364 dep.Owner = depFromMod(m.Owner())
1365 }
1366 return dep
1367 }
1368 for _, m := range s.Paths.AllModules {
1369 deps = append(deps, depFromMod(m))
1370 }
1371
1372 s.Info = &SiteInfo{
1373 title: lang.GetString("title"),
1374 Author: lang.GetStringMap("author"),
1375 Social: lang.GetStringMapString("social"),
1376 LanguageCode: lang.GetString("languageCode"),
1377 Copyright: lang.GetString("copyright"),
1378 language: lang,
1379 LanguagePrefix: languagePrefix,
1380 Languages: languages,
1381 defaultContentLanguageInSubdir: defaultContentInSubDir,
1382 sectionPagesMenu: lang.GetString("sectionPagesMenu"),
1383 BuildDrafts: s.Cfg.GetBool("buildDrafts"),
1384 canonifyURLs: s.Cfg.GetBool("canonifyURLs"),
1385 relativeURLs: s.Cfg.GetBool("relativeURLs"),
1386 uglyURLs: uglyURLs,
1387 permalinks: permalinks,
1388 owner: s.h,
1389 s: s,
1390 hugoInfo: hugo.NewInfo(s.Cfg.GetString("environment"), deps),
1391 }
1392
1393 rssOutputFormat, found := s.outputFormats[page.KindHome].GetByName(output.RSSFormat.Name)
1394
1395 if found {
1396 s.Info.RSSLink = s.permalink(rssOutputFormat.BaseFilename())
1397 }
1398
1399 return nil
1400 }
1401
1402 func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) {
1403 for _, fs := range s.BaseFs.SourceFilesystems.FileSystems() {
1404 if p := fs.Path(e.Name); p != "" {
1405 return identity.NewPathIdentity(fs.Name, filepath.ToSlash(p)), true
1406 }
1407 }
1408 return identity.PathIdentity{}, false
1409 }
1410
1411 func (s *Site) readAndProcessContent(buildConfig BuildCfg, filenames ...string) error {
1412 sourceSpec := source.NewSourceSpec(s.PathSpec, buildConfig.ContentInclusionFilter, s.BaseFs.Content.Fs)
1413
1414 proc := newPagesProcessor(s.h, sourceSpec)
1415
1416 c := newPagesCollector(sourceSpec, s.h.getContentMaps(), s.Log, s.h.ContentChanges, proc, filenames...)
1417
1418 if err := c.Collect(); err != nil {
1419 return err
1420 }
1421
1422 return nil
1423 }
1424
1425 func (s *Site) getMenusFromConfig() navigation.Menus {
1426 ret := navigation.Menus{}
1427
1428 if menus := s.language.GetStringMap("menus"); menus != nil {
1429 for name, menu := range menus {
1430 m, err := cast.ToSliceE(menu)
1431 if err != nil {
1432 s.Log.Errorf("menus in site config contain errors\n")
1433 s.Log.Errorln(err)
1434 } else {
1435 handleErr := func(err error) {
1436 if err == nil {
1437 return
1438 }
1439 s.Log.Errorf("menus in site config contain errors\n")
1440 s.Log.Errorln(err)
1441 }
1442
1443 for _, entry := range m {
1444 s.Log.Debugf("found menu: %q, in site config\n", name)
1445
1446 menuEntry := navigation.MenuEntry{Menu: name}
1447 ime, err := maps.ToStringMapE(entry)
1448 handleErr(err)
1449
1450 err = menuEntry.MarshallMap(ime)
1451 handleErr(err)
1452
1453 // TODO(bep) clean up all of this
1454 menuEntry.ConfiguredURL = s.Info.createNodeMenuEntryURL(menuEntry.ConfiguredURL)
1455
1456 if ret[name] == nil {
1457 ret[name] = navigation.Menu{}
1458 }
1459 ret[name] = ret[name].Add(&menuEntry)
1460 }
1461 }
1462 }
1463 return ret
1464 }
1465 return ret
1466 }
1467
1468 func (s *SiteInfo) createNodeMenuEntryURL(in string) string {
1469 if !strings.HasPrefix(in, "/") {
1470 return in
1471 }
1472 // make it match the nodes
1473 menuEntryURL := in
1474 menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.s.PathSpec.URLize(menuEntryURL))
1475 if !s.canonifyURLs {
1476 menuEntryURL = paths.AddContextRoot(s.s.PathSpec.BaseURL.String(), menuEntryURL)
1477 }
1478 return menuEntryURL
1479 }
1480
1481 func (s *Site) assembleMenus() {
1482 s.menus = make(navigation.Menus)
1483
1484 type twoD struct {
1485 MenuName, EntryName string
1486 }
1487 flat := map[twoD]*navigation.MenuEntry{}
1488 children := map[twoD]navigation.Menu{}
1489
1490 // add menu entries from config to flat hash
1491 menuConfig := s.getMenusFromConfig()
1492 for name, menu := range menuConfig {
1493 for _, me := range menu {
1494 if types.IsNil(me.Page) && me.PageRef != "" {
1495 // Try to resolve the page.
1496 me.Page, _ = s.getPageNew(nil, me.PageRef)
1497 }
1498 flat[twoD{name, me.KeyName()}] = me
1499 }
1500 }
1501
1502 sectionPagesMenu := s.Info.sectionPagesMenu
1503
1504 if sectionPagesMenu != "" {
1505 s.pageMap.sections.Walk(func(s string, v any) bool {
1506 p := v.(*contentNode).p
1507 if p.IsHome() {
1508 return false
1509 }
1510 // From Hugo 0.22 we have nested sections, but until we get a
1511 // feel of how that would work in this setting, let us keep
1512 // this menu for the top level only.
1513 id := p.Section()
1514 if _, ok := flat[twoD{sectionPagesMenu, id}]; ok {
1515 return false
1516 }
1517
1518 me := navigation.MenuEntry{
1519 Identifier: id,
1520 Name: p.LinkTitle(),
1521 Weight: p.Weight(),
1522 Page: p,
1523 }
1524 flat[twoD{sectionPagesMenu, me.KeyName()}] = &me
1525
1526 return false
1527 })
1528 }
1529
1530 // Add menu entries provided by pages
1531 s.pageMap.pageTrees.WalkRenderable(func(ss string, n *contentNode) bool {
1532 p := n.p
1533
1534 for name, me := range p.pageMenus.menus() {
1535 if _, ok := flat[twoD{name, me.KeyName()}]; ok {
1536 err := p.wrapError(fmt.Errorf("duplicate menu entry with identifier %q in menu %q", me.KeyName(), name))
1537 s.Log.Warnln(err)
1538 continue
1539 }
1540 flat[twoD{name, me.KeyName()}] = me
1541 }
1542
1543 return false
1544 })
1545
1546 // Create Children Menus First
1547 for _, e := range flat {
1548 if e.Parent != "" {
1549 children[twoD{e.Menu, e.Parent}] = children[twoD{e.Menu, e.Parent}].Add(e)
1550 }
1551 }
1552
1553 // Placing Children in Parents (in flat)
1554 for p, childmenu := range children {
1555 _, ok := flat[twoD{p.MenuName, p.EntryName}]
1556 if !ok {
1557 // if parent does not exist, create one without a URL
1558 flat[twoD{p.MenuName, p.EntryName}] = &navigation.MenuEntry{Name: p.EntryName}
1559 }
1560 flat[twoD{p.MenuName, p.EntryName}].Children = childmenu
1561 }
1562
1563 // Assembling Top Level of Tree
1564 for menu, e := range flat {
1565 if e.Parent == "" {
1566 _, ok := s.menus[menu.MenuName]
1567 if !ok {
1568 s.menus[menu.MenuName] = navigation.Menu{}
1569 }
1570 s.menus[menu.MenuName] = s.menus[menu.MenuName].Add(e)
1571 }
1572 }
1573 }
1574
1575 // get any language code to prefix the target file path with.
1576 func (s *Site) getLanguageTargetPathLang(alwaysInSubDir bool) string {
1577 if s.h.IsMultihost() {
1578 return s.Language().Lang
1579 }
1580
1581 return s.getLanguagePermalinkLang(alwaysInSubDir)
1582 }
1583
1584 // get any lanaguagecode to prefix the relative permalink with.
1585 func (s *Site) getLanguagePermalinkLang(alwaysInSubDir bool) string {
1586 if !s.Info.IsMultiLingual() || s.h.IsMultihost() {
1587 return ""
1588 }
1589
1590 if alwaysInSubDir {
1591 return s.Language().Lang
1592 }
1593
1594 isDefault := s.Language().Lang == s.multilingual().DefaultLang.Lang
1595
1596 if !isDefault || s.Info.defaultContentLanguageInSubdir {
1597 return s.Language().Lang
1598 }
1599
1600 return ""
1601 }
1602
1603 func (s *Site) getTaxonomyKey(key string) string {
1604 if s.PathSpec.DisablePathToLower {
1605 return s.PathSpec.MakePath(key)
1606 }
1607 return strings.ToLower(s.PathSpec.MakePath(key))
1608 }
1609
1610 // Prepare site for a new full build.
1611 func (s *Site) resetBuildState(sourceChanged bool) {
1612 s.relatedDocsHandler = s.relatedDocsHandler.Clone()
1613 s.init.Reset()
1614
1615 if sourceChanged {
1616 s.pageMap.contentMap.pageReverseIndex.Reset()
1617 s.PageCollections = newPageCollections(s.pageMap)
1618 s.pageMap.withEveryBundlePage(func(p *pageState) bool {
1619 p.pagePages = &pagePages{}
1620 if p.bucket != nil {
1621 p.bucket.pagesMapBucketPages = &pagesMapBucketPages{}
1622 }
1623 p.parent = nil
1624 p.Scratcher = maps.NewScratcher()
1625 return false
1626 })
1627 } else {
1628 s.pageMap.withEveryBundlePage(func(p *pageState) bool {
1629 p.Scratcher = maps.NewScratcher()
1630 return false
1631 })
1632 }
1633 }
1634
1635 func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
1636 var errors []error
1637 for e := range results {
1638 errors = append(errors, e)
1639 }
1640
1641 errs <- s.h.pickOneAndLogTheRest(errors)
1642
1643 close(errs)
1644 }
1645
1646 // GetPage looks up a page of a given type for the given ref.
1647 // In Hugo <= 0.44 you had to add Page Kind (section, home) etc. as the first
1648 // argument and then either a unix styled path (with or without a leading slash))
1649 // or path elements separated.
1650 // When we now remove the Kind from this API, we need to make the transition as painless
1651 // as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }},
1652 // i.e. 2 arguments, so we test for that.
1653 func (s *SiteInfo) GetPage(ref ...string) (page.Page, error) {
1654 p, err := s.s.getPageOldVersion(ref...)
1655
1656 if p == nil {
1657 // The nil struct has meaning in some situations, mostly to avoid breaking
1658 // existing sites doing $nilpage.IsDescendant($p), which will always return
1659 // false.
1660 p = page.NilPage
1661 }
1662
1663 return p, err
1664 }
1665
1666 func (s *SiteInfo) GetPageWithTemplateInfo(info tpl.Info, ref ...string) (page.Page, error) {
1667 p, err := s.GetPage(ref...)
1668 if p != nil {
1669 // Track pages referenced by templates/shortcodes
1670 // when in server mode.
1671 if im, ok := info.(identity.Manager); ok {
1672 im.Add(p)
1673 }
1674 }
1675 return p, err
1676 }
1677
1678 func (s *Site) permalink(link string) string {
1679 return s.PathSpec.PermalinkForBaseURL(link, s.PathSpec.BaseURL.String())
1680 }
1681
1682 func (s *Site) absURLPath(targetPath string) string {
1683 var path string
1684 if s.Info.relativeURLs {
1685 path = helpers.GetDottedRelativePath(targetPath)
1686 } else {
1687 url := s.PathSpec.BaseURL.String()
1688 if !strings.HasSuffix(url, "/") {
1689 url += "/"
1690 }
1691 path = url
1692 }
1693
1694 return path
1695 }
1696
1697 func (s *Site) lookupLayouts(layouts ...string) tpl.Template {
1698 for _, l := range layouts {
1699 if templ, found := s.Tmpl().Lookup(l); found {
1700 return templ
1701 }
1702 }
1703
1704 return nil
1705 }
1706
1707 func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath string, d any, templ tpl.Template) error {
1708 s.Log.Debugf("Render XML for %q to %q", name, targetPath)
1709 renderBuffer := bp.GetBuffer()
1710 defer bp.PutBuffer(renderBuffer)
1711
1712 if err := s.renderForTemplate(name, "", d, renderBuffer, templ); err != nil {
1713 return err
1714 }
1715
1716 pd := publisher.Descriptor{
1717 Src: renderBuffer,
1718 TargetPath: targetPath,
1719 StatCounter: statCounter,
1720 // For the minification part of XML,
1721 // we currently only use the MIME type.
1722 OutputFormat: output.RSSFormat,
1723 AbsURLPath: s.absURLPath(targetPath),
1724 }
1725
1726 return s.publisher.Publish(pd)
1727 }
1728
1729 func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, templ tpl.Template) error {
1730 s.Log.Debugf("Render %s to %q", name, targetPath)
1731 s.h.IncrPageRender()
1732 renderBuffer := bp.GetBuffer()
1733 defer bp.PutBuffer(renderBuffer)
1734
1735 of := p.outputFormat()
1736
1737 if err := s.renderForTemplate(p.Kind(), of.Name, p, renderBuffer, templ); err != nil {
1738 return err
1739 }
1740
1741 if renderBuffer.Len() == 0 {
1742 return nil
1743 }
1744
1745 isHTML := of.IsHTML
1746 isRSS := of.Name == "RSS"
1747
1748 pd := publisher.Descriptor{
1749 Src: renderBuffer,
1750 TargetPath: targetPath,
1751 StatCounter: statCounter,
1752 OutputFormat: p.outputFormat(),
1753 }
1754
1755 if isRSS {
1756 // Always canonify URLs in RSS
1757 pd.AbsURLPath = s.absURLPath(targetPath)
1758 } else if isHTML {
1759 if s.Info.relativeURLs || s.Info.canonifyURLs {
1760 pd.AbsURLPath = s.absURLPath(targetPath)
1761 }
1762
1763 if s.running() && s.Cfg.GetBool("watch") && !s.Cfg.GetBool("disableLiveReload") {
1764 pd.LiveReloadBaseURL = s.PathSpec.BaseURL.URL()
1765 if s.Cfg.GetInt("liveReloadPort") != -1 {
1766 pd.LiveReloadBaseURL.Host = fmt.Sprintf("%s:%d", pd.LiveReloadBaseURL.Hostname(), s.Cfg.GetInt("liveReloadPort"))
1767 }
1768 }
1769
1770 // For performance reasons we only inject the Hugo generator tag on the home page.
1771 if p.IsHome() {
1772 pd.AddHugoGeneratorTag = !s.Cfg.GetBool("disableHugoGeneratorInject")
1773 }
1774
1775 }
1776
1777 return s.publisher.Publish(pd)
1778 }
1779
1780 var infoOnMissingLayout = map[string]bool{
1781 // The 404 layout is very much optional in Hugo, but we do look for it.
1782 "404": true,
1783 }
1784
1785 // hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
1786 // where ITEM is the thing being hooked.
1787 type hookRendererTemplate struct {
1788 templateHandler tpl.TemplateHandler
1789 identity.SearchProvider
1790 templ tpl.Template
1791 resolvePosition func(ctx any) text.Position
1792 }
1793
1794 func (hr hookRendererTemplate) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
1795 return hr.templateHandler.Execute(hr.templ, w, ctx)
1796 }
1797
1798 func (hr hookRendererTemplate) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error {
1799 return hr.templateHandler.Execute(hr.templ, w, ctx)
1800 }
1801
1802 func (hr hookRendererTemplate) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
1803 return hr.templateHandler.Execute(hr.templ, w, ctx)
1804 }
1805
1806 func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
1807 return hr.resolvePosition(ctx)
1808 }
1809
1810 func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
1811 return false
1812 }
1813
1814 func (s *Site) renderForTemplate(name, outputFormat string, d any, w io.Writer, templ tpl.Template) (err error) {
1815 if templ == nil {
1816 s.logMissingLayout(name, "", "", outputFormat)
1817 return nil
1818 }
1819
1820 if err = s.Tmpl().Execute(templ, w, d); err != nil {
1821 return fmt.Errorf("render of %q failed: %w", name, err)
1822 }
1823 return
1824 }
1825
1826 func (s *Site) lookupTemplate(layouts ...string) (tpl.Template, bool) {
1827 for _, l := range layouts {
1828 if templ, found := s.Tmpl().Lookup(l); found {
1829 return templ, true
1830 }
1831 }
1832
1833 return nil, false
1834 }
1835
1836 func (s *Site) publish(statCounter *uint64, path string, r io.Reader, fs afero.Fs) (err error) {
1837 s.PathSpec.ProcessingStats.Incr(statCounter)
1838
1839 return helpers.WriteToDisk(filepath.Clean(path), r, fs)
1840 }
1841
1842 func (s *Site) kindFromFileInfoOrSections(fi *fileInfo, sections []string) string {
1843 if fi.TranslationBaseName() == "_index" {
1844 if fi.Dir() == "" {
1845 return page.KindHome
1846 }
1847
1848 return s.kindFromSections(sections)
1849
1850 }
1851
1852 return page.KindPage
1853 }
1854
1855 func (s *Site) kindFromSections(sections []string) string {
1856 if len(sections) == 0 {
1857 return page.KindHome
1858 }
1859
1860 return s.kindFromSectionPath(path.Join(sections...))
1861 }
1862
1863 func (s *Site) kindFromSectionPath(sectionPath string) string {
1864 for _, plural := range s.siteCfg.taxonomiesConfig {
1865 if plural == sectionPath {
1866 return page.KindTaxonomy
1867 }
1868
1869 if strings.HasPrefix(sectionPath, plural) {
1870 return page.KindTerm
1871 }
1872
1873 }
1874
1875 return page.KindSection
1876 }
1877
1878 func (s *Site) newPage(
1879 n *contentNode,
1880 parentbBucket *pagesMapBucket,
1881 kind, title string,
1882 sections ...string) *pageState {
1883 m := map[string]any{}
1884 if title != "" {
1885 m["title"] = title
1886 }
1887
1888 p, err := newPageFromMeta(
1889 n,
1890 parentbBucket,
1891 m,
1892 &pageMeta{
1893 s: s,
1894 kind: kind,
1895 sections: sections,
1896 })
1897 if err != nil {
1898 panic(err)
1899 }
1900
1901 return p
1902 }
1903
1904 func (s *Site) shouldBuild(p page.Page) bool {
1905 return shouldBuild(s.BuildFuture, s.BuildExpired,
1906 s.BuildDrafts, p.Draft(), p.PublishDate(), p.ExpiryDate())
1907 }
1908
1909 func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bool,
1910 publishDate time.Time, expiryDate time.Time) bool {
1911 if !(buildDrafts || !Draft) {
1912 return false
1913 }
1914 hnow := htime.Now()
1915 if !buildFuture && !publishDate.IsZero() && publishDate.After(hnow) {
1916 return false
1917 }
1918 if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(hnow) {
1919 return false
1920 }
1921 return true
1922 }