hugolib: Add hugo.Sites and .Site.IsDefault(), modify .Site.Sites

Changes:

- Add hugo.Sites to return all sites for all dimensions
- Modify .Site.Sites to return all sites for all dimensions
- Deprecate .Site.Sites in favor of hugo.Sites
- Add .Site.IsDefault() to report whether the current site is the
  default site across all dimensions
- Consolidate tests

Closes #14479
Closes #14481

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Joe Mooring
2026-02-13 15:47:18 -08:00
committed by Bjørn Erik Pedersen
parent 21be4afd49
commit ab62320d6b
9 changed files with 264 additions and 110 deletions
+31 -9
View File
@@ -75,6 +75,8 @@ type HugoInfo struct {
conf ConfigProvider
deps []*Dependency
sitesProvider SitesProvider
store *hstore.Scratch
// Context gives access to some of the context scoped variables.
@@ -141,6 +143,14 @@ func (i HugoInfo) IsMultilingual() bool {
return i.conf.IsMultilingual()
}
// Sites returns all sites for all dimensions.
func (i HugoInfo) Sites() any {
if i.sitesProvider == nil {
return nil
}
return i.sitesProvider.Sites()
}
type contextKey uint8
const (
@@ -174,9 +184,20 @@ type ConfigProvider interface {
IsMultilingual() bool
}
// SitesProvider provides access to all sites.
type SitesProvider interface {
Sites() any
}
// HugoInfoOptions defines the providers required to initialize HugoInfo.
type HugoInfoOptions struct {
Conf ConfigProvider
SitesProvider SitesProvider
}
// NewInfo creates a new Hugo Info object.
func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
if conf.Environment() == "" {
func NewInfo(opts HugoInfoOptions, deps []*Dependency) HugoInfo {
if opts.Conf.Environment() == "" {
panic("environment not set")
}
var (
@@ -193,13 +214,14 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
}
return HugoInfo{
CommitHash: commitHash,
BuildDate: buildDate,
Environment: conf.Environment(),
conf: conf,
deps: deps,
store: hstore.NewScratch(),
GoVersion: goVersion,
CommitHash: commitHash,
BuildDate: buildDate,
Environment: opts.Conf.Environment(),
conf: opts.Conf,
deps: deps,
sitesProvider: opts.SitesProvider,
store: hstore.NewScratch(),
GoVersion: goVersion,
}
}
+36
View File
@@ -75,3 +75,39 @@ multihost={{ hugo.IsMultihost }}
"multihost=false",
)
}
func TestHugoSites(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
defaultContentLanguage = 'fr'
defaultContentLanguageInSubdir = true
defaultContentVersionInSubdir = true
defaultContentRoleInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.fr]
weight = 2
[languages.de]
weight = 3
[roles]
[roles.guest]
weight = 1
[roles.member]
weight = 2
[versions]
[versions.'v1.0.0']
weight = 1
[versions.'v2.0.0']
weight = 2
-- layouts/home.html --
{{ range hugo.Sites }}{{ .Language.Name }}-{{ .Role.Name }}-{{ .Version.Name }}|{{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/guest/v1.0.0/en/index.html", "en-guest-v1.0.0|en-member-v1.0.0|en-guest-v2.0.0|en-member-v2.0.0|fr-guest-v1.0.0|fr-member-v1.0.0|fr-guest-v2.0.0|fr-member-v2.0.0|de-guest-v1.0.0|de-member-v1.0.0|de-guest-v2.0.0|de-member-v2.0.0|")
}
+13 -5
View File
@@ -26,8 +26,10 @@ import (
func TestHugoInfo(t *testing.T) {
c := qt.New(t)
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
hugoInfo := NewInfo(conf, nil)
opts := HugoInfoOptions{
Conf: testConfig{environment: "production", workingDir: "/mywork", running: false},
}
hugoInfo := NewInfo(opts, nil)
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
c.Assert(fmt.Sprintf("%T", version.VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
@@ -46,7 +48,11 @@ func TestHugoInfo(t *testing.T) {
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
c.Assert(hugoInfo.IsServer(), qt.Equals, false)
devHugoInfo := NewInfo(testConfig{environment: "development", running: true}, nil)
opts = HugoInfoOptions{
Conf: testConfig{environment: "development", running: true},
}
devHugoInfo := NewInfo(opts, nil)
c.Assert(devHugoInfo.IsDevelopment(), qt.Equals, true)
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
c.Assert(devHugoInfo.IsServer(), qt.Equals, true)
@@ -73,8 +79,10 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
func TestMarkupScope(t *testing.T) {
c := qt.New(t)
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
info := NewInfo(conf, nil)
opts := HugoInfoOptions{
Conf: testConfig{environment: "production", workingDir: "/mywork", running: false},
}
info := NewInfo(opts, nil)
ctx := context.Background()
+14 -2
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"io"
"iter"
"slices"
"strings"
"sync"
"sync/atomic"
@@ -102,8 +103,9 @@ type HugoSites struct {
previousPageTreesWalkContext *doctree.WalkContext[contentNode] // Set for rebuilds only.
previousSeenTerms *hmaps.Map[term, sitesmatrix.Vectors] // Set for rebuilds only.
printUnusedTemplatesInit sync.Once
printPathWarningsInit sync.Once
printUnusedTemplatesInit sync.Once
printPathWarningsInit sync.Once
printSiteSitesDeprecationInit sync.Once
// File change events with filename stored in this map will be skipped.
skipRebuildForFilenamesMu sync.Mutex
@@ -122,6 +124,16 @@ type HugoSites struct {
buildCounter atomic.Uint64
}
// hugoSitesSitesProvider is a wrapper that implements hugo.SitesProvider.
// This avoids naming conflict with HugoSites.Sites field.
type hugoSitesSitesProvider struct {
h *HugoSites
}
func (sp hugoSitesSitesProvider) Sites() any {
return slices.Collect(sp.h.allSites(nil))
}
type progressReporter struct {
mu sync.Mutex
t time.Time
-84
View File
@@ -1,84 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import "testing"
func TestSitesAndLanguageOrder(t *testing.T) {
files := `
-- hugo.toml --
defaultContentLanguage = "fr"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.fr]
weight = 2
[languages.de]
weight = 3
-- layouts/home.html --
{{ $bundle := site.GetPage "bundle" }}
Bundle all translations: {{ range $bundle.AllTranslations }}{{ .Lang }}|{{ end }}$
Bundle translations: {{ range $bundle.Translations }}{{ .Lang }}|{{ end }}$
Site languages: {{ range site.Languages }}{{ .Lang }}|{{ end }}$
Sites: {{ range site.Sites }}{{ .Language.Lang }}|{{ end }}$
-- content/bundle/index.fr.md --
---
title: "Bundle Fr"
---
-- content/bundle/index.en.md --
---
title: "Bundle En"
---
-- content/bundle/index.de.md --
---
title: "Bundle De"
---
`
b := Test(t, files)
b.AssertFileContent("public/en/index.html",
"Bundle all translations: en|fr|de|$",
"Bundle translations: fr|de|$",
"Site languages: en|fr|de|$",
"Sites: en|fr|de|$",
)
}
func TestSitesOrder(t *testing.T) {
files := `
-- hugo.toml --
defaultContentLanguage = "fr"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.fr]
weight = 2
[languages.de]
weight = 3
[roles]
[roles.guest]
weight = 1
[roles.member]
weight = 2
-- layouts/home.html --
Sites: {{ range site.Sites }}{{ .Language.Lang }}|{{ end }}$
Languages: {{ range site.Languages }}{{ .Lang }}|{{ end }}
`
b := Test(t, files)
// Note that before v0.152.0 the order of these slices where different .Sites pulled the defaultContentLanguage first.
b.AssertFileContent("public/en/index.html", "Sites: en|fr|de|$", "Languages: en|fr|de|")
}
+25 -5
View File
@@ -517,7 +517,14 @@ func newHugoSites(
dependencies = append(dependencies, depFromMod(m))
}
h.hugoInfo = hugo.NewInfo(h.Configs.GetFirstLanguageConfig(), dependencies)
// Create a sites provider that avoids naming conflict with HugoSites.Sites field.
sp := hugoSitesSitesProvider{h: h}
opts := hugo.HugoInfoOptions{
Conf: h.Configs.GetFirstLanguageConfig(),
SitesProvider: sp,
}
h.hugoInfo = hugo.NewInfo(opts, dependencies)
var prototype *deps.Deps
@@ -627,15 +634,28 @@ func (s *Site) LanguageCode() string {
return s.Language().LanguageCode()
}
// Returns all Sites for all languages.
// Returns all sites for all dimensions.
// Deprecated: Use hugo.Sites instead.
func (s *Site) Sites() page.Sites {
sites := make(page.Sites, len(s.h.Sites))
for i, s := range s.h.Sites {
sites[i] = s.Site()
s.h.printSiteSitesDeprecationInit.Do(func() {
hugo.Deprecate(".Site.Sites", "Use hugo.Sites instead.", "v0.156.0")
})
var sites page.Sites
for _, v1 := range s.h.sitesVersionsRoles {
for _, v2 := range v1 {
for _, s := range v2 {
sites = append(sites, s.Site())
}
}
}
return sites
}
// IsDefault reports whether this site is the default across all dimensions.
func (s *Site) IsDefault() bool {
return s.siteLanguageVersionRole.isDefault()
}
// Returns Site currently rendering.
func (s *Site) Current() page.Site {
return s.h.currentSite
+117
View File
@@ -0,0 +1,117 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import "testing"
func TestSiteIsDefault(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ['rss','sitemap','taxonomy','term']
defaultContentLanguage = 'fr'
defaultContentLanguageInSubdir = true
defaultContentVersionInSubdir = true
defaultContentRoleInSubdir = true
[languages]
[languages.en]
weight = 1
title = 'English'
[languages.fr]
weight = 2
title = 'French'
[languages.de]
weight = 3
title = 'German'
[roles]
[roles.guest]
weight = 1
[roles.member]
weight = 2
[versions]
[versions.'v1.0.0']
weight = 1
[versions.'v2.0.0']
weight = 2
-- content/p1.en.md --
---
title: Page 1 EN
---
-- content/p1.fr.md --
---
title: Page 1 FR
---
-- content/p1.de.md --
---
title: Page 1 DE
---
-- layouts/_default/single.html --
Current site is default: {{ .Site.IsDefault }}
{{ range hugo.Sites }}
{{ .Language.Name }}-{{ .Role.Name }}-{{ .Version.Name }}: IsDefault={{ .IsDefault }}
{{ end }}
`
b := Test(t, files)
b.AssertFileContent("public/guest/v1.0.0/en/p1/index.html",
"Current site is default: false",
"en-guest-v1.0.0: IsDefault=false",
"en-member-v1.0.0: IsDefault=false",
"fr-guest-v1.0.0: IsDefault=true",
"fr-member-v1.0.0: IsDefault=false",
"de-guest-v1.0.0: IsDefault=false",
"de-member-v1.0.0: IsDefault=false",
)
b.AssertFileContent("public/guest/v1.0.0/fr/p1/index.html",
"Current site is default: true",
"fr-guest-v1.0.0: IsDefault=true",
)
b.AssertFileContent("public/guest/v1.0.0/de/p1/index.html",
"Current site is default: false",
"fr-guest-v1.0.0: IsDefault=true",
)
}
func TestSiteSites(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
defaultContentLanguage = 'fr'
defaultContentLanguageInSubdir = true
defaultContentVersionInSubdir = true
defaultContentRoleInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.fr]
weight = 2
[languages.de]
weight = 3
[roles]
[roles.guest]
weight = 1
[roles.member]
weight = 2
[versions]
[versions.'v1.0.0']
weight = 1
[versions.'v2.0.0']
weight = 2
-- layouts/home.html --
{{ range .Site.Sites }}{{ .Language.Name }}-{{ .Role.Name }}-{{ .Version.Name }}|{{ end }}
`
b := Test(t, files, TestOptInfo())
b.AssertFileContent("public/guest/v1.0.0/fr/index.html", "en-guest-v1.0.0|en-member-v1.0.0|en-guest-v2.0.0|en-member-v2.0.0|fr-guest-v1.0.0|fr-member-v1.0.0|fr-guest-v2.0.0|fr-member-v2.0.0|de-guest-v1.0.0|de-member-v1.0.0|de-guest-v2.0.0|de-member-v2.0.0|")
b.AssertLogContains(".Site.Sites was deprecated")
}
+10 -2
View File
@@ -27,8 +27,16 @@ import (
func TestPageMatcher(t *testing.T) {
c := qt.New(t)
developmentTestSite := testSite{h: hugo.NewInfo(testConfig{environment: "development"}, nil)}
productionTestSite := testSite{h: hugo.NewInfo(testConfig{environment: "production"}, nil)}
opts := hugo.HugoInfoOptions{
Conf: testConfig{environment: "development"},
}
developmentTestSite := testSite{h: hugo.NewInfo(opts, nil)}
opts = hugo.HugoInfoOptions{
Conf: testConfig{environment: "production"},
}
productionTestSite := testSite{h: hugo.NewInfo(opts, nil)}
dec := cascadeConfigDecoder{}
+18 -3
View File
@@ -77,12 +77,15 @@ type Site interface {
// Returns the configured copyright information for this Site.
Copyright() string
// Returns all Sites for all languages.
// Returns all sites for all dimensions.
Sites() Sites
// Returns Site currently rendering.
Current() Site
// Reports whether this site is the default across all dimensions.
IsDefault() bool
// Returns a struct with some information about the build.
Hugo() hugo.HugoInfo
@@ -120,8 +123,9 @@ type Site interface {
LanguagePrefix() string
hstore.StoreProvider
// String returns a string representation of the site.
// Note that this represenetation may change in the future.
// Note that this representation may change in the future.
String() string
// For internal use only.
@@ -239,6 +243,10 @@ func (s *siteWrapper) Current() Site {
return s.s.Current()
}
func (s *siteWrapper) IsDefault() bool {
return s.s.IsDefault()
}
func (s *siteWrapper) Config() SiteConfig {
return s.s.Config()
}
@@ -350,6 +358,10 @@ func (t testSite) Current() Site {
return t
}
func (t testSite) IsDefault() bool {
return true
}
func (s testSite) LanguagePrefix() string {
return ""
}
@@ -439,8 +451,11 @@ func (s testSite) CheckReady() {
// NewDummyHugoSite creates a new minimal test site.
func NewDummyHugoSite(conf config.AllProvider) Site {
opts := hugo.HugoInfoOptions{
Conf: conf,
}
return testSite{
h: hugo.NewInfo(conf, nil),
h: hugo.NewInfo(opts, nil),
l: &langs.Language{
Lang: "en",
},