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 }