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
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page, matrix sitesmat
|
||||
return nil, errors.New("no alias template found")
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
p = page.NopPage
|
||||
}
|
||||
|
||||
data := aliasPage{
|
||||
permalink,
|
||||
p,
|
||||
|
||||
@@ -160,11 +160,19 @@ func createTargetPathDescriptor(p *pageState) (page.TargetPathDescriptor, error)
|
||||
|
||||
// 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
|
||||
|
||||
+1
-5
@@ -1056,7 +1056,7 @@ func (s *siteRefLinker) refLink(ref string, source any, relative bool, outputFor
|
||||
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
|
||||
}
|
||||
@@ -1770,10 +1770,6 @@ func (s *Site) render(ctx *siteRenderContext) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.renderMainLanguageRedirect(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+52
-22
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"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"
|
||||
|
||||
@@ -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 err := s.renderPaginator(p, templ); err != nil {
|
||||
if sendErr(err) {
|
||||
@@ -357,32 +368,51 @@ func (s *Site) renderAliases() error {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
if s.h.Conf.IsMultihost() || !(s.h.Conf.DefaultContentLanguageInSubdir() || s.h.Conf.IsMultilingual()) {
|
||||
// No need for a redirect
|
||||
func (s *Site) renderDefaultDimensionRedirect(home *pageState) error {
|
||||
if s.conf.DisableDefaultLanguageRedirect || s.conf.DisableDefaultDimensionRedirect {
|
||||
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
|
||||
}
|
||||
shouldAdd := s.conf.DefaultContentLanguageInSubdir && !s.Conf.IsMultihost()
|
||||
shouldAdd = shouldAdd || s.conf.DefaultContentVersionInSubdir || s.conf.DefaultContentRoleInSubdir
|
||||
if !shouldAdd {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1272,3 +1272,115 @@ disableKinds = ["rss", "sitemap", "taxonomy", "term"]
|
||||
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 (
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
)
|
||||
@@ -25,6 +26,8 @@ import (
|
||||
// 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.
|
||||
@@ -65,6 +68,11 @@ func (o OutputFormat) RelPermalink() string {
|
||||
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 {
|
||||
@@ -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.
|
||||
// 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 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 {
|
||||
dir := p.Path(0)
|
||||
|
||||
if p.prefixPath != "" {
|
||||
dir = "/" + p.prefixPath + dir
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!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>
|
||||
|
||||
Reference in New Issue
Block a user