From 30b9c19c7691aa3d90854c92a355bd8a248bb5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sat, 12 Apr 2025 12:37:39 +0200 Subject: [PATCH] tpl: Make any layout set in front matter higher priority Fixes #13541 --- hugolib/alias.go | 2 +- hugolib/content_map_test.go | 1 + hugolib/page.go | 16 ++-- tpl/tplimpl/templatedescriptor.go | 82 +++++++++---------- tpl/tplimpl/templatedescriptor_test.go | 30 +++---- tpl/tplimpl/templatestore.go | 57 ++++++------- tpl/tplimpl/templatestore_integration_test.go | 42 ++++++++-- 7 files changed, 128 insertions(+), 102 deletions(-) diff --git a/hugolib/alias.go b/hugolib/alias.go index 3beee44db..0bb3165c7 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -53,7 +53,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, err if ps, ok := p.(*pageState); ok { base, templateDesc = ps.GetInternalTemplateBasePathAndDescriptor() } - templateDesc.Layout = "" + templateDesc.LayoutFromUser = "" templateDesc.Kind = "" templateDesc.OutputFormat = output.AliasHTMLFormat.Name diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go index aed2a7f13..c24790495 100644 --- a/hugolib/content_map_test.go +++ b/hugolib/content_map_test.go @@ -538,6 +538,7 @@ title: p1 -- content/p1/c.html --

c

