hugolib: Allow page-relative aliases
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 30 Mar 2019 16:08:25 +0000 (17:08 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 2 Apr 2019 08:32:47 +0000 (10:32 +0200)
Fixes #5757

docs/content/en/content-management/urls.md
hugolib/alias.go
hugolib/alias_test.go
hugolib/page__meta.go
hugolib/site_render.go
hugolib/site_stats_test.go
hugolib/site_url_test.go

index 25bd0916516e54a8906c2f31a992a23402968ae4..5ba9e758ecf4d902bc9523dae0c759ccc71eacd2 100644 (file)
@@ -82,9 +82,13 @@ The following is a list of values that can be used in a `permalink` definition i
 
 ## Aliases
 
-For people migrating existing published content to Hugo, there's a good chance you need a mechanism to handle redirecting old URLs.
+Aliases can be used to create redirects to your page from other URLs.
 
-Luckily, redirects can be handled easily with **aliases** in Hugo.
+
+Aliases comes in two forms:
+
+1. Starting with a `/` meaning they are relative to the `BaseURL`, e.g. `/posts/my-blogpost/`
+2. They are relative to the `Page` they're defined in, e.g. `my-blogpost` or even something like `../blog/my-blogpost` (new in Hugo 0.55).
 
 ### Example: Aliases
 
index 599821c0ac53bea80a76bdd569bb250ee76e11e8..972f7b01c4dd26f56b6863456a3c88e11181cae1 100644 (file)
@@ -18,6 +18,7 @@ import (
        "fmt"
        "html/template"
        "io"
+       "path"
        "path/filepath"
        "runtime"
        "strings"
@@ -28,8 +29,6 @@ import (
        "github.com/gohugoio/hugo/publisher"
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/tpl"
-
-       "github.com/gohugoio/hugo/helpers"
 )
 
 const (
@@ -132,13 +131,14 @@ func (a aliasHandler) targetPathAlias(src string) (string, error) {
                return "", fmt.Errorf("alias \"\" is an empty string")
        }
 
-       alias := filepath.Clean(src)
-       components := strings.Split(alias, helpers.FilePathSeparator)
+       alias := path.Clean(filepath.ToSlash(src))
 
-       if !a.allowRoot && alias == helpers.FilePathSeparator {
+       if !a.allowRoot && alias == "/" {
                return "", fmt.Errorf("alias \"%s\" resolves to website root directory", originalAlias)
        }
 
+       components := strings.Split(alias, "/")
+
        // Validate against directory traversal
        if components[0] == ".." {
                return "", fmt.Errorf("alias \"%s\" traverses outside the website root directory", originalAlias)
@@ -182,15 +182,12 @@ func (a aliasHandler) targetPathAlias(src string) (string, error) {
        }
 
        // Add the final touch
-       alias = strings.TrimPrefix(alias, helpers.FilePathSeparator)
-       if strings.HasSuffix(alias, helpers.FilePathSeparator) {
+       alias = strings.TrimPrefix(alias, "/")
+       if strings.HasSuffix(alias, "/") {
                alias = alias + "index.html"
        } else if !strings.HasSuffix(alias, ".html") {
-               alias = alias + helpers.FilePathSeparator + "index.html"
-       }
-       if originalAlias != alias {
-               a.log.INFO.Printf("Alias \"%s\" translated to \"%s\"\n", originalAlias, alias)
+               alias = alias + "/" + "index.html"
        }
 
-       return alias, nil
+       return filepath.FromSlash(alias), nil
 }
index f968caf2354e8f6eaeb1f5b4bfa078d609886a96..095ae1be2eae75bb99f53843c6966db3daffcddc 100644 (file)
@@ -25,14 +25,14 @@ import (
 
 const pageWithAlias = `---
 title: Has Alias
-aliases: ["foo/bar/"]
+aliases: ["/foo/bar/", "rel"]
 ---
 For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.
 `
 
 const pageWithAliasMultipleOutputs = `---
 title: Has Alias for HTML and AMP
-aliases: ["foo/bar/"]
+aliases: ["/foo/bar/"]
 outputs: ["HTML", "AMP", "JSON"]
 ---
 For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.
@@ -46,16 +46,17 @@ func TestAlias(t *testing.T) {
        assert := require.New(t)
 
        b := newTestSitesBuilder(t)
-       b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias)
+       b.WithSimpleConfigFile().WithContent("blog/page.md", pageWithAlias)
        b.CreateSites().Build(BuildCfg{})
 
        assert.Equal(1, len(b.H.Sites))
        require.Len(t, b.H.Sites[0].RegularPages(), 1)
 
        // the real page
-       b.AssertFileContent("public/page/index.html", "For some moments the old man")
-       // the alias redirector
+       b.AssertFileContent("public/blog/page/index.html", "For some moments the old man")
+       // the alias redirectors
        b.AssertFileContent("public/foo/bar/index.html", "<meta http-equiv=\"refresh\" content=\"0; ")
+       b.AssertFileContent("public/blog/rel/index.html", "<meta http-equiv=\"refresh\" content=\"0; ")
 }
 
 func TestAliasMultipleOutputFormats(t *testing.T) {
@@ -64,7 +65,7 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
        assert := require.New(t)
 
        b := newTestSitesBuilder(t)
-       b.WithSimpleConfigFile().WithContent("page.md", pageWithAliasMultipleOutputs)
+       b.WithSimpleConfigFile().WithContent("blog/page.md", pageWithAliasMultipleOutputs)
 
        b.WithTemplates(
                "_default/single.html", basicTemplate,
@@ -74,9 +75,9 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
        b.CreateSites().Build(BuildCfg{})
 
        // the real pages
-       b.AssertFileContent("public/page/index.html", "For some moments the old man")
-       b.AssertFileContent("public/amp/page/index.html", "For some moments the old man")
-       b.AssertFileContent("public/page/index.json", "For some moments the old man")
+       b.AssertFileContent("public/blog/page/index.html", "For some moments the old man")
+       b.AssertFileContent("public/amp/blog/page/index.html", "For some moments the old man")
+       b.AssertFileContent("public/blog/page/index.json", "For some moments the old man")
 
        // the alias redirectors
        b.AssertFileContent("public/foo/bar/index.html", "<meta http-equiv=\"refresh\" content=\"0; ")
@@ -135,7 +136,7 @@ func TestTargetPathHTMLRedirectAlias(t *testing.T) {
                        continue
                }
                if err == nil && path != test.expected {
-                       t.Errorf("Expected: \"%s\", got: \"%s\"", test.expected, path)
+                       t.Errorf("Expected: %q, got: %q", test.expected, path)
                }
        }
 }
index 9f5f369b3ad3e4893ca48a7a6c8fa1d385387175..1e013db663783bf0655a99f80100ce2153ded9ec 100644 (file)
@@ -16,6 +16,7 @@ package hugolib
 import (
        "fmt"
        "path"
+       "path/filepath"
        "regexp"
        "strings"
        "time"
@@ -414,10 +415,11 @@ func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{}
                        pm.params[loki] = pm.weight
                case "aliases":
                        pm.aliases = cast.ToStringSlice(v)
-                       for _, alias := range pm.aliases {
+                       for i, alias := range pm.aliases {
                                if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
-                                       return fmt.Errorf("only relative aliases are supported, %v provided", alias)
+                                       return fmt.Errorf("http* aliases not supported: %q", alias)
                                }
+                               pm.aliases[i] = filepath.ToSlash(alias)
                        }
                        pm.params[loki] = pm.aliases
                case "sitemap":
index c528e61a8001db760f8d4e549bd391a96c91205a..3c0897d2ef2e81b3e8d8762029455f0706bc99fe 100644 (file)
@@ -303,7 +303,20 @@ func (s *Site) renderAliases() error {
                        f := of.Format
 
                        for _, a := range p.Aliases() {
-                               if f.Path != "" {
+                               isRelative := !strings.HasPrefix(a, "/")
+
+                               if isRelative {
+                                       // Make alias relative, where "." will be on the
+                                       // same directory level as the current page.
+                                       // TODO(bep) ugly URLs doesn't seem to be supported in
+                                       // aliases, I'm not sure why not.
+                                       basePath := of.RelPermalink()
+                                       if strings.HasSuffix(basePath, "/") {
+                                               basePath = path.Join(basePath, "..")
+                                       }
+                                       a = path.Join(basePath, a)
+
+                               } else if f.Path != "" {
                                        // Make sure AMP and similar doesn't clash with regular aliases.
                                        a = path.Join(f.Path, a)
                                }
index 522b5636bc42de5abab32280a9ffb93cf8f4f36f..c722037b4ebca75b927013882f484190eab92887 100644 (file)
@@ -55,7 +55,7 @@ tags:
 %s
 categories:
 %s
-aliases: [Ali%d]
+aliases: [/Ali%d]
 ---
 # Doc
 `
index 10aa3bb282ab5a1d83b99f1ce4ef81b16e262c62..9827f994b2b76d4c2b60f55a7d311846af2cc7e3 100644 (file)
@@ -26,7 +26,7 @@ import (
        "github.com/stretchr/testify/require"
 )
 
-const slugDoc1 = "---\ntitle: slug doc 1\nslug: slug-doc-1\naliases:\n - sd1/foo/\n - sd2\n - sd3/\n - sd4.html\n---\nslug doc 1 content\n"
+const slugDoc1 = "---\ntitle: slug doc 1\nslug: slug-doc-1\naliases:\n - /sd1/foo/\n - /sd2\n - /sd3/\n - /sd4.html\n---\nslug doc 1 content\n"
 
 const slugDoc2 = `---
 title: slug doc 2