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