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