hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }