From 0cc9cfa885ad486f4114f969be902031d3cfac5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 12 Feb 2025 22:36:24 +0100 Subject: [PATCH] resources/page: Revise the new contentbasename permalinks tokens * Make it work for all pages, including those created from content adapters and not backed by a file. * Allow the `slug` to win, so the new tokens are: `:contentbasename`: 1. ContentBaseName `:slugorcontentbasename`: 1. Slug 2. ContentBaseName Note that a page will always have a `ContentBaseName`, so no need to fall back to e.g. the title. Closes #11722 --- docs/content/en/content-management/urls.md | 8 -- resources/page/permalinks.go | 19 ++-- resources/page/permalinks_integration_test.go | 66 +++++++++++++ resources/page/permalinks_test.go | 94 ++++++++++++------- resources/page/testhelpers_test.go | 2 +- source/fileInfo.go | 12 ++- 6 files changed, 146 insertions(+), 55 deletions(-) diff --git a/docs/content/en/content-management/urls.md b/docs/content/en/content-management/urls.md index 62ccfb6669f..ab0c7148a42 100644 --- a/docs/content/en/content-management/urls.md +++ b/docs/content/en/content-management/urls.md @@ -317,14 +317,6 @@ Use these tokens when defining the URL pattern. You can also use these tokens wh `:slugorfilename` : The slug as defined in front matter, else the content's file name without extension, applicable to the `page` page kind. -`:contentbasename` -: The content base name, as defined in [`File.ContentBaseName`], applicable to pages backed by a file. - -`:contentbasenameorslug` -: The content base name, else the slug as defined above. - -[`File.ContentBaseName`]: /methods/page/file/#contentbasename - For time-related values, you can also use the layout string components defined in Go's [time package]. For example: [time package]: https://pkg.go.dev/time#pkg-constants diff --git a/resources/page/permalinks.go b/resources/page/permalinks.go index 56dfff46f30..4018fcb3015 100644 --- a/resources/page/permalinks.go +++ b/resources/page/permalinks.go @@ -93,7 +93,7 @@ func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]ma "slugorfilename": p.pageToPermalinkSlugElseFilename, "filename": p.pageToPermalinkFilename, "contentbasename": p.pageToPermalinkContentBaseName, - "contentbasenameorslug": p.pageToPermalinkContentBaseNameOrSlug, + "slugorcontentbasename": p.pageToPermalinkSlugOrContentBaseName, } p.expanders = make(map[string]map[string]func(Page) (string, error)) @@ -311,22 +311,19 @@ func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, er // pageToPermalinkContentBaseName returns the URL-safe form of the content base name. func (l PermalinkExpander) pageToPermalinkContentBaseName(p Page, _ string) (string, error) { - if p.File() == nil { - return "", nil - } - return l.urlize(p.File().ContentBaseName()), nil + return l.urlize(p.PathInfo().BaseNameNoIdentifier()), nil } -// pageToPermalinkContentBaseNameOrSlug returns the URL-safe form of the content base name, or the slug. -func (l PermalinkExpander) pageToPermalinkContentBaseNameOrSlug(p Page, a string) (string, error) { +// pageToPermalinkSlugOrContentBaseName returns the URL-safe form of the slug, content base name, or the title. +func (l PermalinkExpander) pageToPermalinkSlugOrContentBaseName(p Page, a string) (string, error) { + if p.Slug() != "" { + return l.urlize(p.Slug()), nil + } name, err := l.pageToPermalinkContentBaseName(p, a) if err != nil { return "", nil } - if name != "" { - return name, nil - } - return l.pageToPermalinkSlugElseTitle(p, a) + return name, nil } func (l PermalinkExpander) translationBaseName(p Page) string { diff --git a/resources/page/permalinks_integration_test.go b/resources/page/permalinks_integration_test.go index 52bcd686080..6ef450edd0d 100644 --- a/resources/page/permalinks_integration_test.go +++ b/resources/page/permalinks_integration_test.go @@ -277,3 +277,69 @@ title: p2 // We strip colons from paths constructed by Hugo (they are not supported on Windows). b.AssertFileExists("public/cd/p2/index.html", true) } + +func TestPermalinksContentbasenameContentAdapter(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +[permalinks] +[permalinks.page] +a = "/:slugorcontentbasename/" +b = "/:sections/:contentbasename/" +-- content/_content.gotmpl -- +{{ $.AddPage (dict "kind" "page" "path" "a/b/contentbasename1" "title" "My A Page No Slug") }} +{{ $.AddPage (dict "kind" "page" "path" "a/b/contentbasename2" "slug" "myslug" "title" "My A Page With Slug") }} + {{ $.AddPage (dict "kind" "section" "path" "b/c" "title" "My B Section") }} +{{ $.AddPage (dict "kind" "page" "path" "b/c/contentbasename3" "title" "My B Page No Slug") }} +-- layouts/_default/single.html -- +{{ .Title }}|{{ .RelPermalink }}|{{ .Kind }}| +` + b := hugolib.Test(t, files) + + b.AssertFileContent("public/contentbasename1/index.html", "My A Page No Slug|/contentbasename1/|page|") + b.AssertFileContent("public/myslug/index.html", "My A Page With Slug|/myslug/|page|") +} + +func TestPermalinksContentbasenameWithAndWithoutFile(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +[permalinks.section] +a = "/mya/:contentbasename/" +[permalinks.page] +a = "/myapage/:contentbasename/" +[permalinks.term] +categories = "/myc/:slugorcontentbasename/" +-- content/b/c/_index.md -- +--- +title: "C section" +--- +-- content/a/b/index.md -- +--- +title: "My Title" +categories: ["c1", "c2"] +--- +-- content/categories/c2/_index.md -- +--- +title: "C2" +slug: "c2slug" +--- +-- layouts/_default/single.html -- +{{ .Title }}|{{ .RelPermalink }}|{{ .Kind }}| +-- layouts/_default/list.html -- +{{ .Title }}|{{ .RelPermalink }}|{{ .Kind }}| +` + b := hugolib.Test(t, files) + + // Sections. + b.AssertFileContent("public/mya/a/index.html", "As|/mya/a/|section|") + + // Pages. + b.AssertFileContent("public/myapage/b/index.html", "My Title|/myapage/b/|page|") + + // Taxonomies. + b.AssertFileContent("public/myc/c1/index.html", "C1|/myc/c1/|term|") + b.AssertFileContent("public/myc/c2slug/index.html", "C2|/myc/c2slug/|term|") +} diff --git a/resources/page/permalinks_test.go b/resources/page/permalinks_test.go index 71c5ded6502..ad2dbfa7604 100644 --- a/resources/page/permalinks_test.go +++ b/resources/page/permalinks_test.go @@ -22,6 +22,7 @@ import ( "time" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/source" ) // testdataPermalinks is used by a couple of tests; the expandsTo content is @@ -29,32 +30,53 @@ import ( var testdataPermalinks = []struct { spec string valid bool + withPage func(p *testPage) expandsTo string }{ - {":title", true, "spf13-vim-3.0-release-and-new-website"}, - {"/:year-:month-:title", true, "/2012-04-spf13-vim-3.0-release-and-new-website"}, - {"/:year/:yearday/:month/:monthname/:day/:weekday/:weekdayname/", true, "/2012/97/04/April/06/5/Friday/"}, // Dates - {"/:section/", true, "/blue/"}, // Section - {"/:title/", true, "/spf13-vim-3.0-release-and-new-website/"}, // Title - {"/:slug/", true, "/the-slug/"}, // Slug - {"/:slugorfilename/", true, "/the-slug/"}, // Slug or filename - {"/:filename/", true, "/test-page/"}, // Filename - {"/:06-:1-:2-:Monday", true, "/12-4-6-Friday"}, // Dates with Go formatting - {"/:2006_01_02_15_04_05.000", true, "/2012_04_06_03_01_59.000"}, // Complicated custom date format - {"/:sections/", true, "/a/b/c/"}, // Sections - {"/:sections[last]/", true, "/c/"}, // Sections - {"/:sections[0]/:sections[last]/", true, "/a/c/"}, // Sections - {"/\\:filename", true, "/:filename"}, // Escape sequence - {"/special\\::slug/", true, "/special:the-slug/"}, // Escape sequence - {"/:contentbasename/", true, "/index/"}, // Content base name - {"/:contentbasenameorslug/", true, "/index/"}, // Content base name or slug - + {":title", true, nil, "spf13-vim-3.0-release-and-new-website"}, + {"/:year-:month-:title", true, nil, "/2012-04-spf13-vim-3.0-release-and-new-website"}, + {"/:year/:yearday/:month/:monthname/:day/:weekday/:weekdayname/", true, nil, "/2012/97/04/April/06/5/Friday/"}, // Dates + {"/:section/", true, nil, "/blue/"}, // Section + {"/:title/", true, nil, "/spf13-vim-3.0-release-and-new-website/"}, // Title + {"/:slug/", true, nil, "/the-slug/"}, // Slug + {"/:slugorfilename/", true, nil, "/the-slug/"}, // Slug or filename + {"/:filename/", true, nil, "/test-page/"}, // Filename + {"/:06-:1-:2-:Monday", true, nil, "/12-4-6-Friday"}, // Dates with Go formatting + {"/:2006_01_02_15_04_05.000", true, nil, "/2012_04_06_03_01_59.000"}, // Complicated custom date format + {"/:sections/", true, nil, "/a/b/c/"}, // Sections + {"/:sections[last]/", true, nil, "/c/"}, // Sections + {"/:sections[0]/:sections[last]/", true, nil, "/a/c/"}, // Sections + {"/\\:filename", true, nil, "/:filename"}, // Escape sequence + {"/special\\::slug/", true, nil, "/special:the-slug/"}, + // contentbasename, slug or title. // Escape sequence + {"/:contentbasename/", true, nil, "/test-page/"}, + {"/:contentbasename/", true, func(p *testPage) { + p.slug = "myslug" + p.file = source.NewContentFileInfoFrom("/", "_index.md") + }, "/myslug/"}, + {"/:contentbasename/", true, func(p *testPage) { + p.slug = "" + p.title = "mytitle" + p.file = source.NewContentFileInfoFrom("/", "_index.md") + }, "/mytitle/"}, + // slug, contentbasename or title // Content base name + {"/:slugorcontentbasename/", true, func(p *testPage) { + p.slug = "" + }, "/test-page/"}, + {"/:slugorcontentbasename/", true, func(p *testPage) { + p.slug = "myslug" + }, "/myslug/"}, + {"/:slugorcontentbasename/", true, func(p *testPage) { + p.slug = "" + p.title = "mytitle" + p.file = source.NewContentFileInfoFrom("/", "_index.md") + }, "/mytitle/"}, // Failures - {"/blog/:fred", false, ""}, - {"/:year//:title", false, ""}, - {"/:TITLE", false, ""}, // case is not normalized - {"/:2017", false, ""}, // invalid date format - {"/:2006-01-02", false, ""}, // valid date format but invalid attribute name + {"/blog/:fred", false, nil, ""}, + {"/:year//:title", false, nil, ""}, + {"/:TITLE", false, nil, ""}, // case is not normalized + {"/:2017", false, nil, ""}, // invalid date format + {"/:2006-01-02", false, nil, ""}, // valid date format but invalid attribute name } func urlize(uri string) string { @@ -67,21 +89,29 @@ func TestPermalinkExpansion(t *testing.T) { c := qt.New(t) - page := newTestPageWithFile("/test-page/index.md") - page.title = "Spf13 Vim 3.0 Release and new website" - d, _ := time.Parse("2006-01-02 15:04:05", "2012-04-06 03:01:59") - page.date = d - page.section = "blue" - page.slug = "The Slug" - page.kind = "page" + newPage := func() *testPage { + page := newTestPageWithFile("/test-page/index.md") + page.title = "Spf13 Vim 3.0 Release and new website" + d, _ := time.Parse("2006-01-02 15:04:05", "2012-04-06 03:01:59") + page.date = d + page.section = "blue" + page.slug = "The Slug" + page.kind = "page" + return page + } - for _, item := range testdataPermalinks { + for i, item := range testdataPermalinks { if !item.valid { continue } + page := newPage() + if item.withPage != nil { + item.withPage(page) + } + specNameCleaner := regexp.MustCompile(`[\:\/\[\]]`) - name := specNameCleaner.ReplaceAllString(item.spec, "") + name := fmt.Sprintf("[%d] %s", i, specNameCleaner.ReplaceAllString(item.spec, "_")) c.Run(name, func(c *qt.C) { patterns := map[string]map[string]string{ diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index 8e6dfb79ac2..2802839a95f 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -52,7 +52,7 @@ func newTestPage() *testPage { func newTestPageWithFile(filename string) *testPage { filename = filepath.FromSlash(filename) - file := source.NewFileInfoFrom(filename, filename) + file := source.NewContentFileInfoFrom(filename, filename) l, err := langs.NewLanguage( "en", diff --git a/source/fileInfo.go b/source/fileInfo.go index 8403c808894..dfa5cda26af 100644 --- a/source/fileInfo.go +++ b/source/fileInfo.go @@ -22,7 +22,7 @@ import ( "github.com/gohugoio/hugo/common/hashing" "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/common/hugio" @@ -132,11 +132,17 @@ func (fi *File) p() *paths.Path { return fi.fim.Meta().PathInfo.Unnormalized() } +var contentPathParser = &paths.PathParser{ + IsContentExt: func(ext string) bool { + return true + }, +} + // Used in tests. -func NewFileInfoFrom(path, filename string) *File { +func NewContentFileInfoFrom(path, filename string) *File { meta := &hugofs.FileMeta{ Filename: filename, - PathInfo: media.DefaultPathParser.Parse("", filepath.ToSlash(path)), + PathInfo: contentPathParser.Parse(files.ComponentFolderContent, filepath.ToSlash(path)), } return NewFileInfo(hugofs.NewFileMetaInfo(nil, meta))