Add slice-based permalinks config with PageMatcher target

Closes #14744
Clses #4641

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bjørn Erik Pedersen
2026-04-15 23:38:25 +02:00
parent e3413d927f
commit 017a7cd63a
9 changed files with 526 additions and 161 deletions
+10 -1
View File
@@ -553,7 +553,8 @@ type SiteVectorProvider interface {
SiteVector() sitesmatrix.Vector
}
// GetSiteVector returns the site vector for a Page.
// GetSiteVector returns the site vector for a Page,
// or the zero value if the Page does not implement SiteVectorProvider.
func GetSiteVector(p Page) sitesmatrix.Vector {
if sp, ok := p.(SiteVectorProvider); ok {
return sp.SiteVector()
@@ -561,6 +562,14 @@ func GetSiteVector(p Page) sitesmatrix.Vector {
return sitesmatrix.Vector{}
}
// LookupSiteVector returns the site vector for a Page and whether it was found.
func LookupSiteVector(p Page) (sitesmatrix.Vector, bool) {
if sp, ok := p.(SiteVectorProvider); ok {
return sp.SiteVector(), true
}
return sitesmatrix.Vector{}, false
}
// PageWithContext is a Page with a context.Context.
type PageWithContext struct {
Page
+48 -15
View File
@@ -20,6 +20,7 @@ import (
"slices"
"strings"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
@@ -58,6 +59,18 @@ type PageMatcher struct {
// Compiled values.
// The site vectors to apply this to.
SitesMatrixCompiled sitesmatrix.VectorProvider `mapstructure:"-"`
kindGlob glob.Glob
pathGlob glob.Glob
environmentGlob glob.Glob
}
// Equal compares the configured fields; compiled state is ignored.
func (m PageMatcher) Equal(other PageMatcher) bool {
return m.Path == other.Path &&
m.Kind == other.Kind &&
m.Lang == other.Lang &&
m.Environment == other.Environment &&
m.Sites.Equal(other.Sites)
}
func (m PageMatcher) Matches(p Page) bool {
@@ -70,30 +83,23 @@ func (m PageMatcher) Match(kind, path, environment string, sitesMatrix sitesmatr
return false
}
}
if m.Kind != "" {
g, err := hglob.GetGlob(m.Kind)
if err == nil && !g.Match(kind) {
return false
}
if m.kindGlob != nil && !m.kindGlob.Match(kind) {
return false
}
if m.Path != "" {
g, err := hglob.GetGlob(m.Path)
if m.pathGlob != nil {
// TODO(bep) Path() vs filepath vs leading slash.
p := strings.ToLower(filepath.ToSlash(path))
if !(strings.HasPrefix(p, "/")) {
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
if err == nil && !g.Match(p) {
if !m.pathGlob.Match(p) {
return false
}
}
if m.Environment != "" {
g, err := hglob.GetGlob(m.Environment)
if err == nil && !g.Match(environment) {
return false
}
if m.environmentGlob != nil && !m.environmentGlob.Match(environment) {
return false
}
return true
@@ -117,7 +123,7 @@ func isGlobWithExtension(s string) bool {
func checkCascadePattern(logger loggers.Logger, m PageMatcher) {
if m.Lang != "" {
hugo.Deprecate("cascade.target.language", "cascade.target.sites.matrix instead, see https://gohugo.io/content-management/front-matter/#target", "v0.150.0")
hugo.DeprecateWithLogger("cascade.target.language", "cascade.target.sites.matrix instead, see https://gohugo.io/content-management/front-matter/#target", "v0.150.0", logger.Logger())
}
}
@@ -249,8 +255,35 @@ func (d cascadeConfigDecoder) decodePageMatcher(m any, v *PageMatcher) error {
return nil
}
func (v *PageMatcher) compileGlobs() error {
var err error
if v.Kind != "" {
v.kindGlob, err = hglob.GetGlob(v.Kind)
if err != nil {
return err
}
}
if v.Path != "" {
v.pathGlob, err = hglob.GetGlob(v.Path)
if err != nil {
return err
}
}
if v.Environment != "" {
v.environmentGlob, err = hglob.GetGlob(v.Environment)
if err != nil {
return err
}
}
return nil
}
// DecodeCascadeConfigOptions
func (v *PageMatcher) compileSitesMatrix(defaults sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error {
if err := v.compileGlobs(); err != nil {
return err
}
if v.Sites.Matrix.IsZero() && defaults == nil {
// Nothing to do.
v.SitesMatrixCompiled = nil
+21 -23
View File
@@ -44,35 +44,33 @@ func TestPageMatcher(t *testing.T) {
&testPage{path: "p3", kind: "page", lang: "en", site: developmentTestSite}
c.Run("Matches", func(c *qt.C) {
m := PageMatcher{Kind: "section"}
matches := func(m PageMatcher, p Page) bool {
c.Assert(m.compileGlobs(), qt.IsNil)
return m.Matches(p)
}
c.Assert(m.Matches(p1), qt.Equals, true)
c.Assert(m.Matches(p2), qt.Equals, false)
c.Assert(matches(PageMatcher{Kind: "section"}, p1), qt.Equals, true)
c.Assert(matches(PageMatcher{Kind: "section"}, p2), qt.Equals, false)
m = PageMatcher{Kind: "page"}
c.Assert(m.Matches(p1), qt.Equals, false)
c.Assert(m.Matches(p2), qt.Equals, true)
c.Assert(m.Matches(p3), qt.Equals, true)
c.Assert(matches(PageMatcher{Kind: "page"}, p1), qt.Equals, false)
c.Assert(matches(PageMatcher{Kind: "page"}, p2), qt.Equals, true)
c.Assert(matches(PageMatcher{Kind: "page"}, p3), qt.Equals, true)
m = PageMatcher{Kind: "page", Path: "/p2"}
c.Assert(m.Matches(p1), qt.Equals, false)
c.Assert(m.Matches(p2), qt.Equals, true)
c.Assert(m.Matches(p3), qt.Equals, false)
c.Assert(matches(PageMatcher{Kind: "page", Path: "/p2"}, p1), qt.Equals, false)
c.Assert(matches(PageMatcher{Kind: "page", Path: "/p2"}, p2), qt.Equals, true)
c.Assert(matches(PageMatcher{Kind: "page", Path: "/p2"}, p3), qt.Equals, false)
m = PageMatcher{Path: "/p*"}
c.Assert(m.Matches(p1), qt.Equals, true)
c.Assert(m.Matches(p2), qt.Equals, true)
c.Assert(m.Matches(p3), qt.Equals, true)
c.Assert(matches(PageMatcher{Path: "/p*"}, p1), qt.Equals, true)
c.Assert(matches(PageMatcher{Path: "/p*"}, p2), qt.Equals, true)
c.Assert(matches(PageMatcher{Path: "/p*"}, p3), qt.Equals, true)
m = PageMatcher{Environment: "development"}
c.Assert(m.Matches(p1), qt.Equals, true)
c.Assert(m.Matches(p2), qt.Equals, false)
c.Assert(m.Matches(p3), qt.Equals, true)
c.Assert(matches(PageMatcher{Environment: "development"}, p1), qt.Equals, true)
c.Assert(matches(PageMatcher{Environment: "development"}, p2), qt.Equals, false)
c.Assert(matches(PageMatcher{Environment: "development"}, p3), qt.Equals, true)
m = PageMatcher{Environment: "production"}
c.Assert(m.Matches(p1), qt.Equals, false)
c.Assert(m.Matches(p2), qt.Equals, true)
c.Assert(m.Matches(p3), qt.Equals, false)
c.Assert(matches(PageMatcher{Environment: "production"}, p1), qt.Equals, false)
c.Assert(matches(PageMatcher{Environment: "production"}, p2), qt.Equals, true)
c.Assert(matches(PageMatcher{Environment: "production"}, p3), qt.Equals, false)
})
c.Run("Decode", func(c *qt.C) {
+134 -65
View File
@@ -26,18 +26,41 @@ import (
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/mitchellh/mapstructure"
)
// PermalinkExpander holds permalink mappings per section.
// PermalinkConfig holds a single permalink rule with a target matcher and a pattern.
type PermalinkConfig struct {
Target PageMatcher
Pattern string
}
// PermalinksConfig is an ordered slice of permalink rules.
// For any given Page, the first matching rule wins.
type PermalinksConfig []PermalinkConfig
// InitConfig compiles the sites matrix for each permalink target.
func (c PermalinksConfig) InitConfig(logger loggers.Logger, defaultSitesMatrix sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error {
for i := range c {
if err := c[i].Target.compileSitesMatrix(nil, configuredDimensions); err != nil {
return fmt.Errorf("failed to compile permalink target %d: %w", i, err)
}
}
return nil
}
// PermalinkExpander holds permalink mappings.
type PermalinkExpander struct {
// knownPermalinkAttributes maps :tags in a permalink specification to a
// function which, given a page and the tag, returns the resulting string
// to be used to replace that tag.
knownPermalinkAttributes map[string]pageToPermaAttribute
expanders map[string]map[string]func(Page) (string, error)
configs PermalinksConfig
urlize func(uri string) string
@@ -80,9 +103,10 @@ func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
// NewPermalinkExpander creates a new PermalinkExpander configured by the given
// urlize func.
func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]map[string]string) (PermalinkExpander, error) {
func NewPermalinkExpander(urlize func(uri string) string, configs PermalinksConfig) (PermalinkExpander, error) {
p := PermalinkExpander{
urlize: urlize,
configs: configs,
patternCache: hmaps.NewCache[string, func(Page) (string, error)](),
}
@@ -106,14 +130,15 @@ func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]ma
"slugorcontentbasename": p.pageToPermalinkSlugOrContentBaseName,
}
p.expanders = make(map[string]map[string]func(Page) (string, error))
for kind, patterns := range patterns {
e, err := p.parse(patterns)
if err != nil {
// Validate all patterns and compile matcher globs at init time.
for i := range configs {
if _, err := p.getOrParsePattern(configs[i].Pattern); err != nil {
return p, err
}
p.expanders[kind] = e
if err := configs[i].Target.compileGlobs(); err != nil {
return p, err
}
}
return p, nil
@@ -141,20 +166,29 @@ func (l PermalinkExpander) ExpandPattern(pattern string, p Page) (string, error)
return expand(p)
}
// Expand expands the path in p according to the rules defined for the given key.
// If no rules are found for the given key, an empty string is returned.
func (l PermalinkExpander) Expand(key string, p Page) (string, error) {
expanders, found := l.expanders[p.Kind()]
if !found {
// Expand expands the path in p according to the first matching permalink rule.
// If no rules match, an empty string is returned.
func (l PermalinkExpander) Expand(p Page) (string, error) {
kind := p.Kind()
if !hstrings.InSlice(permalinksKindsSupport, kind) {
return "", nil
}
expand, found := expanders[key]
if !found {
return "", nil
var siteVector sitesmatrix.VectorProvider
if sv, ok := LookupSiteVector(p); ok {
siteVector = sv
}
return expand(p)
var environment string
var environmentResolved bool
for _, cfg := range l.configs {
if cfg.Target.Environment != "" && !environmentResolved {
environment = p.Site().Hugo().Environment()
environmentResolved = true
}
if cfg.Target.Match(kind, p.Path(), environment, siteVector) {
return l.ExpandPattern(cfg.Pattern, p)
}
}
return "", nil
}
// Allow " " and / to represent the root section.
@@ -220,23 +254,6 @@ func (l PermalinkExpander) getOrParsePattern(pattern string) (func(Page) (string
})
}
func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) {
expanders := make(map[string]func(Page) (string, error))
for k, pattern := range patterns {
k = strings.Trim(k, sectionCutSet)
expander, err := l.getOrParsePattern(pattern)
if err != nil {
return nil, err
}
expanders[k] = expander
}
return expanders, nil
}
// pageToPermaAttribute is the type of a function which, given a page and a tag
// can return a string to go in that position in the page (or an error)
type pageToPermaAttribute func(Page, string) (string, error)
@@ -483,50 +500,102 @@ func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string {
}
}
var permalinksKindsSupport = []string{kinds.KindPage, kinds.KindSection, kinds.KindTaxonomy, kinds.KindTerm}
var permalinksKindsSupport = []string{kinds.KindPage, kinds.KindHome, kinds.KindSection, kinds.KindTaxonomy, kinds.KindTerm}
// DecodePermalinksConfig decodes the permalinks configuration in the given map
func DecodePermalinksConfig(m map[string]any) (map[string]map[string]string, error) {
permalinksConfig := make(map[string]map[string]string)
func sectionToPathGlob(section string) string {
section = strings.Trim(section, sectionCutSet)
if section == "" {
return "/*"
}
return "/{" + section + "," + section + "/**}"
}
permalinksConfig[kinds.KindPage] = make(map[string]string)
permalinksConfig[kinds.KindSection] = make(map[string]string)
permalinksConfig[kinds.KindTaxonomy] = make(map[string]string)
permalinksConfig[kinds.KindTerm] = make(map[string]string)
// DecodePermalinksConfig decodes the permalinks configuration.
// It supports both the new slice-based format and the legacy map-based formats.
func DecodePermalinksConfig(in any) (PermalinksConfig, error) {
if in == nil {
return nil, nil
}
switch v := in.(type) {
// Check legacy formats first.
case map[string]any:
return decodePermalinksMap(v)
case hmaps.Params:
return decodePermalinksMap(v)
default:
if ms, err := hmaps.ToSliceStringMap(in); err == nil {
// New slice format.
return decodePermalinksSlice(ms)
}
return nil, fmt.Errorf("permalinks: unsupported config type %T", in)
}
}
func decodePermalinksSlice(ms []map[string]any) (PermalinksConfig, error) {
var configs PermalinksConfig
for _, m := range ms {
m = hmaps.CleanConfigStringMap(m)
var cfg PermalinkConfig
if targetVal, ok := m["target"]; ok {
if err := mapstructure.WeakDecode(targetVal, &cfg.Target); err != nil {
return nil, fmt.Errorf("permalinks: failed to decode target: %w", err)
}
cfg.Target.Kind = strings.ToLower(cfg.Target.Kind)
cfg.Target.Path = filepath.ToSlash(strings.ToLower(cfg.Target.Path))
}
if patternVal, ok := m["pattern"]; ok {
cfg.Pattern, ok = patternVal.(string)
if !ok {
return nil, fmt.Errorf("permalinks: pattern must be a string, got %T", patternVal)
}
} else {
return nil, fmt.Errorf("permalinks: missing pattern")
}
configs = append(configs, cfg)
}
return configs, nil
}
func decodePermalinksMap(m map[string]any) (PermalinksConfig, error) {
var configs PermalinksConfig
config := hmaps.CleanConfigStringMap(m)
for k, v := range config {
switch v := v.(type) {
case string:
// [permalinks]
// key = '...'
// To successfully be backward compatible, "default" patterns need to be set for both page and term
permalinksConfig[kinds.KindPage][k] = v
permalinksConfig[kinds.KindTerm][k] = v
// Backward compat: set for both page and term.
configs = append(configs,
PermalinkConfig{Target: PageMatcher{Kind: kinds.KindPage, Path: sectionToPathGlob(k)}, Pattern: v},
PermalinkConfig{Target: PageMatcher{Kind: kinds.KindTerm, Path: sectionToPathGlob(k)}, Pattern: v},
)
case hmaps.Params:
// [permalinks.key]
// xyz = ???
if hstrings.InSlice(permalinksKindsSupport, k) {
// TODO: warn if we overwrite an already set value
for k2, v2 := range v {
switch v2 := v2.(type) {
case string:
permalinksConfig[k][k2] = v2
default:
return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q for kind %q", v2, k2, k)
}
}
} else {
// [permalinks.kind]
// section = '...'
if !hstrings.InSlice(permalinksKindsSupport, k) {
return nil, fmt.Errorf("permalinks configuration not supported for kind %q, supported kinds are %v", k, permalinksKindsSupport)
}
for k2, v2 := range v {
switch v2 := v2.(type) {
case string:
configs = append(configs,
PermalinkConfig{Target: PageMatcher{Kind: k, Path: sectionToPathGlob(k2)}, Pattern: v2},
)
default:
return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q for kind %q", v2, k2, k)
}
}
default:
return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q", v, k)
}
}
return permalinksConfig, nil
return configs, nil
}
+267 -12
View File
@@ -135,12 +135,8 @@ slug: "mytagslug"
b.AssertFileContent("public/tagsslug/tagsslug/index.html", "List|taxonomy|/tagsslug/tagsslug/|")
permalinksConf := b.H.Configs.Base.Permalinks
b.Assert(permalinksConf, qt.DeepEquals, map[string]map[string]string{
"page": {"withallbutlastsection": "/:sections[:last]/:slug/", "withallbutlastsectionslug": "/:sectionslugs[:last]/:slug/", "withpageslug": "/pageslug/:slug/", "withsectionslug": "/sectionslug/:sectionslug/:slug/", "withsectionslugs": "/sectionslugs/:sectionslugs/:slug/"},
"section": {"nofilefilename": "/sectionnofilefilename/:contentbasename/", "nofileslug": "/sectionnofileslug/:slug/", "nofiletitle1": "/sectionnofiletitle1/:title/", "nofiletitle2": "/sectionnofiletitle2/:sections[:last]/", "withfilefilename": "/sectionwithfilefilename/:contentbasename/", "withfilefiletitle": "/sectionwithfilefiletitle/:title/", "withfileslug": "/sectionwithfileslug/:slug/"},
"taxonomy": {"tags": "/tagsslug/:slug/"},
"term": {"tags": "/tagsslug/tag/:slug/"},
})
// 5 page + 7 section + 1 taxonomy + 1 term = 14 rules.
b.Assert(len(permalinksConf), qt.Equals, 14)
}
func TestPermalinksOldSetup(t *testing.T) {
@@ -172,12 +168,8 @@ slug: "p1slugvalue"
b.AssertFileContent("public/pageslug/p1slugvalue/index.html", "Single|page|/pageslug/p1slugvalue/|")
permalinksConf := b.H.Configs.Base.Permalinks
b.Assert(permalinksConf, qt.DeepEquals, map[string]map[string]string{
"page": {"withpageslug": "/pageslug/:slug/"},
"section": {},
"taxonomy": {},
"term": {"withpageslug": "/pageslug/:slug/"},
})
// Old flat format: 1 entry creates 2 rules (page + term).
b.Assert(len(permalinksConf), qt.Equals, 2)
}
func TestPermalinksNestedSections(t *testing.T) {
@@ -515,3 +507,266 @@ title: Tag A (set in front matter)
})
}
}
func TestPermalinksNewSliceFormat(t *testing.T) {
t.Parallel()
files := `
-- layouts/list.html --
List|{{ .Kind }}|{{ .RelPermalink }}|
-- layouts/single.html --
Single|{{ .Kind }}|{{ .RelPermalink }}|
-- hugo.yaml --
permalinks:
- target:
kind: page
path: "/books/**"
pattern: /books/:year/:slug/
- target:
kind: section
path: "/{books,books/**}"
pattern: /libros/:sections[1:]
- target:
kind: page
pattern: /other/:slug/
-- content/books/_index.md --
---
title: Books
---
-- content/books/fiction/_index.md --
---
title: Fiction
---
-- content/books/fiction/book1.md --
---
title: Book One
date: 2023-06-15
slug: book-one
---
-- content/other/p1.md --
---
title: Other Page
slug: other-page
---
-- content/unmatched/p2.md --
---
title: Unmatched Page
slug: unmatched-page
---
`
b := hugolib.Test(t, files)
// Page in /books section gets the books-specific pattern.
b.AssertFileContent("public/books/2023/book-one/index.html", "Single|page|/books/2023/book-one/|")
// Section page for books/fiction gets the section pattern.
b.AssertFileContent("public/libros/fiction/index.html", "List|section|/libros/fiction/|")
// Page outside /books matches the default page pattern.
b.AssertFileContent("public/other/other-page/index.html", "Single|page|/other/other-page/|")
// Page in unmatched section also matches the default page pattern.
b.AssertFileContent("public/other/unmatched-page/index.html", "Single|page|/other/unmatched-page/|")
}
func TestPermalinksNewSliceFormatEnvironment(t *testing.T) {
t.Parallel()
files := `
-- layouts/list.html --
List|{{ .Kind }}|{{ .RelPermalink }}|
-- layouts/single.html --
Single|{{ .Kind }}|{{ .RelPermalink }}|
-- hugo.toml --
disableKinds = ['home','rss','sitemap','taxonomy','term']
[[permalinks]]
pattern = "/testing/:slug/"
[permalinks.target]
path = "/books/**"
environment = "test"
[[permalinks]]
pattern = "/prod/:slug/"
[permalinks.target]
path = "/books/**"
environment = "production"
-- content/books/fiction/book1.md --
---
title: Book One
date: 2023-06-15
slug: book-one
---
`
b := hugolib.Test(t, files)
b.AssertPublishDir("prod", "! testing")
}
func TestPermalinksNewSliceFormatSitesMatrix(t *testing.T) {
t.Parallel()
files := `
-- layouts/single.html --
Single|{{ .Kind }}|{{ .RelPermalink }}|{{ .Language.Lang }}|
-- hugo.yaml --
defaultContentLanguage: en
defaultContentLanguageInSubdir: true
disableKinds: ['home','rss','section','sitemap','taxonomy','term']
languages:
en:
weight: 1
de:
weight: 2
permalinks:
- target:
kind: page
sites:
matrix:
languages: ["en"]
pattern: /en-posts/:slug/
- target:
kind: page
sites:
matrix:
languages: ["de"]
pattern: /de-posts/:slug/
- target:
kind: page
pattern: /other/:slug/
-- content/p1.en.md --
---
title: Hello
slug: hello
---
-- content/p1.de.md --
---
title: Hallo
slug: hallo
---
`
b := hugolib.Test(t, files)
// English page matches the en-specific rule.
b.AssertFileContent("public/en/en-posts/hello/index.html", "Single|page|/en/en-posts/hello/|en|")
// German page matches the de-specific rule.
b.AssertFileContent("public/de/de-posts/hallo/index.html", "Single|page|/de/de-posts/hallo/|de|")
}
// We only apply permalink patterns to kinds of type hone, page, section, taxonomy and term.
func TestPermalinksNewSliceFormatAsteriskKind(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
enableRobotsTXT = true
[[permalinks]]
pattern = "/mylink/:slug/"
[permalinks.target]
kind = "*"
-- content/mysection/p1.md --
---
title: My Page
slug: my-page
---
-- layouts/all.html --
{{ .Title }}|{{ .RelPermalink }}|{{ .Kind }}|
-- layouts/404.html --
404.
`
b := hugolib.Test(t, files)
b.AssertPublishDir(`
404.html
index.html
index.xml
mylink/categories/index.html
mylink/my-page/index.html
mylink/mysections/index.html
mylink/tags/index.html
sitemap.xml
`)
}
func TestPermalinksHomeKind(t *testing.T) {
t.Parallel()
files := `
-- layouts/home.html --
Home|{{ .Kind }}|{{ .RelPermalink }}|
-- layouts/list.html --
List|{{ .Kind }}|{{ .RelPermalink }}|
-- hugo.yaml --
disableKinds: ['rss','sitemap','taxonomy','term']
permalinks:
- target:
kind: home
pattern: /welcome/
- target:
kind: section
pattern: /s/:slug/
-- content/mysection/_index.md --
---
title: My Section
slug: my-section
---
`
b := hugolib.Test(t, files, hugolib.TestOptWarn())
t.Log(b.LogString())
b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
b.AssertFileContent("public/welcome/index.html", "Home|home|/welcome/|")
b.AssertFileContent("public/s/my-section/index.html", "List|section|/s/my-section/|")
}
func TestPermalinksHomeKindLegacyMap(t *testing.T) {
t.Parallel()
files := `
-- layouts/home.html --
Home|{{ .Kind }}|{{ .RelPermalink }}|
-- hugo.toml --
disableKinds = ['rss','sitemap','taxonomy','term']
[permalinks.home]
"/" = '/welcome/'
`
b := hugolib.Test(t, files, hugolib.TestOptWarn())
t.Log(b.LogString())
b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
b.AssertFileContent("public/welcome/index.html", "Home|home|/welcome/|")
}
func TestPermalinksNewSliceFormatRootSection(t *testing.T) {
t.Parallel()
files := `
-- layouts/single.html --
Single|{{ .Kind }}|{{ .RelPermalink }}|
-- hugo.yaml --
permalinks:
- target:
kind: page
path: "/*"
pattern: /root/:slug/
- target:
kind: page
pattern: /deep/:slug/
-- content/p1.md --
---
title: Root Page
slug: root-page
---
-- content/sub/p2.md --
---
title: Sub Page
slug: sub-page
---
`
b := hugolib.Test(t, files)
// Root section page matches /* (non-recursive).
b.AssertFileContent("public/root/root-page/index.html", "Single|page|/root/root-page/|")
// Nested page matches the default rule.
b.AssertFileContent("public/deep/sub-page/index.html", "Single|page|/deep/sub-page/|")
}
+35 -36
View File
@@ -129,7 +129,7 @@ func TestPermalinkExpansion(t *testing.T) {
page.section = "blue"
page.slug = "The Slug"
page.kind = "page"
// page.pathInfo
page.path = "/posts/test-page"
return page
}
@@ -147,14 +147,12 @@ func TestPermalinkExpansion(t *testing.T) {
name := fmt.Sprintf("[%d] %s", i, specNameCleaner.ReplaceAllString(item.spec, "_"))
c.Run(name, func(c *qt.C) {
patterns := map[string]map[string]string{
"page": {
"posts": item.spec,
},
configs := PermalinksConfig{
{Target: PageMatcher{Kind: "page", Path: "/posts/**"}, Pattern: item.spec},
}
expander, err := NewPermalinkExpander(urlize, patterns)
expander, err := NewPermalinkExpander(urlize, configs)
c.Assert(err, qt.IsNil)
expanded, err := expander.Expand("posts", page)
expanded, err := expander.Expand(page)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, item.expandsTo)
@@ -178,39 +176,42 @@ func TestPermalinkExpansionMultiSection(t *testing.T) {
page.section = "blue"
page.slug = "The Slug"
page.kind = "page"
page.path = "/posts/my-page"
page_slug_fallback := newTestPageWithFile("/page-filename/index.md")
page_slug_fallback.title = "Page Title"
page_slug_fallback.kind = "page"
pageSlugFallback := newTestPageWithFile("/page-filename/index.md")
pageSlugFallback.title = "Page Title"
pageSlugFallback.kind = "page"
permalinksConfig := map[string]map[string]string{
"page": {
"posts": "/:slug",
"blog": "/:section/:year",
"recipes": "/:slugorfilename",
"special": "/special\\::slug",
},
configs := PermalinksConfig{
{Target: PageMatcher{Kind: "page", Path: "/posts/**"}, Pattern: "/:slug"},
{Target: PageMatcher{Kind: "page", Path: "/blog/**"}, Pattern: "/:section/:year"},
{Target: PageMatcher{Kind: "page", Path: "/recipes/**"}, Pattern: "/:slugorfilename"},
{Target: PageMatcher{Kind: "page", Path: "/special/**"}, Pattern: "/special\\::slug"},
}
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
expander, err := NewPermalinkExpander(urlize, configs)
c.Assert(err, qt.IsNil)
expanded, err := expander.Expand("posts", page)
expanded, err := expander.Expand(page)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, "/the-slug")
expanded, err = expander.Expand("blog", page)
page.path = "/blog/my-page"
expanded, err = expander.Expand(page)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, "/blue/2012")
expanded, err = expander.Expand("posts", page_slug_fallback)
pageSlugFallback.path = "/posts/my-page"
expanded, err = expander.Expand(pageSlugFallback)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, "/page-title")
expanded, err = expander.Expand("recipes", page_slug_fallback)
pageSlugFallback.path = "/recipes/my-page"
expanded, err = expander.Expand(pageSlugFallback)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, "/page-filename")
expanded, err = expander.Expand("special", page)
page.path = "/special/my-page"
expanded, err = expander.Expand(page)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, "/special:the-slug")
}
@@ -220,13 +221,11 @@ func TestPermalinkExpansionConcurrent(t *testing.T) {
c := qt.New(t)
permalinksConfig := map[string]map[string]string{
"page": {
"posts": "/:slug/",
},
configs := PermalinksConfig{
{Target: PageMatcher{Kind: "page", Path: "/posts/**"}, Pattern: "/:slug/"},
}
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
expander, err := NewPermalinkExpander(urlize, configs)
c.Assert(err, qt.IsNil)
var wg sync.WaitGroup
@@ -237,9 +236,10 @@ func TestPermalinkExpansionConcurrent(t *testing.T) {
defer wg.Done()
page := newTestPage()
page.kind = "page"
page.path = "/posts/my-page"
for j := 1; j < 20; j++ {
page.slug = fmt.Sprintf("slug%d", i+j)
expanded, err := expander.Expand("posts", page)
expanded, err := expander.Expand(page)
c.Assert(err, qt.IsNil)
c.Assert(expanded, qt.Equals, fmt.Sprintf("/%s/", page.slug))
}
@@ -253,7 +253,7 @@ func TestPermalinkExpansionSliceSyntax(t *testing.T) {
t.Parallel()
c := qt.New(t)
exp, err := NewPermalinkExpander(urlize, nil)
exp, err := NewPermalinkExpander(urlize, PermalinksConfig{})
c.Assert(err, qt.IsNil)
slice4 := []string{"a", "b", "c", "d"}
fn4 := func(s string) []string {
@@ -302,19 +302,18 @@ func BenchmarkPermalinkExpand(b *testing.B) {
d, _ := time.Parse("2006-01-02", "2019-02-28")
page.date = d
page.kind = "page"
page.path = "/posts/my-page"
permalinksConfig := map[string]map[string]string{
"page": {
"posts": "/:year-:month-:title",
},
configs := PermalinksConfig{
{Target: PageMatcher{Kind: "page", Path: "/posts/**"}, Pattern: "/:year-:month-:title"},
}
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
expander, err := NewPermalinkExpander(urlize, configs)
if err != nil {
b.Fatal(err)
}
for b.Loop() {
s, err := expander.Expand("posts", page)
s, err := expander.Expand(page)
if err != nil {
b.Fatal(err)
}