Skip to content

Commit

Permalink
resources/page: Allow section&taxonomy pages to have a permalink conf…
Browse files Browse the repository at this point in the history
…iguration

Allows using permalink configuration for sections (branch bundles) and
also for taxonomy pages. Extends the current permalink configuration to
be able to specified per page kind while also staying backward compatible:
all permalink patterns not dedicated to a certain kind, get automatically
added for both normal pages and term pages.

Fixes gohugoio#8523
  • Loading branch information
Mai-Lapyst committed Mar 19, 2023
1 parent b0b1b76 commit 2c59705
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 23 deletions.
1 change: 1 addition & 0 deletions hugofs/files/classifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const (
ContentClassBranch ContentClass = "branch"
ContentClassFile ContentClass = "zfile" // Sort below
ContentClassContent ContentClass = "zcontent"
ContentClassZero ContentClass = "zero" // Special value for zeroFile
)

func (c ContentClass) IsBundle() bool {
Expand Down
35 changes: 21 additions & 14 deletions hugolib/page__paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"strings"

"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs/files"

"github.com/gohugoio/hugo/resources/page"
)
Expand Down Expand Up @@ -108,14 +109,16 @@ func createTargetPathDescriptor(s *Site, p page.Page, pm *pageMeta) (page.Target
)

d := s.Deps
classifier := files.ContentClassZero

if !p.File().IsZero() {
dir = p.File().Dir()
baseName = p.File().TranslationBaseName()
contentBaseName = p.File().ContentBaseName()
classifier = p.File().Classifier()
}

