page__meta.go (19333B)
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 "path" 19 "path/filepath" 20 "regexp" 21 "strings" 22 "sync" 23 "time" 24 25 "github.com/gohugoio/hugo/langs" 26 27 "github.com/gobuffalo/flect" 28 "github.com/gohugoio/hugo/markup/converter" 29 30 "github.com/gohugoio/hugo/hugofs/files" 31 32 "github.com/gohugoio/hugo/common/hugo" 33 34 "github.com/gohugoio/hugo/related" 35 36 "github.com/gohugoio/hugo/source" 37 38 "github.com/gohugoio/hugo/common/maps" 39 "github.com/gohugoio/hugo/config" 40 "github.com/gohugoio/hugo/helpers" 41 42 "github.com/gohugoio/hugo/output" 43 "github.com/gohugoio/hugo/resources/page" 44 "github.com/gohugoio/hugo/resources/page/pagemeta" 45 "github.com/gohugoio/hugo/resources/resource" 46 "github.com/spf13/cast" 47 ) 48 49 var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) 50 51 type pageMeta struct { 52 // kind is the discriminator that identifies the different page types 53 // in the different page collections. This can, as an example, be used 54 // to to filter regular pages, find sections etc. 55 // Kind will, for the pages available to the templates, be one of: 56 // page, home, section, taxonomy and term. 57 // It is of string type to make it easy to reason about in 58 // the templates. 59 kind string 60 61 // This is a standalone page not part of any page collection. These 62 // include sitemap, robotsTXT and similar. It will have no pageOutputs, but 63 // a fixed pageOutput. 64 standalone bool 65 66 draft bool // Only published when running with -D flag 67 buildConfig pagemeta.BuildConfig 68 69 bundleType files.ContentClass 70 71 // Params contains configuration defined in the params section of page frontmatter. 72 params map[string]any 73 74 title string 75 linkTitle string 76 77 summary string 78 79 resourcePath string 80 81 weight int 82 83 markup string 84 contentType string 85 86 // whether the content is in a CJK language. 87 isCJKLanguage bool 88 89 layout string 90 91 aliases []string 92 93 description string 94 keywords []string 95 96 urlPaths pagemeta.URLPath 97 98 resource.Dates 99 100 // Set if this page is bundled inside another. 101 bundled bool 102 103 // A key that maps to translation(s) of this page. This value is fetched 104 // from the page front matter. 105 translationKey string 106 107 // From front matter. 108 configuredOutputFormats output.Formats 109 110 // This is the raw front matter metadata that is going to be assigned to 111 // the Resources above. 112 resourcesMetadata []map[string]any 113 114 f source.File 115 116 sections []string 117 118 // Sitemap overrides from front matter. 119 sitemap config.Sitemap 120 121 s *Site 122 123 contentConverterInit sync.Once 124 contentConverter converter.Converter 125 } 126 127 func (p *pageMeta) Aliases() []string { 128 return p.aliases 129 } 130 131 func (p *pageMeta) Author() page.Author { 132 helpers.Deprecated(".Author", "Use taxonomies.", false) 133 authors := p.Authors() 134 135 for _, author := range authors { 136 return author 137 } 138 return page.Author{} 139 } 140 141 func (p *pageMeta) Authors() page.AuthorList { 142 helpers.Deprecated(".Authors", "Use taxonomies.", false) 143 authorKeys, ok := p.params["authors"] 144 if !ok { 145 return page.AuthorList{} 146 } 147 authors := authorKeys.([]string) 148 if len(authors) < 1 || len(p.s.Info.Authors) < 1 { 149 return page.AuthorList{} 150 } 151 152 al := make(page.AuthorList) 153 for _, author := range authors { 154 a, ok := p.s.Info.Authors[author] 155 if ok { 156 al[author] = a 157 } 158 } 159 return al 160 } 161 162 func (p *pageMeta) BundleType() files.ContentClass { 163 return p.bundleType 164 } 165 166 func (p *pageMeta) Description() string { 167 return p.description 168 } 169 170 func (p *pageMeta) Lang() string { 171 return p.s.Lang() 172 } 173 174 func (p *pageMeta) Draft() bool { 175 return p.draft 176 } 177 178 func (p *pageMeta) File() source.File { 179 return p.f 180 } 181 182 func (p *pageMeta) IsHome() bool { 183 return p.Kind() == page.KindHome 184 } 185 186 func (p *pageMeta) Keywords() []string { 187 return p.keywords 188 } 189 190 func (p *pageMeta) Kind() string { 191 return p.kind 192 } 193 194 func (p *pageMeta) Layout() string { 195 return p.layout 196 } 197 198 func (p *pageMeta) LinkTitle() string { 199 if p.linkTitle != "" { 200 return p.linkTitle 201 } 202 203 return p.Title() 204 } 205 206 func (p *pageMeta) Name() string { 207 if p.resourcePath != "" { 208 return p.resourcePath 209 } 210 return p.Title() 211 } 212 213 func (p *pageMeta) IsNode() bool { 214 return !p.IsPage() 215 } 216 217 func (p *pageMeta) IsPage() bool { 218 return p.Kind() == page.KindPage 219 } 220 221 // Param is a convenience method to do lookups in Page's and Site's Params map, 222 // in that order. 223 // 224 // This method is also implemented on SiteInfo. 225 // TODO(bep) interface 226 func (p *pageMeta) Param(key any) (any, error) { 227 return resource.Param(p, p.s.Info.Params(), key) 228 } 229 230 func (p *pageMeta) Params() maps.Params { 231 return p.params 232 } 233 234 func (p *pageMeta) Path() string { 235 if !p.File().IsZero() { 236 const example = ` 237 {{ $path := "" }} 238 {{ with .File }} 239 {{ $path = .Path }} 240 {{ else }} 241 {{ $path = .Path }} 242 {{ end }} 243 ` 244 helpers.Deprecated(".Path when the page is backed by a file", "We plan to use Path for a canonical source path and you probably want to check the source is a file. To get the current behaviour, you can use a construct similar to the one below:\n"+example, false) 245 246 } 247 248 return p.Pathc() 249 } 250 251 // This is just a bridge method, use Path in templates. 252 func (p *pageMeta) Pathc() string { 253 if !p.File().IsZero() { 254 return p.File().Path() 255 } 256 return p.SectionsPath() 257 } 258 259 // RelatedKeywords implements the related.Document interface needed for fast page searches. 260 func (p *pageMeta) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) { 261 v, err := p.Param(cfg.Name) 262 if err != nil { 263 return nil, err 264 } 265 266 return cfg.ToKeywords(v) 267 } 268 269 func (p *pageMeta) IsSection() bool { 270 return p.Kind() == page.KindSection 271 } 272 273 func (p *pageMeta) Section() string { 274 if p.IsHome() { 275 return "" 276 } 277 278 if p.IsNode() { 279 if len(p.sections) == 0 { 280 // May be a sitemap or similar. 281 return "" 282 } 283 return p.sections[0] 284 } 285 286 if !p.File().IsZero() { 287 return p.File().Section() 288 } 289 290 panic("invalid page state") 291 } 292 293 func (p *pageMeta) SectionsEntries() []string { 294 return p.sections 295 } 296 297 func (p *pageMeta) SectionsPath() string { 298 return path.Join(p.SectionsEntries()...) 299 } 300 301 func (p *pageMeta) Sitemap() config.Sitemap { 302 return p.sitemap 303 } 304 305 func (p *pageMeta) Title() string { 306 return p.title 307 } 308 309 const defaultContentType = "page" 310 311 func (p *pageMeta) Type() string { 312 if p.contentType != "" { 313 return p.contentType 314 } 315 316 if sect := p.Section(); sect != "" { 317 return sect 318 } 319 320 return defaultContentType 321 } 322 323 func (p *pageMeta) Weight() int { 324 return p.weight 325 } 326 327 func (pm *pageMeta) mergeBucketCascades(b1, b2 *pagesMapBucket) { 328 if b1.cascade == nil { 329 b1.cascade = make(map[page.PageMatcher]maps.Params) 330 } 331 332 if b2 != nil && b2.cascade != nil { 333 for k, v := range b2.cascade { 334 335 vv, found := b1.cascade[k] 336 if !found { 337 b1.cascade[k] = v 338 } else { 339 // Merge 340 for ck, cv := range v { 341 if _, found := vv[ck]; !found { 342 vv[ck] = cv 343 } 344 } 345 } 346 } 347 } 348 } 349 350 func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, frontmatter map[string]any) error { 351 pm.params = make(maps.Params) 352 353 if frontmatter == nil && (parentBucket == nil || parentBucket.cascade == nil) { 354 return nil 355 } 356 357 if frontmatter != nil { 358 // Needed for case insensitive fetching of params values 359 maps.PrepareParams(frontmatter) 360 if p.bucket != nil { 361 // Check for any cascade define on itself. 362 if cv, found := frontmatter["cascade"]; found { 363 var err error 364 p.bucket.cascade, err = page.DecodeCascade(cv) 365 if err != nil { 366 return err 367 } 368 } 369 } 370 } else { 371 frontmatter = make(map[string]any) 372 } 373 374 var cascade map[page.PageMatcher]maps.Params 375 376 if p.bucket != nil { 377 if parentBucket != nil { 378 // Merge missing keys from parent into this. 379 pm.mergeBucketCascades(p.bucket, parentBucket) 380 } 381 cascade = p.bucket.cascade 382 } else if parentBucket != nil { 383 cascade = parentBucket.cascade 384 } 385 386 for m, v := range cascade { 387 if !m.Matches(p) { 388 continue 389 } 390 for kk, vv := range v { 391 if _, found := frontmatter[kk]; !found { 392 frontmatter[kk] = vv 393 } 394 } 395 } 396 397 var mtime time.Time 398 var contentBaseName string 399 if !p.File().IsZero() { 400 contentBaseName = p.File().ContentBaseName() 401 if p.File().FileInfo() != nil { 402 mtime = p.File().FileInfo().ModTime() 403 } 404 } 405 406 var gitAuthorDate time.Time 407 if p.gitInfo != nil { 408 gitAuthorDate = p.gitInfo.AuthorDate 409 } 410 411 descriptor := &pagemeta.FrontMatterDescriptor{ 412 Frontmatter: frontmatter, 413 Params: pm.params, 414 Dates: &pm.Dates, 415 PageURLs: &pm.urlPaths, 416 BaseFilename: contentBaseName, 417 ModTime: mtime, 418 GitAuthorDate: gitAuthorDate, 419 Location: langs.GetLocation(pm.s.Language()), 420 } 421 422 // Handle the date separately 423 // TODO(bep) we need to "do more" in this area so this can be split up and 424 // more easily tested without the Page, but the coupling is strong. 425 err := pm.s.frontmatterHandler.HandleDates(descriptor) 426 if err != nil { 427 p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err) 428 } 429 430 pm.buildConfig, err = pagemeta.DecodeBuildConfig(frontmatter["_build"]) 431 if err != nil { 432 return err 433 } 434 435 var sitemapSet bool 436 437 var draft, published, isCJKLanguage *bool 438 for k, v := range frontmatter { 439 loki := strings.ToLower(k) 440 441 if loki == "published" { // Intentionally undocumented 442 vv, err := cast.ToBoolE(v) 443 if err == nil { 444 published = &vv 445 } 446 // published may also be a date 447 continue 448 } 449 450 if pm.s.frontmatterHandler.IsDateKey(loki) { 451 continue 452 } 453 454 switch loki { 455 case "title": 456 pm.title = cast.ToString(v) 457 pm.params[loki] = pm.title 458 case "linktitle": 459 pm.linkTitle = cast.ToString(v) 460 pm.params[loki] = pm.linkTitle 461 case "summary": 462 pm.summary = cast.ToString(v) 463 pm.params[loki] = pm.summary 464 case "description": 465 pm.description = cast.ToString(v) 466 pm.params[loki] = pm.description 467 case "slug": 468 // Don't start or end with a - 469 pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-") 470 pm.params[loki] = pm.Slug() 471 case "url": 472 url := cast.ToString(v) 473 if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { 474 return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle()) 475 } 476 lang := p.s.GetLanguagePrefix() 477 if lang != "" && !strings.HasPrefix(url, "/") && strings.HasPrefix(url, lang+"/") { 478 if strings.HasPrefix(hugo.CurrentVersion.String(), "0.55") { 479 // We added support for page relative URLs in Hugo 0.55 and 480 // this may get its language path added twice. 481 // TODO(bep) eventually remove this. 482 p.s.Log.Warnf(`Front matter in %q with the url %q with no leading / has what looks like the language prefix added. In Hugo 0.55 we added support for page relative URLs in front matter, no language prefix needed. Check the URL and consider to either add a leading / or remove the language prefix.`, p.pathOrTitle(), url) 483 } 484 } 485 pm.urlPaths.URL = url 486 pm.params[loki] = url 487 case "type": 488 pm.contentType = cast.ToString(v) 489 pm.params[loki] = pm.contentType 490 case "keywords": 491 pm.keywords = cast.ToStringSlice(v) 492 pm.params[loki] = pm.keywords 493 case "headless": 494 // Legacy setting for leaf bundles. 495 // This is since Hugo 0.63 handled in a more general way for all 496 // pages. 497 isHeadless := cast.ToBool(v) 498 pm.params[loki] = isHeadless 499 if p.File().TranslationBaseName() == "index" && isHeadless { 500 pm.buildConfig.List = pagemeta.Never 501 pm.buildConfig.Render = pagemeta.Never 502 } 503 case "outputs": 504 o := cast.ToStringSlice(v) 505 if len(o) > 0 { 506 // Output formats are explicitly set in front matter, use those. 507 outFormats, err := p.s.outputFormatsConfig.GetByNames(o...) 508 509 if err != nil { 510 p.s.Log.Errorf("Failed to resolve output formats: %s", err) 511 } else { 512 pm.configuredOutputFormats = outFormats 513 pm.params[loki] = outFormats 514 } 515 516 } 517 case "draft": 518 draft = new(bool) 519 *draft = cast.ToBool(v) 520 case "layout": 521 pm.layout = cast.ToString(v) 522 pm.params[loki] = pm.layout 523 case "markup": 524 pm.markup = cast.ToString(v) 525 pm.params[loki] = pm.markup 526 case "weight": 527 pm.weight = cast.ToInt(v) 528 pm.params[loki] = pm.weight 529 case "aliases": 530 pm.aliases = cast.ToStringSlice(v) 531 for i, alias := range pm.aliases { 532 if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { 533 return fmt.Errorf("http* aliases not supported: %q", alias) 534 } 535 pm.aliases[i] = filepath.ToSlash(alias) 536 } 537 pm.params[loki] = pm.aliases 538 case "sitemap": 539 p.m.sitemap = config.DecodeSitemap(p.s.siteCfg.sitemap, maps.ToStringMap(v)) 540 pm.params[loki] = p.m.sitemap 541 sitemapSet = true 542 case "iscjklanguage": 543 isCJKLanguage = new(bool) 544 *isCJKLanguage = cast.ToBool(v) 545 case "translationkey": 546 pm.translationKey = cast.ToString(v) 547 pm.params[loki] = pm.translationKey 548 case "resources": 549 var resources []map[string]any 550 handled := true 551 552 switch vv := v.(type) { 553 case []map[any]any: 554 for _, vvv := range vv { 555 resources = append(resources, maps.ToStringMap(vvv)) 556 } 557 case []map[string]any: 558 resources = append(resources, vv...) 559 case []any: 560 for _, vvv := range vv { 561 switch vvvv := vvv.(type) { 562 case map[any]any: 563 resources = append(resources, maps.ToStringMap(vvvv)) 564 case map[string]any: 565 resources = append(resources, vvvv) 566 } 567 } 568 default: 569 handled = false 570 } 571 572 if handled { 573 pm.params[loki] = resources 574 pm.resourcesMetadata = resources 575 break 576 } 577 fallthrough 578 579 default: 580 // If not one of the explicit values, store in Params 581 switch vv := v.(type) { 582 case bool: 583 pm.params[loki] = vv 584 case string: 585 pm.params[loki] = vv 586 case int64, int32, int16, int8, int: 587 pm.params[loki] = vv 588 case float64, float32: 589 pm.params[loki] = vv 590 case time.Time: 591 pm.params[loki] = vv 592 default: // handle array of strings as well 593 switch vvv := vv.(type) { 594 case []any: 595 if len(vvv) > 0 { 596 switch vvv[0].(type) { 597 case map[any]any: 598 pm.params[loki] = vvv 599 case map[string]any: 600 pm.params[loki] = vvv 601 case []any: 602 pm.params[loki] = vvv 603 default: 604 a := make([]string, len(vvv)) 605 for i, u := range vvv { 606 a[i] = cast.ToString(u) 607 } 608 609 pm.params[loki] = a 610 } 611 } else { 612 pm.params[loki] = []string{} 613 } 614 default: 615 pm.params[loki] = vv 616 } 617 } 618 } 619 } 620 621 if !sitemapSet { 622 pm.sitemap = p.s.siteCfg.sitemap 623 } 624 625 pm.markup = p.s.ContentSpec.ResolveMarkup(pm.markup) 626 627 if draft != nil && published != nil { 628 pm.draft = *draft 629 p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename()) 630 } else if draft != nil { 631 pm.draft = *draft 632 } else if published != nil { 633 pm.draft = !*published 634 } 635 pm.params["draft"] = pm.draft 636 637 if isCJKLanguage != nil { 638 pm.isCJKLanguage = *isCJKLanguage 639 } else if p.s.siteCfg.hasCJKLanguage && p.source.parsed != nil { 640 if cjkRe.Match(p.source.parsed.Input()) { 641 pm.isCJKLanguage = true 642 } else { 643 pm.isCJKLanguage = false 644 } 645 } 646 647 pm.params["iscjklanguage"] = p.m.isCJKLanguage 648 649 return nil 650 } 651 652 func (p *pageMeta) noListAlways() bool { 653 return p.buildConfig.List != pagemeta.Always 654 } 655 656 func (p *pageMeta) getListFilter(local bool) contentTreeNodeCallback { 657 return newContentTreeFilter(func(n *contentNode) bool { 658 if n == nil { 659 return true 660 } 661 662 var shouldList bool 663 switch n.p.m.buildConfig.List { 664 case pagemeta.Always: 665 shouldList = true 666 case pagemeta.Never: 667 shouldList = false 668 case pagemeta.ListLocally: 669 shouldList = local 670 } 671 672 return !shouldList 673 }) 674 } 675 676 func (p *pageMeta) noRender() bool { 677 return p.buildConfig.Render != pagemeta.Always 678 } 679 680 func (p *pageMeta) noLink() bool { 681 return p.buildConfig.Render == pagemeta.Never 682 } 683 684 func (p *pageMeta) applyDefaultValues(n *contentNode) error { 685 if p.buildConfig.IsZero() { 686 p.buildConfig, _ = pagemeta.DecodeBuildConfig(nil) 687 } 688 689 if !p.s.isEnabled(p.Kind()) { 690 (&p.buildConfig).Disable() 691 } 692 693 if p.markup == "" { 694 if !p.File().IsZero() { 695 // Fall back to file extension 696 p.markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) 697 } 698 if p.markup == "" { 699 p.markup = "markdown" 700 } 701 } 702 703 if p.title == "" && p.f.IsZero() { 704 switch p.Kind() { 705 case page.KindHome: 706 p.title = p.s.Info.title 707 case page.KindSection: 708 var sectionName string 709 if n != nil { 710 sectionName = n.rootSection() 711 } else { 712 sectionName = p.sections[0] 713 } 714 715 sectionName = helpers.FirstUpper(sectionName) 716 if p.s.Cfg.GetBool("pluralizeListTitles") { 717 p.title = flect.Pluralize(sectionName) 718 } else { 719 p.title = sectionName 720 } 721 case page.KindTerm: 722 // TODO(bep) improve 723 key := p.sections[len(p.sections)-1] 724 p.title = strings.Replace(p.s.titleFunc(key), "-", " ", -1) 725 case page.KindTaxonomy: 726 p.title = p.s.titleFunc(p.sections[0]) 727 case kind404: 728 p.title = "404 Page not found" 729 730 } 731 } 732 733 if p.IsNode() { 734 p.bundleType = files.ContentClassBranch 735 } else { 736 source := p.File() 737 if fi, ok := source.(*fileInfo); ok { 738 class := fi.FileInfo().Meta().Classifier 739 switch class { 740 case files.ContentClassBranch, files.ContentClassLeaf: 741 p.bundleType = class 742 } 743 } 744 } 745 746 return nil 747 } 748 749 func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.Converter, error) { 750 if ps == nil { 751 panic("no Page provided") 752 } 753 cp := p.s.ContentSpec.Converters.Get(markup) 754 if cp == nil { 755 return converter.NopConverter, fmt.Errorf("no content renderer found for markup %q", p.markup) 756 } 757 758 var id string 759 var filename string 760 var path string 761 if !p.f.IsZero() { 762 id = p.f.UniqueID() 763 filename = p.f.Filename() 764 path = p.f.Path() 765 } else { 766 path = p.Pathc() 767 } 768 769 cpp, err := cp.New( 770 converter.DocumentContext{ 771 Document: newPageForRenderHook(ps), 772 DocumentID: id, 773 DocumentName: path, 774 Filename: filename, 775 }, 776 ) 777 if err != nil { 778 return converter.NopConverter, err 779 } 780 781 return cpp, nil 782 } 783 784 // The output formats this page will be rendered to. 785 func (m *pageMeta) outputFormats() output.Formats { 786 if len(m.configuredOutputFormats) > 0 { 787 return m.configuredOutputFormats 788 } 789 790 return m.s.outputFormats[m.Kind()] 791 } 792 793 func (p *pageMeta) Slug() string { 794 return p.urlPaths.Slug 795 } 796 797 func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any { 798 v := m.Params()[strings.ToLower(key)] 799 800 if v == nil { 801 return nil 802 } 803 804 switch val := v.(type) { 805 case bool: 806 return val 807 case string: 808 if stringToLower { 809 return strings.ToLower(val) 810 } 811 return val 812 case int64, int32, int16, int8, int: 813 return cast.ToInt(v) 814 case float64, float32: 815 return cast.ToFloat64(v) 816 case time.Time: 817 return val 818 case []string: 819 if stringToLower { 820 return helpers.SliceToLower(val) 821 } 822 return v 823 default: 824 return v 825 } 826 } 827 828 func getParamToLower(m resource.ResourceParamsProvider, key string) any { 829 return getParam(m, key, true) 830 }