Minify minifiers.MinifyConfig `mapstructure:"-"`
// Permalink configuration.
- Permalinks map[string]map[string]string `mapstructure:"-"`
+ Permalinks page.PermalinksConfig `mapstructure:"-"`
// Taxonomy configuration.
Taxonomies map[string]string `mapstructure:"-"`
}
// Legacy permalink tokens
- vs := fmt.Sprintf("%v", c.Permalinks)
- if strings.Contains(vs, ":filename") {
- hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
- }
- if strings.Contains(vs, ":slugorfilename") {
- hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
+ for _, pc := range c.Permalinks {
+ if strings.Contains(pc.Pattern, ":filename") {
+ hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
+ }
+ if strings.Contains(pc.Pattern, ":slugorfilename") {
+ hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
+ }
}
// Legacy render hook values.
key: "permalinks",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
- p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.GetStringMap(d.key))
+ p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.Get(d.key))
return err
},
+ getInitializer: func(c *Config) configInitializer { return c.Permalinks },
},
"sitemap": {
key: "sitemap",
}
}
- opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
+ opath, err := d.ResourceSpec.Permalinks.Expand(p)
if err != nil {
return desc, err
}
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()
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
"slices"
"strings"
+ "github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
// 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 {
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
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())
}
}
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
&testPage{path: "p3", kind: "page", lang: "en", site: developmentTestSite}
c.Run("Matches", func(c *qt.C) {
- m := PageMatcher{Kind: "section"}
-
- c.Assert(m.Matches(p1), qt.Equals, true)
- c.Assert(m.Matches(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)
-
- 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)
-
- 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)
-
- 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)
-
- 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)
+ matches := func(m PageMatcher, p Page) bool {
+ c.Assert(m.compileGlobs(), qt.IsNil)
+ return m.Matches(p)
+ }
+
+ c.Assert(matches(PageMatcher{Kind: "section"}, p1), qt.Equals, true)
+ c.Assert(matches(PageMatcher{Kind: "section"}, p2), qt.Equals, false)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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) {
"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
// 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)](),
}
"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
+ }
+ if err := configs[i].Target.compileGlobs(); err != nil {
return p, err
}
- p.expanders[kind] = e
+
}
return p, nil
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.
})
}
-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)
}
}
-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 + "/**}"
+}
+
+// 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
+ }
- 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)
+ 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
}
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) {
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) {
})
}
}
+
+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/|")
+}
page.section = "blue"
page.slug = "The Slug"
page.kind = "page"
- // page.pathInfo
+ page.path = "/posts/test-page"
return page
}
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)
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"
-
- permalinksConfig := map[string]map[string]string{
- "page": {
- "posts": "/:slug",
- "blog": "/:section/:year",
- "recipes": "/:slugorfilename",
- "special": "/special\\::slug",
- },
+ pageSlugFallback := newTestPageWithFile("/page-filename/index.md")
+ pageSlugFallback.title = "Page Title"
+ pageSlugFallback.kind = "page"
+
+ 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")
}
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
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))
}
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 {
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)
}