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:])
-}
}
}
}
-
-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)
- })
- }
-}
[outputFormats.print]
isHTML = IS_HTML
+ permalinkable = true
mediaType = 'text/html'
path = 'print'
isHTML = true
mediaType = 'text/html'
path = 'print'
+ permalinkable = true
[outputs]
page = ['html', 'print']
`<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/|",
+ )
+}
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")
})
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:
import (
"fmt"
+ "path"
+ "strings"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/output"
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
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
}
}
}
+ 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
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,
// 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
v = p.Section()
case "lang":
v = p.Lang()
- case "aliases":
- v = p.Aliases()
case "name":
v = p.Name()
case "keywords":
PageMetaProvider
Param(key any) (any, error)
+ Aliases() []string
PageMetaInternalProvider
resource.LanguageProvider