tpl: Make any layout set in front matter higher priority - hugo - [fork] hugo port for 9front
 (HTM) git clone git@git.drkhsh.at/hugo.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 30b9c19c7691aa3d90854c92a355bd8a248bb5b0
 (DIR) parent c8710625b7c01a0d580f9d896b1fea96ec5463d1
 (HTM) Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Sat, 12 Apr 2025 12:37:39 +0200
       
       tpl: Make any layout set in front matter higher priority
       
       Fixes #13541
       
       Diffstat:
         M hugolib/alias.go                    |       2 +-
         M hugolib/content_map_test.go         |       1 +
         M hugolib/page.go                     |      16 ++++++++--------
         M tpl/tplimpl/templatedescriptor.go   |      82 ++++++++++++++++----------------
         M tpl/tplimpl/templatedescriptor_tes… |      30 +++++++++++++++---------------
         M tpl/tplimpl/templatestore.go        |      57 ++++++++++++++-----------------
         M tpl/tplimpl/templatestore_integrat… |      42 +++++++++++++++++++++++++++----
       
       7 files changed, 128 insertions(+), 102 deletions(-)
       ---
 (DIR) diff --git 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
        
 (DIR) diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go
       @@ -538,6 +538,7 @@ title: p1
        -- content/p1/c.html --
        <p>c</p>
        -- layouts/_default/single.html --
       +Path: {{ .Path }}|{{.Kind }}
        |{{ (.Resources.Get "a.html").RelPermalink -}}
        |{{ (.Resources.Get "b.html").RelPermalink -}}
        |{{ (.Resources.Get "c.html").Publish }}
 (DIR) diff --git 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{
 (DIR) diff --git 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 {
 (DIR) diff --git 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},
                        },
                }
        
 (DIR) diff --git 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)
        
 (DIR) diff --git 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()