* The alias handling was left as is when we added new dimensions in v0.152.0, which meant that if you had `defaultContentVersionInSubDir=true` set, the alias page in the root would not be correctly created.
* Also, if you had multihost setup with multiple dimensions, the publish would be incorrect: The language key was added last instead of first.
* The home page alias handling is reworked and made more robust, which also fixes some subtle issues:
* It now supports redircects for multiple HTML output formats for the home page.
* We skip creating aliases for disabled home pages.
* With potentially multiple output formats with one of them canonical, we added a new `Canonical` method to the `Page.OutputFormats`, which allows you to do this in a template:
```handlebars
{{ with .OutputFormats.Canonical }}<link rel="{{ .Rel }}" href="{{ .Permalink }}">{{ end }}
```
Fixes #14354
Fixes #14356
DefaultOutputFormat string
// Disable generation of redirect to the default language when DefaultContentLanguageInSubdir is enabled.
+ // Note that this currently is an alias for DisableDefaultDimensionRedirect introduced in v0.154.4.
+ // It's not obvious how a more fine grained setup would work.
DisableDefaultLanguageRedirect bool
+ // Disable generation of redirect to the default dimension when DefaultContentRoleInSubdir or DefaultContentVersionInSubdir or DefaultContentLanguageInSubdir is enabled.
+ DisableDefaultDimensionRedirect bool
+
// Disable creation of alias redirect pages.
DisableAliases bool
return nil, errors.New("no alias template found")
}
+ if p == nil {
+ p = page.NopPage
+ }
+
data := aliasPage{
permalink,
p,
// Add path prefixes.
// Add any role first, as that is a natural candidate for external ACL checks.
+ // For multihost sites, the file path needs to start with the language code.
+ if s.h.Conf.IsMultihost() {
+ addPrefix(s.getLanguageTargetPathLang(true), "")
+ }
rolePrefix := s.getPrefixRole()
addPrefix(rolePrefix, rolePrefix)
versionPrefix := s.getPrefixVersion()
addPrefix(versionPrefix, versionPrefix)
- addPrefix(s.getLanguageTargetPathLang(alwaysInSubDir), s.getLanguagePermalinkLang(alwaysInSubDir))
+ if s.h.Conf.IsMultihost() {
+ addPrefix("", s.getLanguagePermalinkLang(alwaysInSubDir))
+ } else {
+ addPrefix(s.getLanguageTargetPathLang(alwaysInSubDir), s.getLanguagePermalinkLang(alwaysInSubDir))
+ }
if desc.URL != "" && strings.IndexByte(desc.URL, ':') >= 0 {
// Attempt to parse and expand an url
if outputFormat != "" {
o := target.OutputFormats().Get(outputFormat)
- if o == nil {
+ if o.IsZero() {
s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), p, pos)
return s.notFoundURL, nil
}
return
}
- if err = s.renderMainLanguageRedirect(); err != nil {
- return
- }
-
return
}
"github.com/bep/logg"
"github.com/gohugoio/go-radix"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/tpl/tplimpl"
}
}
+ if p.IsHome() && p.outputFormat().IsHTML && s.siteVector.IsFirst() {
+ if err = s.renderDefaultDimensionRedirect(p); err != nil {
+ if sendErr(err) {
+ continue
+ } else {
+ return
+ }
+ }
+ }
+
if p.paginator != nil && p.paginator.current != nil {
if err := s.renderPaginator(p, templ); err != nil {
if sendErr(err) {
return w.Walk(context.TODO())
}
-// renderMainLanguageRedirect creates a redirect to the main language home,
+// renderDefaultDimensionRedirect creates a redirect to the main dimension's home,
// depending on if it lives in sub folder (e.g. /en) or not.
-func (s *Site) renderMainLanguageRedirect() error {
- if s.conf.DisableDefaultLanguageRedirect {
+func (s *Site) renderDefaultDimensionRedirect(home *pageState) error {
+ if s.conf.DisableDefaultLanguageRedirect || s.conf.DisableDefaultDimensionRedirect {
return nil
}
- if s.h.Conf.IsMultihost() || !(s.h.Conf.DefaultContentLanguageInSubdir() || s.h.Conf.IsMultilingual()) {
- // No need for a redirect
+
+ shouldAdd := s.conf.DefaultContentLanguageInSubdir && !s.Conf.IsMultihost()
+ shouldAdd = shouldAdd || s.conf.DefaultContentVersionInSubdir || s.conf.DefaultContentRoleInSubdir
+ if !shouldAdd {
return nil
}
- html, found := s.conf.OutputFormats.Config.GetByName("html")
- if found {
- mainLang := s.conf.DefaultContentLanguage
- if s.conf.DefaultContentLanguageInSubdir {
- mainLangURL := s.PathSpec.AbsURL(mainLang+"/", false)
- s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL)
- if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil {
- return err
- }
- } else {
- mainLangURL := s.PathSpec.AbsURL("", false)
- s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL)
- if err := s.publishDestAlias(true, mainLang, mainLangURL, html, nil); err != nil {
- return err
- }
+ homeLink := home.pageOutput.targetPaths().Link // This doesn't have any baseURL paths in it.
+
+ of := home.outputFormat()
+ homePermalink := home.Permalink()
+
+ var ps []string
+ if of.Path != "" {
+ // For OutputFormats with a path, creating more than one alias will easily create path clashes without much value.
+ ps = []string{paths.AddLeadingAndTrailingSlash(of.Path)}
+ } else {
+ ps = []string{"/"}
+ // /guest/v1.0.0/en/
+ // /,/guest/,/guest/v1.0.0 = /guest/v1.0.0/en/
+ // /guest/v1.0.0/
+ // /,/guest/ = /guest/v1.0.0/
+ parts := strings.Split(strings.Trim(homeLink, "/"), "/")
+
+ for i := 0; i < len(parts)-1; i++ {
+ ps = append(ps, paths.AddLeadingAndTrailingSlash(strings.Join(parts[0:i+1], "/")))
+ }
+ }
+
+ if s.h.Configs.IsMultihost {
+ prefix := "/" + s.LanguagePrefix()
+ for i, p := range ps {
+ ps[i] = path.Join(prefix, p)
+ }
+ }
+
+ for _, p := range ps {
+ if err := s.publishDestAlias(true, p, homePermalink, of, home); err != nil {
+ return err
}
}
b.AssertFileContent("public/s1/index.html", "section|/s1/|")
}
}
+
+const defaultContentDimensionRedirectsFiletemplate = `
+-- hugo.toml --
+baseURL = "BASE_URL_PLACEHOLDER"
+disableKinds = ["sitemap", "taxonomy", "term"]
+defaultContentLanguageInSubDir = true
+defaultContentVersionInSubDir = true
+defaultContentRoleInSubDir = true
+disableDefaultLanguageRedirect = false
+disableDefaultDimensionRedirect = false
+defaultContentLanguage = "en"
+[outputs]
+home = [ 'rss', 'amp', 'html']
+-- layouts/home.html --
+Home.
+`
+
+// Issue 14354
+func TestSitesMatrixRedirects(t *testing.T) {
+ t.Parallel()
+
+ files := strings.ReplaceAll(defaultContentDimensionRedirectsFiletemplate, "BASE_URL_PLACEHOLDER", "https://example.org/")
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "url=https://example.org/guest/v1.0.0/en/\"", `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/">`)
+ b.AssertFileContent("public/amp/index.html", "url=https://example.org/guest/v1.0.0/en/amp/\"", `<link rel="canonical" href="https://example.org/guest/v1.0.0/en/">`)
+ b.AssertFileContent("public/guest/index.html", "url=https://example.org/guest/v1.0.0/en/\"")
+ b.AssertFileContent("public/guest/v1.0.0/index.html", "url=https://example.org/guest/v1.0.0/en/\"")
+ b.AssertFileContent("public/guest/v1.0.0/en/index.html", "Home.")
+ b.AssertFileContent("public/guest/v1.0.0/en/amp/index.html", "Home.")
+}
+
+func TestSitesMatrixRedirectsBasePathInBaseURL(t *testing.T) {
+ t.Parallel()
+
+ files := strings.ReplaceAll(defaultContentDimensionRedirectsFiletemplate, "BASE_URL_PLACEHOLDER", "https://example.org/docs/")
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "url=https://example.org/docs/guest/v1.0.0/en/\"")
+ b.AssertFileContent("public/amp/index.html", "url=https://example.org/docs/guest/v1.0.0/en/amp/\"", `<link rel="canonical" href="https://example.org/docs/guest/v1.0.0/en/">`)
+ b.AssertFileContent("public/guest/index.html", "url=https://example.org/docs/guest/v1.0.0/en/\"")
+ b.AssertFileContent("public/guest/v1.0.0/index.html", "url=https://example.org/docs/guest/v1.0.0/en/\"")
+ b.AssertFileContent("public/guest/v1.0.0/en/index.html", "Home.")
+}
+
+func TestSitesMatrixRedirectsDisableDefaultLanguageRedirect(t *testing.T) {
+ t.Parallel()
+
+ files := strings.ReplaceAll(defaultContentDimensionRedirectsFiletemplate, "BASE_URL_PLACEHOLDER", "https://example.org/")
+
+ runTest := func(files string) {
+ b := hugolib.Test(t, files)
+
+ b.AssertFileExists("public/index.html", false)
+ b.AssertFileExists("public/guest/index.html", false)
+ b.AssertFileExists("public/guest/v1.0.0/index.html", false)
+ b.AssertFileContent("public/guest/v1.0.0/en/index.html", "Home.")
+ }
+
+ runTest(strings.ReplaceAll(files, "disableDefaultLanguageRedirect = false", "disableDefaultLanguageRedirect = true"))
+ runTest(strings.ReplaceAll(files, "disableDefaultDimensionRedirect = false", "disableDefaultDimensionRedirect = true"))
+}
+
+func TestSitesMatrixRedirectsDefaultContentVersionInBase(t *testing.T) {
+ t.Parallel()
+
+ files := strings.ReplaceAll(defaultContentDimensionRedirectsFiletemplate, "BASE_URL_PLACEHOLDER", "https://example.org/")
+ files = strings.ReplaceAll(files, "defaultContentVersionInSubDir = true", "defaultContentVersionInSubDir = false")
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "url=https://example.org/guest/en/\"")
+ b.AssertFileContent("public/guest/index.html", "url=https://example.org/guest/en/\"")
+ b.AssertFileExists("public/guest/v1.0.0/index.html", false)
+ b.AssertFileContent("public/guest/en/index.html", "Home.")
+}
+
+func TestSitesMatrixRedirectsMultiHost(t *testing.T) {
+ t.Parallel()
+ //
+ files := `
+-- hugo.toml --
+disableKinds = ["sitemap", "taxonomy", "term"]
+defaultContentLanguageInSubDir = true
+defaultContentVersionInSubDir = true
+defaultContentRoleInSubDir = true
+disableDefaultLanguageRedirect = false
+disableDefaultDimensionRedirect = false
+defaultContentLanguage = "en"
+[languages]
+[languages.en]
+weight = 1
+baseURL = "https://en.example.org/"
+[languages.nn]
+weight = 2
+baseURL = "https://nn.example.org/"
+[outputs]
+home = [ 'rss', 'amp', 'html']
+-- layouts/home.html --
+Home.
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/en/index.html", "url=https://en.example.org/guest/v1.0.0/\"", `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/">`)
+ b.AssertFileContent("public/en/amp/index.html", "url=https://en.example.org/guest/v1.0.0/amp/\"", `<link rel="canonical" href="https://en.example.org/guest/v1.0.0/">`)
+ b.AssertFileContent("public/en/guest/index.html", "url=https://en.example.org/guest/v1.0.0/\"")
+ b.AssertFileContent("public/en/guest/v1.0.0/index.html", "Home.")
+ b.AssertFileContent("public/en/guest/v1.0.0/amp/index.html", "Home.")
+}
import (
"strings"
+ "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
)
// OutputFormats holds a list of the relevant output formats for a given page.
type OutputFormats []OutputFormat
+var _ types.Zeroer = OutputFormat{}
+
// OutputFormat links to a representation of a resource.
type OutputFormat struct {
// Rel contains a value that can be used to construct a rel link.
return o.relPermalink
}
+// IsZero checks whether this OutputFormat is the zero value.
+func (o OutputFormat) IsZero() bool {
+ return o.Format.Name == ""
+}
+
func NewOutputFormat(relPermalink, permalink string, isCanonical bool, f output.Format) OutputFormat {
isUserConfigured := true
for _, d := range output.DefaultFormats {
}
// Get gets a OutputFormat given its name, i.e. json, html etc.
-// It returns nil if none found.
-func (o OutputFormats) Get(name string) *OutputFormat {
+// It returns a zero OutputFormat if not found.
+func (o OutputFormats) Get(name string) OutputFormat {
for _, f := range o {
if strings.EqualFold(f.Format.Name, name) {
- return &f
+ return f
+ }
+ }
+ return OutputFormat{}
+}
+
+// Canonical returns the first canonical OutputFormat for this page,
+// or a zero OutputFormat if not found.
+func (o OutputFormats) Canonical() OutputFormat {
+ const canonical = "canonical"
+ for _, f := range o {
+ if strings.EqualFold(f.Rel, canonical) {
+ return f
}
}
- return nil
+ return OutputFormat{}
}
func (p *pagePathBuilder) PathFile() string {
dir := p.Path(0)
+
if p.prefixPath != "" {
dir = "/" + p.prefixPath + dir
}
-<!DOCTYPE html>
+<!doctype html>
<html lang="{{ site.Language.LanguageCode }}">
<head>
<title>{{ .Permalink }}</title>
- <link rel="canonical" href="{{ .Permalink }}">
- <meta charset="utf-8">
+ {{ with .OutputFormats.Canonical }}<link rel="{{ .Rel }}" href="{{ .Permalink }}">{{ end }}
+ <meta charset="utf-8" />
<meta http-equiv="refresh" content="0; url={{ .Permalink }}">
</head>
</html>