hugolib: Allow page-relative aliases

Fixes #5757
This commit is contained in:
Bjørn Erik Pedersen 2019-03-30 17:08:25 +01:00
parent a55640de8e
commit 92baa14fd3
7 changed files with 46 additions and 29 deletions

View file

@ -82,9 +82,13 @@ The following is a list of values that can be used in a `permalink` definition i
## Aliases ## 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 ### Example: Aliases

View file

@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@ -28,8 +29,6 @@ import (
"github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/helpers"
) )
const ( const (
@ -132,13 +131,14 @@ func (a aliasHandler) targetPathAlias(src string) (string, error) {
return "", fmt.Errorf("alias \"\" is an empty string") return "", fmt.Errorf("alias \"\" is an empty string")
} }
alias := filepath.Clean(src) alias := path.Clean(filepath.ToSlash(src))
components := strings.Split(alias, helpers.FilePathSeparator)
if !a.allowRoot && alias == helpers.FilePathSeparator { if !a.allowRoot && alias == "/" {
return "", fmt.Errorf("alias \"%s\" resolves to website root directory", originalAlias) return "", fmt.Errorf("alias \"%s\" resolves to website root directory", originalAlias)
} }
components := strings.Split(alias, "/")
// Validate against directory traversal // Validate against directory traversal
if components[0] == ".." { if components[0] == ".." {
return "", fmt.Errorf("alias \"%s\" traverses outside the website root directory", originalAlias) 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 // Add the final touch
alias = strings.TrimPrefix(alias, helpers.FilePathSeparator) alias = strings.TrimPrefix(alias, "/")
if strings.HasSuffix(alias, helpers.FilePathSeparator) { if strings.HasSuffix(alias, "/") {
alias = alias + "index.html" alias = alias + "index.html"
} else if !strings.HasSuffix(alias, ".html") { } else if !strings.HasSuffix(alias, ".html") {
alias = alias + helpers.FilePathSeparator + "index.html" alias = alias + "/" + "index.html"
}
if originalAlias != alias {
a.log.INFO.Printf("Alias \"%s\" translated to \"%s\"\n", originalAlias, alias)
} }
return alias, nil return filepath.FromSlash(alias), nil
} }

View file

@ -25,14 +25,14 @@ import (
const pageWithAlias = `--- const pageWithAlias = `---
title: Has Alias 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. 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 = `--- const pageWithAliasMultipleOutputs = `---
title: Has Alias for HTML and AMP title: Has Alias for HTML and AMP
aliases: ["foo/bar/"] aliases: ["/foo/bar/"]
outputs: ["HTML", "AMP", "JSON"] 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. 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) assert := require.New(t)
b := newTestSitesBuilder(t) b := newTestSitesBuilder(t)
b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias) b.WithSimpleConfigFile().WithContent("blog/page.md", pageWithAlias)
b.CreateSites().Build(BuildCfg{}) b.CreateSites().Build(BuildCfg{})
assert.Equal(1, len(b.H.Sites)) assert.Equal(1, len(b.H.Sites))
require.Len(t, b.H.Sites[0].RegularPages(), 1) require.Len(t, b.H.Sites[0].RegularPages(), 1)
// the real page // the real page
b.AssertFileContent("public/page/index.html", "For some moments the old man") b.AssertFileContent("public/blog/page/index.html", "For some moments the old man")
// the alias redirector // the alias redirectors
b.AssertFileContent("public/foo/bar/index.html", "<meta http-equiv=\"refresh\" content=\"0; ") 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) { func TestAliasMultipleOutputFormats(t *testing.T) {
@ -64,7 +65,7 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
assert := require.New(t) assert := require.New(t)
b := newTestSitesBuilder(t) b := newTestSitesBuilder(t)
b.WithSimpleConfigFile().WithContent("page.md", pageWithAliasMultipleOutputs) b.WithSimpleConfigFile().WithContent("blog/page.md", pageWithAliasMultipleOutputs)
b.WithTemplates( b.WithTemplates(
"_default/single.html", basicTemplate, "_default/single.html", basicTemplate,
@ -74,9 +75,9 @@ func TestAliasMultipleOutputFormats(t *testing.T) {
b.CreateSites().Build(BuildCfg{}) b.CreateSites().Build(BuildCfg{})
// the real pages // the real pages
b.AssertFileContent("public/page/index.html", "For some moments the old man") b.AssertFileContent("public/blog/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/amp/blog/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.json", "For some moments the old man")
// the alias redirectors // the alias redirectors
b.AssertFileContent("public/foo/bar/index.html", "<meta http-equiv=\"refresh\" content=\"0; ") b.AssertFileContent("public/foo/bar/index.html", "<meta http-equiv=\"refresh\" content=\"0; ")
@ -135,7 +136,7 @@ func TestTargetPathHTMLRedirectAlias(t *testing.T) {
continue continue
} }
if err == nil && path != test.expected { if err == nil && path != test.expected {
t.Errorf("Expected: \"%s\", got: \"%s\"", test.expected, path) t.Errorf("Expected: %q, got: %q", test.expected, path)
} }
} }
} }

View file

@ -16,6 +16,7 @@ package hugolib
import ( import (
"fmt" "fmt"
"path" "path"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -414,10 +415,11 @@ func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{}
pm.params[loki] = pm.weight pm.params[loki] = pm.weight
case "aliases": case "aliases":
pm.aliases = cast.ToStringSlice(v) 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://") { 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 pm.params[loki] = pm.aliases
case "sitemap": case "sitemap":

View file

@ -303,7 +303,20 @@ func (s *Site) renderAliases() error {
f := of.Format f := of.Format
for _, a := range p.Aliases() { 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. // Make sure AMP and similar doesn't clash with regular aliases.
a = path.Join(f.Path, a) a = path.Join(f.Path, a)
} }

View file

@ -55,7 +55,7 @@ tags:
%s %s
categories: categories:
%s %s
aliases: [Ali%d] aliases: [/Ali%d]
--- ---
# Doc # Doc
` `

View file

@ -26,7 +26,7 @@ import (
"github.com/stretchr/testify/require" "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 = `--- const slugDoc2 = `---
title: slug doc 2 title: slug doc 2