if baseName != contentBaseName {
if classifier == files.ContentClassLeaf {
// See https://github.com/gohugoio/hugo/issues/4870
// A leaf bundle
dir = strings.TrimSuffix(dir, contentBaseName+helpers.FilePathSeparator)
Expand Down Expand Up @@ -143,22 +146,26 @@ func createTargetPathDescriptor(s *Site, p page.Page, pm *pageMeta) (page.Target
desc.PrefixFilePath = s.getLanguageTargetPathLang(alwaysInSubDir)
desc.PrefixLink = s.getLanguagePermalinkLang(alwaysInSubDir)

// Expand only page.KindPage and page.KindTaxonomy; don't expand other Kinds of Pages
// like page.KindSection or page.KindTaxonomyTerm because they are "shallower" and
// the permalink configuration values are likely to be redundant, e.g.
// naively expanding /category/:slug/ would give /category/categories/ for
// the "categories" page.KindTaxonomyTerm.
if p.Kind() == page.KindPage || p.Kind() == page.KindTerm {
opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
if err != nil {
return desc, err
}
opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
if err != nil {
return desc, err
}

if opath != "" {
opath, _ = url.QueryUnescape(opath)
desc.ExpandedPermalink = opath
if opath != "" {
opath, _ = url.QueryUnescape(opath)
if strings.HasSuffix(opath, "//") {
// When rewriting the _index of the section the permalink config is applied to,
// we get douple slashes at the end sometimes; clear them up here
opath = strings.TrimSuffix(opath, "/")
}

desc.ExpandedPermalink = opath

if !p.File().IsZero() {
s.Log.Debugf("Set expanded permalink path for %s %s to %#v", p.Kind(), p.File().Path(), opath)
} else {
s.Log.Debugf("Set expanded permalink path for %s in %v to %#v", p.Kind(), desc.Sections, opath)
}
}

return desc, nil
Expand Down
7 changes: 6 additions & 1 deletion resources/page/page_generate/generate_page_wrappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func generateFileIsZeroWrappers(c *codegen.Inspector) error {
methods := c.MethodsFromTypes([]reflect.Type{reflect.TypeOf((*source.File)(nil)).Elem()}, nil)

for _, m := range methods {
if m.Name == "IsZero" {
if m.Name == "IsZero" || m.Name == "Classifier" {
continue
}
fmt.Fprint(&buff, m.DeclarationNamed("zeroFile"))
Expand Down Expand Up @@ -255,6 +255,11 @@ func (zeroFile) IsZero() bool {
return true
}
func (z zeroFile) Classifier() files.ContentClass {
z.log.Warnln(".File.Classifier on zero object. Wrap it in if or with: {{ with .File }}{{ .Classifier }}{{ end }}")
return files.ContentClassZero
}
%s
`, header, importsString(pkgImports), buff.String())
Expand Down
103 changes: 95 additions & 8 deletions resources/page/permalinks.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import (

"errors"

"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/helpers"

jww "github.com/spf13/jwalterweatherman"
)

// PermalinkExpander holds permalin mappings per section.
Expand All @@ -35,7 +39,7 @@ type PermalinkExpander struct {
// to be used to replace that tag.
knownPermalinkAttributes map[string]pageToPermaAttribute

expanders map[string]func(Page) (string, error)
expanders map[string]map[string]func(Page) (string, error)

ps *helpers.PathSpec
}
Expand Down Expand Up @@ -66,6 +70,42 @@ func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
return nil, false
}

type PatternConfig map[string](map[string]string)

func (c PatternConfig) getPatterns(kind string) map[string]string {
patterns, ok := c[kind]
if (!ok) {
patterns = make(map[string]string)
c[kind] = patterns
}
return patterns
}

func (c PatternConfig) addPattern(kind string, k string, v string) {
patterns := c.getPatterns(kind)
patterns[k] = v
}

func (c PatternConfig) loadFromParams(kind string, p maps.Params, logger loggers.Logger) {
patterns := c.getPatterns(kind)
for k, v := range p {
switch v := v.(type) {
case string:
if old_value, ok := patterns[k]; ok {
logger.Warnf("Permalinks configuration overwrites pattern in kind '%s' for key '%s' with '%s' (which was '%s')\n", kind, k, v, old_value)
}
patterns[k] = v

default:
logger.Warnln("Parmalinks configuration requires string as value in per-kind configuration")
}
}
}

func isPatternKind(s string) bool {
return (s == "page") || (s == "section") || (s == "taxonomy") || (s == "term")
}

// NewPermalinkExpander creates a new PermalinkExpander configured by the given
// PathSpec.
func NewPermalinkExpander(ps *helpers.PathSpec) (PermalinkExpander, error) {
Expand All @@ -87,25 +127,62 @@ func NewPermalinkExpander(ps *helpers.PathSpec) (PermalinkExpander, error) {
"filename": p.pageToPermalinkFilename,
}

patterns := ps.Cfg.GetStringMapString("permalinks")
if patterns == nil {
config := ps.Cfg.GetStringMap("permalinks")
if config == nil {
return p, nil
}

e, err := p.parse(patterns)
if err != nil {
return p, err
logger := loggers.NewBasicLogger(jww.LevelInfo)

pattern_config := make(PatternConfig)
for k, v := range config {
switch v := v.(type) {
case string:
// [permalinks]
// key = '...'

// To sucessfully be backward compatible, "default" patterns need to be set for both page and term
pattern_config.addPattern("page", k, v)
pattern_config.addPattern("term", k, v)

case maps.Params:
// [permalinks.key]
// xyz = ???
if !isPatternKind(k) {
logger.Warnln("Permalinks configuration only allows per-kind configuration for 'page' and 'section'; unknown kind: ", k)
} else {
pattern_config.loadFromParams(k, v, logger)
}

default:
logger.Warnln("Permalinks configuration seems broken")

}
}

p.expanders = e
p.expanders = make(map[string]map[string]func(Page) (string, error))

for kind, patterns := range pattern_config {
e, err := p.parse(patterns)
if err != nil {
return p, err
}
p.expanders[kind] = e
}

return p, nil
}

// 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) {
expand, found := l.expanders[key]
expanders, found := l.expanders[p.Kind()]

if !found {
return "", nil
}

expand, found := expanders[key]

if !found {
return "", nil
Expand Down Expand Up @@ -180,6 +257,10 @@ var attributeRegexp = regexp.MustCompile(`:\w+(\[.+?\])?`)

// validate determines if a PathPattern is well-formed
func (l PermalinkExpander) validate(pp string) bool {
if len(pp) < 1 {
return true
}

fragments := strings.Split(pp[1:], "/")
bail := false
for i := range fragments {
Expand Down Expand Up @@ -244,6 +325,10 @@ func (l PermalinkExpander) pageToPermalinkDate(p Page, dateField string) (string

// pageToPermalinkTitle returns the URL-safe form of the title
func (l PermalinkExpander) pageToPermalinkTitle(p Page, _ string) (string, error) {
if p.File().TranslationBaseName() == "_index" {
return "", nil
}

return l.ps.URLize(p.Title()), nil
}

Expand All @@ -254,6 +339,8 @@ func (l PermalinkExpander) pageToPermalinkFilename(p Page, _ string) (string, er
// Page bundles; the directory name will hopefully have a better name.
dir := strings.TrimSuffix(p.File().Dir(), helpers.FilePathSeparator)
_, name = filepath.Split(dir)
} else if name == "_index" {
return "", nil
}

return l.ps.URLize(name), nil
Expand Down
5 changes: 5 additions & 0 deletions resources/page/permalinks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func TestPermalinkExpansion(t *testing.T) {
page.date = d
page.section = "blue"
page.slug = "The Slug"
page.kind = "page"

for _, item := range testdataPermalinks {
if !item.valid {
Expand Down Expand Up @@ -103,9 +104,11 @@ func TestPermalinkExpansionMultiSection(t *testing.T) {
page.date = d
page.section = "blue"
page.slug = "The Slug"
page.kind = "page"

page_slug_fallback := newTestPageWithFile("/page-filename/index.md")
page_slug_fallback.title = "Page Title"
page_slug_fallback.kind = "page"

permalinksConfig := map[string]string{
"posts": "/:slug",
Expand Down Expand Up @@ -158,6 +161,7 @@ func TestPermalinkExpansionConcurrent(t *testing.T) {
go func(i int) {
defer wg.Done()
page := newTestPage()
page.kind = "page"
for j := 1; j < 20; j++ {
page.slug = fmt.Sprintf("slug%d", i+j)
expanded, err := expander.Expand("posts", page)
Expand Down Expand Up @@ -215,6 +219,7 @@ func BenchmarkPermalinkExpand(b *testing.B) {
page.title = "Hugo Rocks"
d, _ := time.Parse("2006-01-02", "2019-02-28")
page.date = d
page.kind = "page"

permalinksConfig := map[string]string{
"posts": "/:year-:month-:title",
Expand Down
6 changes: 6 additions & 0 deletions resources/page/zero_file.autogen.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package page
import (
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/source"
)

Expand All @@ -34,6 +35,11 @@ func (zeroFile) IsZero() bool {
return true
}

func (z zeroFile) Classifier() files.ContentClass {
z.log.Warnln(".File.Classifier on zero object. Wrap it in if or with: {{ with .File }}{{ .Classifier }}{{ end }}")
return files.ContentClassZero
}

func (z zeroFile) Path() (o0 string) {
z.log.Warnln(".File.Path on zero object. Wrap it in if or with: {{ with .File }}{{ .Path }}{{ end }}")
return
Expand Down
8 changes: 8 additions & 0 deletions source/fileInfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ type FileWithoutOverlap interface {
// if file is a leaf bundle.
ContentBaseName() string

// Classifier is the ContentClass of the file
Classifier() files.ContentClass

// UniqueID is the MD5 hash of the file's path and is for most practical applications,
// Hugo content files being one of them, considered to be unique.
UniqueID() string
Expand Down Expand Up @@ -169,6 +172,11 @@ func (fi *FileInfo) ContentBaseName() string {
return fi.contentBaseName
}

// Classifier is the ContentClass of the file
func (fi *FileInfo) Classifier() files.ContentClass {
return fi.classifier;
}

// Section returns a file's section.
func (fi *FileInfo) Section() string {
fi.init()
Expand Down

0 comments on commit 2c59705

Please sign in to comment.