Repository: gohugoio/hugo (language: go)

Issue:
Bug in relURL with leading slash when baseURL includes a subdirectory
This is not a new issue; behavior present in v0.54.0 through v0.100.2.

config.toml

```toml
baseURL = 'https://example.org/foo/'
```

template

```
{{ $url := "/foo/bar" }}
{{ $url | relURL }}
{{ $url | absURL }}
```

result

```
/foo/foo/bar
http://localhost:1313/foo/bar
```

Notice that the subdirectory from `baseURL` is repeated when using `relURL`, but it is _not_ repeated when using `absURL`. 

This has come up a few times in the forum, usually in conjunction with menu iteration:
- <https://discourse.gohugo.io/t/39014>
- <https://discourse.gohugo.io/t/38614>

Note that the same differences are present when comparing `relLangURL` and `absLangURL`.


Discussion / hints:
So, this is a bug, but not as described. It's `absURL` that behaves badly. The `relURL` behaviour may seem convienient, but how would you then construct `/foo/foo/bar`? 



>So, this is a bug, but not as described. It's absURL that behaves badly. The relURL behaviour may seem convienient, but how would you then construct /foo/foo/bar?

OK, looking at the code, my conclusion above wasn't right, we:

* Treat argument with a leading slash as host relative (or to be more precise, language root-relative)
* Treat argument without a leading slash as relative to the baseURL.

So, given that, the example above would be:

```
baseURL = 'https://example.org/foo/'

{{ $url := "/foo/bar" }}
{{ $url | relURL }}
{{ $url | absURL }}

/foo/bar
http://localhost:1313/foo/bar
```

And 

