]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
hugolib: Add hugo.Sites and .Site.IsDefault(), modify .Site.Sites
authorJoe Mooring <joe.mooring@veriphor.com>
Fri, 13 Feb 2026 23:47:18 +0000 (15:47 -0800)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 16 Feb 2026 17:57:26 +0000 (18:57 +0100)
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>
common/hugo/hugo.go
common/hugo/hugo_integration_test.go
common/hugo/hugo_test.go
hugolib/hugo_sites.go
hugolib/hugo_sites_test.go [deleted file]
hugolib/site.go
hugolib/site_sites_test.go [new file with mode: 0644]
resources/page/page_matcher_test.go
resources/page/site.go

index 810ccc01490e3147e32c721451bde63ccfe7d2fc..eec0fa1cd76b83fa9bd77144d693a47698b6a3fa 100644 (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,
        }
 }
 
index 93860019960d638a39883d9345f8dcb860ce40ba..160bea47238b5b009696d21fc50dd0453acaf925 100644 (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|")
+}
index da0d119c5febdde69d71df63c7dac912adaec0f3..c73cb598ee7bad7d3c831f7e16fef3c17b540ae6 100644 (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()
 
index 8e24308624477bfe5345fd7afa2a5fe6c3ad576c..51bd2370db309807c0b04ebf68ea5e6e89d1a469 100644 (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
diff --git a/hugolib/hugo_sites_test.go b/hugolib/hugo_sites_test.go
deleted file mode 100644 (file)
index 4bdcfa7..0000000
+++ /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|")
-}
index 31096d0face480b50fa3127290b86c590d7a6954..14b95f8e8f42017971160a14d16d88b0f1a15bc1 100644 (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
diff --git a/hugolib/site_sites_test.go b/hugolib/site_sites_test.go
new file mode 100644 (file)
index 0000000..9f18263
--- /dev/null
@@ -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")
+}
index 3dafef7b9913cf9d1632d281a4c189310cfd6b08..478a43a06282fe442716b24f1ba94d578f6e1967 100644 (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{}
 
index d5028240b5164105ce389f94179b3ee563a1444c..3fb78c4ec8470c65895a37c4af1987347c81f8de 100644 (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",
                },