diff --git a/common/paths/path.go b/common/paths/path.go index 06f573faa..e9b9108f5 100644 --- a/common/paths/path.go +++ b/common/paths/path.go @@ -428,38 +428,3 @@ func ToSlashPreserveLeading(s string) string { func IsSameFilePath(s1, s2 string) bool { 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:]) -} diff --git a/common/paths/path_test.go b/common/paths/path_test.go index f64fb4f6a..3fa4c03b2 100644 --- a/common/paths/path_test.go +++ b/common/paths/path_test.go @@ -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) - }) - } -} diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go index f72f5f3b4..320ca7dd7 100644 --- a/hugolib/alias_test.go +++ b/hugolib/alias_test.go @@ -203,6 +203,7 @@ disableKinds = ['home', 'rss', 'sitemap', 'taxonomy', 'term'] [outputFormats.print] isHTML = IS_HTML + permalinkable = true mediaType = 'text/html' path = 'print' @@ -416,6 +417,7 @@ defaultContentVersionInSubdir = true isHTML = true mediaType = 'text/html' path = 'print' + permalinkable = true [outputs] page = ['html', 'print'] @@ -652,3 +654,32 @@ aliases: [/p2-alias] ``, ) } + +// 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/|", + ) +} diff --git a/hugolib/disableKinds_test.go b/hugolib/disableKinds_test.go index 25a8c94f6..9b02dd4c5 100644 --- a/hugolib/disableKinds_test.go +++ b/hugolib/disableKinds_test.go @@ -190,7 +190,7 @@ DATA t.Run("Build config, no render link", func(t *testing.T) { files := filesForDisabledKind("") 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") }) diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 3776cb3ab..35dcee909 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -384,10 +384,6 @@ func (m *pageMeta) prepareRebuild() { m.pageConfig.Dates = m.datesOriginal } -func (m *pageMeta) Aliases() []string { - return m.pageConfig.Aliases -} - func (m *pageMeta) BundleType() string { switch m.pathInfo.Type() { case paths.TypeLeaf: diff --git a/hugolib/page__output.go b/hugolib/page__output.go index 2c5e1ca04..d21c6860d 100644 --- a/hugolib/page__output.go +++ b/hugolib/page__output.go @@ -15,6 +15,8 @@ package hugolib import ( "fmt" + "path" + "strings" "github.com/gohugoio/hugo/identity" "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. } +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() { po.renderState++ po.renderOnce = true diff --git a/hugolib/site.go b/hugolib/site.go index 0e3660c86..cf585613c 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -1760,21 +1760,6 @@ func (s *Site) render(ctx *siteRenderContext) (err error) { 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 { return } diff --git a/hugolib/site_render.go b/hugolib/site_render.go index 42d6ebdb4..c482e61ce 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -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 { // Nothing more to do for this page. continue @@ -300,71 +314,17 @@ func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error { return nil } -// renderAliases renders shell pages that simply have a redirect in the header. -func (s *Site) renderAliases() error { - w := &doctree.NodeShiftTreeWalker[contentNode]{ - Tree: s.pageMap.treePages, - Handle: func(key string, n contentNode) (radix.WalkFlag, error) { - p := n.(*pageState) - - // We cannot alias a page that's not rendered. - 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 - }, +func (s *Site) renderAliasesForPage(p *pageState) error { + po := p.pageOutput + f := po.f + plink := p.Permalink() + for _, a := range p.Aliases() { + err := s.writeDestAlias(a, plink, f, p) + if err != nil { + return err + } } - return w.Walk(context.TODO()) + return nil } // renderDefaultSiteRedirect creates a redirect to the default site's home, diff --git a/resources/page/page.go b/resources/page/page.go index 406843283..74400d7ed 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -197,9 +197,6 @@ type PageMetaProvider interface { // The 4 page dates resource.Dated - // Aliases forms the base for redirects generation. - Aliases() []string - // BundleType returns the bundle type: `leaf`, `branch` or an empty string. BundleType() string @@ -278,8 +275,6 @@ func NamedPageMetaValue(p PageMetaLanguageResource, nameLower string) (any, bool v = p.Section() case "lang": v = p.Lang() - case "aliases": - v = p.Aliases() case "name": v = p.Name() case "keywords": @@ -347,6 +342,7 @@ type PageWithoutContent interface { PageMetaProvider Param(key any) (any, error) + Aliases() []string PageMetaInternalProvider resource.LanguageProvider