Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
More TODOs
  • Loading branch information
bep committed Oct 31, 2025
commit b6d5e5b91caa3f1d0e4d70f2adb4b70d803ad914
2 changes: 1 addition & 1 deletion config/allconfig/allconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ func (c *Configs) Init(logger loggers.Logger) error {
}

// Apply default project mounts.
if err := modules.ApplyProjectConfigDefaults(c.Modules[0], c.configLangs...); err != nil {
if err := modules.ApplyProjectConfigDefaults(logger.Logger(), c.Modules[0], c.configLangs...); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion config/allconfig/alldecoders.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "module",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Module, err = modules.DecodeConfig(p.p)
p.c.Module, err = modules.DecodeConfig(p.logger.Logger(), p.p)
return err
},
},
Expand Down
4 changes: 2 additions & 2 deletions hugolib/filesystems/basefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,10 +709,10 @@ func (b *sourceFilesystemsBuilder) createOverlayFs(

patterns, hasLegacyIncludes, hasLegacyExcludes := mount.FilesToFilter()
if hasLegacyIncludes {
hugo.Deprecate("module.mounts.includeFiles", "Replaced by the simpler 'files' setting, see https://gohugo.io/configuration/module/#files", "v0.152.0")
hugo.Deprecate("module.mounts.includeFiles", "Replaced by the simpler 'files' setting, see https://gohugo.io/configuration/module/#files", "v0.153.0")
}
if hasLegacyExcludes {
hugo.Deprecate("module.mounts.excludeFiles", "Replaced by the simpler 'files' setting, see https://gohugo.io/configuration/module/#files", "v0.152.0")
hugo.Deprecate("module.mounts.excludeFiles", "Replaced by the simpler 'files' setting, see https://gohugo.io/configuration/module/#files", "v0.153.0")
}

inclusionFilter, err := hglob.NewFilenameFilterV2(patterns)
Expand Down
17 changes: 13 additions & 4 deletions hugolib/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"iter"
"path/filepath"
"slices"
"strconv"
"strings"
"sync/atomic"
Expand Down Expand Up @@ -495,8 +496,7 @@ func (ps *pageState) AllTranslations() page.Pages {
if ps.m.pageConfig.TranslationKey != "" {
// translationKey set by user.
pas, _ := ps.s.h.translationKeyPages.Get(ps.m.pageConfig.TranslationKey)
pasc := make(page.Pages, len(pas))
copy(pasc, pas)
pasc := slices.Clone(pas)
page.SortByLanguage(pasc)
return pasc, nil
}
Expand Down Expand Up @@ -537,7 +537,6 @@ func (ps *pageState) siteVectors() sitesmatrix.VectorIterator {

// TODO1 name.
func (ps *pageState) Rotate(dimensionStr string) (page.Pages, error) {
// TODO1: For language, consider the special case with translationKey.
dimensionStr = strings.ToLower(dimensionStr)
key := ps.Path() + "/" + "rotate-" + dimensionStr
d, err := sitesmatrix.ParseDimension(dimensionStr)
Expand All @@ -557,6 +556,17 @@ func (ps *pageState) Rotate(dimensionStr string) (page.Pages, error) {
},
)

if dimensionStr == "language" && ps.m.pageConfig.TranslationKey != "" {
// translationKey set by user.
// This is an old construct back from when languages were introduced.
// We keep it for backward compatibility.
// ALso see AllTranslations.
pas, _ := ps.s.h.translationKeyPages.Get(ps.m.pageConfig.TranslationKey)
pasc := slices.Clone(pas)
page.SortByLanguage(pasc)
return pasc, nil
}

pas = pagePredicates.ShouldLink.Filter(pas)
page.SortByDims(pas)
return pas, nil
Expand Down Expand Up @@ -858,7 +868,6 @@ func (ps *pageState) posOffset(offset int) text.Position {
// shiftToOutputFormat is serialized. The output format idx refers to the
// full set of output formats for all sites.
// This is serialized.
// TODO1 with the added dimensions, we need to compress the pageOutputs slice.
func (ps *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
if err := ps.initPage(); err != nil {
return err
Expand Down
7 changes: 2 additions & 5 deletions hugolib/page__paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package hugolib

import (
"net/url"
"path"
"strings"

"github.com/gohugoio/hugo/output"
Expand Down Expand Up @@ -185,11 +186,7 @@ func createTargetPathDescriptor(p *pageState) (page.TargetPathDescriptor, error)

if opath != "" {
opath, _ = url.QueryUnescape(opath)
if strings.HasSuffix(opath, "//") {
// When rewriting the _index of the section the permalink config is applied to,
// we get double slashes at the end sometimes; clear them up here
opath = strings.TrimSuffix(opath, "/")
}
opath = path.Clean(opath)

desc.ExpandedPermalink = opath

Expand Down
35 changes: 35 additions & 0 deletions hugolib/page_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,41 @@ Resources: {{ range .Resources }}{{ .RelPermalink }}|{{ .Content }}|{{ end }}|
)
}

func TestTranslationKeyRotate(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy']
defaultContentLanguage = 'en'
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.pt]
weight = 2
-- content/foo.md --
---
title: Foo
translationkey: "mykey"
---
-- content/bar.md --
---
title: Bar
translationkey: "mykey"
---
-- layouts/all.html --
Rotate(language): {{ with .Rotate "language" }}{{ range . }}{{ template "printp" . }}|{{ end }}{{ end }}$
{{ define "printp" }}{{ .RelPermalink }}:{{ with .Site }}{{ template "prints" . }}{{ end }}{{ end }}
{{ define "prints" }}/l:{{ .Language.Name }}/v:{{ .Version.Name }}/r:{{ .Role.Name }}{{ end }}
`

b := Test(t, files)

b.AssertFileContent("public/en/foo/index.html", "Rotate(language): /en/bar/:/l:en/v:v1/r:guest|/en/foo/:/l:en/v:v1/r:guest|$")
b.AssertFileContent("public/en/bar/index.html", "Rotate(language): /en/bar/:/l:en/v:v1/r:guest|/en/foo/:/l:en/v:v1/r:guest|$")
}

func TestChompBOM(t *testing.T) {
t.Parallel()
c := qt.New(t)
Expand Down
2 changes: 1 addition & 1 deletion hugolib/segments/segments.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func (s *segmentsBuilder) buildOne(f []SegmentMatcherFields) (predicate.PR[Segme
}
}
if fields.Lang != "" {
hugo.DeprecateWithLogger("config segments.[...]lang ", "Use sites.matrix instead, see https://gohugo.io/configuration/segments/#segment-definition", "v0.152.0", s.logger.Logger())
hugo.DeprecateWithLogger("config segments.[...]lang ", "Use sites.matrix instead, see https://gohugo.io/configuration/segments/#segment-definition", "v0.153.0", s.logger.Logger())
fields.Sites.Matrix.Languages = []string{fields.Lang}
}
if !fields.Sites.Matrix.IsZero() {
Expand Down
3 changes: 1 addition & 2 deletions hugolib/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,9 +523,8 @@ func newHugoSites(
tplimpl.StoreOptions{
Fs: s.BaseFs.Layouts.Fs,
Log: s.Log,
DefaultContentLanguage: s.Conf.DefaultContentLanguage(),
Watching: s.Conf.Watching(),
PathHandler: s.Conf.PathParser(),
PathParser: s.Conf.PathParser(),
Metrics: d.Metrics,
OutputFormats: s.conf.OutputFormats.Config,
MediaTypes: s.conf.MediaTypes.Config,
Expand Down
4 changes: 2 additions & 2 deletions modules/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ LOOP:
}
}

config, err := decodeConfig(tc.cfg, c.moduleConfig.replacementsMap)
config, err := decodeConfig(c.logger.Logger(), tc.cfg, c.moduleConfig.replacementsMap)
if err != nil {
return err
}
Expand Down Expand Up @@ -796,7 +796,7 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
return nil, fmt.Errorf("%s: mount target must be one of: %v", errMsg, files.ComponentFolders)
}

if err := mnt.init(); err != nil {
if err := mnt.init(c.logger.Logger()); err != nil {
return nil, fmt.Errorf("%s: %w", errMsg, err)
}

Expand Down
19 changes: 11 additions & 8 deletions modules/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"slices"
"strings"

"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/types"
Expand Down Expand Up @@ -63,7 +64,7 @@ var DefaultModuleConfig = Config{

// ApplyProjectConfigDefaults applies default/missing module configuration for
// the main project.
func ApplyProjectConfigDefaults(mod Module, cfgs ...config.AllProvider) error {
func ApplyProjectConfigDefaults(logger logg.Logger, mod Module, cfgs ...config.AllProvider) error {
moda := mod.(*moduleAdapter)

// To bridge between old and new configuration format we need
Expand Down Expand Up @@ -157,7 +158,7 @@ func ApplyProjectConfigDefaults(mod Module, cfgs ...config.AllProvider) error {

if dir != "" {
mnt := Mount{Lang: lang, Source: dir, Target: component}
if err := mnt.init(); err != nil {
if err := mnt.init(logger); err != nil {
return fmt.Errorf("failed to init mount %q %d: %w", lang, i, err)
}
mounts = append(mounts, mnt)
Expand All @@ -173,11 +174,11 @@ func ApplyProjectConfigDefaults(mod Module, cfgs ...config.AllProvider) error {
}

// DecodeConfig creates a modules Config from a given Hugo configuration.
func DecodeConfig(cfg config.Provider) (Config, error) {
return decodeConfig(cfg, nil)
func DecodeConfig(logger logg.Logger, cfg config.Provider) (Config, error) {
return decodeConfig(logger, cfg, nil)
}

func decodeConfig(cfg config.Provider, pathReplacements map[string]string) (Config, error) {
func decodeConfig(logger logg.Logger, cfg config.Provider, pathReplacements map[string]string) (Config, error) {
c := DefaultModuleConfig
c.replacementsMap = pathReplacements

Expand Down Expand Up @@ -228,7 +229,7 @@ func decodeConfig(cfg config.Provider, pathReplacements map[string]string) (Conf
for i, mnt := range c.Mounts {
mnt.Source = filepath.Clean(mnt.Source)
mnt.Target = filepath.Clean(mnt.Target)
if err := mnt.init(); err != nil {
if err := mnt.init(logger); err != nil {
return c, fmt.Errorf("failed to init mount %d: %w", i, err)
}
c.Mounts[i] = mnt
Expand Down Expand Up @@ -492,11 +493,13 @@ func (m Mount) ComponentAndName() (string, string) {
return c, n
}

func (m *Mount) init() error {
func (m *Mount) init(logger logg.Logger) error {
if m.Lang != "" {
// We moved this to a more flixeble setup in Hugo 0.148.0.
m.Sites.Matrix.Languages = append(m.Sites.Matrix.Languages, m.Lang)
m.Lang = "" // TODO1 deprecate.
m.Lang = ""

hugo.DeprecateWithLogger("module.mounts.lang", "Replaced by the more powerful 'sites.matrix' setting, see https://gohugo.io/configuration/module/#mounts", "v0.153.0", logger)
}

if len(m.Sites.Matrix.Languages) == 0 {
Expand Down
17 changes: 17 additions & 0 deletions modules/config_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,20 @@ lang = 'en'
b.Assert(contentMount.Source, qt.Equals, "content")
b.Assert(contentMount.Sites.Matrix.Languages, qt.DeepEquals, []string{"en"})
}

func TestMountsLangIsDeprecated(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
[module]
[[module.mounts]]
source = 'content'
target = 'content'
lang = 'en'
-- layouts/all.html --
All.
`

b := hugolib.Test(t, files, hugolib.TestOptInfo())
b.AssertLogContains("deprecated")
}
9 changes: 5 additions & 4 deletions modules/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"

"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/version"

"github.com/gohugoio/hugo/config"
Expand Down Expand Up @@ -77,7 +78,7 @@ lang="en"
cfg, err := config.FromConfigString(fmt.Sprintf(tomlConfig, tempDir), "toml")
c.Assert(err, qt.IsNil)

mcfg, err := DecodeConfig(cfg)
mcfg, err := DecodeConfig(loggers.NewDefault().Logger(), cfg)
c.Assert(err, qt.IsNil)

v056 := version.VersionString("0.56.0")
Expand Down Expand Up @@ -119,7 +120,7 @@ path="github.com/bep/mycomponent"
cfg, err := config.FromConfigString(tomlConfig, "toml")
c.Assert(err, qt.IsNil)

mcfg, err := DecodeConfig(cfg)
mcfg, err := DecodeConfig(loggers.NewDefault().Logger(), cfg)
c.Assert(err, qt.IsNil)
c.Assert(mcfg.Replacements, qt.DeepEquals, []string{"a->b", "github.com/bep/mycomponent->c"})
c.Assert(mcfg.replacementsMap, qt.DeepEquals, map[string]string{
Expand Down Expand Up @@ -147,7 +148,7 @@ path="a"
cfg, err := config.FromConfigString(tomlConfig, "toml")
c.Assert(err, qt.IsNil)

modCfg, err := DecodeConfig(cfg)
modCfg, err := DecodeConfig(loggers.NewDefault().Logger(), cfg)
c.Assert(err, qt.IsNil)
c.Assert(len(modCfg.Imports), qt.Equals, 3)
c.Assert(modCfg.Imports[0].Path, qt.Equals, "a")
Expand All @@ -163,7 +164,7 @@ theme = ["a", "b"]
cfg, err := config.FromConfigString(tomlConfig, "toml")
c.Assert(err, qt.IsNil)

mcfg, err := DecodeConfig(cfg)
mcfg, err := DecodeConfig(loggers.NewDefault().Logger(), cfg)
c.Assert(err, qt.IsNil)

c.Assert(len(mcfg.Imports), qt.Equals, 2)
Expand Down
4 changes: 3 additions & 1 deletion resources/page/page_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,12 @@ func (p *pagePathBuilder) Path(upperOffset int) string {
}
p.b.Reset()

var hadTrailingSlash bool
for _, el := range p.els[:upper] {
if !strings.HasPrefix(el, "/") {
if !hadTrailingSlash && !strings.HasPrefix(el, "/") {
p.b.WriteByte('/')
}
hadTrailingSlash = strings.HasSuffix(el, "/")
p.b.WriteString(el)
}
return p.b.String()
Expand Down
24 changes: 12 additions & 12 deletions tpl/tplimpl/templatedescriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ type descriptorHandler struct {

// Note that this in this setup is usually a descriptor constructed from a page,
// so we want to find the best match for that page.
func (s descriptorHandler) compareDescriptors(category Category, this, other TemplateDescriptor, dimsThis, dimsOther sitesmatrix.VectorProvider) weight {
func (s descriptorHandler) compareDescriptors(category Category, this, other TemplateDescriptor, sitesMatrixThis, sitesMatrixOther sitesmatrix.VectorProvider) weight {
if this.LayoutFromUserMustMatch && this.LayoutFromUser != other.LayoutFromTemplate {
return weightNoMatch
}

w := this.doCompare(category, s.opts.DefaultContentLanguage, other, dimsThis, dimsOther)
w := this.doCompare(category, other, sitesMatrixThis, sitesMatrixOther)

if w.w1 <= 0 {
if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
Expand All @@ -91,7 +91,7 @@ func (s descriptorHandler) compareDescriptors(category Category, this, other Tem
}

//lint:ignore ST1006 this vs other makes it easier to reason about.
func (this TemplateDescriptor) doCompare(category Category, defaultContentLanguage string, other TemplateDescriptor, dimsThis, dimsOther sitesmatrix.VectorProvider) weight {
func (this TemplateDescriptor) doCompare(category Category, other TemplateDescriptor, sitesMatrixThis, sitesMatrixOther sitesmatrix.VectorProvider) weight {
w := weightNoMatch

if !this.AlwaysAllowPlainText {
Expand All @@ -113,11 +113,11 @@ func (this TemplateDescriptor) doCompare(category Category, defaultContentLangua
}
}

if dimsOther != nil {
// dimsThis is usually a Site, i.e. a single vector.
if sitesMatrixOther != nil {
// sitesMatrixThis is usually a single Site.
// But we also use this method to find all base template variants for a given template,
// and in that case we may get multiple vectors (e.g. multiple languages).
if dimsThis == nil || !dimsOther.HasAnyVector(dimsThis) {
if sitesMatrixThis == nil || !sitesMatrixOther.HasAnyVector(sitesMatrixThis) {
return w
}
}
Expand Down Expand Up @@ -164,7 +164,7 @@ func (this TemplateDescriptor) doCompare(category Category, defaultContentLangua
weightLayoutAll = 2 // the "all" layout
weightOutputFormat = 4 // a configured output format (e.g. rss, html, json)
weightMediaType = 1 // a configured media type (e.g. text/html, text/plain)
weightDims = 1 // a configured language (e.g. en, nn, fr, ...)
weightSitesMatrix = 1 // a configured language (e.g. en, nn, fr, ...)
weightVariant1 = 6 // currently used for render hooks, e.g. "link", "image"
weightVariant2 = 4 // currently used for render hooks, e.g. the language "go" in code blocks.

Expand Down Expand Up @@ -204,11 +204,11 @@ func (this TemplateDescriptor) doCompare(category Category, defaultContentLangua
w.w2 = weight2Group2
}

if dimsOther != nil {
// dimsThis is usually a Site, i.e. a single vector.
if dimsThis != nil && dimsOther.HasAnyVector(dimsThis) {
w.w1 += weightDims
if wp, ok := dimsOther.(types.WeightProvider); ok {
if sitesMatrixOther != nil {
// sitesMatrixThis is usually a single Site.
if sitesMatrixThis != nil && sitesMatrixOther.HasAnyVector(sitesMatrixThis) {
w.w1 += weightSitesMatrix
if wp, ok := sitesMatrixOther.(types.WeightProvider); ok {
w.wdim = wp.Weight()
} else {
w.wdim = weight3
Expand Down
Loading