]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
hugolib: Fix multilingual alias generation
authorJoe Mooring <joe.mooring@veriphor.com>
Sat, 17 Jan 2026 21:56:33 +0000 (13:56 -0800)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 21 Jan 2026 09:34:32 +0000 (10:34 +0100)
Closes #14388

common/paths/path.go
common/paths/path_test.go
hugolib/alias_test.go
hugolib/site_render.go
resources/page/page_paths.go

index e9b9108f514ecc23652f4f01c7cf8f3bee27e372..06f573faac9ce46954825f46058fbd034fe8e95e 100644 (file)
@@ -428,3 +428,38 @@ func ToSlashPreserveLeading(s string) string {
 func IsSameFilePath(s1, s2 string) bool {
        return path.Clean(ToSlashTrim(s1)) == path.Clean(ToSlashTrim(s2))
 }
+
+// InjectSegment returns a clean path with forward slashes by inserting segment
+// into s immediately following prefix. The prefix must be a complete path
+// component match (e.g., "en" matches "en/docs" but not "entertainment").
+// If prefix is empty, segment is prepended to s. If prefix is not found or
+// is a partial match, the cleaned version of s is returned.
+func InjectSegment(s, prefix, segment string) string {
+       if s == "" && prefix == "" && segment == "" {
+               return ""
+       }
+
+       s = path.Clean(filepath.ToSlash(s))
+       prefix = path.Clean(filepath.ToSlash(prefix))
+       segment = path.Clean(filepath.ToSlash(segment))
+
+       if prefix == "." {
+               return path.Join(segment, s)
+       }
+
+       if !strings.HasPrefix(s, prefix) {
+               return s
+       }
+
+       lp := len(prefix)
+       ls := len(s)
+
+       // Ensure prefix matches a full component.
+       if ls > lp && prefix != "/" {
+               if s[lp] != '/' {
+                       return s
+               }
+       }
+
+       return path.Join(prefix, segment, s[lp:])
+}
index 3fa4c03b2805d266faa50056a5856cebd4c8e91c..f64fb4f6a40a3609e9955cb1481a88823bceb440 100644 (file)
@@ -369,3 +369,100 @@ func TestPathEscape(t *testing.T) {
                }
        }
 }
+
+func TestInjectSegment(t *testing.T) {
+       c := qt.New(t)
+
+       tests := []struct {
+               name     string
+               s        string
+               prefix   string
+               segment  string
+               expected string
+       }{
+               {
+                       name:     "rooted match",
+                       s:        "/abc/def",
+                       prefix:   "/abc",
+                       segment:  "seg",
+                       expected: "/abc/seg/def",
+               },
+               {
+                       name:     "relative match",
+                       s:        "abc/def",
+                       prefix:   "abc",
+                       segment:  "seg",
+                       expected: "abc/seg/def",
+               },
+               {
+                       name:     "rootedness mismatch - s rooted",
+                       s:        "/abc/def",
+                       prefix:   "abc",
+                       segment:  "seg",
+                       expected: "/abc/def",
+               },
+               {
+                       name:     "rootedness mismatch - prefix rooted",
+                       s:        "abc/def",
+                       prefix:   "/abc",
+                       segment:  "seg",
+                       expected: "abc/def",
+               },
+               {
+                       name:     "partial component mismatch",
+                       s:        "/abcdef",
+                       prefix:   "/abc",
+                       segment:  "seg",
+                       expected: "/abcdef",
+               },
+               {
+                       name:     "root slash prefix",
+                       s:        "/abc",
+                       prefix:   "/",
+                       segment:  "seg",
+                       expected: "/seg/abc",
+               },
+               {
+                       name:     "all empty",
+                       s:        "",
+                       prefix:   "",
+                       segment:  "",
+                       expected: "",
+               },
+               {
+                       name:     "empty s and prefix",
+                       s:        "",
+                       prefix:   "",
+                       segment:  "seg",
+                       expected: "seg",
+               },
+               {
+                       name:     "empty segment",
+                       s:        "/abc/def",
+                       prefix:   "/abc",
+                       segment:  "",
+                       expected: "/abc/def",
+               },
+               {
+                       name:     "empty prefix with relative s",
+                       s:        "abc",
+                       prefix:   "",
+                       segment:  "seg",
+                       expected: "seg/abc",
+               },
+               {
+                       name:     "empty prefix with rooted s",
+                       s:        "/abc",
+                       prefix:   "",
+                       segment:  "seg",
+                       expected: "seg/abc",
+               },
+       }
+
+       for _, tt := range tests {
+               c.Run(tt.name, func(c *qt.C) {
+                       actual := InjectSegment(tt.s, tt.prefix, tt.segment)
+                       c.Assert(actual, qt.Equals, tt.expected)
+               })
+       }
+}
index 0fc75523db352743b441adacb09320757d7ccc9f..f72f5f3b4fdd80fdd7b7aad66a4b775a0b5dc321 100644 (file)
@@ -232,10 +232,10 @@ aliases: [/p2-alias]
 -- layouts/all.html --
 {{ .Title }}
 `
+       // ------------------------------------------------------------------------
+       // Test 1: Create aliases for the html output format only
+       // ------------------------------------------------------------------------
 
-       // Test 1
-       // Expected site structure:
-       //
        // public/
        // ├── foo/
        // │  ├── s1/
@@ -270,6 +270,7 @@ aliases: [/p2-alias]
        f := strings.ReplaceAll(files, "IS_HTML", "false")
        b := Test(t, f)
 
+       // output format: html
        b.AssertFileContent("public/foo/s1-alias/index.html",
                `<title>https://example.org/foo/s1/</title>`,
                `<link rel="canonical" href="https://example.org/foo/s1/">`,
