github.com/neohugo/neohugo@v0.123.8/hugolib/page__meta.go (about) 1 // Copyright 2024 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 "context" 18 "fmt" 19 "path/filepath" 20 "regexp" 21 "strings" 22 "time" 23 24 "github.com/gobuffalo/flect" 25 "github.com/neohugo/neohugo/identity" 26 "github.com/neohugo/neohugo/langs" 27 "github.com/neohugo/neohugo/markup/converter" 28 "github.com/neohugo/neohugo/related" 29 xmaps "golang.org/x/exp/maps" 30 31 "github.com/neohugo/neohugo/source" 32 33 "github.com/neohugo/neohugo/common/constants" 34 "github.com/neohugo/neohugo/common/loggers" 35 "github.com/neohugo/neohugo/common/maps" 36 "github.com/neohugo/neohugo/common/neohugo" 37 "github.com/neohugo/neohugo/common/paths" 38 "github.com/neohugo/neohugo/config" 39 "github.com/neohugo/neohugo/helpers" 40 41 "github.com/neohugo/neohugo/output" 42 "github.com/neohugo/neohugo/resources/kinds" 43 "github.com/neohugo/neohugo/resources/page" 44 "github.com/neohugo/neohugo/resources/page/pagemeta" 45 "github.com/neohugo/neohugo/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 term string // Set for kind == KindTerm. 53 singular string // Set for kind == KindTerm and kind == KindTaxonomy. 54 55 resource.Staler 56 pageMetaParams 57 pageMetaFrontMatter 58 59 // Set for standalone pages, e.g. robotsTXT. 60 standaloneOutputFormat output.Format 61 62 resourcePath string // Set for bundled pages; path relative to its bundle root. 63 bundled bool // Set if this page is bundled inside another. 64 65 pathInfo *paths.Path // Always set. This the canonical path to the Page. 66 f *source.File 67 68 content *cachedContent // The source and the parsed page content. 69 70 s *Site // The site this page belongs to. 71 } 72 73 // Prepare for a rebuild of the data passed in from front matter. 74 func (m *pageMeta) setMetaPostPrepareRebuild() { 75 params := xmaps.Clone[map[string]any](m.paramsOriginal) 76 m.pageMetaParams.pageConfig.Params = params 77 m.pageMetaFrontMatter = pageMetaFrontMatter{} 78 } 79 80 type pageMetaParams struct { 81 setMetaPostCount int 82 setMetaPostCascadeChanged bool 83 84 pageConfig *pagemeta.PageConfig 85 86 // These are only set in watch mode. 87 datesOriginal pagemeta.Dates 88 paramsOriginal map[string]any // contains the original params as defined in the front matter. 89 cascadeOriginal map[page.PageMatcher]maps.Params // contains the original cascade as defined in the front matter. 90 } 91 92 // From page front matter. 93 type pageMetaFrontMatter struct { 94 configuredOutputFormats output.Formats // outputs defined in front matter. 95 } 96 97 func (m *pageMetaParams) init(preserveOringal bool) { 98 if preserveOringal { 99 m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params) 100 m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.Cascade) 101 } 102 } 103 104 func (p *pageMeta) Aliases() []string { 105 return p.pageConfig.Aliases 106 } 107 108 // Deprecated: use taxonomies. 109 func (p *pageMeta) Author() page.Author { 110 neohugo.Deprecate(".Author", "Use taxonomies.", "v0.98.0") 111 authors := p.Authors() 112 113 for _, author := range authors { 114 return author 115 } 116 return page.Author{} 117 } 118 119 // Deprecated: use taxonomies. 120 func (p *pageMeta) Authors() page.AuthorList { 121 neohugo.Deprecate(".Author", "Use taxonomies.", "v0.112.0") 122 return nil 123 } 124 125 func (p *pageMeta) BundleType() string { 126 switch p.pathInfo.BundleType() { 127 case paths.PathTypeLeaf: 128 return "leaf" 129 case paths.PathTypeBranch: 130 return "branch" 131 default: 132 return "" 133 } 134 } 135 136 func (p *pageMeta) Date() time.Time { 137 return p.pageConfig.Date 138 } 139 140 func (p *pageMeta) PublishDate() time.Time { 141 return p.pageConfig.PublishDate 142 } 143 144 func (p *pageMeta) Lastmod() time.Time { 145 return p.pageConfig.Lastmod 146 } 147 148 func (p *pageMeta) ExpiryDate() time.Time { 149 return p.pageConfig.ExpiryDate 150 } 151 152 func (p *pageMeta) Description() string { 153 return p.pageConfig.Description 154 } 155 156 func (p *pageMeta) Lang() string { 157 return p.s.Lang() 158 } 159 160 func (p *pageMeta) Draft() bool { 161 return p.pageConfig.Draft 162 } 163 164 func (p *pageMeta) File() *source.File { 165 return p.f 166 } 167 168 func (p *pageMeta) IsHome() bool { 169 return p.Kind() == kinds.KindHome 170 } 171 172 func (p *pageMeta) Keywords() []string { 173 return p.pageConfig.Keywords 174 } 175 176 func (p *pageMeta) Kind() string { 177 return p.pageConfig.Kind 178 } 179 180 func (p *pageMeta) Layout() string { 181 return p.pageConfig.Layout 182 } 183 184 func (p *pageMeta) LinkTitle() string { 185 if p.pageConfig.LinkTitle != "" { 186 return p.pageConfig.LinkTitle 187 } 188 189 return p.Title() 190 } 191 192 func (p *pageMeta) Name() string { 193 if p.resourcePath != "" { 194 return p.resourcePath 195 } 196 if p.pageConfig.Kind == kinds.KindTerm { 197 return p.pathInfo.Unnormalized().BaseNameNoIdentifier() 198 } 199 return p.Title() 200 } 201 202 func (p *pageMeta) IsNode() bool { 203 return !p.IsPage() 204 } 205 206 func (p *pageMeta) IsPage() bool { 207 return p.Kind() == kinds.KindPage 208 } 209 210 // Param is a convenience method to do lookups in Page's and Site's Params map, 211 // in that order. 212 // 213 // This method is also implemented on SiteInfo. 214 // TODO(bep) interface 215 func (p *pageMeta) Param(key any) (any, error) { 216 return resource.Param(p, p.s.Params(), key) 217 } 218 219 func (p *pageMeta) Params() maps.Params { 220 return p.pageConfig.Params 221 } 222 223 func (p *pageMeta) Path() string { 224 return p.pathInfo.Base() 225 } 226 227 func (p *pageMeta) PathInfo() *paths.Path { 228 return p.pathInfo 229 } 230 231 // RelatedKeywords implements the related.Document interface needed for fast page searches. 232 func (p *pageMeta) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) { 233 v, err := p.Param(cfg.Name) 234 if err != nil { 235 return nil, err 236 } 237 238 return cfg.ToKeywords(v) 239 } 240 241 func (p *pageMeta) IsSection() bool { 242 return p.Kind() == kinds.KindSection 243 } 244 245 func (p *pageMeta) Section() string { 246 return p.pathInfo.Section() 247 } 248 249 func (p *pageMeta) Sitemap() config.SitemapConfig { 250 return p.pageConfig.Sitemap 251 } 252 253 func (p *pageMeta) Title() string { 254 return p.pageConfig.Title 255 } 256 257 const defaultContentType = "page" 258 259 func (p *pageMeta) Type() string { 260 if p.pageConfig.Type != "" { 261 return p.pageConfig.Type 262 } 263 264 if sect := p.Section(); sect != "" { 265 return sect 266 } 267 268 return defaultContentType 269 } 270 271 func (p *pageMeta) Weight() int { 272 return p.pageConfig.Weight 273 } 274 275 func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf config.AllProvider) error { 276 frontmatter := pi.frontMatter 277 if frontmatter != nil { 278 pcfg := p.pageConfig 279 if pcfg == nil { 280 panic("pageConfig not set") 281 } 282 // Needed for case insensitive fetching of params values 283 maps.PrepareParams(frontmatter) 284 pcfg.Params = frontmatter 285 // Check for any cascade define on itself. 286 if cv, found := frontmatter["cascade"]; found { 287 var err error 288 cascade, err := page.DecodeCascade(logger, cv) 289 if err != nil { 290 return err 291 } 292 pcfg.Cascade = cascade 293 } 294 295 // Look for path, lang and kind, all of which values we need early on. 296 if v, found := frontmatter["path"]; found { 297 pcfg.Path = paths.ToSlashPreserveLeading(cast.ToString(v)) 298 pcfg.Params["path"] = pcfg.Path 299 } 300 if v, found := frontmatter["lang"]; found { 301 lang := strings.ToLower(cast.ToString(v)) 302 if _, ok := conf.PathParser().LanguageIndex[lang]; ok { 303 pcfg.Lang = lang 304 pcfg.Params["lang"] = pcfg.Lang 305 } 306 } 307 if v, found := frontmatter["kind"]; found { 308 s := cast.ToString(v) 309 if s != "" { 310 pcfg.Kind = kinds.GetKindMain(s) 311 if pcfg.Kind == "" { 312 return fmt.Errorf("unknown kind %q in front matter", s) 313 } 314 pcfg.Params["kind"] = pcfg.Kind 315 } 316 } 317 } else if p.pageMetaParams.pageConfig.Params == nil { 318 p.pageConfig.Params = make(maps.Params) 319 } 320 321 p.pageMetaParams.init(conf.Watching()) 322 323 return nil 324 } 325 326 func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error { 327 ps.m.setMetaPostCount++ 328 var cascadeHashPre uint64 329 if ps.m.setMetaPostCount > 1 { 330 cascadeHashPre = identity.HashUint64(ps.m.pageConfig.Cascade) 331 ps.m.pageConfig.Cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal) 332 333 } 334 335 // Apply cascades first so they can be overridden later. 336 if cascade != nil { 337 if ps.m.pageConfig.Cascade != nil { 338 for k, v := range cascade { 339 vv, found := ps.m.pageConfig.Cascade[k] 340 if !found { 341 ps.m.pageConfig.Cascade[k] = v 342 } else { 343 // Merge 344 for ck, cv := range v { 345 if _, found := vv[ck]; !found { 346 vv[ck] = cv 347 } 348 } 349 } 350 } 351 cascade = ps.m.pageConfig.Cascade 352 } else { 353 ps.m.pageConfig.Cascade = cascade 354 } 355 } 356 357 if cascade == nil { 358 cascade = ps.m.pageConfig.Cascade 359 } 360 361 if ps.m.setMetaPostCount > 1 { 362 ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.pageConfig.Cascade) 363 if !ps.m.setMetaPostCascadeChanged { 364 // No changes, restore any value that may be changed by aggregation. 365 ps.m.pageConfig.Dates = ps.m.datesOriginal 366 return nil 367 } 368 ps.m.setMetaPostPrepareRebuild() 369 370 } 371 372 // Cascade is also applied to itself. 373 for m, v := range cascade { 374 if !m.Matches(ps) { 375 continue 376 } 377 for kk, vv := range v { 378 if _, found := ps.m.pageConfig.Params[kk]; !found { 379 ps.m.pageConfig.Params[kk] = vv 380 } 381 } 382 } 383 384 if err := ps.setMetaPostParams(); err != nil { 385 return err 386 } 387 388 if err := ps.m.applyDefaultValues(); err != nil { 389 return err 390 } 391 392 // Store away any original values that may be changed from aggregation. 393 ps.m.datesOriginal = ps.m.pageConfig.Dates 394 395 return nil 396 } 397 398 func (p *pageState) setMetaPostParams() error { 399 pm := p.m 400 var mtime time.Time 401 var contentBaseName string 402 if p.File() != nil { 403 contentBaseName = p.File().ContentBaseName() 404 if p.File().FileInfo() != nil { 405 mtime = p.File().FileInfo().ModTime() 406 } 407 } 408 409 var gitAuthorDate time.Time 410 if !p.gitInfo.IsZero() { 411 gitAuthorDate = p.gitInfo.AuthorDate 412 } 413 414 descriptor := &pagemeta.FrontMatterDescriptor{ 415 PageConfig: pm.pageConfig, 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 var buildConfig any 431 var isNewBuildKeyword bool 432 if v, ok := pm.pageConfig.Params["_build"]; ok { 433 buildConfig = v 434 } else { 435 buildConfig = pm.pageConfig.Params["build"] 436 isNewBuildKeyword = true 437 } 438 pm.pageConfig.Build, err = pagemeta.DecodeBuildConfig(buildConfig) 439 if err != nil { 440 var msgDetail string 441 if isNewBuildKeyword { 442 msgDetail = `. We renamed the _build keyword to build in Hugo 0.123.0. We recommend putting user defined params in the params section, e.g.: 443 --- 444 title: "My Title" 445 params: 446 build: "My Build" 447 --- 448 ยด 449 450 ` 451 } 452 return fmt.Errorf("failed to decode build config in front matter: %s%s", err, msgDetail) 453 } 454 455 var sitemapSet bool 456 457 pcfg := pm.pageConfig 458 459 params := pcfg.Params 460 461 var draft, published, isCJKLanguage *bool 462 var userParams map[string]any 463 for k, v := range pcfg.Params { 464 loki := strings.ToLower(k) 465 466 if loki == "params" { 467 vv, err := maps.ToStringMapE(v) 468 if err != nil { 469 return err 470 } 471 userParams = vv 472 delete(pcfg.Params, k) 473 continue 474 } 475 476 if loki == "published" { // Intentionally undocumented 477 vv, err := cast.ToBoolE(v) 478 if err == nil { 479 published = &vv 480 } 481 // published may also be a date 482 continue 483 } 484 485 if pm.s.frontmatterHandler.IsDateKey(loki) { 486 continue 487 } 488 489 switch loki { 490 case "title": 491 pcfg.Title = cast.ToString(v) 492 params[loki] = pcfg.Title 493 case "linktitle": 494 pcfg.LinkTitle = cast.ToString(v) 495 params[loki] = pcfg.LinkTitle 496 case "summary": 497 pcfg.Summary = cast.ToString(v) 498 params[loki] = pcfg.Summary 499 case "description": 500 pcfg.Description = cast.ToString(v) 501 params[loki] = pcfg.Description 502 case "slug": 503 // Don't start or end with a - 504 pcfg.Slug = strings.Trim(cast.ToString(v), "-") 505 params[loki] = pm.Slug() 506 case "url": 507 url := cast.ToString(v) 508 if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { 509 return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle()) 510 } 511 pcfg.URL = url 512 params[loki] = url 513 case "type": 514 pcfg.Type = cast.ToString(v) 515 params[loki] = pcfg.Type 516 case "keywords": 517 pcfg.Keywords = cast.ToStringSlice(v) 518 params[loki] = pcfg.Keywords 519 case "headless": 520 // Legacy setting for leaf bundles. 521 // This is since Hugo 0.63 handled in a more general way for all 522 // pages. 523 isHeadless := cast.ToBool(v) 524 params[loki] = isHeadless 525 if isHeadless { 526 pm.pageConfig.Build.List = pagemeta.Never 527 pm.pageConfig.Build.Render = pagemeta.Never 528 } 529 case "outputs": 530 o := cast.ToStringSlice(v) 531 // lower case names: 532 for i, s := range o { 533 o[i] = strings.ToLower(s) 534 } 535 if len(o) > 0 { 536 // Output formats are explicitly set in front matter, use those. 537 outFormats, err := p.s.conf.OutputFormats.Config.GetByNames(o...) 538 if err != nil { 539 p.s.Log.Errorf("Failed to resolve output formats: %s", err) 540 } else { 541 pm.configuredOutputFormats = outFormats 542 params[loki] = outFormats 543 } 544 } 545 case "draft": 546 draft = new(bool) 547 *draft = cast.ToBool(v) 548 case "layout": 549 pcfg.Layout = cast.ToString(v) 550 params[loki] = pcfg.Layout 551 case "markup": 552 pcfg.Markup = cast.ToString(v) 553 params[loki] = pcfg.Markup 554 case "weight": 555 pcfg.Weight = cast.ToInt(v) 556 params[loki] = pcfg.Weight 557 case "aliases": 558 pcfg.Aliases = cast.ToStringSlice(v) 559 for i, alias := range pcfg.Aliases { 560 if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { 561 return fmt.Errorf("http* aliases not supported: %q", alias) 562 } 563 pcfg.Aliases[i] = filepath.ToSlash(alias) 564 } 565 params[loki] = pcfg.Aliases 566 case "sitemap": 567 pcfg.Sitemap, err = config.DecodeSitemap(p.s.conf.Sitemap, maps.ToStringMap(v)) 568 if err != nil { 569 return fmt.Errorf("failed to decode sitemap config in front matter: %s", err) 570 } 571 sitemapSet = true 572 case "iscjklanguage": 573 isCJKLanguage = new(bool) 574 *isCJKLanguage = cast.ToBool(v) 575 case "translationkey": 576 pcfg.TranslationKey = cast.ToString(v) 577 params[loki] = pcfg.TranslationKey 578 case "resources": 579 var resources []map[string]any 580 handled := true 581 582 switch vv := v.(type) { 583 case []map[any]any: 584 for _, vvv := range vv { 585 resources = append(resources, maps.ToStringMap(vvv)) 586 } 587 case []map[string]any: 588 resources = append(resources, vv...) 589 case []any: 590 for _, vvv := range vv { 591 switch vvvv := vvv.(type) { 592 case map[any]any: 593 resources = append(resources, maps.ToStringMap(vvvv)) 594 case map[string]any: 595 resources = append(resources, vvvv) 596 } 597 } 598 default: 599 handled = false 600 } 601 602 if handled { 603 pcfg.Resources = resources 604 break 605 } 606 fallthrough 607 default: 608 // If not one of the explicit values, store in Params 609 switch vv := v.(type) { 610 case []any: 611 if len(vv) > 0 { 612 allStrings := true 613 for _, vvv := range vv { 614 if _, ok := vvv.(string); !ok { 615 allStrings = false 616 break 617 } 618 } 619 if allStrings { 620 // We need tags, keywords etc. to be []string, not []interface{}. 621 a := make([]string, len(vv)) 622 for i, u := range vv { 623 a[i] = cast.ToString(u) 624 } 625 params[loki] = a 626 } else { 627 params[loki] = vv 628 } 629 } else { 630 params[loki] = []string{} 631 } 632 633 default: 634 params[loki] = vv 635 } 636 } 637 } 638 639 for k, v := range userParams { 640 if _, found := params[k]; found { 641 p.s.Log.Warnidf(constants.WarnFrontMatterParamsOverrides, "Hugo front matter key %q is overridden in params section.", k) 642 } 643 params[strings.ToLower(k)] = v 644 } 645 646 if !sitemapSet { 647 pcfg.Sitemap = p.s.conf.Sitemap 648 } 649 650 pcfg.Markup = p.s.ContentSpec.ResolveMarkup(pcfg.Markup) 651 652 if draft != nil && published != nil { 653 pcfg.Draft = *draft 654 p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename()) 655 } else if draft != nil { 656 pcfg.Draft = *draft 657 } else if published != nil { 658 pcfg.Draft = !*published 659 } 660 params["draft"] = pcfg.Draft 661 662 if isCJKLanguage != nil { 663 pcfg.IsCJKLanguage = *isCJKLanguage 664 } else if p.s.conf.HasCJKLanguage && p.m.content.pi.openSource != nil { 665 if cjkRe.Match(p.m.content.mustSource()) { 666 pcfg.IsCJKLanguage = true 667 } else { 668 pcfg.IsCJKLanguage = false 669 } 670 } 671 672 params["iscjklanguage"] = pcfg.IsCJKLanguage 673 674 return nil 675 } 676 677 // shouldList returns whether this page should be included in the list of pages. 678 // glogal indicates site.Pages etc. 679 func (p *pageMeta) shouldList(global bool) bool { 680 if p.isStandalone() { 681 // Never list 404, sitemap and similar. 682 return false 683 } 684 685 switch p.pageConfig.Build.List { 686 case pagemeta.Always: 687 return true 688 case pagemeta.Never: 689 return false 690 case pagemeta.ListLocally: 691 return !global 692 } 693 return false 694 } 695 696 func (p *pageMeta) shouldListAny() bool { 697 return p.shouldList(true) || p.shouldList(false) 698 } 699 700 func (p *pageMeta) isStandalone() bool { 701 return !p.standaloneOutputFormat.IsZero() 702 } 703 704 func (p *pageMeta) shouldBeCheckedForMenuDefinitions() bool { 705 if !p.shouldList(false) { 706 return false 707 } 708 709 return p.pageConfig.Kind == kinds.KindHome || p.pageConfig.Kind == kinds.KindSection || p.pageConfig.Kind == kinds.KindPage 710 } 711 712 func (p *pageMeta) noRender() bool { 713 return p.pageConfig.Build.Render != pagemeta.Always 714 } 715 716 func (p *pageMeta) noLink() bool { 717 return p.pageConfig.Build.Render == pagemeta.Never 718 } 719 720 func (p *pageMeta) applyDefaultValues() error { 721 if p.pageConfig.Build.IsZero() { 722 p.pageConfig.Build, _ = pagemeta.DecodeBuildConfig(nil) 723 } 724 725 if !p.s.conf.IsKindEnabled(p.Kind()) { 726 (&p.pageConfig.Build).Disable() 727 } 728 729 if p.pageConfig.Markup == "" { 730 if p.File() != nil { 731 // Fall back to file extension 732 p.pageConfig.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) 733 } 734 if p.pageConfig.Markup == "" { 735 p.pageConfig.Markup = "markdown" 736 } 737 } 738 739 if p.pageConfig.Title == "" && p.f == nil { 740 switch p.Kind() { 741 case kinds.KindHome: 742 p.pageConfig.Title = p.s.Title() 743 case kinds.KindSection: 744 sectionName := p.pathInfo.Unnormalized().BaseNameNoIdentifier() 745 if p.s.conf.PluralizeListTitles { 746 sectionName = flect.Pluralize(sectionName) 747 } 748 if p.s.conf.CapitalizeListTitles { 749 sectionName = p.s.conf.C.CreateTitle(sectionName) 750 } 751 p.pageConfig.Title = sectionName 752 case kinds.KindTerm: 753 if p.term != "" { 754 if p.s.conf.CapitalizeListTitles { 755 p.pageConfig.Title = p.s.conf.C.CreateTitle(p.term) 756 } else { 757 p.pageConfig.Title = p.term 758 } 759 } else { 760 panic("term not set") 761 } 762 case kinds.KindTaxonomy: 763 if p.s.conf.CapitalizeListTitles { 764 p.pageConfig.Title = strings.Replace(p.s.conf.C.CreateTitle(p.pathInfo.Unnormalized().BaseNameNoIdentifier()), "-", " ", -1) 765 } else { 766 p.pageConfig.Title = strings.Replace(p.pathInfo.Unnormalized().BaseNameNoIdentifier(), "-", " ", -1) 767 } 768 case kinds.KindStatus404: 769 p.pageConfig.Title = "404 Page not found" 770 } 771 } 772 773 return nil 774 } 775 776 func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.Converter, error) { 777 if ps == nil { 778 panic("no Page provided") 779 } 780 cp := p.s.ContentSpec.Converters.Get(markup) 781 if cp == nil { 782 return converter.NopConverter, fmt.Errorf("no content renderer found for markup %q, page: %s", markup, ps.getPageInfoForError()) 783 } 784 785 var id string 786 var filename string 787 var path string 788 if p.f != nil { 789 id = p.f.UniqueID() 790 filename = p.f.Filename() 791 path = p.f.Path() 792 } else { 793 path = p.Path() 794 } 795 796 cpp, err := cp.New( 797 converter.DocumentContext{ 798 Document: newPageForRenderHook(ps), 799 DocumentID: id, 800 DocumentName: path, 801 Filename: filename, 802 }, 803 ) 804 if err != nil { 805 return converter.NopConverter, err 806 } 807 808 return cpp, nil 809 } 810 811 // The output formats this page will be rendered to. 812 func (m *pageMeta) outputFormats() output.Formats { 813 if len(m.configuredOutputFormats) > 0 { 814 return m.configuredOutputFormats 815 } 816 return m.s.conf.C.KindOutputFormats[m.Kind()] 817 } 818 819 func (p *pageMeta) Slug() string { 820 return p.pageConfig.Slug 821 } 822 823 func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any { 824 v := m.Params()[strings.ToLower(key)] 825 826 if v == nil { 827 return nil 828 } 829 830 switch val := v.(type) { 831 case bool: 832 return val 833 case string: 834 if stringToLower { 835 return strings.ToLower(val) 836 } 837 return val 838 case int64, int32, int16, int8, int: 839 return cast.ToInt(v) 840 case float64, float32: 841 return cast.ToFloat64(v) 842 case time.Time: 843 return val 844 case []string: 845 if stringToLower { 846 return helpers.SliceToLower(val) 847 } 848 return v 849 default: 850 return v 851 } 852 } 853 854 func getParamToLower(m resource.ResourceParamsProvider, key string) any { 855 return getParam(m, key, true) 856 } 857 858 func (ps *pageState) initLazyProviders() error { 859 ps.init.Add(func(ctx context.Context) (any, error) { 860 pp, err := newPagePaths(ps) 861 if err != nil { 862 return nil, err 863 } 864 865 var outputFormatsForPage output.Formats 866 var renderFormats output.Formats 867 868 if ps.m.standaloneOutputFormat.IsZero() { 869 outputFormatsForPage = ps.m.outputFormats() 870 renderFormats = ps.s.h.renderFormats 871 } else { 872 // One of the fixed output format pages, e.g. 404. 873 outputFormatsForPage = output.Formats{ps.m.standaloneOutputFormat} 874 renderFormats = outputFormatsForPage 875 } 876 877 // Prepare output formats for all sites. 878 // We do this even if this page does not get rendered on 879 // its own. It may be referenced via one of the site collections etc. 880 // it will then need an output format. 881 ps.pageOutputs = make([]*pageOutput, len(renderFormats)) 882 created := make(map[string]*pageOutput) 883 shouldRenderPage := !ps.m.noRender() 884 885 for i, f := range renderFormats { 886 887 if po, found := created[f.Name]; found { 888 ps.pageOutputs[i] = po 889 continue 890 } 891 892 render := shouldRenderPage 893 if render { 894 _, render = outputFormatsForPage.GetByName(f.Name) 895 } 896 897 po := newPageOutput(ps, pp, f, render) 898 899 // Create a content provider for the first, 900 // we may be able to reuse it. 901 if i == 0 { 902 contentProvider, err := newPageContentOutput(po) 903 if err != nil { 904 return nil, err 905 } 906 po.setContentProvider(contentProvider) 907 } 908 909 ps.pageOutputs[i] = po 910 created[f.Name] = po 911 912 } 913 914 if err := ps.initCommonProviders(pp); err != nil { 915 return nil, err 916 } 917 918 return nil, nil 919 }) 920 921 return nil 922 }