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 }