From: Joe Mooring Date: Fri, 13 Feb 2026 23:47:18 +0000 (-0800) Subject: hugolib: Add hugo.Sites and .Site.IsDefault(), modify .Site.Sites X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=ab62320d6bceece0faa7029f8bd79d546d0f64be;p=brevno-suite%2Fhugo 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 --- diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go index 810ccc014..eec0fa1cd 100644 --- a/common/hugo/hugo.go +++ b/common/hugo/hugo.go @@ -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, } } diff --git a/common/hugo/hugo_integration_test.go b/common/hugo/hugo_integration_test.go index 938600199..160bea472 100644 --- a/common/hugo/hugo_integration_test.go +++ b/common/hugo/hugo_integration_test.go @@ -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|") +} diff --git a/common/hugo/hugo_test.go b/common/hugo/hugo_test.go index da0d119c5..c73cb598e 100644 --- a/common/hugo/hugo_test.go +++ b/common/hugo/hugo_test.go @@ -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() diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 8e2430862..51bd2370d 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -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 diff --git a/hugolib/hugo_sites_test.go b/hugolib/hugo_sites_test.go deleted file mode 100644 index 4bdcfa7ee..000000000 --- a/hugolib/hugo_sites_test.go +++ /dev/null @@ -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|") -} diff --git a/hugolib/site.go b/hugolib/site.go index 31096d0fa..14b95f8e8 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -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 diff --git a/hugolib/site_sites_test.go b/hugolib/site_sites_test.go new file mode 100644 index 000000000..9f1826319 --- /dev/null +++ b/hugolib/site_sites_test.go @@ -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") +} diff --git a/resources/page/page_matcher_test.go b/resources/page/page_matcher_test.go index 3dafef7b9..478a43a06 100644 --- a/resources/page/page_matcher_test.go +++ b/resources/page/page_matcher_test.go @@ -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{} diff --git a/resources/page/site.go b/resources/page/site.go index d5028240b..3fb78c4ec 100644 --- a/resources/page/site.go +++ b/resources/page/site.go @@ -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", },