```
{{ $url := "foo/bar" }}
{{ $url | relURL }}
{{ $url | absURL }}

/foo/foo/bar
http://localhost:1313/foo/foo/bar

Fix patch:
diff --git a/helpers/url.go b/helpers/url.go
index 193dd3c8641..7cb998ca25b 100644
--- a/helpers/url.go
+++ b/helpers/url.go
@@ -103,17 +103,11 @@ func (p *PathSpec) AbsURL(in string, addLanguage bool) string {
 	}
 
 	if url.IsAbs() || strings.HasPrefix(in, "//") {
+		// It  is already  absolute, return it as is.
 		return in
 	}
 
-	var baseURL string
-	if strings.HasPrefix(in, "/") {
-		u := p.BaseURL.URL()
-		u.Path = ""
-		baseURL = u.String()
-	} else {
-		baseURL = p.BaseURL.String()
-	}
+	baseURL := p.getBaseURLRoot(in)
 
 	if addLanguage {
 		prefix := p.GetLanguagePrefix()
@@ -140,13 +134,22 @@ func (p *PathSpec) AbsURL(in string, addLanguage bool) string {
 			}
 		}
 	}
+
 	return paths.MakePermalink(baseURL, in).String()
 }
 
-// RelURL creates a URL relative to the BaseURL root.
-// Note: The result URL will not include the context root if canonifyURLs is enabled.
+func (p *PathSpec) getBaseURLRoot(path string) string {
+	if strings.HasPrefix(path, "/") {
+		// Treat it as relative to the server root.
+		return p.BaseURLNoPathString
+	} else {
+		// Treat it as relative to the baseURL.
+		return p.BaseURLString
+	}
+}
+
 func (p *PathSpec) RelURL(in string, addLanguage bool) string {
-	baseURL := p.BaseURL.String()
+	baseURL := p.getBaseURLRoot(in)
 	canonifyURLs := p.CanonifyURLs
 	if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") {
 		return in
diff --git a/hugolib/paths/paths.go b/hugolib/paths/paths.go
index 501665676c2..e80215b92a0 100644
--- a/hugolib/paths/paths.go
+++ b/hugolib/paths/paths.go
@@ -34,6 +34,8 @@ type Paths struct {
 	Cfg config.Provider
 
 	BaseURL
+	BaseURLString       string
+	BaseURLNoPathString string
 
 	// If the baseURL contains a base path, e.g. https://example.com/docs, then "/docs" will be the BasePath.
 	BasePath string
@@ -145,10 +147,17 @@ func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) {
 		}
 	}
 
+	var baseURLString = baseURL.String()
+	var baseURLNoPath = baseURL.URL()
+	baseURLNoPath.Path = ""
+	var baseURLNoPathString = baseURLNoPath.String()
+
 	p := &Paths{
-		Fs:      fs,
-		Cfg:     cfg,
-		BaseURL: baseURL,
+		Fs:                  fs,
+		Cfg:                 cfg,
+		BaseURL:             baseURL,
+		BaseURLString:       baseURLString,
+		BaseURLNoPathString: baseURLNoPathString,
 
 		DisablePathToLower: cfg.GetBool("disablePathToLower"),
 		RemovePathAccents:  cfg.GetBool("removePathAccents"),


Test patch:
diff --git a/helpers/pathspec_test.go b/helpers/pathspec_test.go
index c1129951671..84448050d5e 100644
--- a/helpers/pathspec_test.go
+++ b/helpers/pathspec_test.go
@@ -32,7 +32,7 @@ func TestNewPathSpecFromConfig(t *testing.T) {
 	v.Set("uglyURLs", true)
 	v.Set("canonifyURLs", true)
 	v.Set("paginatePath", "side")
-	v.Set("baseURL", "http://base.com")
+	v.Set("baseURL", "http://base.com/foo")
 	v.Set("themesDir", "thethemes")
 	v.Set("layoutDir", "thelayouts")
 	v.Set("workingDir", "thework")
@@ -53,7 +53,10 @@ func TestNewPathSpecFromConfig(t *testing.T) {
 	c.Assert(p.Language.Lang, qt.Equals, "no")
 	c.Assert(p.PaginatePath, qt.Equals, "side")
 
-	c.Assert(p.BaseURL.String(), qt.Equals, "http://base.com")
+	c.Assert(p.BaseURL.String(), qt.Equals, "http://base.com/foo")
+	c.Assert(p.BaseURLString, qt.Equals, "http://base.com/foo")
+	c.Assert(p.BaseURLNoPathString, qt.Equals, "http://base.com")
+
 	c.Assert(p.ThemesDir, qt.Equals, "thethemes")
 	c.Assert(p.WorkingDir, qt.Equals, "thework")
 }
diff --git a/helpers/url_test.go b/helpers/url_test.go
index f899e1cdbb9..e248036aee9 100644
--- a/helpers/url_test.go
+++ b/helpers/url_test.go
@@ -17,6 +17,7 @@ import (
 	"strings"
 	"testing"
 
+	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/langs"
 )
@@ -59,6 +60,7 @@ func TestAbsURL(t *testing.T) {
 }
 
 func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, lang string) {
+	c := qt.New(t)
 	v := newTestCfg()
 	v.Set("multilingual", multilingual)
 	v.Set("defaultContentLanguage", "en")
@@ -69,6 +71,10 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
 		baseURL  string
 		expected string
 	}{
+		// Issue 9994
+		{"foo/bar", "https://example.org/foo/", "https://example.org/foo/MULTIfoo/bar"},
+		{"/foo/bar", "https://example.org/foo/", "https://example.org/MULTIfoo/bar"},
+
 		{"/test/foo", "http://base/", "http://base/MULTItest/foo"},
 		{"/" + lang + "/test/foo", "http://base/", "http://base/" + lang + "/test/foo"},
 		{"", "http://base/ace/", "http://base/ace/MULTI"},
@@ -113,9 +119,8 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
 		} else {
 			expected = strings.Replace(expected, "MULTI", "", 1)
 		}
-		if output != expected {
-			t.Fatalf("Expected %#v, got %#v\n", expected, output)
-		}
+
+		c.Assert(output, qt.Equals, expected)
 	}
 }
 
@@ -132,6 +137,7 @@ func TestRelURL(t *testing.T) {
 }
 
 func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, lang string) {
+	c := qt.New(t)
 	v := newTestCfg()
 	v.Set("multilingual", multilingual)
 	v.Set("defaultContentLanguage", "en")
@@ -143,13 +149,18 @@ func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
 		canonify bool
 		expected string
 	}{
+
+		// Issue 9994
+		{"/foo/bar", "https://example.org/foo/", false, "MULTI/foo/bar"},
+		{"foo/bar", "https://example.org/foo/", false, "/fooMULTI/foo/bar"},
+
 		{"/test/foo", "http://base/", false, "MULTI/test/foo"},
 		{"/" + lang + "/test/foo", "http://base/", false, "/" + lang + "/test/foo"},
 		{lang + "/test/foo", "http://base/", false, "/" + lang + "/test/foo"},
 		{"test.css", "http://base/sub", false, "/subMULTI/test.css"},
 		{"test.css", "http://base/sub", true, "MULTI/test.css"},
 		{"/test/", "http://base/", false, "MULTI/test/"},
-		{"/test/", "http://base/sub/", false, "/subMULTI/test/"},
+		{"test/", "http://base/sub/", false, "/subMULTI/test/"},
 		{"/test/", "http://base/sub/", true, "MULTI/test/"},
 		{"", "http://base/ace/", false, "/aceMULTI/"},
 		{"", "http://base/ace", false, "/aceMULTI"},
@@ -189,9 +200,8 @@ func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
 			expected = strings.Replace(expected, "MULTI", "", 1)
 		}
 
-		if output != expected {
-			t.Errorf("[%d][%t] Expected %#v, got %#v\n", i, test.canonify, expected, output)
-		}
+		c.Assert(output, qt.Equals, expected, qt.Commentf("[%d] %s", i, test.input))
+
 	}
 }