mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
For multiple dimensions setups, fix alias handling and multihost publish path
* 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
This commit is contained in:
@@ -607,8 +607,13 @@ type RootConfig struct {
|
|||||||
DefaultOutputFormat string
|
DefaultOutputFormat string
|
||||||
|
|
||||||
// Disable generation of redirect to the default language when DefaultContentLanguageInSubdir is enabled.
|
// 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
|
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.
|
// Disable creation of alias redirect pages.
|
||||||
DisableAliases bool
|
DisableAliases bool
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page, matrix sitesmat
|
|||||||
return nil, errors.New("no alias template found")
|
return nil, errors.New("no alias template found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p == nil {
|
||||||
|
p = page.NopPage
|
||||||
|
}
|
||||||
|
|
||||||
data := aliasPage{
|
data := aliasPage{
|
||||||
permalink,
|
permalink,
|
||||||
p,
|
p,
|
||||||
|
|||||||
@@ -160,11 +160,19 @@ func createTargetPathDescriptor(p *pageState) (page.TargetPathDescriptor, error)
|
|||||||
|
|
||||||
// Add path prefixes.
|
// Add path prefixes.
|
||||||
// Add any role first, as that is a natural candidate for external ACL checks.
|
// 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()
|
rolePrefix := s.getPrefixRole()
|
||||||
addPrefix(rolePrefix, rolePrefix)
|
addPrefix(rolePrefix, rolePrefix)
|
||||||
versionPrefix := s.getPrefixVersion()
|
versionPrefix := s.getPrefixVersion()
|
||||||
addPrefix(versionPrefix, versionPrefix)
|
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 {
|
if desc.URL != "" && strings.IndexByte(desc.URL, ':') >= 0 {
|
||||||
// Attempt to parse and expand an url
|
// Attempt to parse and expand an url
|
||||||
|
|||||||
+1
-5
@@ -1056,7 +1056,7 @@ func (s *siteRefLinker) refLink(ref string, source any, relative bool, outputFor
|
|||||||
if outputFormat != "" {
|
if outputFormat != "" {
|
||||||
o := target.OutputFormats().Get(outputFormat)
|
o := target.OutputFormats().Get(outputFormat)
|
||||||
|
|
||||||
if o == nil {
|
if o.IsZero() {
|
||||||
s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), p, pos)
|
s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), p, pos)
|
||||||
return s.notFoundURL, nil
|
return s.notFoundURL, nil
|
||||||
}
|
}
|
||||||
@@ -1770,10 +1770,6 @@ func (s *Site) render(ctx *siteRenderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.renderMainLanguageRedirect(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+52
-22
@@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bep/logg"
|
"github.com/bep/logg"
|
||||||
"github.com/gohugoio/go-radix"
|
"github.com/gohugoio/go-radix"
|
||||||
|
"github.com/gohugoio/hugo/common/paths"
|
||||||
"github.com/gohugoio/hugo/hugolib/doctree"
|
"github.com/gohugoio/hugo/hugolib/doctree"
|
||||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||||
|
|
||||||
@@ -202,6 +203,16 @@ func pageRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 p.paginator != nil && p.paginator.current != nil {
|
||||||
if err := s.renderPaginator(p, templ); err != nil {
|
if err := s.renderPaginator(p, templ); err != nil {
|
||||||
if sendErr(err) {
|
if sendErr(err) {
|
||||||
@@ -357,32 +368,51 @@ func (s *Site) renderAliases() error {
|
|||||||
return w.Walk(context.TODO())
|
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.
|
// depending on if it lives in sub folder (e.g. /en) or not.
|
||||||
func (s *Site) renderMainLanguageRedirect() error {
|
func (s *Site) renderDefaultDimensionRedirect(home *pageState) error {
|
||||||
if s.conf.DisableDefaultLanguageRedirect {
|
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
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
html, found := s.conf.OutputFormats.Config.GetByName("html")
|
shouldAdd := s.conf.DefaultContentLanguageInSubdir && !s.Conf.IsMultihost()
|
||||||
if found {
|
shouldAdd = shouldAdd || s.conf.DefaultContentVersionInSubdir || s.conf.DefaultContentRoleInSubdir
|
||||||
mainLang := s.conf.DefaultContentLanguage
|
if !shouldAdd {
|
||||||
if s.conf.DefaultContentLanguageInSubdir {
|
return nil
|
||||||
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 {
|
homeLink := home.pageOutput.targetPaths().Link // This doesn't have any baseURL paths in it.
|
||||||
return err
|
|
||||||
}
|
of := home.outputFormat()
|
||||||
} else {
|
homePermalink := home.Permalink()
|
||||||
mainLangURL := s.PathSpec.AbsURL("", false)
|
|
||||||
s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL)
|
var ps []string
|
||||||
if err := s.publishDestAlias(true, mainLang, mainLangURL, html, nil); err != nil {
|
if of.Path != "" {
|
||||||
return err
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1272,3 +1272,115 @@ disableKinds = ["rss", "sitemap", "taxonomy", "term"]
|
|||||||
b.AssertFileContent("public/s1/index.html", "section|/s1/|")
|
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.")
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package page
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/common/types"
|
||||||
"github.com/gohugoio/hugo/media"
|
"github.com/gohugoio/hugo/media"
|
||||||
"github.com/gohugoio/hugo/output"
|
"github.com/gohugoio/hugo/output"
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,8 @@ import (
|
|||||||
// OutputFormats holds a list of the relevant output formats for a given page.
|
// OutputFormats holds a list of the relevant output formats for a given page.
|
||||||
type OutputFormats []OutputFormat
|
type OutputFormats []OutputFormat
|
||||||
|
|
||||||
|
var _ types.Zeroer = OutputFormat{}
|
||||||
|
|
||||||
// OutputFormat links to a representation of a resource.
|
// OutputFormat links to a representation of a resource.
|
||||||
type OutputFormat struct {
|
type OutputFormat struct {
|
||||||
// Rel contains a value that can be used to construct a rel link.
|
// Rel contains a value that can be used to construct a rel link.
|
||||||
@@ -65,6 +68,11 @@ func (o OutputFormat) RelPermalink() string {
|
|||||||
return o.relPermalink
|
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 {
|
func NewOutputFormat(relPermalink, permalink string, isCanonical bool, f output.Format) OutputFormat {
|
||||||
isUserConfigured := true
|
isUserConfigured := true
|
||||||
for _, d := range output.DefaultFormats {
|
for _, d := range output.DefaultFormats {
|
||||||
@@ -84,12 +92,24 @@ func NewOutputFormat(relPermalink, permalink string, isCanonical bool, f output.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get gets a OutputFormat given its name, i.e. json, html etc.
|
// Get gets a OutputFormat given its name, i.e. json, html etc.
|
||||||
// It returns nil if none found.
|
// It returns a zero OutputFormat if not found.
|
||||||
func (o OutputFormats) Get(name string) *OutputFormat {
|
func (o OutputFormats) Get(name string) OutputFormat {
|
||||||
for _, f := range o {
|
for _, f := range o {
|
||||||
if strings.EqualFold(f.Format.Name, name) {
|
if strings.EqualFold(f.Format.Name, name) {
|
||||||
return &f
|
return f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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 OutputFormat{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ func (p *pagePathBuilder) PathDirBase() string {
|
|||||||
|
|
||||||
func (p *pagePathBuilder) PathFile() string {
|
func (p *pagePathBuilder) PathFile() string {
|
||||||
dir := p.Path(0)
|
dir := p.Path(0)
|
||||||
|
|
||||||
if p.prefixPath != "" {
|
if p.prefixPath != "" {
|
||||||
dir = "/" + p.prefixPath + dir
|
dir = "/" + p.prefixPath + dir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="{{ site.Language.LanguageCode }}">
|
<html lang="{{ site.Language.LanguageCode }}">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ .Permalink }}</title>
|
<title>{{ .Permalink }}</title>
|
||||||
<link rel="canonical" href="{{ .Permalink }}">
|
{{ with .OutputFormats.Canonical }}<link rel="{{ .Rel }}" href="{{ .Permalink }}">{{ end }}
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="refresh" content="0; url={{ .Permalink }}">
|
<meta http-equiv="refresh" content="0; url={{ .Permalink }}">
|
||||||
</head>
|
</head>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user