@@ -291,14 +292,16 @@ aliases: [/p2-alias]
                `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s2/p2/">`,
        )
 
+       // output format: print
        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:
-       //
+       // ------------------------------------------------------------------------
+       // Test 2: Create aliases for the html and print output formats
+       // ------------------------------------------------------------------------
+
        // public/
        // ├── foo/
        // │  ├── s1/
@@ -341,6 +344,7 @@ aliases: [/p2-alias]
        f = strings.ReplaceAll(files, "IS_HTML", "true")
        b = Test(t, f)
 
+       // output format: html
        b.AssertFileContent("public/foo/s1-alias/index.html",
                `<title>https://example.org/foo/s1/</title>`,
                `<link rel="canonical" href="https://example.org/foo/s1/">`,
@@ -361,6 +365,8 @@ aliases: [/p2-alias]
                `<link rel="canonical" href="https://example.org/foo/s2/p2/">`,
                `<meta http-equiv="refresh" content="0; url=https://example.org/foo/s2/p2/">`,
        )
+
+       // output format: print
        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/">`,
@@ -382,3 +388,267 @@ aliases: [/p2-alias]
                `<meta http-equiv="refresh" content="0; url=https://example.org/print/foo/s2/p2/">`,
        )
 }
+
+// Issue 14388
+func TestIssue14388(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- hugo.toml --
+baseURL      = 'https://example.org'
+disableKinds = ['home', 'rss', 'sitemap', 'taxonomy', 'term']
+
+defaultContentLanguage = 'en'
+
+defaultContentLanguageInSubdir = true
+defaultContentRoleInSubdir     = true
+defaultContentVersionInSubdir  = true
+
+[languages.en]
+  weight = 1
+  # baseURL      = 'https://en.example.org/'
+
+[languages.de]
+  weight = 2
+  # baseURL      = 'https://de.example.org/'
+
+[outputFormats.print]
+  isHTML    = true
+  mediaType = 'text/html'
+  path      = 'print'
+
+[outputs]
+  page    = ['html', 'print']
+  section = ['html', 'print']
+-- content/foo/s1/_index.de.md --
+---
+title: s1 de
+aliases: [s1-alias]
+---
+-- content/foo/s1/_index.en.md --
+---
+title: s1 en
+aliases: [s1-alias]
+---
+-- content/foo/s1/p1.de.md --
+---
+title: p1 de
+aliases: [p1-alias]
+---
+-- content/foo/s1/p1.en.md --
+---
+title: p1 en
+aliases: [p1-alias]
+---
+-- content/foo/s2/_index.de.md --
+---
+title: s2 de
+aliases: [/s2-alias]
+---
+-- content/foo/s2/_index.en.md --
+---
+title: s2 en
+aliases: [/s2-alias]
+---
+-- content/foo/s2/p2.de.md --
+---
+title: p2 de
+aliases: [/p2-alias]
+---
+-- content/foo/s2/p2.en.md --
+---
+title: p2 en
+aliases: [/p2-alias]
+---
+-- layouts/all.html --
+{{ .Title }}
+`
+       // ------------------------------------------------------------------------
+       // Test 1: Multilingual single-host
+       // ------------------------------------------------------------------------
+
+       b := Test(t, files)
+
+       // language: de, output format: html
+       b.AssertFileContent("public/guest/v1.0.0/de/foo/s1-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/de/foo/s1/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/de/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/de/foo/s1/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/de/foo/s1/p1-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/de/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/de/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/de/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/de/s2-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/de/foo/s2/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/de/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/de/foo/s2/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/de/p2-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/de/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/de/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/de/foo/s2/p2/">`,
+       )
+
+       // language: de, output format: print
+       b.AssertFileContent("public/guest/v1.0.0/de/print/foo/s1-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/de/print/foo/s1/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/de/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/de/print/foo/s1/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/de/print/foo/s1/p1-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/de/print/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/de/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/de/print/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/de/print/s2-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/de/print/foo/s2/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/de/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/de/print/foo/s2/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/de/print/p2-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/de/print/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/de/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/de/print/foo/s2/p2/">`,
+       )
+
+       // language: en, output format: html
+       b.AssertFileContent("public/guest/v1.0.0/en/foo/s1-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/en/foo/s1/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/en/foo/s1/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/en/foo/s1/p1-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/en/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/en/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/en/s2-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/en/foo/s2/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/en/foo/s2/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/en/p2-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/en/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/en/foo/s2/p2/">`,
+       )
+
+       // language: en, output format: print
+       b.AssertFileContent("public/guest/v1.0.0/en/print/foo/s1-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/en/print/foo/s1/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/en/print/foo/s1/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/en/print/foo/s1/p1-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/en/print/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/en/print/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/en/print/s2-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/en/print/foo/s2/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/en/print/foo/s2/">`,
+       )
+       b.AssertFileContent("public/guest/v1.0.0/en/print/p2-alias/index.html",
+               `<title>https://example.org/guest/v1.0.0/en/print/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://example.org/guest/v1.0.0/en/print/foo/s2/p2/">`,
+       )
+
+       // ------------------------------------------------------------------------
+       // Test 2: Multilingual multihost
+       // ------------------------------------------------------------------------
+
+       files = strings.ReplaceAll(files, "# baseURL", "baseURL")
+       b = Test(t, files)
+
+       // language: de, output format: html
+       b.AssertFileContent("public/de/guest/v1.0.0/foo/s1-alias/index.html",
+               `<title>https://de.example.org/guest/v1.0.0/foo/s1/</title>`,
+               `<link rel="canonical" href="https://de.example.org/guest/v1.0.0/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://de.example.org/guest/v1.0.0/foo/s1/">`,
+       )
+       b.AssertFileContent("public/de/guest/v1.0.0/foo/s1/p1-alias/index.html",
+               `<title>https://de.example.org/guest/v1.0.0/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://de.example.org/guest/v1.0.0/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://de.example.org/guest/v1.0.0/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/de/guest/v1.0.0/s2-alias/index.html",
+               `<title>https://de.example.org/guest/v1.0.0/foo/s2/</title>`,
+               `<link rel="canonical" href="https://de.example.org/guest/v1.0.0/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://de.example.org/guest/v1.0.0/foo/s2/">`,
+       )
+       b.AssertFileContent("public/de/guest/v1.0.0/p2-alias/index.html",
+               `<title>https://de.example.org/guest/v1.0.0/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://de.example.org/guest/v1.0.0/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://de.example.org/guest/v1.0.0/foo/s2/p2/">`,
+       )
+
+       // language: de, output format: print
+       b.AssertFileContent("public/de/guest/v1.0.0/print/foo/s1-alias/index.html",
+               `<title>https://de.example.org/guest/v1.0.0/print/foo/s1/</title>`,
+               `<link rel="canonical" href="https://de.example.org/guest/v1.0.0/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://de.example.org/guest/v1.0.0/print/foo/s1/">`,
+       )
+       b.AssertFileContent("public/de/guest/v1.0.0/print/foo/s1/p1-alias/index.html",
+               `<title>https://de.example.org/guest/v1.0.0/print/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://de.example.org/guest/v1.0.0/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://de.example.org/guest/v1.0.0/print/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/de/guest/v1.0.0/print/s2-alias/index.html",
+               `<title>https://de.example.org/guest/v1.0.0/print/foo/s2/</title>`,
+               `<link rel="canonical" href="https://de.example.org/guest/v1.0.0/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://de.example.org/guest/v1.0.0/print/foo/s2/">`,
+       )
+       b.AssertFileContent("public/de/guest/v1.0.0/print/p2-alias/index.html",
+               `<title>https://de.example.org/guest/v1.0.0/print/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://de.example.org/guest/v1.0.0/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://de.example.org/guest/v1.0.0/print/foo/s2/p2/">`,
+       )
+
+       // language: en, output format: html
+       b.AssertFileContent("public/en/guest/v1.0.0/foo/s1-alias/index.html",
+               `<title>https://en.example.org/guest/v1.0.0/foo/s1/</title>`,
+               `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/foo/s1/">`,
+       )
+       b.AssertFileContent("public/en/guest/v1.0.0/foo/s1/p1-alias/index.html",
+               `<title>https://en.example.org/guest/v1.0.0/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/en/guest/v1.0.0/s2-alias/index.html",
+               `<title>https://en.example.org/guest/v1.0.0/foo/s2/</title>`,
+               `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/foo/s2/">`,
+       )
+       b.AssertFileContent("public/en/guest/v1.0.0/p2-alias/index.html",
+               `<title>https://en.example.org/guest/v1.0.0/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/foo/s2/p2/">`,
+       )
+
+       // language: en, output format: print
+       b.AssertFileContent("public/en/guest/v1.0.0/print/foo/s1-alias/index.html",
+               `<title>https://en.example.org/guest/v1.0.0/print/foo/s1/</title>`,
+               `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/foo/s1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/print/foo/s1/">`,
+       )
+       b.AssertFileContent("public/en/guest/v1.0.0/print/foo/s1/p1-alias/index.html",
+               `<title>https://en.example.org/guest/v1.0.0/print/foo/s1/p1/</title>`,
+               `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/foo/s1/p1/">`,
+               `<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/print/foo/s1/p1/">`,
+       )
+       b.AssertFileContent("public/en/guest/v1.0.0/print/s2-alias/index.html",
+               `<title>https://en.example.org/guest/v1.0.0/print/foo/s2/</title>`,
+               `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/foo/s2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/print/foo/s2/">`,
+       )
+       b.AssertFileContent("public/en/guest/v1.0.0/print/p2-alias/index.html",
+               `<title>https://en.example.org/guest/v1.0.0/print/foo/s2/p2/</title>`,
+               `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/foo/s2/p2/">`,
+               `<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/print/foo/s2/p2/">`,
+       )
+}
index ea16a0498e2b5048ed454f30f6a8f8b638b42ba2..42d6ebdb4e063f2e1fc41ebd283347ed4ebbb813 100644 (file)
@@ -333,31 +333,28 @@ func (s *Site) renderAliases() error {
 
                                for _, a := range p.Aliases() {
                                        isRelative := !strings.HasPrefix(a, "/")
+                                       prefix := path.Join("/", p.targetPathDescriptor.PrefixFilePath)
 
+                                       var baseDir string
                                        if isRelative {
-                                               // 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(f.Path, basePath, a)
+                                               // Form the baseDir by taking the resource's base target
+                                               // and moving up one level to the parent. Then, inject
+                                               // the Output Format path between the content dimension
+                                               // prefixes and the remaining path.
+                                               parentContext := path.Join(p.targetPaths().SubResourceBaseTarget, "..")
+                                               baseDir = paths.InjectSegment(parentContext, prefix, f.Path)
                                        } else {
-                                               // 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)
+                                               // Form the baseDir by prepending the content dimension
+                                               // prefixes with the Output Format path.
+                                               baseDir = path.Join(prefix, f.Path)
                                        }
 
+                                       a = path.Join(baseDir, a)
+
                                        if s.conf.C.IsUglyURLSection(p.Section()) && !strings.HasSuffix(a, ".html") {
                                                a += ".html"
                                        }
 
-                                       lang := p.Language().Lang
-
-                                       if s.h.Configs.IsMultihost && !strings.HasPrefix(a, "/"+lang) {
-                                               // These need to be in its language root.
-                                               a = path.Join(lang, a)
-                                       }
-
                                        err := s.writeDestAlias(a, plink, f, p)
                                        if err != nil {
                                                return radix.WalkStop, err
index 912d6036fb83ccf7b3840c94356cf8e0167b1ade..4f24488c3f2f721d611c6902379105fa33fba85d 100644 (file)
@@ -50,10 +50,18 @@ type TargetPathDescriptor struct {
        // 2) the file base name (TranslationBaseName).
        BaseName string
 
-       // Typically a language prefix added to file paths.
+       // PrefixFilePath contains zero or more content dimensions used as a file
+       // path prefix. Dimensions are ordered role/version/language for
+       // single-host and language/role/version for multihost.
+       // Example (single-host): guest/v1.0.0/en
+       // Example (multihost):   en/guest/v1.0.0
        PrefixFilePath string
 
-       // Typically a language prefix added to links.
+       // PrefixLink contains zero or more content dimensions used as a link
+       // prefix. Dimensions are ordered role/version/language for single-host,
+       // but the language dimension is excluded for multihost.
+       // Example (single-host): guest/v1.0.0/en
+       // Example (multihost):   guest/v1.0.0
        PrefixLink string
 
        // If in multihost mode etc., every link/path needs to be prefixed, even