diff --git a/commands/server.go b/commands/server.go index b52e38c1737..a7fedff9ca7 100644 --- a/commands/server.go +++ b/commands/server.go @@ -220,6 +220,15 @@ func (c *commandeer) serve(port int) { w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") w.Header().Set("Pragma", "no-cache") } + + if c.Cfg.GetBool("trimTrailingSlash") { + path := strings.Split(r.URL.Path, "/") + + if !strings.Contains(path[len(path)-1], ".") && !strings.HasSuffix(r.URL.Path, "/") { + r.URL.Path += ".html" + } + } + h.ServeHTTP(w, r) }) } diff --git a/helpers/pathspec.go b/helpers/pathspec.go index 643d0564657..37174bdab73 100644 --- a/helpers/pathspec.go +++ b/helpers/pathspec.go @@ -26,6 +26,7 @@ type PathSpec struct { disablePathToLower bool removePathAccents bool + trimTrailingSlash bool uglyURLs bool canonifyURLs bool @@ -77,6 +78,7 @@ func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) (*PathSpec, error) { Cfg: cfg, disablePathToLower: cfg.GetBool("disablePathToLower"), removePathAccents: cfg.GetBool("removePathAccents"), + trimTrailingSlash: cfg.GetBool("trimTrailingSlash"), uglyURLs: cfg.GetBool("uglyURLs"), canonifyURLs: cfg.GetBool("canonifyURLs"), multilingual: cfg.GetBool("multilingual"), diff --git a/helpers/pathspec_test.go b/helpers/pathspec_test.go index 04ec7cac7fe..cba4d4e7ecb 100644 --- a/helpers/pathspec_test.go +++ b/helpers/pathspec_test.go @@ -27,6 +27,7 @@ func TestNewPathSpecFromConfig(t *testing.T) { l := NewLanguage("no", v) v.Set("disablePathToLower", true) v.Set("removePathAccents", true) + v.Set("trimTrailingSlash", true) v.Set("uglyURLs", true) v.Set("multilingual", true) v.Set("defaultContentLanguageInSubdir", true) @@ -48,6 +49,7 @@ func TestNewPathSpecFromConfig(t *testing.T) { require.True(t, p.disablePathToLower) require.True(t, p.multilingual) require.True(t, p.removePathAccents) + require.True(t, p.trimTrailingSlash) require.True(t, p.uglyURLs) require.Equal(t, "no", p.defaultContentLanguage) require.Equal(t, "no", p.language.Lang) diff --git a/helpers/url.go b/helpers/url.go index 9c1a643ccc6..35e0fd1a4a2 100644 --- a/helpers/url.go +++ b/helpers/url.go @@ -323,14 +323,18 @@ func (p *PathSpec) URLizeAndPrep(in string) string { // URLPrep applies misc sanitation to the given URL. func (p *PathSpec) URLPrep(in string) string { - if p.uglyURLs { + if p.uglyURLs && !p.trimTrailingSlash { return Uglify(SanitizeURL(in)) } pretty := PrettifyURL(SanitizeURL(in)) if path.Ext(pretty) == ".xml" { return pretty } - url, err := purell.NormalizeURLString(pretty, purell.FlagAddTrailingSlash) + flag := purell.FlagAddTrailingSlash + if p.trimTrailingSlash { + flag = purell.FlagRemoveTrailingSlash + } + url, err := purell.NormalizeURLString(pretty, flag) if err != nil { return pretty } diff --git a/helpers/url_test.go b/helpers/url_test.go index 9572547c7ca..22d78898e6c 100644 --- a/helpers/url_test.go +++ b/helpers/url_test.go @@ -241,19 +241,34 @@ func TestMakePermalink(t *testing.T) { func TestURLPrep(t *testing.T) { type test struct { - ugly bool - input string - output string + trimTrailingSlash bool + uglyURLs bool + input string + output string } data := []test{ - {false, "/section/name.html", "/section/name/"}, - {true, "/section/name/index.html", "/section/name.html"}, + // trimTrailingSlash=false, uglyURLs=false + {false, false, "/section/name.html", "/section/name/"}, + {false, false, "/section/name/index.html", "/section/name/"}, + + // trimTrailingSlash=false, uglyURLs=true + {false, true, "/section/name.html", "/section/name.html"}, + {false, true, "/section/name/index.html", "/section/name.html"}, + + // trimTrailingSlash=true, uglyURLs=false + {true, false, "/section/name.html", "/section/name"}, + {true, false, "/section/name/index.html", "/section/name"}, + + // trimTrailingSlash=true, uglyURLs=true + {true, true, "/section/name.html", "/section/name"}, + {true, true, "/section/name/index.html", "/section/name"}, } for i, d := range data { v := viper.New() - v.Set("uglyURLs", d.ugly) + v.Set("trimTrailingSlash", d.trimTrailingSlash) + v.Set("uglyURLs", d.uglyURLs) l := NewDefaultLanguage(v) p, _ := NewPathSpec(hugofs.NewMem(v), l) diff --git a/hugolib/config.go b/hugolib/config.go index d0ade018fca..4cc2bec125d 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -116,6 +116,7 @@ func loadDefaultSettingsFor(v *viper.Viper) error { v.SetDefault("ignoreCache", false) v.SetDefault("canonifyURLs", false) v.SetDefault("relativeURLs", false) + v.SetDefault("trimTrailingSlash", false) v.SetDefault("removePathAccents", false) v.SetDefault("titleCaseStyle", "AP") v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"}) diff --git a/hugolib/node_as_page_test.go b/hugolib/node_as_page_test.go index 6cadafc0d29..497143ebe64 100644 --- a/hugolib/node_as_page_test.go +++ b/hugolib/node_as_page_test.go @@ -38,13 +38,15 @@ import ( func TestNodesAsPage(t *testing.T) { t.Parallel() for _, preserveTaxonomyNames := range []bool{false, true} { - for _, ugly := range []bool{true, false} { - doTestNodeAsPage(t, ugly, preserveTaxonomyNames) + for _, trimTrailingSlash := range []bool{true, false} { + for _, uglyURLs := range []bool{true, false} { + doTestNodeAsPage(t, uglyURLs, trimTrailingSlash, preserveTaxonomyNames) + } } } } -func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { +func doTestNodeAsPage(t *testing.T, uglyURLs bool, trimTrailingSlash bool, preserveTaxonomyNames bool) { /* Will have to decide what to name the node content files, but: @@ -61,13 +63,16 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { th = testHelper{cfg, fs, t} ) - cfg.Set("uglyURLs", ugly) + cfg.Set("uglyURLs", uglyURLs) + cfg.Set("trimTrailingSlash", trimTrailingSlash) cfg.Set("preserveTaxonomyNames", preserveTaxonomyNames) cfg.Set("paginate", 1) cfg.Set("title", "Hugo Rocks") cfg.Set("rssURI", "customrss.xml") + isUgly := trimTrailingSlash || uglyURLs + writeLayoutsForNodeAsPageTests(t, fs) writeNodePagesForNodeAsPageTests(t, fs, "") @@ -90,7 +95,7 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { "GetPage: Section1 ", ) - th.assertFileContent(expectedFilePath(ugly, "public", "sect1", "regular1"), "Single Title: Page 01", "Content Page 01") + th.assertFileContent(expectedFilePath(isUgly, "public", "sect1", "regular1"), "Single Title: Page 01", "Content Page 01") nodes := sites.findAllPagesByKindNotIn(KindPage) @@ -116,24 +121,24 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { require.True(t, first.IsPage()) // Check Home paginator - th.assertFileContent(expectedFilePath(ugly, "public", "page", "2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "page", "2"), "Pag: Page 02") // Check Sections - th.assertFileContent(expectedFilePath(ugly, "public", "sect1"), + th.assertFileContent(expectedFilePath(isUgly, "public", "sect1"), "Section Title: Section", "Section1 Content!", "Date: 2009-01-04", "Lastmod: 2009-01-05", ) - th.assertFileContent(expectedFilePath(ugly, "public", "sect2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "sect2"), "Section Title: Section", "Section2 Content!", "Date: 2009-01-06", "Lastmod: 2009-01-07", ) // Check Sections paginator - th.assertFileContent(expectedFilePath(ugly, "public", "sect1", "page", "2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "sect1", "page", "2"), "Pag: Page 02") sections := sites.findAllPagesByKind(KindSection) @@ -141,13 +146,13 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { require.Len(t, sections, 2) // Check taxonomy lists - th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo"), + th.assertFileContent(expectedFilePath(isUgly, "public", "categories", "hugo"), "Taxonomy Title: Taxonomy Hugo", "Taxonomy Hugo Content!", "Date: 2009-01-08", "Lastmod: 2009-01-09", ) - th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo-rocks"), + th.assertFileContent(expectedFilePath(isUgly, "public", "categories", "hugo-rocks"), "Taxonomy Title: Taxonomy Hugo Rocks", ) @@ -157,7 +162,7 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { require.NotNil(t, web) require.Len(t, web.Data["Pages"].(Pages), 4) - th.assertFileContent(expectedFilePath(ugly, "public", "categories", "web"), + th.assertFileContent(expectedFilePath(isUgly, "public", "categories", "web"), "Taxonomy Title: Taxonomy Web", "Taxonomy Web Content!", "Date: 2009-01-10", @@ -165,19 +170,19 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { ) // Check taxonomy list paginator - th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo", "page", "2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "categories", "hugo", "page", "2"), "Taxonomy Title: Taxonomy Hugo", "Pag: Page 02") // Check taxonomy terms - th.assertFileContent(expectedFilePath(ugly, "public", "categories"), + th.assertFileContent(expectedFilePath(isUgly, "public", "categories"), "Taxonomy Terms Title: Taxonomy Term Categories", "Taxonomy Term Categories Content!", "k/v: hugo", "Date: 2009-01-14", "Lastmod: 2009-01-15", ) // Check taxonomy terms paginator - th.assertFileContent(expectedFilePath(ugly, "public", "categories", "page", "2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "categories", "page", "2"), "Taxonomy Terms Title: Taxonomy Term Categories", "Pag: Taxonomy Web") @@ -193,23 +198,28 @@ func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { func TestNodesWithNoContentFile(t *testing.T) { t.Parallel() - for _, ugly := range []bool{false, true} { - doTestNodesWithNoContentFile(t, ugly) + for _, trimTrailingSlash := range []bool{true, false} { + for _, uglyURLs := range []bool{false, true} { + doTestNodesWithNoContentFile(t, uglyURLs, trimTrailingSlash) + } } } -func doTestNodesWithNoContentFile(t *testing.T, ugly bool) { +func doTestNodesWithNoContentFile(t *testing.T, uglyURLs bool, trimTrailingSlash bool) { var ( cfg, fs = newTestCfg() th = testHelper{cfg, fs, t} ) - cfg.Set("uglyURLs", ugly) + cfg.Set("trimTrailingSlash", trimTrailingSlash) + cfg.Set("uglyURLs", uglyURLs) cfg.Set("paginate", 1) cfg.Set("title", "Hugo Rocks!") cfg.Set("rssURI", "customrss.xml") + isUgly := trimTrailingSlash || uglyURLs + writeLayoutsForNodeAsPageTests(t, fs) writeRegularPagesForNodeAsPageTests(t, fs) @@ -237,21 +247,23 @@ func doTestNodesWithNoContentFile(t *testing.T, ugly bool) { ) // Taxonomy list - th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo"), + th.assertFileContent(expectedFilePath(isUgly, "public", "categories", "hugo"), "Taxonomy Title: Hugo", "Date: 2010-06-12", "Lastmod: 2010-06-13", ) // Taxonomy terms - th.assertFileContent(expectedFilePath(ugly, "public", "categories"), + th.assertFileContent(expectedFilePath(isUgly, "public", "categories"), "Taxonomy Terms Title: Categories", ) pages := s.findPagesByKind(KindTaxonomyTerm) for _, p := range pages { var want string - if ugly { + if trimTrailingSlash { + want = "/" + p.s.PathSpec.URLize(p.Title) + } else if uglyURLs { want = "/" + p.s.PathSpec.URLize(p.Title) + ".html" } else { want = "/" + p.s.PathSpec.URLize(p.Title) + "/" @@ -262,13 +274,13 @@ func doTestNodesWithNoContentFile(t *testing.T, ugly bool) { } // Sections - th.assertFileContent(expectedFilePath(ugly, "public", "sect1"), + th.assertFileContent(expectedFilePath(isUgly, "public", "sect1"), "Section Title: Sect1s", "Date: 2010-06-12", "Lastmod: 2010-06-13", ) - th.assertFileContent(expectedFilePath(ugly, "public", "sect2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "sect2"), "Section Title: Sect2s", "Date: 2008-07-06", "Lastmod: 2008-07-09", @@ -285,14 +297,16 @@ func doTestNodesWithNoContentFile(t *testing.T, ugly bool) { func TestNodesAsPageMultilingual(t *testing.T) { t.Parallel() - for _, ugly := range []bool{false, true} { - t.Run(fmt.Sprintf("ugly=%t", ugly), func(t *testing.T) { - doTestNodesAsPageMultilingual(t, ugly) - }) + for _, trimTrailingSlash := range []bool{false, true} { + for _, uglyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("trimTrailingSlash=%t,uglyURLs=%t", trimTrailingSlash, uglyURLs), func(t *testing.T) { + doTestNodesAsPageMultilingual(t, uglyURLs, trimTrailingSlash) + }) + } } } -func doTestNodesAsPageMultilingual(t *testing.T, ugly bool) { +func doTestNodesAsPageMultilingual(t *testing.T, uglyURLs bool, trimTrailingSlash bool) { mf := afero.NewMemMapFs() @@ -325,7 +339,10 @@ title = "Deutsche Hugo" cfg, err := LoadConfig(mf, "", "config.toml") require.NoError(t, err) - cfg.Set("uglyURLs", ugly) + cfg.Set("trimTrailingSlash", trimTrailingSlash) + cfg.Set("uglyURLs", uglyURLs) + + isUgly := trimTrailingSlash || uglyURLs fs := hugofs.NewFrom(mf, cfg) @@ -372,7 +389,7 @@ title = "Deutsche Hugo" require.Equal(t, "en", deHome.Translations()[1].Language().Lang) require.Equal(t, "nn", deHome.Translations()[0].Language().Lang) // See issue #3179 - require.Equal(t, expetedPermalink(false, "/de/"), deHome.Permalink()) + require.Equal(t, expectedPermalink(false, false, "/de/"), deHome.Permalink()) enSect := sites.Sites[1].getPage("section", "sect1") require.NotNil(t, enSect) @@ -381,7 +398,7 @@ title = "Deutsche Hugo" require.Equal(t, "de", enSect.Translations()[1].Language().Lang) require.Equal(t, "nn", enSect.Translations()[0].Language().Lang) - require.Equal(t, expetedPermalink(ugly, "/en/sect1/"), enSect.Permalink()) + require.Equal(t, expectedPermalink(uglyURLs, trimTrailingSlash, "/en/sect1/"), enSect.Permalink()) th.assertFileContent(filepath.Join("public", "nn", "index.html"), "Index Title: Hugo på norsk") @@ -391,31 +408,31 @@ title = "Deutsche Hugo" "Index Title: Home Sweet Home!", "Content!") // Taxonomy list - th.assertFileContent(expectedFilePath(ugly, "public", "nn", "categories", "hugo"), + th.assertFileContent(expectedFilePath(isUgly, "public", "nn", "categories", "hugo"), "Taxonomy Title: Hugo") - th.assertFileContent(expectedFilePath(ugly, "public", "en", "categories", "hugo"), + th.assertFileContent(expectedFilePath(isUgly, "public", "en", "categories", "hugo"), "Taxonomy Title: Taxonomy Hugo") // Taxonomy terms - th.assertFileContent(expectedFilePath(ugly, "public", "nn", "categories"), + th.assertFileContent(expectedFilePath(isUgly, "public", "nn", "categories"), "Taxonomy Terms Title: Categories") - th.assertFileContent(expectedFilePath(ugly, "public", "en", "categories"), + th.assertFileContent(expectedFilePath(isUgly, "public", "en", "categories"), "Taxonomy Terms Title: Taxonomy Term Categories") // Sections - th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect1"), + th.assertFileContent(expectedFilePath(isUgly, "public", "nn", "sect1"), "Section Title: Sect1s") - th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "nn", "sect2"), "Section Title: Sect2s") - th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect1"), + th.assertFileContent(expectedFilePath(isUgly, "public", "en", "sect1"), "Section Title: Section1") - th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "en", "sect2"), "Section Title: Section2") // Regular pages - th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect1", "regular1"), + th.assertFileContent(expectedFilePath(isUgly, "public", "en", "sect1", "regular1"), "Single Title: Page 01") - th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect1", "regular2"), + th.assertFileContent(expectedFilePath(isUgly, "public", "nn", "sect1", "regular2"), "Single Title: Page 02") // RSS @@ -816,15 +833,18 @@ Lastmod: {{ .Lastmod.Format "2006-01-02" }} `) } -func expectedFilePath(ugly bool, path ...string) string { - if ugly { +func expectedFilePath(isUgly bool, path ...string) string { + if isUgly { return filepath.Join(append(path[0:len(path)-1], path[len(path)-1]+".html")...) } return filepath.Join(append(path, "index.html")...) } -func expetedPermalink(ugly bool, path string) string { - if ugly { +func expectedPermalink(uglyURLs bool, trimTrailingSlash bool, path string) string { + if trimTrailingSlash { + return strings.TrimSuffix(path, "/") + } + if uglyURLs { return strings.TrimSuffix(path, "/") + ".html" } return path diff --git a/hugolib/page_paths.go b/hugolib/page_paths.go index 73fd622788e..9ae111defb2 100644 --- a/hugolib/page_paths.go +++ b/hugolib/page_paths.go @@ -62,6 +62,9 @@ type targetPathDescriptor struct { // The expanded permalink if defined for the section, ready to use. ExpandedPermalink string + // Whether to trim the trailing slash from URLs + TrimTrailingSlash bool + // Some types cannot have uglyURLs, even if globally enabled, RSS being one example. UglyURLs bool } @@ -81,12 +84,13 @@ func (p *Page) createTargetPathDescriptor(t output.Format) (targetPathDescriptor func (p *Page) initTargetPathDescriptor() error { d := &targetPathDescriptor{ - PathSpec: p.s.PathSpec, - Kind: p.Kind, - Sections: p.sections, - UglyURLs: p.s.Info.uglyURLs, - Dir: filepath.ToSlash(p.Source.Dir()), - URL: p.URLPath.URL, + PathSpec: p.s.PathSpec, + Kind: p.Kind, + Sections: p.sections, + TrimTrailingSlash: p.s.Info.trimTrailingSlash, + UglyURLs: p.s.Info.uglyURLs, + Dir: filepath.ToSlash(p.Source.Dir()), + URL: p.URLPath.URL, } if p.Slug != "" { @@ -139,7 +143,7 @@ func createTargetPath(d targetPathDescriptor) string { // the index base even when uglyURLs is enabled. needsBase := true - isUgly := d.UglyURLs && !d.Type.NoUgly + isUgly := (d.UglyURLs || d.TrimTrailingSlash) && !d.Type.NoUgly // If the page output format's base name is the same as the page base name, // we treat it as an ugly path, i.e. @@ -164,7 +168,11 @@ func createTargetPath(d targetPathDescriptor) string { if d.URL != "" { pagePath = filepath.Join(pagePath, d.URL) if strings.HasSuffix(d.URL, "/") || !strings.Contains(d.URL, ".") { - pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) + if d.TrimTrailingSlash { + pagePath += d.Type.MediaType.Delimiter + d.Type.MediaType.Suffix + } else { + pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) + } } } else { if d.ExpandedPermalink != "" { @@ -244,6 +252,10 @@ func (p *Page) createRelativePermalinkForOutputFormat(f output.Format) string { // For /index.json etc. we must use the full path. if strings.HasSuffix(f.BaseFilename(), "html") { tp = strings.TrimSuffix(tp, f.BaseFilename()) + + if p.s.Info.trimTrailingSlash { + tp = strings.TrimSuffix(tp, ".html") + } } return p.s.PathSpec.URLizeFilename(tp) diff --git a/hugolib/page_paths_test.go b/hugolib/page_paths_test.go index 80dc390ccc1..725439ffcc8 100644 --- a/hugolib/page_paths_test.go +++ b/hugolib/page_paths_test.go @@ -41,150 +41,155 @@ func TestPageTargetPath(t *testing.T) { } for _, langPrefix := range []string{"", "no"} { - for _, uglyURLs := range []bool{false, true} { - t.Run(fmt.Sprintf("langPrefix=%q,uglyURLs=%t", langPrefix, uglyURLs), - func(t *testing.T) { - - tests := []struct { - name string - d targetPathDescriptor - expected string - }{ - {"JSON home", targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "/index.json"}, - {"AMP home", targetPathDescriptor{Kind: KindHome, Type: output.AMPFormat}, "/amp/index.html"}, - {"HTML home", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: output.HTMLFormat}, "/index.html"}, - {"Netlify redirects", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: noExtDelimFormat}, "/_redirects"}, - {"HTML section list", targetPathDescriptor{ - Kind: KindSection, - Sections: []string{"sect1"}, - BaseName: "_index", - Type: output.HTMLFormat}, "/sect1/index.html"}, - {"HTML taxonomy list", targetPathDescriptor{ - Kind: KindTaxonomy, - Sections: []string{"tags", "hugo"}, - BaseName: "_index", - Type: output.HTMLFormat}, "/tags/hugo/index.html"}, - {"HTML taxonomy term", targetPathDescriptor{ - Kind: KindTaxonomy, - Sections: []string{"tags"}, - BaseName: "_index", - Type: output.HTMLFormat}, "/tags/index.html"}, - { - "HTML page", targetPathDescriptor{ - Kind: KindPage, - Dir: "/a/b", - BaseName: "mypage", - Sections: []string{"a"}, - Type: output.HTMLFormat}, "/a/b/mypage/index.html"}, - - { - // Issue #3396 - "HTML page with index as base", targetPathDescriptor{ - Kind: KindPage, - Dir: "/a/b", - BaseName: "index", - Sections: []string{"a"}, - Type: output.HTMLFormat}, "/a/b/index.html"}, - - { - "HTML page with special chars", targetPathDescriptor{ - Kind: KindPage, - Dir: "/a/b", - BaseName: "My Page!", - Type: output.HTMLFormat}, "/a/b/My-Page/index.html"}, - {"RSS home", targetPathDescriptor{Kind: kindRSS, Type: output.RSSFormat}, "/index.xml"}, - {"RSS section list", targetPathDescriptor{ - Kind: kindRSS, - Sections: []string{"sect1"}, - Type: output.RSSFormat}, "/sect1/index.xml"}, - { - "AMP page", targetPathDescriptor{ - Kind: KindPage, - Dir: "/a/b/c", - BaseName: "myamp", - Type: output.AMPFormat}, "/amp/a/b/c/myamp/index.html"}, - { - "AMP page with URL with suffix", targetPathDescriptor{ - Kind: KindPage, - Dir: "/sect/", - BaseName: "mypage", - URL: "/some/other/url.xhtml", - Type: output.HTMLFormat}, "/some/other/url.xhtml"}, - { - "JSON page with URL without suffix", targetPathDescriptor{ - Kind: KindPage, - Dir: "/sect/", - BaseName: "mypage", - URL: "/some/other/path/", - Type: output.JSONFormat}, "/some/other/path/index.json"}, - { - "JSON page with URL without suffix and no trailing slash", targetPathDescriptor{ - Kind: KindPage, - Dir: "/sect/", - BaseName: "mypage", - URL: "/some/other/path", - Type: output.JSONFormat}, "/some/other/path/index.json"}, - { - "HTML page with expanded permalink", targetPathDescriptor{ - Kind: KindPage, - Dir: "/a/b", - BaseName: "mypage", - ExpandedPermalink: "/2017/10/my-title", - Type: output.HTMLFormat}, "/2017/10/my-title/index.html"}, - { - "Paginated HTML home", targetPathDescriptor{ - Kind: KindHome, + for _, trimTrailingSlash := range []bool{false, true} { + for _, uglyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("langPrefix=%q,trimTrailingSlash=%t,uglyURLs=%t", langPrefix, trimTrailingSlash, uglyURLs), + func(t *testing.T) { + + tests := []struct { + name string + d targetPathDescriptor + expected string + }{ + {"JSON home", targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "/index.json"}, + {"AMP home", targetPathDescriptor{Kind: KindHome, Type: output.AMPFormat}, "/amp/index.html"}, + {"HTML home", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: output.HTMLFormat}, "/index.html"}, + {"Netlify redirects", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: noExtDelimFormat}, "/_redirects"}, + {"HTML section list", targetPathDescriptor{ + Kind: KindSection, + Sections: []string{"sect1"}, BaseName: "_index", - Type: output.HTMLFormat, - Addends: "page/3"}, "/page/3/index.html"}, - { - "Paginated Taxonomy list", targetPathDescriptor{ + Type: output.HTMLFormat}, "/sect1/index.html"}, + {"HTML taxonomy list", targetPathDescriptor{ Kind: KindTaxonomy, - BaseName: "_index", Sections: []string{"tags", "hugo"}, - Type: output.HTMLFormat, - Addends: "page/3"}, "/tags/hugo/page/3/index.html"}, - { - "Regular page with addend", targetPathDescriptor{ - Kind: KindPage, - Dir: "/a/b", - BaseName: "mypage", - Addends: "c/d/e", - Type: output.HTMLFormat}, "/a/b/mypage/c/d/e/index.html"}, - } - - for i, test := range tests { - test.d.PathSpec = pathSpec - test.d.UglyURLs = uglyURLs - test.d.LangPrefix = langPrefix - test.d.Dir = filepath.FromSlash(test.d.Dir) - isUgly := uglyURLs && !test.d.Type.NoUgly - - expected := test.expected - - // TODO(bep) simplify - if test.d.Kind == KindPage && test.d.BaseName == test.d.Type.BaseName { - - } else if test.d.Kind == KindHome && test.d.Type.Path != "" { - } else if (!strings.HasPrefix(expected, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly { - expected = strings.Replace(expected, - "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix, - "."+test.d.Type.MediaType.Suffix, -1) + BaseName: "_index", + Type: output.HTMLFormat}, "/tags/hugo/index.html"}, + {"HTML taxonomy term", targetPathDescriptor{ + Kind: KindTaxonomy, + Sections: []string{"tags"}, + BaseName: "_index", + Type: output.HTMLFormat}, "/tags/index.html"}, + { + "HTML page", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + Sections: []string{"a"}, + Type: output.HTMLFormat}, "/a/b/mypage/index.html"}, + + { + // Issue #3396 + "HTML page with index as base", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "index", + Sections: []string{"a"}, + Type: output.HTMLFormat}, "/a/b/index.html"}, + + { + "HTML page with special chars", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "My Page!", + Type: output.HTMLFormat}, "/a/b/My-Page/index.html"}, + {"RSS home", targetPathDescriptor{Kind: kindRSS, Type: output.RSSFormat}, "/index.xml"}, + {"RSS section list", targetPathDescriptor{ + Kind: kindRSS, + Sections: []string{"sect1"}, + Type: output.RSSFormat}, "/sect1/index.xml"}, + { + "AMP page", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b/c", + BaseName: "myamp", + Type: output.AMPFormat}, "/amp/a/b/c/myamp/index.html"}, + { + "AMP page with URL with suffix", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/url.xhtml", + Type: output.HTMLFormat}, "/some/other/url.xhtml"}, + { + "JSON page with URL without suffix", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/path/", + Type: output.JSONFormat}, "/some/other/path/index.json"}, + { + "JSON page with URL without suffix and no trailing slash", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/path", + Type: output.JSONFormat}, "/some/other/path/index.json"}, + { + "HTML page with expanded permalink", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + ExpandedPermalink: "/2017/10/my-title", + Type: output.HTMLFormat}, "/2017/10/my-title/index.html"}, + { + "Paginated HTML home", targetPathDescriptor{ + Kind: KindHome, + BaseName: "_index", + Type: output.HTMLFormat, + Addends: "page/3"}, "/page/3/index.html"}, + { + "Paginated Taxonomy list", targetPathDescriptor{ + Kind: KindTaxonomy, + BaseName: "_index", + Sections: []string{"tags", "hugo"}, + Type: output.HTMLFormat, + Addends: "page/3"}, "/tags/hugo/page/3/index.html"}, + { + "Regular page with addend", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + Addends: "c/d/e", + Type: output.HTMLFormat}, "/a/b/mypage/c/d/e/index.html"}, } - if test.d.LangPrefix != "" && !(test.d.Kind == KindPage && test.d.URL != "") { - expected = "/" + test.d.LangPrefix + expected - } + for i, test := range tests { + test.d.PathSpec = pathSpec + test.d.TrimTrailingSlash = trimTrailingSlash + test.d.UglyURLs = uglyURLs + test.d.LangPrefix = langPrefix + test.d.Dir = filepath.FromSlash(test.d.Dir) + isUgly := (uglyURLs || trimTrailingSlash) && !test.d.Type.NoUgly + + expected := test.expected + + // TODO(bep) simplify + if test.d.Kind == KindPage && test.d.BaseName == test.d.Type.BaseName { + + } else if test.d.Kind == KindHome && test.d.Type.Path != "" { + } else if !strings.HasPrefix(expected, "/index") || test.d.Addends != "" { + if (isUgly && test.d.URL == "") || (trimTrailingSlash && test.d.URL != "") { + expected = strings.Replace(expected, + "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix, + "."+test.d.Type.MediaType.Suffix, -1) + } + } + + if test.d.LangPrefix != "" && !(test.d.Kind == KindPage && test.d.URL != "") { + expected = "/" + test.d.LangPrefix + expected + } - expected = filepath.FromSlash(expected) + expected = filepath.FromSlash(expected) - pagePath := createTargetPath(test.d) + pagePath := createTargetPath(test.d) - if pagePath != expected { - t.Fatalf("[%d] [%s] targetPath expected %q, got: %q", i, test.name, expected, pagePath) + if pagePath != expected { + t.Fatalf("[%d] [%s] targetPath expected %q, got: %q", i, test.name, expected, pagePath) + } } - } - }) + }) + } } } } diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go index 6f899efaeac..76ce11cf377 100644 --- a/hugolib/page_permalink_test.go +++ b/hugolib/page_permalink_test.go @@ -28,47 +28,139 @@ func TestPermalink(t *testing.T) { t.Parallel() tests := []struct { - file string - base template.URL - slug string - url string - uglyURLs bool - canonifyURLs bool - expectedAbs string - expectedRel string + file string + base template.URL + slug string + url string + uglyURLs bool + canonifyURLs bool + trimTrailingSlash bool + expectedAbs string + expectedRel string }{ - {"x/y/z/boofar.md", "", "", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, - {"x/y/z/boofar.md", "", "", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, - // Issue #1174 - {"x/y/z/boofar.md", "http://gopher.com/", "", "", false, true, "http://gopher.com/x/y/z/boofar/", "/x/y/z/boofar/"}, - {"x/y/z/boofar.md", "http://gopher.com/", "", "", true, true, "http://gopher.com/x/y/z/boofar.html", "/x/y/z/boofar.html"}, - {"x/y/z/boofar.md", "", "boofar", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, - {"x/y/z/boofar.md", "http://barnew/", "", "", false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"}, - {"x/y/z/boofar.md", "http://barnew/", "boofar", "", false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"}, - {"x/y/z/boofar.md", "", "", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, - {"x/y/z/boofar.md", "", "", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, - {"x/y/z/boofar.md", "", "boofar", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, - {"x/y/z/boofar.md", "http://barnew/", "", "", true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"}, - {"x/y/z/boofar.md", "http://barnew/", "boofar", "", true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"}, - {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, false, "http://barnew/boo/x/y/z/booslug.html", "/boo/x/y/z/booslug.html"}, - {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, true, "http://barnew/boo/x/y/z/booslug/", "/x/y/z/booslug/"}, - {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, false, "http://barnew/boo/x/y/z/booslug/", "/boo/x/y/z/booslug/"}, - {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, true, "http://barnew/boo/x/y/z/booslug.html", "/x/y/z/booslug.html"}, - {"x/y/z/boofar.md", "http://barnew/boo", "booslug", "", true, true, "http://barnew/boo/x/y/z/booslug.html", "/x/y/z/booslug.html"}, - - // test URL overrides - {"x/y/z/boofar.md", "", "", "/z/y/q/", false, false, "/z/y/q/", "/z/y/q/"}, + // canonifyURLs=false, trimTrailingSlash=false, uglyURLs=false + {"x/y/z/boofar.md", "", "", "", false, false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", false, false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "", false, false, false, "http://barnew/boo/x/y/z/boofar/", "/boo/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "", "boofar", "", false, false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "", "", "/z/y/q/", false, false, false, "/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", false, false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, false, false, "http://barnew/boo/x/y/z/booslug/", "/boo/x/y/z/booslug/"}, + {"x/y/z/boofar.md", "http://barnew/", "", "/z/y/q/", false, false, false, "http://barnew/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "/z/y/q/", false, false, false, "http://barnew/boo/z/y/q/", "/boo/z/y/q/"}, + {"x/y/z/boofar.md", "", "boofar", "/z/y/q/", false, false, false, "/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "/z/y/q/", false, false, false, "http://barnew/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "/z/y/q/", false, false, false, "http://barnew/boo/z/y/q/", "/boo/z/y/q/"}, + + // canonifyURLs=true, trimTrailingSlash=false, uglyURLs=false + {"x/y/z/boofar.md", "", "", "", false, true, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", false, true, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "", false, true, false, "http://barnew/boo/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "", "boofar", "", false, true, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "", "", "/z/y/q/", false, true, false, "/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", false, true, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, true, false, "http://barnew/boo/x/y/z/booslug/", "/x/y/z/booslug/"}, + {"x/y/z/boofar.md", "http://barnew/", "", "/z/y/q/", false, true, false, "http://barnew/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "/z/y/q/", false, true, false, "http://barnew/boo/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "", "boofar", "/z/y/q/", false, true, false, "/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "/z/y/q/", false, true, false, "http://barnew/z/y/q/", "/z/y/q/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "/z/y/q/", false, true, false, "http://barnew/boo/z/y/q/", "/z/y/q/"}, + + // canonifyURLs=false, trimTrailingSlash=true, uglyURLs=false + {"x/y/z/boofar.md", "", "", "", false, false, true, "/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", false, false, true, "http://barnew/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "", false, false, true, "http://barnew/boo/x/y/z/boofar", "/boo/x/y/z/boofar"}, + {"x/y/z/boofar.md", "", "boofar", "", false, false, true, "/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "", "", "/z/y/q.html", false, false, true, "/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", false, false, true, "http://barnew/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, false, true, "http://barnew/boo/x/y/z/booslug", "/boo/x/y/z/booslug"}, + {"x/y/z/boofar.md", "http://barnew/", "", "/z/y/q.html", false, false, true, "http://barnew/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "/z/y/q.html", false, false, true, "http://barnew/boo/z/y/q", "/boo/z/y/q"}, + {"x/y/z/boofar.md", "", "boofar", "/z/y/q.html", false, false, true, "/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "/z/y/q.html", false, false, true, "http://barnew/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "/z/y/q.html", false, false, true, "http://barnew/boo/z/y/q", "/boo/z/y/q"}, + + // canonifyURLs=false, trimTrailingSlash=false, uglyURLs=true + {"x/y/z/boofar.md", "", "", "", true, false, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", true, false, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "", true, false, false, "http://barnew/boo/x/y/z/boofar.html", "/boo/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "", "boofar", "", true, false, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "", "", "/z/y/q.html", true, false, false, "/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", true, false, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, false, false, "http://barnew/boo/x/y/z/booslug.html", "/boo/x/y/z/booslug.html"}, + {"x/y/z/boofar.md", "http://barnew/", "", "/z/y/q.html", true, false, false, "http://barnew/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "/z/y/q.html", true, false, false, "http://barnew/boo/z/y/q.html", "/boo/z/y/q.html"}, + {"x/y/z/boofar.md", "", "boofar", "/z/y/q.html", true, false, false, "/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "/z/y/q.html", true, false, false, "http://barnew/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "/z/y/q.html", true, false, false, "http://barnew/boo/z/y/q.html", "/boo/z/y/q.html"}, + + // canonifyURLs=true, trimTrailingSlash=true, uglyURLs=false + {"x/y/z/boofar.md", "", "", "", false, true, true, "/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", false, true, true, "http://barnew/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "", false, true, true, "http://barnew/boo/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "", "boofar", "", false, true, true, "/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "", "", "/z/y/q.html", false, true, true, "/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", false, true, true, "http://barnew/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, true, true, "http://barnew/boo/x/y/z/booslug", "/x/y/z/booslug"}, + {"x/y/z/boofar.md", "http://barnew/", "", "/z/y/q.html", false, true, true, "http://barnew/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "/z/y/q.html", false, true, true, "http://barnew/boo/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "", "boofar", "/z/y/q.html", false, true, true, "/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "/z/y/q.html", false, true, true, "http://barnew/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "/z/y/q.html", false, true, true, "http://barnew/boo/z/y/q", "/z/y/q"}, + + // canonifyURLs=true, trimTrailingSlash=false, uglyURLs=true + {"x/y/z/boofar.md", "", "", "", true, true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", true, true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "", true, true, false, "http://barnew/boo/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "", "boofar", "", true, true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "", "", "/z/y/q.html", true, true, false, "/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", true, true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, true, false, "http://barnew/boo/x/y/z/booslug.html", "/x/y/z/booslug.html"}, + {"x/y/z/boofar.md", "http://barnew/", "", "/z/y/q.html", true, true, false, "http://barnew/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "/z/y/q.html", true, true, false, "http://barnew/boo/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "", "boofar", "/z/y/q.html", true, true, false, "/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "/z/y/q.html", true, true, false, "http://barnew/z/y/q.html", "/z/y/q.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "/z/y/q.html", true, true, false, "http://barnew/boo/z/y/q.html", "/z/y/q.html"}, + + // canonifyURLs=false, trimTrailingSlash=true, uglyURLs=true + {"x/y/z/boofar.md", "", "", "", true, false, true, "/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", true, false, true, "http://barnew/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "", true, false, true, "http://barnew/boo/x/y/z/boofar", "/boo/x/y/z/boofar"}, + {"x/y/z/boofar.md", "", "boofar", "", true, false, true, "/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "", "", "/z/y/q.html", true, false, true, "/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", true, false, true, "http://barnew/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, false, true, "http://barnew/boo/x/y/z/booslug", "/boo/x/y/z/booslug"}, + {"x/y/z/boofar.md", "http://barnew/", "", "/z/y/q.html", true, false, true, "http://barnew/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "/z/y/q.html", true, false, true, "http://barnew/boo/z/y/q", "/boo/z/y/q"}, + {"x/y/z/boofar.md", "", "boofar", "/z/y/q.html", true, false, true, "/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "/z/y/q.html", true, false, true, "http://barnew/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "/z/y/q.html", true, false, true, "http://barnew/boo/z/y/q", "/boo/z/y/q"}, + + // canonifyURLs=true, trimTrailingSlash=true, uglyURLs=true + {"x/y/z/boofar.md", "", "", "", true, true, true, "/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", true, true, true, "http://barnew/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "", true, true, true, "http://barnew/boo/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "", "boofar", "", true, true, true, "/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "", "", "/z/y/q.html", true, true, true, "/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", true, true, true, "http://barnew/x/y/z/boofar", "/x/y/z/boofar"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, true, true, "http://barnew/boo/x/y/z/booslug", "/x/y/z/booslug"}, + {"x/y/z/boofar.md", "http://barnew/", "", "/z/y/q.html", true, true, true, "http://barnew/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "", "/z/y/q.html", true, true, true, "http://barnew/boo/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "", "boofar", "/z/y/q.html", true, true, true, "/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "/z/y/q.html", true, true, true, "http://barnew/z/y/q", "/z/y/q"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "/z/y/q.html", true, true, true, "http://barnew/boo/z/y/q", "/z/y/q"}, } for i, test := range tests { + t.Run(fmt.Sprintf("uglyURLs=%t,canonifyURLs=%t,trimTrailingSlash=%t", test.uglyURLs, test.canonifyURLs, test.trimTrailingSlash), func(t *testing.T) { + cfg, fs := newTestCfg() - cfg, fs := newTestCfg() + cfg.Set("uglyURLs", test.uglyURLs) + cfg.Set("canonifyURLs", test.canonifyURLs) + cfg.Set("trimTrailingSlash", test.trimTrailingSlash) + cfg.Set("baseURL", test.base) - cfg.Set("uglyURLs", test.uglyURLs) - cfg.Set("canonifyURLs", test.canonifyURLs) - cfg.Set("baseURL", test.base) - - pageContent := fmt.Sprintf(`--- + pageContent := fmt.Sprintf(`--- title: Page slug: %q url: %q @@ -76,25 +168,26 @@ url: %q Content `, test.slug, test.url) - writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.file)), pageContent) + writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.file)), pageContent) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - require.Len(t, s.RegularPages, 1) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + require.Len(t, s.RegularPages, 1) - p := s.RegularPages[0] + p := s.RegularPages[0] - u := p.Permalink() + u := p.Permalink() - expected := test.expectedAbs - if u != expected { - t.Fatalf("[%d] Expected abs url: %s, got: %s", i, expected, u) - } + expected := test.expectedAbs + if u != expected { + t.Fatalf("[%d] Expected abs url: %s, got: %s", i, expected, u) + } - u = p.RelPermalink() + u = p.RelPermalink() - expected = test.expectedRel - if u != expected { - t.Errorf("[%d] Expected rel url: %s, got: %s", i, expected, u) - } + expected = test.expectedRel + if u != expected { + t.Errorf("[%d] Expected rel url: %s, got: %s", i, expected, u) + } + }) } } diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 7723c02c443..f1a6a653302 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -1484,37 +1484,39 @@ func TestShouldBuild(t *testing.T) { func TestPathIssues(t *testing.T) { t.Parallel() for _, disablePathToLower := range []bool{false, true} { - for _, uglyURLs := range []bool{false, true} { - t.Run(fmt.Sprintf("disablePathToLower=%t,uglyURLs=%t", disablePathToLower, uglyURLs), func(t *testing.T) { + for _, trimTrailingSlash := range []bool{false, true} { + for _, uglyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("disablePathToLower=%t,trimTrailingSlash=%t,uglyURLs=%t", disablePathToLower, trimTrailingSlash, uglyURLs), func(t *testing.T) { - cfg, fs := newTestCfg() - th := testHelper{cfg, fs, t} + cfg, fs := newTestCfg() + th := testHelper{cfg, fs, t} - cfg.Set("permalinks", map[string]string{ - "post": ":section/:title", - }) + cfg.Set("permalinks", map[string]string{ + "post": ":section/:title", + }) - cfg.Set("uglyURLs", uglyURLs) - cfg.Set("disablePathToLower", disablePathToLower) - cfg.Set("paginate", 1) + cfg.Set("trimTrailingSlash", trimTrailingSlash) + cfg.Set("uglyURLs", uglyURLs) + cfg.Set("disablePathToLower", disablePathToLower) + cfg.Set("paginate", 1) - writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "{{.Content}}") - writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), - "P{{.Paginator.PageNumber}}|URL: {{.Paginator.URL}}|{{ if .Paginator.HasNext }}Next: {{.Paginator.Next.URL }}{{ end }}") + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), + "P{{.Paginator.PageNumber}}|URL: {{.Paginator.URL}}|{{ if .Paginator.HasNext }}Next: {{.Paginator.Next.URL }}{{ end }}") - for i := 0; i < 3; i++ { - writeSource(t, fs, filepath.Join("content", "post", fmt.Sprintf("doc%d.md", i)), - fmt.Sprintf(`--- + for i := 0; i < 3; i++ { + writeSource(t, fs, filepath.Join("content", "post", fmt.Sprintf("doc%d.md", i)), + fmt.Sprintf(`--- title: "test%d.dot" tags: - ".net" --- # doc1 *some content*`, i)) - } + } - writeSource(t, fs, filepath.Join("content", "Blog", "Blog1.md"), - fmt.Sprintf(`--- + writeSource(t, fs, filepath.Join("content", "Blog", "Blog1.md"), + fmt.Sprintf(`--- title: "testBlog" tags: - "Blog" @@ -1522,46 +1524,55 @@ tags: # doc1 *some blog content*`)) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - require.Len(t, s.RegularPages, 4) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + require.Len(t, s.RegularPages, 4) - pathFunc := func(s string) string { - if uglyURLs { - return strings.Replace(s, "/index.html", ".html", 1) + pathFunc := func(s string) string { + if trimTrailingSlash || uglyURLs { + return strings.Replace(s, "/index.html", ".html", 1) + } + return s } - return s - } - - blog := "blog" - - if disablePathToLower { - blog = "Blog" - } - th.assertFileContent(pathFunc("public/"+blog+"/"+blog+"1/index.html"), "some blog content") + blog := "blog" - th.assertFileContent(pathFunc("public/post/test0.dot/index.html"), "some content") + if disablePathToLower { + blog = "Blog" + } - if uglyURLs { - th.assertFileContent("public/post/page/1.html", `canonical" href="/post.html"/`) - th.assertFileContent("public/post.html", `P1|URL: /post.html|Next: /post/page/2.html`) - th.assertFileContent("public/post/page/2.html", `P2|URL: /post/page/2.html|Next: /post/page/3.html`) - } else { - th.assertFileContent("public/post/page/1/index.html", `canonical" href="/post/"/`) - th.assertFileContent("public/post/index.html", `P1|URL: /post/|Next: /post/page/2/`) - th.assertFileContent("public/post/page/2/index.html", `P2|URL: /post/page/2/|Next: /post/page/3/`) - th.assertFileContent("public/tags/.net/index.html", `P1|URL: /tags/.net/|Next: /tags/.net/page/2/`) + th.assertFileContent(pathFunc("public/"+blog+"/"+blog+"1/index.html"), "some blog content") + + th.assertFileContent(pathFunc("public/post/test0.dot/index.html"), "some content") + + if trimTrailingSlash { + th.assertFileContent("public/post/page/1.html", `canonical" href="/post"/`) + th.assertFileContent("public/post.html", `P1|URL: /post|Next: /post/page/2`) + th.assertFileContent("public/post/page/2.html", `P2|URL: /post/page/2|Next: /post/page/3`) + th.assertFileContent("public/tags/.net.html", `P1|URL: /tags/.net|Next: /tags/.net/page/2`) + } else if uglyURLs { + th.assertFileContent("public/post/page/1.html", `canonical" href="/post.html"/`) + th.assertFileContent("public/post.html", `P1|URL: /post.html|Next: /post/page/2.html`) + th.assertFileContent("public/post/page/2.html", `P2|URL: /post/page/2.html|Next: /post/page/3.html`) + th.assertFileContent("public/tags/.net.html", `P1|URL: /tags/.net.html|Next: /tags/.net/page/2.html`) + } else { + th.assertFileContent("public/post/page/1/index.html", `canonical" href="/post/"/`) + th.assertFileContent("public/post/index.html", `P1|URL: /post/|Next: /post/page/2/`) + th.assertFileContent("public/post/page/2/index.html", `P2|URL: /post/page/2/|Next: /post/page/3/`) + th.assertFileContent("public/tags/.net/index.html", `P1|URL: /tags/.net/|Next: /tags/.net/page/2/`) - } + } - p := s.RegularPages[0] - if uglyURLs { - require.Equal(t, "/post/test0.dot.html", p.RelPermalink()) - } else { - require.Equal(t, "/post/test0.dot/", p.RelPermalink()) - } + p := s.RegularPages[0] + if trimTrailingSlash { + require.Equal(t, "/post/test0.dot", p.RelPermalink()) + } else if uglyURLs { + require.Equal(t, "/post/test0.dot.html", p.RelPermalink()) + } else { + require.Equal(t, "/post/test0.dot/", p.RelPermalink()) + } - }) + }) + } } } } diff --git a/hugolib/pagination.go b/hugolib/pagination.go index 4733cf7c846..83a9a83a2f1 100644 --- a/hugolib/pagination.go +++ b/hugolib/pagination.go @@ -527,6 +527,11 @@ func newPaginationURLFactory(d targetPathDescriptor) paginationURLFactory { targetPath := createTargetPath(pathDescriptor) targetPath = strings.TrimSuffix(targetPath, d.Type.BaseFilename()) + + if d.TrimTrailingSlash { + targetPath = strings.TrimSuffix(targetPath, ".html") + } + link := d.PathSpec.PrependBasePath(targetPath) // Note: The targetPath is massaged with MakePathSanitized diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go index edfac3f3e86..743849064b0 100644 --- a/hugolib/pagination_test.go +++ b/hugolib/pagination_test.go @@ -207,52 +207,72 @@ func TestPaginationURLFactory(t *testing.T) { for _, uglyURLs := range []bool{false, true} { for _, canonifyURLs := range []bool{false, true} { - t.Run(fmt.Sprintf("uglyURLs=%t,canonifyURLs=%t", uglyURLs, canonifyURLs), func(t *testing.T) { - - tests := []struct { - name string - d targetPathDescriptor - baseURL string - page int - expected string - }{ - {"HTML home page 32", - targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/", 32, "/zoo/32/"}, - {"JSON home page 42", - targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "http://example.com/", 42, "/zoo/42/"}, - // Issue #1252 - {"BaseURL with sub path", - targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/sub/", 999, "/sub/zoo/999/"}, - } - - for _, test := range tests { - d := test.d - cfg.Set("baseURL", test.baseURL) - cfg.Set("canonifyURLs", canonifyURLs) - cfg.Set("uglyURLs", uglyURLs) - d.UglyURLs = uglyURLs - - expected := test.expected - - if canonifyURLs { - expected = strings.Replace(expected, "/sub", "", 1) + for _, trimTrailingSlash := range []bool{false, true} { + t.Run(fmt.Sprintf("uglyURLs=%t,canonifyURLs=%t,trimTrailingSlash=%t", uglyURLs, canonifyURLs, trimTrailingSlash), func(t *testing.T) { + + tests := []struct { + name string + d targetPathDescriptor + baseURL string + page int + expected string + }{ + {"HTML home page 32", + targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/", 32, "/zoo/32/"}, + {"JSON home page 42", + targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "http://example.com/", 42, "/zoo/42/"}, + // Issue #1252 + {"BaseURL with sub path", + targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/sub/", 999, "/sub/zoo/999/"}, } - if uglyURLs { - expected = expected[:len(expected)-1] + "." + test.d.Type.MediaType.Suffix - } + for _, test := range tests { + d := test.d + cfg.Set("baseURL", test.baseURL) + cfg.Set("canonifyURLs", canonifyURLs) + cfg.Set("uglyURLs", uglyURLs) + cfg.Set("trimTrailingSlash", trimTrailingSlash) + + d.TrimTrailingSlash = trimTrailingSlash + d.UglyURLs = uglyURLs + + expected := test.expected + + if canonifyURLs { + expected = strings.Replace(expected, "/sub", "", 1) + } + + if uglyURLs { + expected = expected[:len(expected)-1] + "." + test.d.Type.MediaType.Suffix + } - pathSpec := newTestPathSpec(fs, cfg) - d.PathSpec = pathSpec + if trimTrailingSlash { + if strings.HasSuffix(expected, "/") { + expected = expected[:len(expected)-1] + } - factory := newPaginationURLFactory(d) + // due to uglyURLs being turned off and on + if !strings.HasSuffix(expected, test.d.Type.MediaType.Suffix) { + expected += "." + test.d.Type.MediaType.Suffix + } - got := factory(test.page) + if test.d.Type.MediaType.Suffix == "html" { + expected = strings.TrimSuffix(expected, ".html") + } + } - require.Equal(t, expected, got) + pathSpec := newTestPathSpec(fs, cfg) + d.PathSpec = pathSpec - } - }) + factory := newPaginationURLFactory(d) + + got := factory(test.page) + + require.Equal(t, expected, got) + + } + }) + } } } } @@ -351,7 +371,7 @@ Conten%d Count: {{ .Paginator.TotalNumberOfElements }} Pages: {{ .Paginator.TotalPages }} {{ range .Paginator.Pagers -}} - {{ .PageNumber }}: {{ .URL }} + {{ .PageNumber }}: {{ .URL }} {{ end }} `) diff --git a/hugolib/site.go b/hugolib/site.go index f9430b272a9..bada8b7f4bd 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -372,6 +372,7 @@ type SiteInfo struct { canonifyURLs bool relativeURLs bool uglyURLs bool + trimTrailingSlash bool preserveTaxonomyNames bool Data *map[string]interface{} @@ -1040,6 +1041,7 @@ func (s *Site) initializeSiteInfo() { canonifyURLs: s.Cfg.GetBool("canonifyURLs"), relativeURLs: s.Cfg.GetBool("relativeURLs"), uglyURLs: s.Cfg.GetBool("uglyURLs"), + trimTrailingSlash: s.Cfg.GetBool("trimTrailingSlash"), preserveTaxonomyNames: lang.GetBool("preserveTaxonomyNames"), PageCollections: s.PageCollections, Files: &s.Files, diff --git a/hugolib/site_test.go b/hugolib/site_test.go index d7ca66827df..432da8097d2 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -201,13 +201,15 @@ func TestPageWithUnderScoreIndexInFilename(t *testing.T) { func TestCrossrefs(t *testing.T) { t.Parallel() for _, uglyURLs := range []bool{true, false} { - for _, relative := range []bool{true, false} { - doTestCrossrefs(t, relative, uglyURLs) + for _, trimTrailingSlash := range []bool{true, false} { + for _, relative := range []bool{true, false} { + doTestCrossrefs(t, relative, trimTrailingSlash, uglyURLs) + } } } } -func doTestCrossrefs(t *testing.T, relative, uglyURLs bool) { +func doTestCrossrefs(t *testing.T, relative bool, trimTrailingSlash bool, uglyURLs bool) { baseURL := "http://foo/bar" @@ -224,7 +226,10 @@ func doTestCrossrefs(t *testing.T, relative, uglyURLs bool) { expectedBase = baseURL } - if uglyURLs { + if trimTrailingSlash { + expectedURLSuffix = "" + expectedPathSuffix = ".html" + } else if uglyURLs { expectedURLSuffix = ".html" expectedPathSuffix = ".html" } else { @@ -263,6 +268,7 @@ THE END.`, refShortcode)), cfg, fs := newTestCfg() cfg.Set("baseURL", baseURL) + cfg.Set("trimTrailingSlash", trimTrailingSlash) cfg.Set("uglyURLs", uglyURLs) cfg.Set("verbose", true) @@ -303,12 +309,14 @@ THE END.`, refShortcode)), // Issue #1923 func TestShouldAlwaysHaveUglyURLs(t *testing.T) { t.Parallel() - for _, uglyURLs := range []bool{true, false} { - doTestShouldAlwaysHaveUglyURLs(t, uglyURLs) + for _, trimTrailingSlash := range []bool{true, false} { + for _, uglyURLs := range []bool{true, false} { + doTestShouldAlwaysHaveUglyURLs(t, uglyURLs, trimTrailingSlash) + } } } -func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { +func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool, trimTrailingSlash bool) { cfg, fs := newTestCfg() @@ -321,6 +329,7 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { map[string]interface{}{ "plainIDAnchors": true}) + cfg.Set("trimTrailingSlash", trimTrailingSlash) cfg.Set("uglyURLs", uglyURLs) sources := []source.ByteSource{ @@ -341,7 +350,7 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) var expectedPagePath string - if uglyURLs { + if trimTrailingSlash || uglyURLs { expectedPagePath = "public/sect/doc1.html" } else { expectedPagePath = "public/sect/doc1/index.html" @@ -400,19 +409,21 @@ func TestShouldNotWriteZeroLengthFilesToDestination(t *testing.T) { func TestSectionNaming(t *testing.T) { t.Parallel() for _, canonify := range []bool{true, false} { - for _, uglify := range []bool{true, false} { - for _, pluralize := range []bool{true, false} { - doTestSectionNaming(t, canonify, uglify, pluralize) + for _, trimTrailingSlash := range []bool{true, false} { + for _, uglify := range []bool{true, false} { + for _, pluralize := range []bool{true, false} { + doTestSectionNaming(t, canonify, uglify, pluralize, trimTrailingSlash) + } } } } } -func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { +func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool, trimTrailingSlash bool) { var expectedPathSuffix string - if uglify { + if trimTrailingSlash || uglify { expectedPathSuffix = ".html" } else { expectedPathSuffix = "/index.html" @@ -430,6 +441,7 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { cfg.Set("baseURL", "http://auth/sub/") cfg.Set("uglyURLs", uglify) + cfg.Set("trimTrailingSlash", trimTrailingSlash) cfg.Set("pluralizeListTitles", pluralize) cfg.Set("canonifyURLs", canonify) diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go index 4f8717d72ea..51112630792 100644 --- a/hugolib/taxonomy_test.go +++ b/hugolib/taxonomy_test.go @@ -53,20 +53,23 @@ func TestByCountOrderOfTaxonomies(t *testing.T) { // func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) { for _, uglyURLs := range []bool{false, true} { - for _, preserveTaxonomyNames := range []bool{false, true} { - t.Run(fmt.Sprintf("uglyURLs=%t,preserveTaxonomyNames=%t", uglyURLs, preserveTaxonomyNames), func(t *testing.T) { - doTestTaxonomiesWithAndWithoutContentFile(t, preserveTaxonomyNames, uglyURLs) - }) + for _, trimTrailingSlash := range []bool{false, true} { + for _, preserveTaxonomyNames := range []bool{false, true} { + t.Run(fmt.Sprintf("uglyURLs=%t,trimTrailingSlash=%t,preserveTaxonomyNames=%t", uglyURLs, trimTrailingSlash, preserveTaxonomyNames), func(t *testing.T) { + doTestTaxonomiesWithAndWithoutContentFile(t, preserveTaxonomyNames, trimTrailingSlash, uglyURLs) + }) + } } } } -func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, preserveTaxonomyNames, uglyURLs bool) { +func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, preserveTaxonomyNames bool, trimTrailingSlash bool, uglyURLs bool) { t.Parallel() siteConfig := ` baseURL = "http://example.com/blog" preserveTaxonomyNames = %t +trimTrailingSlash = %t uglyURLs = %t paginate = 1 @@ -91,7 +94,7 @@ others: # Doc ` - siteConfig = fmt.Sprintf(siteConfig, preserveTaxonomyNames, uglyURLs) + siteConfig = fmt.Sprintf(siteConfig, preserveTaxonomyNames, trimTrailingSlash, uglyURLs) th, h := newTestSitesFromConfigWithDefaultTemplates(t, siteConfig) require.Len(t, h.Sites, 1) @@ -122,7 +125,7 @@ others: // 3. the "others" taxonomy with no content pages. pathFunc := func(s string) string { - if uglyURLs { + if trimTrailingSlash || uglyURLs { return strings.Replace(s, "/index.html", ".html", 1) } return s @@ -164,7 +167,9 @@ others: cat1 := s.getPage(KindTaxonomy, "categories", "cat1") require.NotNil(t, cat1) - if uglyURLs { + if trimTrailingSlash { + require.Equal(t, "/blog/categories/cat1", cat1.RelPermalink()) + } else if uglyURLs { require.Equal(t, "/blog/categories/cat1.html", cat1.RelPermalink()) } else { require.Equal(t, "/blog/categories/cat1/", cat1.RelPermalink())