-- layouts/_default/single.html -- +Path: {{ .Path }}|{{.Kind }} |{{ (.Resources.Get "a.html").RelPermalink -}} |{{ (.Resources.Get "b.html").RelPermalink -}} |{{ (.Resources.Get "c.html").Publish }} diff --git a/hugolib/page.go b/hugolib/page.go index b36b9712c..84d4d1ea8 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -482,12 +482,12 @@ func (po *pageOutput) GetInternalTemplateBasePathAndDescriptor() (string, tplimp f := po.f base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type) return base, tplimpl.TemplateDescriptor{ - Kind: p.Kind(), - Lang: p.Language().Lang, - Layout: p.Layout(), - OutputFormat: f.Name, - MediaType: f.MediaType.Type, - IsPlainText: f.IsPlainText, + Kind: p.Kind(), + Lang: p.Language().Lang, + LayoutFromUser: p.Layout(), + OutputFormat: f.Name, + MediaType: f.MediaType.Type, + IsPlainText: f.IsPlainText, } } @@ -495,8 +495,8 @@ func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool dir, d := p.GetInternalTemplateBasePathAndDescriptor() if len(layouts) > 0 { - d.Layout = layouts[0] - d.LayoutMustMatch = true + d.LayoutFromUser = layouts[0] + d.LayoutFromUserMustMatch = true } q := tplimpl.TemplateQuery{ diff --git a/tpl/tplimpl/templatedescriptor.go b/tpl/tplimpl/templatedescriptor.go index f93993092..8e4390fae 100644 --- a/tpl/tplimpl/templatedescriptor.go +++ b/tpl/tplimpl/templatedescriptor.go @@ -22,8 +22,9 @@ const baseNameBaseof = "baseof" // This is used both as a key and in lookups. type TemplateDescriptor struct { // Group 1. - Kind string // page, home, section, taxonomy, term (and only those) - Layout string // list, single, baseof, mycustomlayout. + Kind string // page, home, section, taxonomy, term (and only those) + LayoutFromTemplate string // list, single, all,mycustomlayout + LayoutFromUser string // custom layout set in front matter, e.g. list, single, all, mycustomlayout // Group 2. OutputFormat string // rss, csv ... @@ -34,23 +35,21 @@ type TemplateDescriptor struct { Variant2 string // contextual variant, e.g. "id" in render. // Misc. - LayoutMustMatch bool // If set, we only look for the exact layout. - IsPlainText bool // Whether this is a plain text template. + LayoutFromUserMustMatch bool // If set, we only look for the exact layout. + IsPlainText bool // Whether this is a plain text template. } func (d *TemplateDescriptor) normalizeFromFile() { - // fmt.Println("normalizeFromFile", "kind:", d.Kind, "layout:", d.Layout, "of:", d.OutputFormat) - - if d.Layout == d.OutputFormat { - d.Layout = "" + if d.LayoutFromTemplate == d.OutputFormat { + d.LayoutFromTemplate = "" } if d.Kind == kinds.KindTemporary { d.Kind = "" } - if d.Layout == d.Kind { - d.Layout = "" + if d.LayoutFromTemplate == d.Kind { + d.LayoutFromTemplate = "" } } @@ -61,7 +60,7 @@ type descriptorHandler struct { // Note that this in this setup is usually a descriptor constructed from a page, // so we want to find the best match for that page. func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool, this, other TemplateDescriptor) weight { - if this.LayoutMustMatch && this.Layout != other.Layout { + if this.LayoutFromUserMustMatch && this.LayoutFromUser != other.LayoutFromTemplate { return weightNoMatch } @@ -94,20 +93,15 @@ func (this TemplateDescriptor) doCompare(category Category, isEmbedded bool, oth return w } - if other.Layout != "" && other.Layout != layoutAll && other.Layout != this.Layout { - if isLayoutCustom(this.Layout) { - if this.Kind == "" { - this.Layout = "" - } else if this.Kind == kinds.KindPage { - this.Layout = layoutSingle - } else { - this.Layout = layoutList + if other.LayoutFromTemplate != "" && other.LayoutFromTemplate != layoutAll { + if this.LayoutFromUser == "" { + if other.LayoutFromTemplate != this.LayoutFromTemplate { + return w + } + } else if isLayoutStandard(this.LayoutFromUser) { + if other.LayoutFromTemplate != this.LayoutFromUser { + return w } - } - - // Test again. - if other.Layout != this.Layout { - return w } } @@ -123,7 +117,11 @@ func (this TemplateDescriptor) doCompare(category Category, isEmbedded bool, oth // We want e.g. home page in amp output format (media type text/html) to // find a template even if one isn't specified for that output format, // when one exist for the html output format (same media type). - if category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && (this.Layout != other.Layout && other.Layout != layoutAll))) { + skip := category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && (this.LayoutFromTemplate != other.LayoutFromTemplate && other.LayoutFromTemplate != layoutAll))) + if this.LayoutFromUser != "" { + skip = skip && (this.LayoutFromUser != other.LayoutFromTemplate) + } + if skip { return w } @@ -148,14 +146,14 @@ func (this TemplateDescriptor) doCompare(category Category, isEmbedded bool, oth } const ( - weightKind = 3 // page, home, section, taxonomy, term (and only those) - weightcustomLayout = 4 // custom layout (mylayout, set in e.g. front matter) - weightLayout = 2 // standard layouts (single,list,all) - weightOutputFormat = 2 // a configured output format (e.g. rss, html, json) - weightMediaType = 1 // a configured media type (e.g. text/html, text/plain) - weightLang = 1 // a configured language (e.g. en, nn, fr, ...) - weightVariant1 = 4 // currently used for render hooks, e.g. "link", "image" - weightVariant2 = 2 // currently used for render hooks, e.g. the language "go" in code blocks. + weightKind = 3 // page, home, section, taxonomy, term (and only those) + weightcustomLayout = 4 // custom layout (mylayout, set in e.g. front matter) + weightLayoutStandard = 2 // standard layouts (single,list,all) + weightOutputFormat = 2 // a configured output format (e.g. rss, html, json) + weightMediaType = 1 // a configured media type (e.g. text/html, text/plain) + weightLang = 1 // a configured language (e.g. en, nn, fr, ...) + weightVariant1 = 4 // currently used for render hooks, e.g. "link", "image" + weightVariant2 = 2 // currently used for render hooks, e.g. the language "go" in code blocks. // We will use the values for group 2 and 3 // if the distance up to the template is shorter than @@ -179,14 +177,16 @@ func (this TemplateDescriptor) doCompare(category Category, isEmbedded bool, oth w.w2 = weight2Group1 } - if other.Layout != "" && other.Layout == this.Layout || other.Layout == layoutAll { - if isLayoutCustom(this.Layout) { - w.w1 += weightcustomLayout - w.w2 = weight2Group2 - } else { - w.w1 += weightLayout - w.w2 = weight2Group1 - } + if this.LayoutFromUser == "" && other.LayoutFromTemplate != "" && (other.LayoutFromTemplate == this.LayoutFromTemplate || other.LayoutFromTemplate == layoutAll) { + w.w1 += weightLayoutStandard + w.w2 = weight2Group1 + + } + + // LayoutCustom is only set in this (usually from Page.Layout). + if this.LayoutFromUser != "" && this.LayoutFromUser == other.LayoutFromTemplate { + w.w1 += weightcustomLayout + w.w2 = weight2Group2 } if other.Lang != "" && other.Lang == this.Lang { diff --git a/tpl/tplimpl/templatedescriptor_test.go b/tpl/tplimpl/templatedescriptor_test.go index 6fb8c2195..20ab47fba 100644 --- a/tpl/tplimpl/templatedescriptor_test.go +++ b/tpl/tplimpl/templatedescriptor_test.go @@ -38,29 +38,29 @@ func TestTemplateDescriptorCompare(t *testing.T) { check( CategoryBaseof, - TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "404", MediaType: "text/html"}, - TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "html", MediaType: "text/html"}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "404", MediaType: "text/html"}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "html", MediaType: "text/html"}, false, ) check( CategoryLayout, TemplateDescriptor{Kind: "", Lang: "en", OutputFormat: "404", MediaType: "text/html"}, - TemplateDescriptor{Kind: "", Layout: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", Lang: "", OutputFormat: "alias", MediaType: "text/html"}, true, ) less( CategoryLayout, - TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html"}, - TemplateDescriptor{Layout: "list", OutputFormat: "html"}, + TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "html"}, + TemplateDescriptor{LayoutFromTemplate: "list", OutputFormat: "html"}, TemplateDescriptor{Kind: kinds.KindHome, OutputFormat: "html"}, ) check( CategoryLayout, - TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "html", MediaType: "text/html"}, - TemplateDescriptor{Kind: kinds.KindHome, Layout: "list", OutputFormat: "myformat", MediaType: "text/html"}, + TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "html", MediaType: "text/html"}, + TemplateDescriptor{Kind: kinds.KindHome, LayoutFromTemplate: "list", OutputFormat: "myformat", MediaType: "text/html"}, false, ) } @@ -78,20 +78,20 @@ func BenchmarkCompareDescriptors(b *testing.B) { d1, d2 TemplateDescriptor }{ { - TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, - TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "404", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, }, { - TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, - TemplateDescriptor{Kind: "", Layout: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "list", OutputFormat: "", MediaType: "application/rss+xml", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, }, { - TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, - TemplateDescriptor{Kind: "", Layout: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "html", MediaType: "text/html", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "", OutputFormat: "alias", MediaType: "text/html", Lang: "", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, }, { - TemplateDescriptor{Kind: "page", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, - TemplateDescriptor{Kind: "", Layout: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "page", LayoutFromTemplate: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "en", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, + TemplateDescriptor{Kind: "", LayoutFromTemplate: "single", OutputFormat: "rss", MediaType: "application/rss+xml", Lang: "nn", Variant1: "", Variant2: "", LayoutFromUserMustMatch: false, IsPlainText: false}, }, } diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go index e761e14d1..8483b7df0 100644 --- a/tpl/tplimpl/templatestore.go +++ b/tpl/tplimpl/templatestore.go @@ -378,11 +378,11 @@ func (q *TemplateQuery) init() { } else if kinds.GetKindMain(q.Desc.Kind) == "" { q.Desc.Kind = "" } - if q.Desc.Layout == "" && q.Desc.Kind != "" { + if q.Desc.LayoutFromTemplate == "" && q.Desc.Kind != "" { if q.Desc.Kind == kinds.KindPage { - q.Desc.Layout = layoutSingle + q.Desc.LayoutFromTemplate = layoutSingle } else { - q.Desc.Layout = layoutList + q.Desc.LayoutFromTemplate = layoutList } } @@ -447,7 +447,7 @@ func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc Te continue } - if vv.D.isKindInLayout(desc.Layout) && s.dh.compareDescriptors(CategoryBaseof, false, descBaseof, vv.D).w1 > 0 { + if vv.D.isKindInLayout(desc.LayoutFromTemplate) && s.dh.compareDescriptors(CategoryBaseof, false, descBaseof, vv.D).w1 > 0 { result = append(result, keyTemplateInfo{Key: k, Info: vv}) } } @@ -549,7 +549,7 @@ func (s *TemplateStore) LookupPartial(pth string) *TemplInfo { ti, _ := s.cacheLookupPartials.GetOrCreate(pth, func() (*TemplInfo, error) { d := s.templateDescriptorFromPath(pth) desc := d.Desc - if desc.Layout != "" { + if desc.LayoutFromTemplate != "" { panic("shortcode template descriptor must not have a layout") } best := s.getBest() @@ -610,7 +610,7 @@ func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer return } s := strings.ReplaceAll(strings.TrimSpace(vv.content), "\n", " ") - ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.Layout, s) + ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.LayoutFromTemplate, s) fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts) } s.treeMain.WalkPrefix(prefix, func(key string, v map[nodeKey]*TemplInfo) (bool, error) { @@ -1573,12 +1573,12 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin } d := TemplateDescriptor{ - Lang: p.Lang(), - OutputFormat: p.OutputFormat(), - MediaType: mediaType.Type, - Kind: p.Kind(), - Layout: layout, - IsPlainText: outputFormat.IsPlainText, + Lang: p.Lang(), + OutputFormat: p.OutputFormat(), + MediaType: mediaType.Type, + Kind: p.Kind(), + LayoutFromTemplate: layout, + IsPlainText: outputFormat.IsPlainText, } d.normalizeFromFile() @@ -1611,7 +1611,7 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin } if category == CategoryPartial { - d.Layout = "" + d.LayoutFromTemplate = "" k1 = p.PathNoIdentifier() } @@ -1626,15 +1626,15 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin } // Legacy layout for home page. - if d.Layout == "index" { + if d.LayoutFromTemplate == "index" { if d.Kind == "" { d.Kind = kinds.KindHome } - d.Layout = "" + d.LayoutFromTemplate = "" } - if d.Layout == d.Kind { - d.Layout = "" + if d.LayoutFromTemplate == d.Kind { + d.LayoutFromTemplate = "" } k1 = strings.TrimPrefix(k1, "/_default") @@ -1645,7 +1645,7 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin if category == CategoryMarkup { // We store all template nodes for a given directory on the same level. k1 = strings.TrimSuffix(k1, "/_markup") - parts := strings.Split(d.Layout, "-") + parts := strings.Split(d.LayoutFromTemplate, "-") if len(parts) < 2 { return "", "", 0, TemplateDescriptor{}, fmt.Errorf("unrecognized render hook template") } @@ -1654,7 +1654,7 @@ func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, strin if len(parts) > 2 { d.Variant2 = parts[2] } - d.Layout = "" // This allows using page layout as part of the key for lookups. + d.LayoutFromTemplate = "" // This allows using page layout as part of the key for lookups. } return k1, k2, category, d, nil @@ -1868,8 +1868,8 @@ func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool { return true } - if ti.D.Layout != "" && best.desc.Layout != "" { - return ti.D.Layout != layoutAll + if ti.D.LayoutFromTemplate != "" && best.desc.LayoutFromTemplate != "" { + return ti.D.LayoutFromTemplate != layoutAll } return w.distance < best.w.distance || ti.PathInfo.Path() < best.templ.PathInfo.Path() @@ -1920,17 +1920,6 @@ type weight struct { distance int } -func (w weight) isEqualWeights(other weight) bool { - return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3 -} - -func isLayoutCustom(s string) bool { - if s == "" || isLayoutStandard(s) { - return false - } - return true -} - func isLayoutStandard(s string) bool { switch s { case layoutAll, layoutList, layoutSingle: @@ -1940,6 +1929,10 @@ func isLayoutStandard(s string) bool { } } +func (w weight) isEqualWeights(other weight) bool { + return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3 +} + func configureSiteStorage(opts SiteOptions, watching bool) *storeSite { funcsv := make(map[string]reflect.Value) diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go index db7cb5084..4644c9639 100644 --- a/tpl/tplimpl/templatestore_integration_test.go +++ b/tpl/tplimpl/templatestore_integration_test.go @@ -510,7 +510,7 @@ baseof: {{ block "main" . }}{{ end }} q := tplimpl.TemplateQuery{ Path: "/baz", Category: tplimpl.CategoryLayout, - Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"}, + Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, LayoutFromTemplate: "single", OutputFormat: "html"}, } for i := 0; i < b.N; i++ { store.LookupPagesLayout(q) @@ -521,7 +521,7 @@ baseof: {{ block "main" . }}{{ end }} q := tplimpl.TemplateQuery{ Path: "/foo/bar", Category: tplimpl.CategoryLayout, - Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, Layout: "single", OutputFormat: "html"}, + Desc: tplimpl.TemplateDescriptor{Kind: kinds.KindPage, LayoutFromTemplate: "single", OutputFormat: "html"}, } for i := 0; i < b.N; i++ { store.LookupPagesLayout(q) @@ -648,9 +648,6 @@ layout: mylayout b := hugolib.Test(t, files, hugolib.TestOptWarn()) - // s := b.H.Sites[0].TemplateStore - // s.PrintDebug("", tplimpl.CategoryLayout, os.Stdout) - b.AssertLogContains("! WARN") // Single pages. @@ -1095,6 +1092,41 @@ s2. }) } +func TestStandardLayoutInFrontMatter13588(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','page','rss','sitemap','taxonomy','term'] +-- content/s1/_index.md -- +--- +title: s1 +--- +-- content/s2/_index.md -- +--- +title: s2 +layout: list +--- +-- content/s3/_index.md -- +--- +title: s3 +layout: single +--- +-- layouts/list.html -- +list.html +-- layouts/section.html -- +section.html +-- layouts/single.html -- +single.html +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/s1/index.html", "section.html") + b.AssertFileContent("public/s2/index.html", "list.html") // fail + b.AssertFileContent("public/s3/index.html", "single.html") // fail +} + func TestSkipDotFiles(t *testing.T) { t.Parallel()