mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 07:18:54 +00:00
Make Page.Aliases more useful in multidimensional setups (note)
There are 2 fundamental changes related to alias handling in this PR: 1. the `Page.Aliases`` method returns a ready-to-use list of aliases, not the raw input data. This means that it's prefixed with correct dimension values (e.g. language code) and output format paths, and ready to use in e.g. Netlify's _redirects file. 2. We only render aliases for output formats that is both defined as `IsHTML` and `Permalinkable`. See #14402
This commit is contained in:
committed by
GitHub
parent
11f7f39913
commit
ee91c707ee
@@ -428,38 +428,3 @@ func ToSlashPreserveLeading(s string) string {
|
|||||||
func IsSameFilePath(s1, s2 string) bool {
|
func IsSameFilePath(s1, s2 string) bool {
|
||||||
return path.Clean(ToSlashTrim(s1)) == path.Clean(ToSlashTrim(s2))
|
return path.Clean(ToSlashTrim(s1)) == path.Clean(ToSlashTrim(s2))
|
||||||
}
|
}
|
||||||
|
|
||||||
// InjectSegment returns a clean path with forward slashes by inserting segment
|
|
||||||
// into s immediately following prefix. The prefix must be a complete path
|
|
||||||
// component match (e.g., "en" matches "en/docs" but not "entertainment").
|
|
||||||
// If prefix is empty, segment is prepended to s. If prefix is not found or
|
|
||||||
// is a partial match, the cleaned version of s is returned.
|
|
||||||
func InjectSegment(s, prefix, segment string) string {
|
|
||||||
if s == "" && prefix == "" && segment == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
s = path.Clean(filepath.ToSlash(s))
|
|
||||||
prefix = path.Clean(filepath.ToSlash(prefix))
|
|
||||||
segment = path.Clean(filepath.ToSlash(segment))
|
|
||||||
|
|
||||||
if prefix == "." {
|
|
||||||
return path.Join(segment, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(s, prefix) {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
lp := len(prefix)
|
|
||||||
ls := len(s)
|
|
||||||
|
|
||||||
// Ensure prefix matches a full component.
|
|
||||||
if ls > lp && prefix != "/" {
|
|
||||||
if s[lp] != '/' {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Join(prefix, segment, s[lp:])
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -369,100 +369,3 @@ func TestPathEscape(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInjectSegment(t *testing.T) {
|
|
||||||
c := qt.New(t)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
s string
|
|
||||||
prefix string
|
|
||||||
segment string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "rooted match",
|
|
||||||
s: "/abc/def",
|
|
||||||
prefix: "/abc",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "/abc/seg/def",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "relative match",
|
|
||||||
s: "abc/def",
|
|
||||||
prefix: "abc",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "abc/seg/def",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rootedness mismatch - s rooted",
|
|
||||||
s: "/abc/def",
|
|
||||||
prefix: "abc",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "/abc/def",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rootedness mismatch - prefix rooted",
|
|
||||||
s: "abc/def",
|
|
||||||
prefix: "/abc",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "abc/def",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "partial component mismatch",
|
|
||||||
s: "/abcdef",
|
|
||||||
prefix: "/abc",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "/abcdef",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "root slash prefix",
|
|
||||||
s: "/abc",
|
|
||||||
prefix: "/",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "/seg/abc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "all empty",
|
|
||||||
s: "",
|
|
||||||
prefix: "",
|
|
||||||
segment: "",
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty s and prefix",
|
|
||||||
s: "",
|
|
||||||
prefix: "",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "seg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty segment",
|
|
||||||
s: "/abc/def",
|
|
||||||
prefix: "/abc",
|
|
||||||
segment: "",
|
|
||||||
expected: "/abc/def",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty prefix with relative s",
|
|
||||||
s: "abc",
|
|
||||||
prefix: "",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "seg/abc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty prefix with rooted s",
|
|
||||||
s: "/abc",
|
|
||||||
prefix: "",
|
|
||||||
segment: "seg",
|
|
||||||
expected: "seg/abc",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
c.Run(tt.name, func(c *qt.C) {
|
|
||||||
actual := InjectSegment(tt.s, tt.prefix, tt.segment)
|
|
||||||
c.Assert(actual, qt.Equals, tt.expected)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ disableKinds = ['home', 'rss', 'sitemap', 'taxonomy', 'term']
|
|||||||
|
|
||||||
[outputFormats.print]
|
[outputFormats.print]
|
||||||
isHTML = IS_HTML
|
isHTML = IS_HTML
|
||||||
|
permalinkable = true
|
||||||
mediaType = 'text/html'
|
mediaType = 'text/html'
|
||||||
path = 'print'
|
path = 'print'
|
||||||
|
|
||||||
@@ -416,6 +417,7 @@ defaultContentVersionInSubdir = true
|
|||||||
isHTML = true
|
isHTML = true
|
||||||
mediaType = 'text/html'
|
mediaType = 'text/html'
|
||||||
path = 'print'
|
path = 'print'
|
||||||
|
permalinkable = true
|
||||||
|
|
||||||
[outputs]
|
[outputs]
|
||||||
page = ['html', 'print']
|
page = ['html', 'print']
|
||||||
@@ -652,3 +654,32 @@ aliases: [/p2-alias]
|
|||||||
`<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/print/foo/s2/p2/">`,
|
`<meta http-equiv="refresh" content="0; url=https://en.example.org/guest/v1.0.0/print/foo/s2/p2/">`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue #14402.
|
||||||
|
func TestComprehensiveAliasesRedirectsFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
files := `
|
||||||
|
-- hugo.toml --
|
||||||
|
disableAliases = true
|
||||||
|
defaultContentLanguageInSubdir = true
|
||||||
|
defaultContentRoleInSubdir = true
|
||||||
|
defaultContentVersionInSubdir = true
|
||||||
|
baseURL = "https://example.org/"
|
||||||
|
-- content/foo/p1.md --
|
||||||
|
---
|
||||||
|
aliases: ["/foo/p2/", "../p3/"]
|
||||||
|
---
|
||||||
|
-- content/foo/p2.md --
|
||||||
|
-- content/p3.md --
|
||||||
|
-- layouts/home.html --
|
||||||
|
Home.
|
||||||
|
{{ range $p := site.RegularPages }}{{ range .Aliases }}{{ . | printf "%-35s" }}=>{{ $p.RelPermalink -}}|{{ end -}}{{ end }}
|
||||||
|
`
|
||||||
|
b := Test(t, files)
|
||||||
|
|
||||||
|
b.AssertFileContent("public/guest/v1.0.0/en/index.html",
|
||||||
|
"/guest/v1.0.0/en/foo/p2 =>/guest/v1.0.0/en/foo/p1/|",
|
||||||
|
"/guest/v1.0.0/en/p3 =>/guest/v1.0.0/en/foo/p1/|",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ DATA
|
|||||||
t.Run("Build config, no render link", func(t *testing.T) {
|
t.Run("Build config, no render link", func(t *testing.T) {
|
||||||
files := filesForDisabledKind("")
|
files := filesForDisabledKind("")
|
||||||
b := Test(t, files)
|
b := Test(t, files)
|
||||||
b.AssertFileExists("public/sect/no-render/index.html", false)
|
b.AssertFileExists("public/sect/no-render-link/index.html", false)
|
||||||
b.AssertFileContent("public/link-alias/index.html", "refresh")
|
b.AssertFileContent("public/link-alias/index.html", "refresh")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -384,10 +384,6 @@ func (m *pageMeta) prepareRebuild() {
|
|||||||
m.pageConfig.Dates = m.datesOriginal
|
m.pageConfig.Dates = m.datesOriginal
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *pageMeta) Aliases() []string {
|
|
||||||
return m.pageConfig.Aliases
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *pageMeta) BundleType() string {
|
func (m *pageMeta) BundleType() string {
|
||||||
switch m.pathInfo.Type() {
|
switch m.pathInfo.Type() {
|
||||||
case paths.TypeLeaf:
|
case paths.TypeLeaf:
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ package hugolib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/identity"
|
"github.com/gohugoio/hugo/identity"
|
||||||
"github.com/gohugoio/hugo/output"
|
"github.com/gohugoio/hugo/output"
|
||||||
@@ -113,6 +115,40 @@ type pageOutput struct {
|
|||||||
renderOnce bool // To make sure we at least try to render it once.
|
renderOnce bool // To make sure we at least try to render it once.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (po *pageOutput) Aliases() []string {
|
||||||
|
conf := po.p.s.conf
|
||||||
|
p := po.p
|
||||||
|
f := po.f
|
||||||
|
|
||||||
|
// This is relatively cheap to create, and the common case is to call this once.
|
||||||
|
// So avoid caching this value.
|
||||||
|
aliases := make([]string, len(po.p.m.pageConfig.Aliases))
|
||||||
|
for i, a := range po.p.m.pageConfig.Aliases {
|
||||||
|
isRelative := !strings.HasPrefix(a, "/")
|
||||||
|
var baseDir string
|
||||||
|
if isRelative {
|
||||||
|
// Form the baseDir by taking the resource's base target
|
||||||
|
// and moving up one level to the parent.
|
||||||
|
parentContext := path.Join(p.targetPaths().SubResourceBaseTarget, "..")
|
||||||
|
baseDir = parentContext
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Form the baseDir by prepending the content dimension
|
||||||
|
// prefixes with the Output Format path.
|
||||||
|
baseDir = path.Join("/", p.targetPathDescriptor.PrefixFilePath, f.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
a = path.Join(baseDir, a)
|
||||||
|
|
||||||
|
if conf.C.IsUglyURLSection(p.Section()) && !strings.HasSuffix(a, ".html") {
|
||||||
|
a += ".html"
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases[i] = a
|
||||||
|
}
|
||||||
|
return aliases
|
||||||
|
}
|
||||||
|
|
||||||
func (po *pageOutput) incrRenderState() {
|
func (po *pageOutput) incrRenderState() {
|
||||||
po.renderState++
|
po.renderState++
|
||||||
po.renderOnce = true
|
po.renderOnce = true
|
||||||
|
|||||||
@@ -1760,21 +1760,6 @@ func (s *Site) render(ctx *siteRenderContext) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.outIdx == 0 && s.h.buildCounter.Load() == 0 {
|
|
||||||
// Note that even if disableAliases is set, the aliases themselves are
|
|
||||||
// preserved on page. The motivation with this is to be able to generate
|
|
||||||
// 301 redirects in a .htaccess file and similar using a custom output format.
|
|
||||||
if !s.conf.DisableAliases {
|
|
||||||
// Aliases must be rendered before pages.
|
|
||||||
// Some sites, Hugo docs included, have faulty alias definitions that point
|
|
||||||
// to itself or another real page. These will be overwritten in the next
|
|
||||||
// step.
|
|
||||||
if err = s.renderAliases(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.renderPages(ctx); err != nil {
|
if err = s.renderPages(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-64
@@ -154,6 +154,20 @@ func pageRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !s.conf.DisableAliases && s.h.buildCounter.Load() == 0 {
|
||||||
|
of := p.outputFormat()
|
||||||
|
if of.IsHTML && of.Permalinkable {
|
||||||
|
// Render any aliases for this page.
|
||||||
|
if err := s.renderAliasesForPage(p); err != nil {
|
||||||
|
if sendErr(err) {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !p.render {
|
if !p.render {
|
||||||
// Nothing more to do for this page.
|
// Nothing more to do for this page.
|
||||||
continue
|
continue
|
||||||
@@ -300,71 +314,17 @@ func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderAliases renders shell pages that simply have a redirect in the header.
|
func (s *Site) renderAliasesForPage(p *pageState) error {
|
||||||
func (s *Site) renderAliases() error {
|
po := p.pageOutput
|
||||||
w := &doctree.NodeShiftTreeWalker[contentNode]{
|
f := po.f
|
||||||
Tree: s.pageMap.treePages,
|
plink := p.Permalink()
|
||||||
Handle: func(key string, n contentNode) (radix.WalkFlag, error) {
|
for _, a := range p.Aliases() {
|
||||||
p := n.(*pageState)
|
err := s.writeDestAlias(a, plink, f, p)
|
||||||
|
if err != nil {
|
||||||
// We cannot alias a page that's not rendered.
|
return err
|
||||||
if p.m.noLink() || p.skipRender() {
|
}
|
||||||
return radix.WalkContinue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.Aliases()) == 0 {
|
|
||||||
return radix.WalkContinue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pathSeen := make(map[string]bool)
|
|
||||||
for _, of := range p.OutputFormats() {
|
|
||||||
if !of.Format.IsHTML {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
f := of.Format
|
|
||||||
|
|
||||||
if pathSeen[f.Path] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pathSeen[f.Path] = true
|
|
||||||
|
|
||||||
plink := of.Permalink()
|
|
||||||
|
|
||||||
for _, a := range p.Aliases() {
|
|
||||||
isRelative := !strings.HasPrefix(a, "/")
|
|
||||||
prefix := path.Join("/", p.targetPathDescriptor.PrefixFilePath)
|
|
||||||
|
|
||||||
var baseDir string
|
|
||||||
if isRelative {
|
|
||||||
// Form the baseDir by taking the resource's base target
|
|
||||||
// and moving up one level to the parent. Then, inject
|
|
||||||
// the Output Format path between the content dimension
|
|
||||||
// prefixes and the remaining path.
|
|
||||||
parentContext := path.Join(p.targetPaths().SubResourceBaseTarget, "..")
|
|
||||||
baseDir = paths.InjectSegment(parentContext, prefix, f.Path)
|
|
||||||
} else {
|
|
||||||
// Form the baseDir by prepending the content dimension
|
|
||||||
// prefixes with the Output Format path.
|
|
||||||
baseDir = path.Join(prefix, f.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
a = path.Join(baseDir, a)
|
|
||||||
|
|
||||||
if s.conf.C.IsUglyURLSection(p.Section()) && !strings.HasSuffix(a, ".html") {
|
|
||||||
a += ".html"
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.writeDestAlias(a, plink, f, p)
|
|
||||||
if err != nil {
|
|
||||||
return radix.WalkStop, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return radix.WalkContinue, nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
return w.Walk(context.TODO())
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderDefaultSiteRedirect creates a redirect to the default site's home,
|
// renderDefaultSiteRedirect creates a redirect to the default site's home,
|
||||||
|
|||||||
@@ -197,9 +197,6 @@ type PageMetaProvider interface {
|
|||||||
// The 4 page dates
|
// The 4 page dates
|
||||||
resource.Dated
|
resource.Dated
|
||||||
|
|
||||||
// Aliases forms the base for redirects generation.
|
|
||||||
Aliases() []string
|
|
||||||
|
|
||||||
// BundleType returns the bundle type: `leaf`, `branch` or an empty string.
|
// BundleType returns the bundle type: `leaf`, `branch` or an empty string.
|
||||||
BundleType() string
|
BundleType() string
|
||||||
|
|
||||||
@@ -278,8 +275,6 @@ func NamedPageMetaValue(p PageMetaLanguageResource, nameLower string) (any, bool
|
|||||||
v = p.Section()
|
v = p.Section()
|
||||||
case "lang":
|
case "lang":
|
||||||
v = p.Lang()
|
v = p.Lang()
|
||||||
case "aliases":
|
|
||||||
v = p.Aliases()
|
|
||||||
case "name":
|
case "name":
|
||||||
v = p.Name()
|
v = p.Name()
|
||||||
case "keywords":
|
case "keywords":
|
||||||
@@ -347,6 +342,7 @@ type PageWithoutContent interface {
|
|||||||
PageMetaProvider
|
PageMetaProvider
|
||||||
|
|
||||||
Param(key any) (any, error)
|
Param(key any) (any, error)
|
||||||
|
Aliases() []string
|
||||||
PageMetaInternalProvider
|
PageMetaInternalProvider
|
||||||
|
|
||||||
resource.LanguageProvider
|
resource.LanguageProvider
|
||||||
|
|||||||
Reference in New Issue
Block a user