]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
hugolib: Fix relative alias generation
authorJoe Mooring <joe@mooring.com>
Sat, 17 Jan 2026 09:33:44 +0000 (01:33 -0800)
committerGitHub <noreply@github.com>
Sat, 17 Jan 2026 09:33:44 +0000 (10:33 +0100)
Closes #14381

hugolib/alias_test.go
hugolib/site_render.go

index b7944b597f817d89086d271e6e22cea91681368d..0fc75523db352743b441adacb09320757d7ccc9f 100644 (file)
@@ -191,3 +191,194 @@ aliases:
 
        b.AssertPublishDir("n/index.html", "yes/index.html", "no/index.html", "yes/index.html")
 }
+
+// Issue 14381
+func TestIssue14381(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- hugo.toml --
+baseURL      = 'https://example.org'
+disableKinds = ['home', 'rss', 'sitemap', 'taxonomy', 'term']
+
+[outputFormats.print]
+  isHTML        = IS_HTML
+  mediaType     = 'text/html'
+  path          = 'print'
+
+[outputs]
+  page    = ['html', 'print']
+  section = ['html', 'print']
+-- content/foo/s1/_index.md --
+---
+title: s1
+aliases: [s1-alias]
+---
+-- content/foo/s1/p1.md --
+---
+title: p1
+aliases: [p1-alias]
+---
+-- content/foo/s2/_index.md --
+---
+title: s2
+aliases: [/s2-alias]
+---
+-- content/foo/s2/p2.md --
+---
+title: p2
+aliases: [/p2-alias]
+---
+-- layouts/all.html --
+{{ .Title }}
+`
+
+       // Test 1
+       // Expected site structure:
+       //
+       // public/
+       // ├── foo/
+       // │  ├── s1/
+       // │  │  ├── p1/
+       // │  │  │  └── index.html
+       // │  │  ├── p1-alias/
+       // │  │  │  └── index.html
+       // │  │  └── index.html
+       // │  ├── s1-alias/
+       // │  │  └── index.html
+       // │  ├── s2/
+       // │  │  ├── p2/
+       // │  │  │  └── index.html
+       // │  │  └── index.html
+       // │  └── index.html
+       // ├── p2-alias/
+       // │  └── index.html
+       // ├── print/
+       // │  └── foo/
+       // │      ├── s1/
+       // │      │  ├── p1/
+       // │      │  │  └── index.html
+       // │      │  └── index.html
+       // │      ├── s2/
+       // │      │  ├── p2/
+       // │      │  │  └── index.html
+       // │      │  └── index.html
+       // │      └── index.html
+       // └── s2-alias/
+       //     └── index.html
+
+       f := strings.ReplaceAll(files, "IS_HTML", "false")
+       b := Test(t, f)
+
+       b.AssertFileContent("public/foo/s1-alias/index.html",
+               `<title>https://example.org/foo/s1/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s1/">`,
+       )
+       b.AssertFileContent("public/foo/s1/p1-alias/index.html",
+               `<title>https://example.org/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/s2-alias/index.html",
+               `<title>https://example.org/foo/s2/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s2/">`,
+       )
+       b.AssertFileContent("public/p2-alias/index.html",
+               `<title>https://example.org/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s2/p2/">`,
+       )
+
+       b.AssertFileExists("public/print/foo/s1-alias/index.html", false)
+       b.AssertFileExists("public/print/foo/s1/p1-alias/index.html", false)
+       b.AssertFileExists("public/print/s2-alias/index.html", false)
+       b.AssertFileExists("public/print/p2-alias/index.html", false)
+
+       // Test 2
+       // Expected site structure:
+       //
+       // public/
+       // ├── foo/
+       // │  ├── s1/
+       // │  │  ├── p1/
+       // │  │  │  └── index.html
+       // │  │  ├── p1-alias/
+       // │  │  │  └── index.html
+       // │  │  └── index.html
+       // │  ├── s1-alias/
+       // │  │  └── index.html
+       // │  ├── s2/
+       // │  │  ├── p2/
+       // │  │  │  └── index.html
+       // │  │  └── index.html
+       // │  └── index.html
+       // ├── p2-alias/
+       // │  └── index.html
+       // ├── print/
+       // │  ├── foo/
+       // │  │  ├── s1/
+       // │  │  │  ├── p1/
+       // │  │  │  │  └── index.html
+       // │  │  │  ├── p1-alias/
+       // │  │  │  │  └── index.html
+       // │  │  │  └── index.html
+       // │  │  ├── s1-alias/
+       // │  │  │  └── index.html
+       // │  │  ├── s2/
+       // │  │  │  ├── p2/
+       // │  │  │  │  └── index.html
+       // │  │  │  └── index.html
+       // │  │  └── index.html
+       // │  ├── p2-alias/
+       // │  │  └── index.html
+       // │  └── s2-alias/
+       // │      └── index.html
+       // └── s2-alias/
+       //     └── index.html
+
+       f = strings.ReplaceAll(files, "IS_HTML", "true")
+       b = Test(t, f)
+
+       b.AssertFileContent("public/foo/s1-alias/index.html",
+               `<title>https://example.org/foo/s1/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s1/">`,
+       )
+       b.AssertFileContent("public/foo/s1/p1-alias/index.html",
+               `<title>https://example.org/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/s2-alias/index.html",
+               `<title>https://example.org/foo/s2/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s2/">`,
+       )
+       b.AssertFileContent("public/p2-alias/index.html",
+               `<title>https://example.org/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s2/p2/">`,
+       )
+       b.AssertFileContent("public/print/foo/s1-alias/index.html",
+               `<title>https://example.org/print/foo/s1/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/print/foo/s1/">`,
+       )
+       b.AssertFileContent("public/print/foo/s1/p1-alias/index.html",
+               `<title>https://example.org/print/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/print/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/print/s2-alias/index.html",
+               `<title>https://example.org/print/foo/s2/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/print/foo/s2/">`,
+       )
+       b.AssertFileContent("public/print/p2-alias/index.html",
+               `<title>https://example.org/print/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://example.org/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/print/foo/s2/p2/">`,
+       )
+}
index 0edb5632964c645de3b16cb4b69db60323f47c16..ea16a0498e2b5048ed454f30f6a8f8b638b42ba2 100644 (file)
@@ -335,13 +335,15 @@ func (s *Site) renderAliases() error {
                                        isRelative := !strings.HasPrefix(a, "/")
 
                                        if isRelative {
-                                               // Make alias relative, where "." will be on the
-                                               // same directory level as the current page.
+                                               // Resolve the alias relative to the current page's
+                                               // directory level, prepended by the output format's
+                                               // "path" setting, which may be empty.
                                                basePath := path.Join(p.targetPaths().SubResourceBaseLink, "..")
-                                               a = path.Join(basePath, a)
-
+                                               a = path.Join(f.Path, basePath, a)
                                        } else {
-                                               // Make sure AMP and similar doesn't clash with regular aliases.
+                                               // Resolve the alias relative to the site root,
+                                               // prepended by the output format's "path" setting,
+                                               // which may be empty.
                                                a = path.Join(f.Path, a)
                                        }