From feb3d494b96267674aa910da526400527fbf3c65 Mon Sep 17 00:00:00 2001 From: hexbinoct Date: Fri, 3 Jul 2026 19:50:25 +0500 Subject: [PATCH] hugolib: Use the output format's suffix for alias paths Alias generation hardcoded "index.html" and only recognized a ".html" suffix, so an alias with an explicit ".htm" extension (or any other suffix configured for the html media type) was written to the wrong path. For example "d.htm" produced the directory "d.htm/index.html" instead of the file "d.htm". Build the alias file name from the output format's BaseName and the media type's first suffix, and detect an explicit file by matching the alias extension against the configured suffixes. The same check now drives the uglyURLs case in Aliases. Fixes #15066 --- hugolib/alias.go | 22 ++++++++---- hugolib/alias_test.go | 78 ++++++++++++++++++++++++++++++++++++++++- hugolib/page__output.go | 4 +-- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/hugolib/alias.go b/hugolib/alias.go index 58d908ce6..32058833e 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -96,7 +96,7 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) { handler := newAliasHandler(s.GetTemplateStore(), s.Log, allowRoot) - targetPath, err := handler.targetPathAlias(path) + targetPath, err := handler.targetPathAlias(path, outputFormat) if err != nil { return err } @@ -120,7 +120,7 @@ func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFo return s.publisher.Publish(pd) } -func (a aliasHandler) targetPathAlias(src string) (string, error) { +func (a aliasHandler) targetPathAlias(src string, of output.Format) (string, error) { originalAlias := src if len(src) <= 0 { return "", fmt.Errorf("alias \"\" is an empty string") @@ -176,13 +176,23 @@ func (a aliasHandler) targetPathAlias(src string) (string, error) { } } - // Add the final touch + // Add the final touch. When the alias does not already end in one of the + // output format's configured suffixes, treat it as a directory and append + // the format's base name and suffix (e.g. index.html). alias = strings.TrimPrefix(alias, "/") + baseFile := of.BaseName + of.MediaType.FirstSuffix.FullSuffix if strings.HasSuffix(alias, "/") { - alias = alias + "index.html" - } else if !strings.HasSuffix(alias, ".html") { - alias = alias + "/" + "index.html" + alias = alias + baseFile + } else if !pathHasOutputFormatSuffix(alias, of) { + alias = alias + "/" + baseFile } return filepath.FromSlash(alias), nil } + +// pathHasOutputFormatSuffix reports whether the last element of p ends in one +// of the suffixes configured for the output format's media type. +func pathHasOutputFormatSuffix(p string, of output.Format) bool { + ext := path.Ext(p) + return ext != "" && of.MediaType.HasSuffix(ext[1:]) +} diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go index 977c6d9e1..e9bfe8e6c 100644 --- a/hugolib/alias_test.go +++ b/hugolib/alias_test.go @@ -21,6 +21,7 @@ import ( "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/output" ) func TestAlias(t *testing.T) { @@ -158,7 +159,7 @@ func TestTargetPathHTMLRedirectAlias(t *testing.T) { } for _, test := range tests { - path, err := h.targetPathAlias(test.value) + path, err := h.targetPathAlias(test.value, output.HTMLFormat) if (err == nil) != test.errIsNil { t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err) continue @@ -789,3 +790,78 @@ build: b.AssertFileExists("public/p2-alias/index.html", false) b.AssertFileExists("public/p3-alias/index.html", false) } + +// See issue 15066. +func TestAliasExplicitExtension(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +uglyURLs = UGLYURLS + +[mediaTypes] + [mediaTypes.'text/html'] + suffixes = SUFFIXES + +[outputFormats] + [outputFormats.html] + mediaType = 'text/html' + +-- content/s1/p1.md -- +--- +title: p1 +aliases: [a, b.foo, c.html, d.htm, e.foo.html, f.foo.htm] +--- +-- layouts/page.html -- +{{ .Title }} +` + + r := strings.NewReplacer("UGLYURLS", "false", "SUFFIXES", "['html', 'htm']") + f := r.Replace(files) + + b := Test(t, f) + + b.AssertFileContent("public/s1/a/index.html", "url=/s1/p1/") + b.AssertFileContent("public/s1/b.foo/index.html", "url=/s1/p1/") + b.AssertFileContent("public/s1/c.html", "url=/s1/p1/") + b.AssertFileContent("public/s1/d.htm", "url=/s1/p1/") + b.AssertFileContent("public/s1/e.foo.html", "url=/s1/p1/") + b.AssertFileContent("public/s1/f.foo.htm", "url=/s1/p1/") + + r = strings.NewReplacer("UGLYURLS", "false", "SUFFIXES", "['htm', 'html']") + f = r.Replace(files) + + b = Test(t, f) + + b.AssertFileContent("public/s1/a/index.htm", "url=/s1/p1/") + b.AssertFileContent("public/s1/b.foo/index.htm", "url=/s1/p1/") + b.AssertFileContent("public/s1/c.html", "url=/s1/p1/") + b.AssertFileContent("public/s1/d.htm", "url=/s1/p1/") + b.AssertFileContent("public/s1/e.foo.html", "url=/s1/p1/") + b.AssertFileContent("public/s1/f.foo.htm", "url=/s1/p1/") + + r = strings.NewReplacer("UGLYURLS", "true", "SUFFIXES", "['html', 'htm']") + f = r.Replace(files) + + b = Test(t, f) + + b.AssertFileContent("public/s1/a.html", "url=/s1/p1.html") + b.AssertFileContent("public/s1/b.foo.html", "url=/s1/p1.html") + b.AssertFileContent("public/s1/c.html", "url=/s1/p1.html") + b.AssertFileContent("public/s1/d.htm", "url=/s1/p1.html") + b.AssertFileContent("public/s1/e.foo.html", "url=/s1/p1.html") + b.AssertFileContent("public/s1/f.foo.htm", "url=/s1/p1.html") + + r = strings.NewReplacer("UGLYURLS", "true", "SUFFIXES", "['htm', 'html']") + f = r.Replace(files) + + b = Test(t, f) + + b.AssertFileContent("public/s1/a.htm", "url=/s1/p1.htm") + b.AssertFileContent("public/s1/b.foo.htm", "url=/s1/p1.htm") + b.AssertFileContent("public/s1/c.html", "url=/s1/p1.htm") + b.AssertFileContent("public/s1/d.htm", "url=/s1/p1.htm") + b.AssertFileContent("public/s1/e.foo.html", "url=/s1/p1.htm") + b.AssertFileContent("public/s1/f.foo.htm", "url=/s1/p1.htm") +} diff --git a/hugolib/page__output.go b/hugolib/page__output.go index 979fb755b..384ed5f9e 100644 --- a/hugolib/page__output.go +++ b/hugolib/page__output.go @@ -140,8 +140,8 @@ func (po *pageOutput) Aliases() []string { a = path.Join(baseDir, a) - if conf.C.IsUglyURLSection(p.Section()) && !strings.HasSuffix(a, ".html") { - a += ".html" + if conf.C.IsUglyURLSection(p.Section()) && !pathHasOutputFormatSuffix(a, f) { + a += f.MediaType.FirstSuffix.FullSuffix } aliases[i] = a