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
This commit is contained in:
hexbinoct
2026-07-03 19:50:25 +05:00
committed by Bjørn Erik Pedersen
parent 65c82178b7
commit feb3d494b9
3 changed files with 95 additions and 9 deletions
+16 -6
View File
@@ -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:])
}
+77 -1
View File
@@ -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")
}
+2 -2
View File
@@ -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