github.com/gohugoio/hugo@v0.88.1/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 "sort" 28 "strconv" 29 "strings" 30 "time" 31 32 "github.com/gohugoio/hugo/common/types" 33 34 "github.com/gohugoio/hugo/common/paths" 35 36 "github.com/gohugoio/hugo/common/constants" 37 38 "github.com/gohugoio/hugo/common/loggers" 39 40 "github.com/gohugoio/hugo/resources" 41 42 "github.com/gohugoio/hugo/identity" 43 44 "github.com/gohugoio/hugo/markup/converter/hooks" 45 46 "github.com/gohugoio/hugo/resources/resource" 47 48 "github.com/gohugoio/hugo/markup/converter" 49 50 "github.com/gohugoio/hugo/hugofs/files" 51 52 "github.com/gohugoio/hugo/common/maps" 53 54 "github.com/pkg/errors" 55 56 "github.com/gohugoio/hugo/common/text" 57 58 "github.com/gohugoio/hugo/common/hugo" 59 "github.com/gohugoio/hugo/publisher" 60 _errors "github.com/pkg/errors" 61 62 "github.com/gohugoio/hugo/langs" 63 64 "github.com/gohugoio/hugo/resources/page" 65 66 "github.com/gohugoio/hugo/config" 67 "github.com/gohugoio/hugo/lazy" 68 69 "github.com/gohugoio/hugo/media" 70 71 "github.com/fsnotify/fsnotify" 72 bp "github.com/gohugoio/hugo/bufferpool" 73 "github.com/gohugoio/hugo/deps" 74 "github.com/gohugoio/hugo/helpers" 75 "github.com/gohugoio/hugo/navigation" 76 "github.com/gohugoio/hugo/output" 77 "github.com/gohugoio/hugo/related" 78 "github.com/gohugoio/hugo/resources/page/pagemeta" 79 "github.com/gohugoio/hugo/source" 80 "github.com/gohugoio/hugo/tpl" 81 82 "github.com/spf13/afero" 83 "github.com/spf13/cast" 84 ) 85 86 // Site contains all the information relevant for constructing a static 87 // site. The basic flow of information is as follows: 88 // 89 // 1. A list of Files is parsed and then converted into Pages. 90 // 91 // 2. Pages contain sections (based on the file they were generated from), 92 // aliases and slugs (included in a pages frontmatter) which are the 93 // various targets that will get generated. There will be canonical 94 // listing. The canonical path can be overruled based on a pattern. 95 // 96 // 3. Taxonomies are created via configuration and will present some aspect of 97 // the final page and typically a perm url. 98 // 99 // 4. All Pages are passed through a template based on their desired 100 // layout based on numerous different elements. 101 // 102 // 5. The entire collection of files is written to disk. 103 type Site struct { 104 105 // The owning container. When multiple languages, there will be multiple 106 // sites . 107 h *HugoSites 108 109 *PageCollections 110 111 taxonomies TaxonomyList 112 113 Sections Taxonomy 114 Info *SiteInfo 115 116 language *langs.Language 117 siteBucket *pagesMapBucket 118 119 siteCfg siteConfigHolder 120 121 disabledKinds map[string]bool 122 123 enableInlineShortcodes 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() (interface{}, 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() (interface{}, 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() (interface{}, error) { 315 s.assembleMenus() 316 return nil, nil 317 }) 318 319 s.init.taxonomies = init.Branch(func() (interface{}, 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 enableInlineShortcodes: s.enableInlineShortcodes, 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 enableInlineShortcodes: cfg.Language.GetBool("enableInlineShortcodes"), 568 siteCfg: siteConfig, 569 570 titleFunc: titleFunc, 571 572 rc: &siteRenderingContext{output.HTMLFormat}, 573 574 frontmatterHandler: frontMatterHandler, 575 relatedDocsHandler: page.NewRelatedDocsHandler(relatedContentConfig), 576 } 577 578 s.prepareInits() 579 580 return s, nil 581 } 582 583 // NewSite creates a new site with the given dependency configuration. 584 // The site will have a template system loaded and ready to use. 585 // Note: This is mainly used in single site tests. 586 func NewSite(cfg deps.DepsCfg) (*Site, error) { 587 s, err := newSite(cfg) 588 if err != nil { 589 return nil, err 590 } 591 592 var l configLoader 593 if err = l.applyDeps(cfg, s); err != nil { 594 return nil, err 595 } 596 597 return s, nil 598 } 599 600 // NewSiteDefaultLang creates a new site in the default language. 601 // The site will have a template system loaded and ready to use. 602 // Note: This is mainly used in single site tests. 603 // TODO(bep) test refactor -- remove 604 func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) { 605 l := configLoader{cfg: config.New()} 606 if err := l.applyConfigDefaults(); err != nil { 607 return nil, err 608 } 609 return newSiteForLang(langs.NewDefaultLanguage(l.cfg), withTemplate...) 610 } 611 612 // NewEnglishSite creates a new site in English language. 613 // The site will have a template system loaded and ready to use. 614 // Note: This is mainly used in single site tests. 615 // TODO(bep) test refactor -- remove 616 func NewEnglishSite(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) { 617 l := configLoader{cfg: config.New()} 618 if err := l.applyConfigDefaults(); err != nil { 619 return nil, err 620 } 621 return newSiteForLang(langs.NewLanguage("en", l.cfg), withTemplate...) 622 } 623 624 // newSiteForLang creates a new site in the given language. 625 func newSiteForLang(lang *langs.Language, withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) { 626 withTemplates := func(templ tpl.TemplateManager) error { 627 for _, wt := range withTemplate { 628 if err := wt(templ); err != nil { 629 return err 630 } 631 } 632 return nil 633 } 634 635 cfg := deps.DepsCfg{WithTemplate: withTemplates, Cfg: lang} 636 637 return NewSiteForCfg(cfg) 638 } 639 640 // NewSiteForCfg creates a new site for the given configuration. 641 // The site will have a template system loaded and ready to use. 642 // Note: This is mainly used in single site tests. 643 func NewSiteForCfg(cfg deps.DepsCfg) (*Site, error) { 644 h, err := NewHugoSites(cfg) 645 if err != nil { 646 return nil, err 647 } 648 return h.Sites[0], nil 649 } 650 651 type SiteInfo struct { 652 Authors page.AuthorList 653 Social SiteSocial 654 655 hugoInfo hugo.Info 656 title string 657 RSSLink string 658 Author map[string]interface{} 659 LanguageCode string 660 Copyright string 661 662 permalinks map[string]string 663 664 LanguagePrefix string 665 Languages langs.Languages 666 667 BuildDrafts bool 668 669 canonifyURLs bool 670 relativeURLs bool 671 uglyURLs func(p page.Page) bool 672 673 owner *HugoSites 674 s *Site 675 language *langs.Language 676 defaultContentLanguageInSubdir bool 677 sectionPagesMenu string 678 } 679 680 func (s *SiteInfo) Pages() page.Pages { 681 return s.s.Pages() 682 } 683 684 func (s *SiteInfo) RegularPages() page.Pages { 685 return s.s.RegularPages() 686 } 687 688 func (s *SiteInfo) AllPages() page.Pages { 689 return s.s.AllPages() 690 } 691 692 func (s *SiteInfo) AllRegularPages() page.Pages { 693 return s.s.AllRegularPages() 694 } 695 696 func (s *SiteInfo) Permalinks() map[string]string { 697 // Remove in 0.61 698 helpers.Deprecated(".Site.Permalinks", "", true) 699 return s.permalinks 700 } 701 702 func (s *SiteInfo) LastChange() time.Time { 703 return s.s.lastmod 704 } 705 706 func (s *SiteInfo) Title() string { 707 return s.title 708 } 709 710 func (s *SiteInfo) Site() page.Site { 711 return s 712 } 713 714 func (s *SiteInfo) Menus() navigation.Menus { 715 return s.s.Menus() 716 } 717 718 // TODO(bep) type 719 func (s *SiteInfo) Taxonomies() interface{} { 720 return s.s.Taxonomies() 721 } 722 723 func (s *SiteInfo) Params() maps.Params { 724 return s.s.Language().Params() 725 } 726 727 func (s *SiteInfo) Data() map[string]interface{} { 728 return s.s.h.Data() 729 } 730 731 func (s *SiteInfo) Language() *langs.Language { 732 return s.language 733 } 734 735 func (s *SiteInfo) Config() SiteConfig { 736 return s.s.siteConfigConfig 737 } 738 739 func (s *SiteInfo) Hugo() hugo.Info { 740 return s.hugoInfo 741 } 742 743 // Sites is a convenience method to get all the Hugo sites/languages configured. 744 func (s *SiteInfo) Sites() page.Sites { 745 return s.s.h.siteInfos() 746 } 747 748 func (s *SiteInfo) String() string { 749 return fmt.Sprintf("Site(%q)", s.title) 750 } 751 752 func (s *SiteInfo) BaseURL() template.URL { 753 return template.URL(s.s.PathSpec.BaseURL.String()) 754 } 755 756 // ServerPort returns the port part of the BaseURL, 0 if none found. 757 func (s *SiteInfo) ServerPort() int { 758 ps := s.s.PathSpec.BaseURL.URL().Port() 759 if ps == "" { 760 return 0 761 } 762 p, err := strconv.Atoi(ps) 763 if err != nil { 764 return 0 765 } 766 return p 767 } 768 769 // GoogleAnalytics is kept here for historic reasons. 770 func (s *SiteInfo) GoogleAnalytics() string { 771 return s.Config().Services.GoogleAnalytics.ID 772 } 773 774 // DisqusShortname is kept here for historic reasons. 775 func (s *SiteInfo) DisqusShortname() string { 776 return s.Config().Services.Disqus.Shortname 777 } 778 779 // SiteSocial is a place to put social details on a site level. These are the 780 // standard keys that themes will expect to have available, but can be 781 // expanded to any others on a per site basis 782 // github 783 // facebook 784 // facebook_admin 785 // twitter 786 // twitter_domain 787 // pinterest 788 // instagram 789 // youtube 790 // linkedin 791 type SiteSocial map[string]string 792 793 // Param is a convenience method to do lookups in SiteInfo's Params map. 794 // 795 // This method is also implemented on Page. 796 func (s *SiteInfo) Param(key interface{}) (interface{}, error) { 797 return resource.Param(s, nil, key) 798 } 799 800 func (s *SiteInfo) IsMultiLingual() bool { 801 return len(s.Languages) > 1 802 } 803 804 func (s *SiteInfo) IsServer() bool { 805 return s.owner.running 806 } 807 808 type siteRefLinker struct { 809 s *Site 810 811 errorLogger *log.Logger 812 notFoundURL string 813 } 814 815 func newSiteRefLinker(cfg config.Provider, s *Site) (siteRefLinker, error) { 816 logger := s.Log.Error() 817 818 notFoundURL := cfg.GetString("refLinksNotFoundURL") 819 errLevel := cfg.GetString("refLinksErrorLevel") 820 if strings.EqualFold(errLevel, "warning") { 821 logger = s.Log.Warn() 822 } 823 return siteRefLinker{s: s, errorLogger: logger, notFoundURL: notFoundURL}, nil 824 } 825 826 func (s siteRefLinker) logNotFound(ref, what string, p page.Page, position text.Position) { 827 if position.IsValid() { 828 s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q: %s: %s", s.s.Lang(), ref, position.String(), what) 829 } else if p == nil { 830 s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q: %s", s.s.Lang(), ref, what) 831 } else { 832 s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q from page %q: %s", s.s.Lang(), ref, p.Path(), what) 833 } 834 } 835 836 func (s *siteRefLinker) refLink(ref string, source interface{}, relative bool, outputFormat string) (string, error) { 837 p, err := unwrapPage(source) 838 if err != nil { 839 return "", err 840 } 841 842 var refURL *url.URL 843 844 ref = filepath.ToSlash(ref) 845 846 refURL, err = url.Parse(ref) 847 848 if err != nil { 849 return s.notFoundURL, err 850 } 851 852 var target page.Page 853 var link string 854 855 if refURL.Path != "" { 856 var err error 857 target, err = s.s.getPageRef(p, refURL.Path) 858 var pos text.Position 859 if err != nil || target == nil { 860 if p, ok := source.(text.Positioner); ok { 861 pos = p.Position() 862 } 863 } 864 865 if err != nil { 866 s.logNotFound(refURL.Path, err.Error(), p, pos) 867 return s.notFoundURL, nil 868 } 869 870 if target == nil { 871 s.logNotFound(refURL.Path, "page not found", p, pos) 872 return s.notFoundURL, nil 873 } 874 875 var permalinker Permalinker = target 876 877 if outputFormat != "" { 878 o := target.OutputFormats().Get(outputFormat) 879 880 if o == nil { 881 s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), p, pos) 882 return s.notFoundURL, nil 883 } 884 permalinker = o 885 } 886 887 if relative { 888 link = permalinker.RelPermalink() 889 } else { 890 link = permalinker.Permalink() 891 } 892 } 893 894 if refURL.Fragment != "" { 895 _ = target 896 link = link + "#" + refURL.Fragment 897 898 if pctx, ok := target.(pageContext); ok { 899 if refURL.Path != "" { 900 if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok { 901 link = link + di.AnchorSuffix() 902 } 903 } 904 } else if pctx, ok := p.(pageContext); ok { 905 if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok { 906 link = link + di.AnchorSuffix() 907 } 908 } 909 910 } 911 912 return link, nil 913 } 914 915 func (s *Site) running() bool { 916 return s.h != nil && s.h.running 917 } 918 919 func (s *Site) multilingual() *Multilingual { 920 return s.h.multilingual 921 } 922 923 type whatChanged struct { 924 source bool 925 files map[string]bool 926 } 927 928 // RegisterMediaTypes will register the Site's media types in the mime 929 // package, so it will behave correctly with Hugo's built-in server. 930 func (s *Site) RegisterMediaTypes() { 931 for _, mt := range s.mediaTypesConfig { 932 for _, suffix := range mt.Suffixes() { 933 _ = mime.AddExtensionType(mt.Delimiter+suffix, mt.Type()+"; charset=utf-8") 934 } 935 } 936 } 937 938 func (s *Site) filterFileEvents(events []fsnotify.Event) []fsnotify.Event { 939 var filtered []fsnotify.Event 940 seen := make(map[fsnotify.Event]bool) 941 942 for _, ev := range events { 943 // Avoid processing the same event twice. 944 if seen[ev] { 945 continue 946 } 947 seen[ev] = true 948 949 if s.SourceSpec.IgnoreFile(ev.Name) { 950 continue 951 } 952 953 // Throw away any directories 954 isRegular, err := s.SourceSpec.IsRegularSourceFile(ev.Name) 955 if err != nil && os.IsNotExist(err) && (ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename) { 956 // Force keep of event 957 isRegular = true 958 } 959 if !isRegular { 960 continue 961 } 962 963 filtered = append(filtered, ev) 964 } 965 966 return filtered 967 } 968 969 func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event { 970 var filtered []fsnotify.Event 971 972 eventMap := make(map[string][]fsnotify.Event) 973 974 // We often get a Remove etc. followed by a Create, a Create followed by a Write. 975 // Remove the superfluous events to mage the update logic simpler. 976 for _, ev := range events { 977 eventMap[ev.Name] = append(eventMap[ev.Name], ev) 978 } 979 980 for _, ev := range events { 981 mapped := eventMap[ev.Name] 982 983 // Keep one 984 found := false 985 var kept fsnotify.Event 986 for i, ev2 := range mapped { 987 if i == 0 { 988 kept = ev2 989 } 990 991 if ev2.Op&fsnotify.Write == fsnotify.Write { 992 kept = ev2 993 found = true 994 } 995 996 if !found && ev2.Op&fsnotify.Create == fsnotify.Create { 997 kept = ev2 998 } 999 } 1000 1001 filtered = append(filtered, kept) 1002 } 1003 1004 return filtered 1005 } 1006 1007 var ( 1008 // These are only used for cache busting, so false positives are fine. 1009 // We also deliberately do not match for file suffixes to also catch 1010 // directory names. 1011 // TODO(bep) consider this when completing the relevant PR rewrite on this. 1012 cssFileRe = regexp.MustCompile("(css|sass|scss)") 1013 cssConfigRe = regexp.MustCompile(`(postcss|tailwind)\.config\.js`) 1014 jsFileRe = regexp.MustCompile("(js|ts|jsx|tsx)") 1015 ) 1016 1017 // reBuild partially rebuilds a site given the filesystem events. 1018 // It returns whatever the content source was changed. 1019 // TODO(bep) clean up/rewrite this method. 1020 func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error { 1021 events = s.filterFileEvents(events) 1022 events = s.translateFileEvents(events) 1023 1024 changeIdentities := make(identity.Identities) 1025 1026 s.Log.Debugf("Rebuild for events %q", events) 1027 1028 h := s.h 1029 1030 // First we need to determine what changed 1031 1032 var ( 1033 sourceChanged = []fsnotify.Event{} 1034 sourceReallyChanged = []fsnotify.Event{} 1035 contentFilesChanged []string 1036 1037 tmplChanged bool 1038 tmplAdded bool 1039 dataChanged bool 1040 i18nChanged bool 1041 1042 sourceFilesChanged = make(map[string]bool) 1043 1044 // prevent spamming the log on changes 1045 logger = helpers.NewDistinctErrorLogger() 1046 ) 1047 1048 var cachePartitions []string 1049 // Special case 1050 // TODO(bep) I have a ongoing branch where I have redone the cache. Consider this there. 1051 var ( 1052 evictCSSRe *regexp.Regexp 1053 evictJSRe *regexp.Regexp 1054 ) 1055 1056 for _, ev := range events { 1057 if assetsFilename, _ := s.BaseFs.Assets.MakePathRelative(ev.Name); assetsFilename != "" { 1058 cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...) 1059 if evictCSSRe == nil { 1060 if cssFileRe.MatchString(assetsFilename) || cssConfigRe.MatchString(assetsFilename) { 1061 evictCSSRe = cssFileRe 1062 } 1063 } 1064 if evictJSRe == nil && jsFileRe.MatchString(assetsFilename) { 1065 evictJSRe = jsFileRe 1066 } 1067 } 1068 1069 id, found := s.eventToIdentity(ev) 1070 if found { 1071 changeIdentities[id] = id 1072 1073 switch id.Type { 1074 case files.ComponentFolderContent: 1075 logger.Println("Source changed", ev) 1076 sourceChanged = append(sourceChanged, ev) 1077 case files.ComponentFolderLayouts: 1078 tmplChanged = true 1079 if !s.Tmpl().HasTemplate(id.Path) { 1080 tmplAdded = true 1081 } 1082 if tmplAdded { 1083 logger.Println("Template added", ev) 1084 } else { 1085 logger.Println("Template changed", ev) 1086 } 1087 1088 case files.ComponentFolderData: 1089 logger.Println("Data changed", ev) 1090 dataChanged = true 1091 case files.ComponentFolderI18n: 1092 logger.Println("i18n changed", ev) 1093 i18nChanged = true 1094 1095 } 1096 } 1097 } 1098 1099 changed := &whatChanged{ 1100 source: len(sourceChanged) > 0, 1101 files: sourceFilesChanged, 1102 } 1103 1104 config.whatChanged = changed 1105 1106 if err := init(config); err != nil { 1107 return err 1108 } 1109 1110 // These in memory resource caches will be rebuilt on demand. 1111 for _, s := range s.h.Sites { 1112 s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...) 1113 if evictCSSRe != nil { 1114 s.ResourceSpec.ResourceCache.DeleteMatches(evictCSSRe) 1115 } 1116 if evictJSRe != nil { 1117 s.ResourceSpec.ResourceCache.DeleteMatches(evictJSRe) 1118 } 1119 } 1120 1121 if tmplChanged || i18nChanged { 1122 sites := s.h.Sites 1123 first := sites[0] 1124 1125 s.h.init.Reset() 1126 1127 // TOD(bep) globals clean 1128 if err := first.Deps.LoadResources(); err != nil { 1129 return err 1130 } 1131 1132 for i := 1; i < len(sites); i++ { 1133 site := sites[i] 1134 var err error 1135 depsCfg := deps.DepsCfg{ 1136 Language: site.language, 1137 MediaTypes: site.mediaTypesConfig, 1138 OutputFormats: site.outputFormatsConfig, 1139 } 1140 site.Deps, err = first.Deps.ForLanguage(depsCfg, func(d *deps.Deps) error { 1141 d.Site = site.Info 1142 return nil 1143 }) 1144 if err != nil { 1145 return err 1146 } 1147 } 1148 } 1149 1150 if dataChanged { 1151 s.h.init.data.Reset() 1152 } 1153 1154 for _, ev := range sourceChanged { 1155 removed := false 1156 1157 if ev.Op&fsnotify.Remove == fsnotify.Remove { 1158 removed = true 1159 } 1160 1161 // Some editors (Vim) sometimes issue only a Rename operation when writing an existing file 1162 // Sometimes a rename operation means that file has been renamed other times it means 1163 // it's been updated 1164 if ev.Op&fsnotify.Rename == fsnotify.Rename { 1165 // If the file is still on disk, it's only been updated, if it's not, it's been moved 1166 if ex, err := afero.Exists(s.Fs.Source, ev.Name); !ex || err != nil { 1167 removed = true 1168 } 1169 } 1170 1171 if removed && files.IsContentFile(ev.Name) { 1172 h.removePageByFilename(ev.Name) 1173 } 1174 1175 sourceReallyChanged = append(sourceReallyChanged, ev) 1176 sourceFilesChanged[ev.Name] = true 1177 } 1178 1179 if config.ErrRecovery || tmplAdded || dataChanged { 1180 h.resetPageState() 1181 } else { 1182 h.resetPageStateFromEvents(changeIdentities) 1183 } 1184 1185 if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 { 1186 var filenamesChanged []string 1187 for _, e := range sourceReallyChanged { 1188 filenamesChanged = append(filenamesChanged, e.Name) 1189 } 1190 if len(contentFilesChanged) > 0 { 1191 filenamesChanged = append(filenamesChanged, contentFilesChanged...) 1192 } 1193 1194 filenamesChanged = helpers.UniqueStringsReuse(filenamesChanged) 1195 1196 if err := s.readAndProcessContent(filenamesChanged...); err != nil { 1197 return err 1198 } 1199 1200 } 1201 1202 return nil 1203 } 1204 1205 func (s *Site) process(config BuildCfg) (err error) { 1206 if err = s.initialize(); err != nil { 1207 err = errors.Wrap(err, "initialize") 1208 return 1209 } 1210 if err = s.readAndProcessContent(); err != nil { 1211 err = errors.Wrap(err, "readAndProcessContent") 1212 return 1213 } 1214 return err 1215 } 1216 1217 func (s *Site) render(ctx *siteRenderContext) (err error) { 1218 if err := page.Clear(); err != nil { 1219 return err 1220 } 1221 1222 if ctx.outIdx == 0 { 1223 // Note that even if disableAliases is set, the aliases themselves are 1224 // preserved on page. The motivation with this is to be able to generate 1225 // 301 redirects in a .htacess file and similar using a custom output format. 1226 if !s.Cfg.GetBool("disableAliases") { 1227 // Aliases must be rendered before pages. 1228 // Some sites, Hugo docs included, have faulty alias definitions that point 1229 // to itself or another real page. These will be overwritten in the next 1230 // step. 1231 if err = s.renderAliases(); err != nil { 1232 return 1233 } 1234 } 1235 } 1236 1237 if err = s.renderPages(ctx); err != nil { 1238 return 1239 } 1240 1241 if ctx.outIdx == 0 { 1242 if err = s.renderSitemap(); err != nil { 1243 return 1244 } 1245 1246 if ctx.multihost { 1247 if err = s.renderRobotsTXT(); err != nil { 1248 return 1249 } 1250 } 1251 1252 if err = s.render404(); err != nil { 1253 return 1254 } 1255 } 1256 1257 if !ctx.renderSingletonPages() { 1258 return 1259 } 1260 1261 if err = s.renderMainLanguageRedirect(); err != nil { 1262 return 1263 } 1264 1265 return 1266 } 1267 1268 func (s *Site) Initialise() (err error) { 1269 return s.initialize() 1270 } 1271 1272 func (s *Site) initialize() (err error) { 1273 return s.initializeSiteInfo() 1274 } 1275 1276 // HomeAbsURL is a convenience method giving the absolute URL to the home page. 1277 func (s *SiteInfo) HomeAbsURL() string { 1278 base := "" 1279 if s.IsMultiLingual() { 1280 base = s.Language().Lang 1281 } 1282 return s.owner.AbsURL(base, false) 1283 } 1284 1285 // SitemapAbsURL is a convenience method giving the absolute URL to the sitemap. 1286 func (s *SiteInfo) SitemapAbsURL() string { 1287 p := s.HomeAbsURL() 1288 if !strings.HasSuffix(p, "/") { 1289 p += "/" 1290 } 1291 p += s.s.siteCfg.sitemap.Filename 1292 return p 1293 } 1294 1295 func (s *Site) initializeSiteInfo() error { 1296 var ( 1297 lang = s.language 1298 languages langs.Languages 1299 ) 1300 1301 if s.h != nil && s.h.multilingual != nil { 1302 languages = s.h.multilingual.Languages 1303 } 1304 1305 permalinks := s.Cfg.GetStringMapString("permalinks") 1306 1307 defaultContentInSubDir := s.Cfg.GetBool("defaultContentLanguageInSubdir") 1308 defaultContentLanguage := s.Cfg.GetString("defaultContentLanguage") 1309 1310 languagePrefix := "" 1311 if s.multilingualEnabled() && (defaultContentInSubDir || lang.Lang != defaultContentLanguage) { 1312 languagePrefix = "/" + lang.Lang 1313 } 1314 1315 uglyURLs := func(p page.Page) bool { 1316 return false 1317 } 1318 1319 v := s.Cfg.Get("uglyURLs") 1320 if v != nil { 1321 switch vv := v.(type) { 1322 case bool: 1323 uglyURLs = func(p page.Page) bool { 1324 return vv 1325 } 1326 case string: 1327 // Is what be get from CLI (--uglyURLs) 1328 vvv := cast.ToBool(vv) 1329 uglyURLs = func(p page.Page) bool { 1330 return vvv 1331 } 1332 default: 1333 m := maps.ToStringMapBool(v) 1334 uglyURLs = func(p page.Page) bool { 1335 return m[p.Section()] 1336 } 1337 } 1338 } 1339 1340 s.Info = &SiteInfo{ 1341 title: lang.GetString("title"), 1342 Author: lang.GetStringMap("author"), 1343 Social: lang.GetStringMapString("social"), 1344 LanguageCode: lang.GetString("languageCode"), 1345 Copyright: lang.GetString("copyright"), 1346 language: lang, 1347 LanguagePrefix: languagePrefix, 1348 Languages: languages, 1349 defaultContentLanguageInSubdir: defaultContentInSubDir, 1350 sectionPagesMenu: lang.GetString("sectionPagesMenu"), 1351 BuildDrafts: s.Cfg.GetBool("buildDrafts"), 1352 canonifyURLs: s.Cfg.GetBool("canonifyURLs"), 1353 relativeURLs: s.Cfg.GetBool("relativeURLs"), 1354 uglyURLs: uglyURLs, 1355 permalinks: permalinks, 1356 owner: s.h, 1357 s: s, 1358 hugoInfo: hugo.NewInfo(s.Cfg.GetString("environment")), 1359 } 1360 1361 rssOutputFormat, found := s.outputFormats[page.KindHome].GetByName(output.RSSFormat.Name) 1362 1363 if found { 1364 s.Info.RSSLink = s.permalink(rssOutputFormat.BaseFilename()) 1365 } 1366 1367 return nil 1368 } 1369 1370 func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) { 1371 for _, fs := range s.BaseFs.SourceFilesystems.FileSystems() { 1372 if p := fs.Path(e.Name); p != "" { 1373 return identity.NewPathIdentity(fs.Name, filepath.ToSlash(p)), true 1374 } 1375 } 1376 return identity.PathIdentity{}, false 1377 } 1378 1379 func (s *Site) readAndProcessContent(filenames ...string) error { 1380 sourceSpec := source.NewSourceSpec(s.PathSpec, s.BaseFs.Content.Fs) 1381 1382 proc := newPagesProcessor(s.h, sourceSpec) 1383 1384 c := newPagesCollector(sourceSpec, s.h.getContentMaps(), s.Log, s.h.ContentChanges, proc, filenames...) 1385 1386 if err := c.Collect(); err != nil { 1387 return err 1388 } 1389 1390 return nil 1391 } 1392 1393 func (s *Site) getMenusFromConfig() navigation.Menus { 1394 ret := navigation.Menus{} 1395 1396 if menus := s.language.GetStringMap("menus"); menus != nil { 1397 for name, menu := range menus { 1398 m, err := cast.ToSliceE(menu) 1399 if err != nil { 1400 s.Log.Errorf("unable to process menus in site config\n") 1401 s.Log.Errorln(err) 1402 } else { 1403 handleErr := func(err error) { 1404 if err == nil { 1405 return 1406 } 1407 s.Log.Errorf("unable to process menus in site config\n") 1408 s.Log.Errorln(err) 1409 1410 } 1411 1412 for _, entry := range m { 1413 s.Log.Debugf("found menu: %q, in site config\n", name) 1414 1415 menuEntry := navigation.MenuEntry{Menu: name} 1416 ime, err := maps.ToStringMapE(entry) 1417 handleErr(err) 1418 1419 err = menuEntry.MarshallMap(ime) 1420 handleErr(err) 1421 1422 // TODO(bep) clean up all of this 1423 menuEntry.ConfiguredURL = s.Info.createNodeMenuEntryURL(menuEntry.ConfiguredURL) 1424 1425 if ret[name] == nil { 1426 ret[name] = navigation.Menu{} 1427 } 1428 ret[name] = ret[name].Add(&menuEntry) 1429 } 1430 } 1431 } 1432 return ret 1433 } 1434 return ret 1435 } 1436 1437 func (s *SiteInfo) createNodeMenuEntryURL(in string) string { 1438 if !strings.HasPrefix(in, "/") { 1439 return in 1440 } 1441 // make it match the nodes 1442 menuEntryURL := in 1443 menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.s.PathSpec.URLize(menuEntryURL)) 1444 if !s.canonifyURLs { 1445 menuEntryURL = paths.AddContextRoot(s.s.PathSpec.BaseURL.String(), menuEntryURL) 1446 } 1447 return menuEntryURL 1448 } 1449 1450 func (s *Site) assembleMenus() { 1451 s.menus = make(navigation.Menus) 1452 1453 type twoD struct { 1454 MenuName, EntryName string 1455 } 1456 flat := map[twoD]*navigation.MenuEntry{} 1457 children := map[twoD]navigation.Menu{} 1458 1459 // add menu entries from config to flat hash 1460 menuConfig := s.getMenusFromConfig() 1461 for name, menu := range menuConfig { 1462 for _, me := range menu { 1463 if types.IsNil(me.Page) && me.PageRef != "" { 1464 // Try to resolve the page. 1465 me.Page, _ = s.getPageNew(nil, me.PageRef) 1466 } 1467 flat[twoD{name, me.KeyName()}] = me 1468 } 1469 } 1470 1471 sectionPagesMenu := s.Info.sectionPagesMenu 1472 1473 if sectionPagesMenu != "" { 1474 s.pageMap.sections.Walk(func(s string, v interface{}) bool { 1475 p := v.(*contentNode).p 1476 if p.IsHome() { 1477 return false 1478 } 1479 // From Hugo 0.22 we have nested sections, but until we get a 1480 // feel of how that would work in this setting, let us keep 1481 // this menu for the top level only. 1482 id := p.Section() 1483 if _, ok := flat[twoD{sectionPagesMenu, id}]; ok { 1484 return false 1485 } 1486 1487 me := navigation.MenuEntry{ 1488 Identifier: id, 1489 Name: p.LinkTitle(), 1490 Weight: p.Weight(), 1491 Page: p, 1492 } 1493 flat[twoD{sectionPagesMenu, me.KeyName()}] = &me 1494 1495 return false 1496 }) 1497 } 1498 1499 // Add menu entries provided by pages 1500 s.pageMap.pageTrees.WalkRenderable(func(ss string, n *contentNode) bool { 1501 p := n.p 1502 1503 for name, me := range p.pageMenus.menus() { 1504 if _, ok := flat[twoD{name, me.KeyName()}]; ok { 1505 err := p.wrapError(errors.Errorf("duplicate menu entry with identifier %q in menu %q", me.KeyName(), name)) 1506 s.Log.Warnln(err) 1507 continue 1508 } 1509 flat[twoD{name, me.KeyName()}] = me 1510 } 1511 1512 return false 1513 }) 1514 1515 // Create Children Menus First 1516 for _, e := range flat { 1517 if e.Parent != "" { 1518 children[twoD{e.Menu, e.Parent}] = children[twoD{e.Menu, e.Parent}].Add(e) 1519 } 1520 } 1521 1522 // Placing Children in Parents (in flat) 1523 for p, childmenu := range children { 1524 _, ok := flat[twoD{p.MenuName, p.EntryName}] 1525 if !ok { 1526 // if parent does not exist, create one without a URL 1527 flat[twoD{p.MenuName, p.EntryName}] = &navigation.MenuEntry{Name: p.EntryName} 1528 } 1529 flat[twoD{p.MenuName, p.EntryName}].Children = childmenu 1530 } 1531 1532 // Assembling Top Level of Tree 1533 for menu, e := range flat { 1534 if e.Parent == "" { 1535 _, ok := s.menus[menu.MenuName] 1536 if !ok { 1537 s.menus[menu.MenuName] = navigation.Menu{} 1538 } 1539 s.menus[menu.MenuName] = s.menus[menu.MenuName].Add(e) 1540 } 1541 } 1542 } 1543 1544 // get any language code to prefix the target file path with. 1545 func (s *Site) getLanguageTargetPathLang(alwaysInSubDir bool) string { 1546 if s.h.IsMultihost() { 1547 return s.Language().Lang 1548 } 1549 1550 return s.getLanguagePermalinkLang(alwaysInSubDir) 1551 } 1552 1553 // get any lanaguagecode to prefix the relative permalink with. 1554 func (s *Site) getLanguagePermalinkLang(alwaysInSubDir bool) string { 1555 if !s.Info.IsMultiLingual() || s.h.IsMultihost() { 1556 return "" 1557 } 1558 1559 if alwaysInSubDir { 1560 return s.Language().Lang 1561 } 1562 1563 isDefault := s.Language().Lang == s.multilingual().DefaultLang.Lang 1564 1565 if !isDefault || s.Info.defaultContentLanguageInSubdir { 1566 return s.Language().Lang 1567 } 1568 1569 return "" 1570 } 1571 1572 func (s *Site) getTaxonomyKey(key string) string { 1573 if s.PathSpec.DisablePathToLower { 1574 return s.PathSpec.MakePath(key) 1575 } 1576 return strings.ToLower(s.PathSpec.MakePath(key)) 1577 } 1578 1579 // Prepare site for a new full build. 1580 func (s *Site) resetBuildState(sourceChanged bool) { 1581 s.relatedDocsHandler = s.relatedDocsHandler.Clone() 1582 s.init.Reset() 1583 1584 if sourceChanged { 1585 s.pageMap.contentMap.pageReverseIndex.Reset() 1586 s.PageCollections = newPageCollections(s.pageMap) 1587 s.pageMap.withEveryBundlePage(func(p *pageState) bool { 1588 p.pagePages = &pagePages{} 1589 if p.bucket != nil { 1590 p.bucket.pagesMapBucketPages = &pagesMapBucketPages{} 1591 } 1592 p.parent = nil 1593 p.Scratcher = maps.NewScratcher() 1594 return false 1595 }) 1596 } else { 1597 s.pageMap.withEveryBundlePage(func(p *pageState) bool { 1598 p.Scratcher = maps.NewScratcher() 1599 return false 1600 }) 1601 } 1602 } 1603 1604 func (s *Site) errorCollator(results <-chan error, errs chan<- error) { 1605 var errors []error 1606 for e := range results { 1607 errors = append(errors, e) 1608 } 1609 1610 errs <- s.h.pickOneAndLogTheRest(errors) 1611 1612 close(errs) 1613 } 1614 1615 // GetPage looks up a page of a given type for the given ref. 1616 // In Hugo <= 0.44 you had to add Page Kind (section, home) etc. as the first 1617 // argument and then either a unix styled path (with or without a leading slash)) 1618 // or path elements separated. 1619 // When we now remove the Kind from this API, we need to make the transition as painless 1620 // as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }}, 1621 // i.e. 2 arguments, so we test for that. 1622 func (s *SiteInfo) GetPage(ref ...string) (page.Page, error) { 1623 p, err := s.s.getPageOldVersion(ref...) 1624 1625 if p == nil { 1626 // The nil struct has meaning in some situations, mostly to avoid breaking 1627 // existing sites doing $nilpage.IsDescendant($p), which will always return 1628 // false. 1629 p = page.NilPage 1630 } 1631 1632 return p, err 1633 } 1634 1635 func (s *SiteInfo) GetPageWithTemplateInfo(info tpl.Info, ref ...string) (page.Page, error) { 1636 p, err := s.GetPage(ref...) 1637 if p != nil { 1638 // Track pages referenced by templates/shortcodes 1639 // when in server mode. 1640 if im, ok := info.(identity.Manager); ok { 1641 im.Add(p) 1642 } 1643 } 1644 return p, err 1645 } 1646 1647 func (s *Site) permalink(link string) string { 1648 return s.PathSpec.PermalinkForBaseURL(link, s.PathSpec.BaseURL.String()) 1649 } 1650 1651 func (s *Site) absURLPath(targetPath string) string { 1652 var path string 1653 if s.Info.relativeURLs { 1654 path = helpers.GetDottedRelativePath(targetPath) 1655 } else { 1656 url := s.PathSpec.BaseURL.String() 1657 if !strings.HasSuffix(url, "/") { 1658 url += "/" 1659 } 1660 path = url 1661 } 1662 1663 return path 1664 } 1665 1666 func (s *Site) lookupLayouts(layouts ...string) tpl.Template { 1667 for _, l := range layouts { 1668 if templ, found := s.Tmpl().Lookup(l); found { 1669 return templ 1670 } 1671 } 1672 1673 return nil 1674 } 1675 1676 func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath string, d interface{}, templ tpl.Template) error { 1677 s.Log.Debugf("Render XML for %q to %q", name, targetPath) 1678 renderBuffer := bp.GetBuffer() 1679 defer bp.PutBuffer(renderBuffer) 1680 1681 if err := s.renderForTemplate(name, "", d, renderBuffer, templ); err != nil { 1682 return err 1683 } 1684 1685 pd := publisher.Descriptor{ 1686 Src: renderBuffer, 1687 TargetPath: targetPath, 1688 StatCounter: statCounter, 1689 // For the minification part of XML, 1690 // we currently only use the MIME type. 1691 OutputFormat: output.RSSFormat, 1692 AbsURLPath: s.absURLPath(targetPath), 1693 } 1694 1695 return s.publisher.Publish(pd) 1696 } 1697 1698 func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, templ tpl.Template) error { 1699 s.Log.Debugf("Render %s to %q", name, targetPath) 1700 renderBuffer := bp.GetBuffer() 1701 defer bp.PutBuffer(renderBuffer) 1702 1703 of := p.outputFormat() 1704 1705 if err := s.renderForTemplate(p.Kind(), of.Name, p, renderBuffer, templ); err != nil { 1706 return err 1707 } 1708 1709 if renderBuffer.Len() == 0 { 1710 return nil 1711 } 1712 1713 isHTML := of.IsHTML 1714 isRSS := of.Name == "RSS" 1715 1716 pd := publisher.Descriptor{ 1717 Src: renderBuffer, 1718 TargetPath: targetPath, 1719 StatCounter: statCounter, 1720 OutputFormat: p.outputFormat(), 1721 } 1722 1723 if isRSS { 1724 // Always canonify URLs in RSS 1725 pd.AbsURLPath = s.absURLPath(targetPath) 1726 } else if isHTML { 1727 if s.Info.relativeURLs || s.Info.canonifyURLs { 1728 pd.AbsURLPath = s.absURLPath(targetPath) 1729 } 1730 1731 if s.running() && s.Cfg.GetBool("watch") && !s.Cfg.GetBool("disableLiveReload") { 1732 pd.LiveReloadBaseURL = s.PathSpec.BaseURL.URL() 1733 if s.Cfg.GetInt("liveReloadPort") != -1 { 1734 pd.LiveReloadBaseURL.Host = fmt.Sprintf("%s:%d", pd.LiveReloadBaseURL.Hostname(), s.Cfg.GetInt("liveReloadPort")) 1735 } 1736 } 1737 1738 // For performance reasons we only inject the Hugo generator tag on the home page. 1739 if p.IsHome() { 1740 pd.AddHugoGeneratorTag = !s.Cfg.GetBool("disableHugoGeneratorInject") 1741 } 1742 1743 } 1744 1745 return s.publisher.Publish(pd) 1746 } 1747 1748 var infoOnMissingLayout = map[string]bool{ 1749 // The 404 layout is very much optional in Hugo, but we do look for it. 1750 "404": true, 1751 } 1752 1753 // hookRenderer is the canonical implementation of all hooks.ITEMRenderer, 1754 // where ITEM is the thing being hooked. 1755 type hookRenderer struct { 1756 templateHandler tpl.TemplateHandler 1757 identity.SearchProvider 1758 templ tpl.Template 1759 } 1760 1761 func (hr hookRenderer) RenderLink(w io.Writer, ctx hooks.LinkContext) error { 1762 return hr.templateHandler.Execute(hr.templ, w, ctx) 1763 } 1764 1765 func (hr hookRenderer) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error { 1766 return hr.templateHandler.Execute(hr.templ, w, ctx) 1767 } 1768 1769 func (s *Site) renderForTemplate(name, outputFormat string, d interface{}, w io.Writer, templ tpl.Template) (err error) { 1770 if templ == nil { 1771 s.logMissingLayout(name, "", "", outputFormat) 1772 return nil 1773 } 1774 1775 if err = s.Tmpl().Execute(templ, w, d); err != nil { 1776 return _errors.Wrapf(err, "render of %q failed", name) 1777 } 1778 return 1779 } 1780 1781 func (s *Site) lookupTemplate(layouts ...string) (tpl.Template, bool) { 1782 for _, l := range layouts { 1783 if templ, found := s.Tmpl().Lookup(l); found { 1784 return templ, true 1785 } 1786 } 1787 1788 return nil, false 1789 } 1790 1791 func (s *Site) publish(statCounter *uint64, path string, r io.Reader) (err error) { 1792 s.PathSpec.ProcessingStats.Incr(statCounter) 1793 1794 return helpers.WriteToDisk(filepath.Clean(path), r, s.BaseFs.PublishFs) 1795 } 1796 1797 func (s *Site) kindFromFileInfoOrSections(fi *fileInfo, sections []string) string { 1798 if fi.TranslationBaseName() == "_index" { 1799 if fi.Dir() == "" { 1800 return page.KindHome 1801 } 1802 1803 return s.kindFromSections(sections) 1804 1805 } 1806 1807 return page.KindPage 1808 } 1809 1810 func (s *Site) kindFromSections(sections []string) string { 1811 if len(sections) == 0 { 1812 return page.KindHome 1813 } 1814 1815 return s.kindFromSectionPath(path.Join(sections...)) 1816 } 1817 1818 func (s *Site) kindFromSectionPath(sectionPath string) string { 1819 for _, plural := range s.siteCfg.taxonomiesConfig { 1820 if plural == sectionPath { 1821 return page.KindTaxonomy 1822 } 1823 1824 if strings.HasPrefix(sectionPath, plural) { 1825 return page.KindTerm 1826 } 1827 1828 } 1829 1830 return page.KindSection 1831 } 1832 1833 func (s *Site) newPage( 1834 n *contentNode, 1835 parentbBucket *pagesMapBucket, 1836 kind, title string, 1837 sections ...string) *pageState { 1838 m := map[string]interface{}{} 1839 if title != "" { 1840 m["title"] = title 1841 } 1842 1843 p, err := newPageFromMeta( 1844 n, 1845 parentbBucket, 1846 m, 1847 &pageMeta{ 1848 s: s, 1849 kind: kind, 1850 sections: sections, 1851 }) 1852 if err != nil { 1853 panic(err) 1854 } 1855 1856 return p 1857 } 1858 1859 func (s *Site) shouldBuild(p page.Page) bool { 1860 return shouldBuild(s.BuildFuture, s.BuildExpired, 1861 s.BuildDrafts, p.Draft(), p.PublishDate(), p.ExpiryDate()) 1862 } 1863 1864 func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bool, 1865 publishDate time.Time, expiryDate time.Time) bool { 1866 if !(buildDrafts || !Draft) { 1867 return false 1868 } 1869 if !buildFuture && !publishDate.IsZero() && publishDate.After(time.Now()) { 1870 return false 1871 } 1872 if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(time.Now()) { 1873 return false 1874 } 1875 return true 1876 }