Files
hugo-mirror/hugolib/content_map_page_assembler.go
Bjørn Erik Pedersen 555dfa207a Speedup and simplify page assembly for deeper content trees
This commit moves to a forked version go-radix (fork source has not had any updates in 3 years.), whith 2 notable changes:

* It's generic (using Go generics) and thus removes a lot of type conversions/assertions.
* It allows nodes to be replaced during walk, which allows to partition the tree for parallel processing without worrying about locking.

For this repo, this means:

* The assembly step now processes nested sections in parallel, which gives a speedup for deep content trees with a slight allocation penalty (see benchmarks below).
* Nodes that needs to be reinserted are inserted directly.
* Also, there are some drive-by fixes of some allocation issues, e.g. avoid wrapping mutexes in returned anonomous functions, a common source of hidden allocations.

```
                                                                                   │ master.bench │           perf-p3.bench            │
                                                                                   │    sec/op    │   sec/op     vs base               │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10     6.958m ± 3%   7.015m ± 3%        ~ (p=0.589 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10    14.25m ± 1%   14.56m ± 8%        ~ (p=0.394 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10    48.07m ± 3%   49.23m ± 3%        ~ (p=0.394 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10    66.66m ± 4%   66.47m ± 6%        ~ (p=0.485 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10    59.57m ± 4%   50.73m ± 5%  -14.85% (p=0.002 n=6)
geomean                                                                               28.54m        27.92m        -2.18%

                                                                                   │ master.bench │           perf-p3.bench            │
                                                                                   │     B/op     │     B/op      vs base              │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10    4.513Mi ± 0%   4.527Mi ± 0%  +0.33% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10   15.35Mi ± 0%   15.49Mi ± 0%  +0.94% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10   62.50Mi ± 0%   63.19Mi ± 0%  +1.10% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10   86.78Mi ± 0%   87.73Mi ± 0%  +1.09% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10   62.96Mi ± 0%   63.66Mi ± 0%  +1.12% (p=0.002 n=6)
geomean                                                                              29.84Mi        30.11Mi       +0.92%

                                                                                   │ master.bench │           perf-p3.bench           │
                                                                                   │  allocs/op   │  allocs/op   vs base              │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10     60.44k ± 0%   60.97k ± 0%  +0.87% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10    205.8k ± 0%   211.4k ± 0%  +2.70% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10    831.1k ± 0%   858.3k ± 0%  +3.27% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10    1.157M ± 0%   1.197M ± 0%  +3.41% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10    839.9k ± 0%   867.8k ± 0%  +3.31% (p=0.002 n=6)
geomean                                                                               398.5k        409.3k       +2.71%
```
2025-11-25 11:37:05 +01:00

1411 lines
37 KiB
Go

// Copyright 2025 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 (
"cmp"
"context"
"fmt"
"path"
"strings"
"time"
"github.com/gohugoio/go-radix"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/spf13/cast"
"golang.org/x/sync/errgroup"
)
// Handles the flow of creating all the pages and resources for all sites.
// This includes applying any cascade configuration passed down the tree.
type allPagesAssembler struct {
// Dependencies.
ctx context.Context
h *HugoSites
m *pageMap
g errgroup.Group
assembleChanges *WhatChanged
assembleSectionsInParallel bool
pwRoot *doctree.NodeShiftTreeWalker[contentNode] // walks pages.
rwRoot *doctree.NodeShiftTreeWalker[contentNode] // walks resources.
// Walking state.
seenTerms *maps.Map[term, sitesmatrix.Vectors]
droppedPages *maps.Map[*Site, []string] // e.g. drafts, expired, future.
seenRootSections *maps.Map[string, bool]
seenHome bool // set before we fan out to multiple goroutines.
}
func newAllPagesAssembler(
ctx context.Context,
h *HugoSites,
m *pageMap,
assembleChanges *WhatChanged,
) *allPagesAssembler {
rw := &doctree.NodeShiftTreeWalker[contentNode]{
Tree: m.treeResources,
LockType: doctree.LockTypeNone,
NoShift: true,
WalkContext: &doctree.WalkContext[contentNode]{},
}
pw := rw.Extend()
pw.Tree = m.treePages
seenRootSections := maps.NewMap[string, bool]()
seenRootSections.Set("", true) // home.
return &allPagesAssembler{
ctx: ctx,
h: h,
m: m,
assembleChanges: assembleChanges,
seenTerms: maps.NewMap[term, sitesmatrix.Vectors](),
droppedPages: maps.NewMap[*Site, []string](),
seenRootSections: seenRootSections,
assembleSectionsInParallel: true,
pwRoot: pw,
rwRoot: rw,
}
}
type sitePagesAssembler struct {
s *Site
assembleChanges *WhatChanged
a *allPagesAssembler
ctx context.Context
}
func (a *allPagesAssembler) createAllPages() error {
defer func() {
for site, dropped := range a.droppedPages.All() {
for _, s := range dropped {
site.pageMap.treePages.Delete(s)
site.pageMap.resourceTrees.DeletePrefix(paths.AddTrailingSlash(s))
}
}
}()
if a.h.Conf.Watching() {
defer func() {
if a.h.isRebuild() && a.h.previousSeenTerms != nil {
// Find removed terms.
for t := range a.h.previousSeenTerms.All() {
if _, found := a.seenTerms.Lookup(t); !found {
// This term has been removed.
a.pwRoot.Tree.Delete(t.view.pluralTreeKey)
a.rwRoot.Tree.DeletePrefix(t.view.pluralTreeKey + "/")
}
}
// Find new terms.
for t := range a.seenTerms.All() {
if _, found := a.h.previousSeenTerms.Lookup(t); !found {
// This term is new.
if n, ok := a.pwRoot.Tree.GetRaw(t.view.pluralTreeKey); ok {
a.assembleChanges.Add(cnh.GetIdentity(n))
}
}
}
}
a.h.previousSeenTerms = a.seenTerms
}()
}
if err := cmp.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil {
return err
}
if err := a.pwRoot.WalkContext.HandleEventsAndHooks(); err != nil {
return err
}
return nil
}
func (a *allPagesAssembler) doCreatePages(prefix string, depth int) error {
var (
sites = a.h.sitesVersionsRolesMap
h = a.h
treePages = a.m.treePages
getViews = func(vec sitesmatrix.Vector) []viewName {
return h.languageSiteForSiteVector(vec).pageMap.cfg.taxonomyConfig.views
}
pw *doctree.NodeShiftTreeWalker[contentNode] // walks pages.
rw *doctree.NodeShiftTreeWalker[contentNode] // walks resources.
isRootWalk = depth == 0
)
if isRootWalk {
pw = a.pwRoot
rw = a.rwRoot
} else {
// Sub-walkers for a specific prefix.
pw = a.pwRoot.Extend()
pw.Prefix = prefix + "/"
rw = a.rwRoot.Extend() // rw will get its prefix(es) set later.
}
resourceOwnerInfo := struct {
n contentNode
s string
}{}
newHomePageMetaSource := func() *pageMetaSource {
pi := a.h.Conf.PathParser().Parse(files.ComponentFolderContent, "/_index.md")
return &pageMetaSource{
pathInfo: pi,
sitesMatrixBase: a.h.Conf.AllSitesMatrix(),
sitesMatrixBaseOnly: true,
pageConfigSource: &pagemeta.PageConfigEarly{
Kind: kinds.KindHome,
},
}
}
if isRootWalk {
if err := a.createMissingPages(); err != nil {
return err
}
if treePages.Len() == 0 {
// No pages, insert a home page to get something to walk on.
p := newHomePageMetaSource()
treePages.InsertRaw(p.pathInfo.Base(), p)
}
}
getCascades := func(wctx *doctree.WalkContext[contentNode], s string) *page.PageMatcherParamsConfigs {
if wctx == nil {
panic("nil walk context")
}
var cascades *page.PageMatcherParamsConfigs
data := wctx.Data()
if s != "" {
_, data := data.LongestPrefix(s)
if data != nil {
cascades = data.(*page.PageMatcherParamsConfigs)
}
}
if cascades == nil {
for s := range a.h.allSiteLanguages(nil) {
cascades = cascades.Append(s.conf.Cascade)
}
}
return cascades
}
doTransformPages := func(s string, n contentNode, cascades *page.PageMatcherParamsConfigs) (n2 contentNode, ns doctree.NodeTransformState, err error) {
defer func() {
cascadesLen := cascades.Len()
if n2 == nil {
if ns < doctree.NodeTransformStateSkip {
ns = doctree.NodeTransformStateSkip
}
} else {
n2.forEeachContentNode(
func(vec sitesmatrix.Vector, nn contentNode) bool {
if pms, ok := nn.(contentNodeCascadeProvider); ok {
cascades = cascades.Prepend(pms.getCascade())
}
return true
},
)
}
if s == "" || cascades.Len() > cascadesLen {
// New cascade values added, pass them downwards.
rw.WalkContext.Data().Insert(paths.AddTrailingSlash(s), cascades)
}
}()
var handlePageMetaSource func(v contentNode, n contentNodesMap, replaceVector bool) (n2 contentNode, err error)
handlePageMetaSource = func(v contentNode, n contentNodesMap, replaceVector bool) (n2 contentNode, err error) {
if n != nil {
n2 = n
}
switch ms := v.(type) {
case *pageMetaSource:
if err := ms.initEarly(a.h, cascades); err != nil {
return nil, err
}
sitesMatrix := ms.pageConfigSource.SitesMatrix
sitesMatrix.ForEachVector(func(vec sitesmatrix.Vector) bool {
site, found := sites[vec]
if !found {
panic(fmt.Sprintf("site not found for %v", vec))
}
var p *pageState
p, err = site.newPageFromPageMetasource(ms, cascades)
if err != nil {
return false
}
var drop bool
if !site.shouldBuild(p) {
switch p.Kind() {
case kinds.KindHome, kinds.KindSection, kinds.KindTaxonomy:
// We need to keep these for the structure, but disable
// them so they don't get listed/rendered.
(&p.m.pageConfig.Build).Disable()
default:
// Skip this page.
a.droppedPages.WithWriteLock(
func(m map[*Site][]string) {
m[site] = append(m[site], s)
},
)
drop = true
}
}
if !drop && n == nil {
if n2 == nil {
// Avoid creating a map for one node.
n2 = p
} else {
// Upgrade to a map.
n = make(contentNodesMap)
ps := n2.(*pageState)
n[ps.s.siteVector] = ps
n2 = n
}
}
if n == nil {
return true
}
pp, found := n[vec]
var w1, w2 int
if wp, ok := pp.(contentNodeContentWeightProvider); ok {
w1 = wp.contentWeight()
}
w2 = p.contentWeight()
if found && !replaceVector && w1 > w2 {
return true
}
n[vec] = p
return true
})
case *pageState:
if n == nil {
n2 = ms
return
}
n[ms.s.siteVector] = ms
case contentNodesMap:
for _, vv := range ms {
var err error
n2, err = handlePageMetaSource(vv, n, replaceVector)
if err != nil {
return nil, err
}
if m, ok := n2.(contentNodesMap); ok {
n = m
}
}
default:
panic(fmt.Sprintf("unexpected type %T", v))
}
return
}
// The common case.
ns = doctree.NodeTransformStateReplaced
handleContentNodeSeq := func(v contentNodeSeq) (contentNode, doctree.NodeTransformState, error) {
is := make(contentNodesMap)
for ms := range v {
_, err := handlePageMetaSource(ms, is, false)
if err != nil {
return nil, 0, fmt.Errorf("failed to create page from pageMetaSource %s: %w", s, err)
}
}
return is, ns, nil
}
switch v := n.(type) {
case contentNodeSeq, contentNodes:
return handleContentNodeSeq(contentNodeToSeq(v))
case *pageMetaSource:
n2, err = handlePageMetaSource(v, nil, false)
if err != nil {
return nil, 0, fmt.Errorf("failed to create page from pageMetaSource %s: %w", s, err)
}
return
case *pageState:
// Nothing to do.
ns = doctree.NodeTransformStateNone
return v, ns, nil
case contentNodesMap:
ns = doctree.NodeTransformStateNone
for _, vv := range v {
switch m := vv.(type) {
case *pageMetaSource:
ns = doctree.NodeTransformStateUpdated
_, err := handlePageMetaSource(m, v, true)
if err != nil {
return nil, 0, fmt.Errorf("failed to create page from pageMetaSource %s: %w", s, err)
}
default:
// Nothing to do.
}
}
return v, ns, nil
default:
panic(fmt.Sprintf("unexpected type %T", n))
}
}
var unpackPageMetaSources func(n contentNode) contentNode
unpackPageMetaSources = func(n contentNode) contentNode {
switch nn := n.(type) {
case *pageState:
return nn.m.pageMetaSource
case contentNodesMap:
if len(nn) == 0 {
return nil
}
var iter contentNodeSeq = func(yield func(contentNode) bool) {
seen := map[*pageMetaSource]struct{}{}
for _, v := range nn {
vv := unpackPageMetaSources(v)
pms := vv.(*pageMetaSource)
if _, found := seen[pms]; !found {
if !yield(pms) {
return
}
seen[pms] = struct{}{}
}
}
}
return iter
case contentNodes:
if len(nn) == 0 {
return nil
}
var iter contentNodeSeq = func(yield func(contentNode) bool) {
seen := map[*pageMetaSource]struct{}{}
for _, v := range nn {
vv := unpackPageMetaSources(v)
pms := vv.(*pageMetaSource)
if _, found := seen[pms]; !found {
if !yield(pms) {
return
}
seen[pms] = struct{}{}
}
}
}
return iter
case *pageMetaSource:
return nn
default:
panic(fmt.Sprintf("unexpected type %T", n))
}
}
transformPages := func(s string, n contentNode, cascades *page.PageMatcherParamsConfigs) (n2 contentNode, ns doctree.NodeTransformState, err error) {
if a.h.isRebuild() {
cascadesPrevious := getCascades(h.previousPageTreesWalkContext, s)
h1, h2 := cascadesPrevious.SourceHash(), cascades.SourceHash()
if h1 != h2 {
// Force rebuild from the source.
n = unpackPageMetaSources(n)
}
}
if n == nil {
panic("nil node")
}
n2, ns, err = doTransformPages(s, n, cascades)
return
}
transformPagesAndCreateMissingHome := func(s string, n contentNode, isResource bool, cascades *page.PageMatcherParamsConfigs) (n2 contentNode, ns doctree.NodeTransformState, err error) {
if n == nil {
panic("nil node " + s)
}
level := strings.Count(s, "/")
if s == "" {
a.seenHome = true
}
if !isResource && s != "" && !a.seenHome {
a.seenHome = true
var homePages contentNode
homePages, ns, err = transformPages("", newHomePageMetaSource(), cascades)
if err != nil {
return
}
treePages.InsertRaw("", homePages)
}
n2, ns, err = transformPages(s, n, cascades)
if err != nil || ns >= doctree.NodeTransformStateSkip {
return
}
if n2 == nil {
ns = doctree.NodeTransformStateSkip
return
}
if isResource {
// Done.
return
}
isTaxonomy := !a.h.getFirstTaxonomyConfig(s).IsZero()
isRootSection := !isTaxonomy && level == 1 && cnh.isBranchNode(n)
if isRootSection {
// This is a root section.
a.seenRootSections.SetIfAbsent(cnh.PathInfo(n).Section(), true)
} else if !isTaxonomy {
p := cnh.PathInfo(n)
rootSection := p.Section()
_, err := a.seenRootSections.GetOrCreate(rootSection, func() (bool, error) {
// Try to preserve the original casing if possible.
sectionUnnormalized := p.Unnormalized().Section()
rootSectionPath := a.h.Conf.PathParser().Parse(files.ComponentFolderContent, "/"+sectionUnnormalized+"/_index.md")
var rootSectionPages contentNode
rootSectionPages, _, err = transformPages(rootSectionPath.Base(), &pageMetaSource{
pathInfo: rootSectionPath,
sitesMatrixBase: n2.(contentNodeForSites).siteVectors(),
pageConfigSource: &pagemeta.PageConfigEarly{
Kind: kinds.KindSection,
},
}, cascades)
if err != nil {
return true, err
}
treePages.InsertRaw(rootSectionPath.Base(), rootSectionPages)
return true, nil
})
if err != nil {
return nil, 0, err
}
}
const eventNameSitesMatrix = "sitesmatrix"
if s == "" || isRootSection {
// Every page needs a home and a root section (.FirstSection).
// We don't know yet what language, version, role combination that will
// be created below, so collect that information and create the missing pages
// on demand.
nm, replaced := contentNodeToContentNodesPage(n2)
missingVectorsForHomeOrRootSection := sitesmatrix.Vectors{}
if s == "" {
// We need a complete set of home pages.
a.h.Conf.AllSitesMatrix().ForEachVector(func(vec sitesmatrix.Vector) bool {
if _, found := nm[vec]; !found {
missingVectorsForHomeOrRootSection[vec] = struct{}{}
}
return true
})
} else {
pw.WalkContext.AddEventListener(eventNameSitesMatrix, s,
func(e *doctree.Event[contentNode]) {
n := e.Source
e.StopPropagation()
n.forEeachContentNode(
func(vec sitesmatrix.Vector, nn contentNode) bool {
if _, found := nm[vec]; !found {
missingVectorsForHomeOrRootSection[vec] = struct{}{}
}
return true
})
},
)
}
// We need to wait until after the walk to have a complete set.
pw.WalkContext.HooksPost().Push(
func() error {
if i := len(missingVectorsForHomeOrRootSection); i > 0 {
// Pick one, the rest will be created later.
vec := missingVectorsForHomeOrRootSection.VectorSample()
kind := kinds.KindSection
if s == "" {
kind = kinds.KindHome
}
pms := &pageMetaSource{
pathInfo: cnh.PathInfo(n),
sitesMatrixBase: missingVectorsForHomeOrRootSection,
sitesMatrixBaseOnly: true,
pageConfigSource: &pagemeta.PageConfigEarly{
Kind: kind,
},
}
nm[vec] = pms
_, ns, err := transformPages(s, nm, cascades)
if err != nil {
return err
}
if ns == doctree.NodeTransformStateReplaced {
// Should not happen.
panic(fmt.Sprintf("expected no replacement for %q", s))
}
if replaced {
pw.Tree.InsertRaw(s, nm)
}
}
return nil
},
)
}
if s != "" {
rw.WalkContext.SendEvent(&doctree.Event[contentNode]{Source: n2, Path: s, Name: eventNameSitesMatrix})
}
return
}
transformPagesAndCreateMissingStructuralNodes := func(s string, n contentNode, isResource bool) (n2 contentNode, ns doctree.NodeTransformState, err error) {
cascades := getCascades(pw.WalkContext, s)
n2, ns, err = transformPagesAndCreateMissingHome(s, n, isResource, cascades)
if err != nil || ns >= doctree.NodeTransformStateSkip {
return
}
n2.forEeachContentNode(
func(vec sitesmatrix.Vector, nn contentNode) bool {
if ps, ok := nn.(*pageState); ok {
if ps.m.noLink() {
return true
}
for _, viewName := range getViews(vec) {
vals := types.ToStringSlicePreserveString(getParam(ps, viewName.plural, false))
if vals == nil {
continue
}
for _, v := range vals {
if v == "" {
continue
}
t := term{view: viewName, term: v}
a.seenTerms.WithWriteLock(func(m map[term]sitesmatrix.Vectors) {
vectors, found := m[t]
if !found {
m[t] = sitesmatrix.Vectors{
vec: struct{}{},
}
return
}
vectors[vec] = struct{}{}
})
}
}
}
return true
},
)
return
}
shouldSkipOrTerminate := func(s string) (ns doctree.NodeTransformState) {
owner := resourceOwnerInfo.n
if owner == nil {
return doctree.NodeTransformStateTerminate
}
if !cnh.isBranchNode(owner) {
return
}
// A resourceKey always represents a filename with extension.
// A page key points to the logical path of a page, which when sourced from the filesystem
// may represent a directory (bundles) or a single content file (e.g. p1.md).
// So, to avoid any overlapping ambiguity, we start looking from the owning directory.
for {
s = path.Dir(s)
ownerKey, found := treePages.LongestPrefixRaw(s)
if !found {
return doctree.NodeTransformStateTerminate
}
if ownerKey == resourceOwnerInfo.s {
break
}
if s != ownerKey && strings.HasPrefix(s, ownerKey) {
// Keep looking
continue
}
// Stop walking downwards, someone else owns this resource.
rw.SkipPrefix(ownerKey + "/")
return doctree.NodeTransformStateSkip
}
return
}
forEeachResourceOwnerPage := func(fn func(p *pageState) bool) bool {
switch nn := resourceOwnerInfo.n.(type) {
case *pageState:
return fn(nn)
case contentNodesMap:
for _, p := range nn {
if !fn(p.(*pageState)) {
return false
}
}
default:
panic(fmt.Sprintf("unknown type %T", nn))
}
return true
}
rw.Transform = func(s string, n contentNode) (n2 contentNode, ns doctree.NodeTransformState, err error) {
if ns = shouldSkipOrTerminate(s); ns >= doctree.NodeTransformStateSkip {
return
}
if cnh.isPageNode(n) {
return transformPagesAndCreateMissingStructuralNodes(s, n, true)
}
nodes := make(contentNodesMap)
ns = doctree.NodeTransformStateReplaced
n2 = nodes
forEeachResourceOwnerPage(
func(p *pageState) bool {
duplicateResourceFiles := a.h.Cfg.IsMultihost()
if !duplicateResourceFiles && p.m.pageConfigSource.ContentMediaType.IsMarkdown() {
duplicateResourceFiles = p.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
}
if _, found := nodes[p.s.siteVector]; !found {
var rs *resourceSource
match := cnh.findContentNodeForSiteVector(p.s.siteVector, duplicateResourceFiles, contentNodeToSeq(n))
if match == nil {
return true
}
rs = match.(*resourceSource)
if rs != nil {
if rs.state == resourceStateNew {
nodes[p.s.siteVector] = rs.assignSiteVector(p.s.siteVector)
} else {
nodes[p.s.siteVector] = rs.clone().assignSiteVector(p.s.siteVector)
}
}
}
return true
},
)
return
}
// Create missing term pages.
pw.WalkContext.HooksPost().Push(
func() error {
for k, v := range a.seenTerms.All() {
viewTermKey := "/" + k.view.plural + "/" + k.term
pi := a.h.Conf.PathParser().Parse(files.ComponentFolderContent, viewTermKey+"/_index.md")
termKey := pi.Base()
n, found := pw.Tree.GetRaw(termKey)
if found {
// Merge.
n.forEeachContentNode(
func(vec sitesmatrix.Vector, nn contentNode) bool {
delete(v, vec)
return true
},
)
}
if len(v) > 0 {
p := &pageMetaSource{
pathInfo: pi,
sitesMatrixBase: v,
pageConfigSource: &pagemeta.PageConfigEarly{
Kind: kinds.KindTerm,
},
}
var n2 contentNode = p
if found {
n2 = contentNodes{n, p}
}
n2, ns, err := transformPages(termKey, n2, getCascades(pw.WalkContext, termKey))
if err != nil {
return fmt.Errorf("failed to create term page %q: %w", termKey, err)
}
switch ns {
case doctree.NodeTransformStateReplaced:
pw.Tree.InsertRaw(termKey, n2)
}
}
}
return nil
},
)
pw.Transform = func(s string, n contentNode) (n2 contentNode, ns doctree.NodeTransformState, err error) {
n2, ns, err = transformPagesAndCreateMissingStructuralNodes(s, n, false)
if err != nil || ns >= doctree.NodeTransformStateSkip {
return
}
if iep, ok := n2.(contentNodeIsEmptyProvider); ok && iep.isEmpty() {
ns = doctree.NodeTransformStateDeleted
}
if ns == doctree.NodeTransformStateDeleted {
return
}
// Walk nested resources.
resourceOwnerInfo.s = s
resourceOwnerInfo.n = n2
rw = rw.WithPrefix(s + "/")
if err := rw.Walk(a.ctx); err != nil {
return nil, 0, err
}
const maxDepth = 3
if a.assembleSectionsInParallel && s != "" && depth < maxDepth && cnh.isBranchNode(n) && a.h.getFirstTaxonomyConfig(s).IsZero() {
// Handle this branch in its own goroutine.
pw.SkipPrefix(s + "/")
a.g.Go(func() error {
return a.doCreatePages(s, depth+1)
})
}
return
}
pw.Handle = nil
if err := pw.Walk(a.ctx); err != nil {
return err
}
return nil
}
// Calculate and apply aggregate values to the page tree (e.g. dates).
func (sa *sitePagesAssembler) applyAggregates() error {
sectionPageCount := map[string]int{}
pw := &doctree.NodeShiftTreeWalker[contentNode]{
Tree: sa.s.pageMap.treePages,
LockType: doctree.LockTypeRead,
WalkContext: &doctree.WalkContext[contentNode]{},
}
rw := pw.Extend()
rw.Tree = sa.s.pageMap.treeResources
sa.s.lastmod = time.Time{}
rebuild := sa.s.h.isRebuild()
pw.Handle = func(keyPage string, n contentNode) (radix.WalkFlag, error) {
pageBundle := n.(*pageState)
if pageBundle.Kind() == kinds.KindTerm {
// Delay this until they're created.
return radix.WalkContinue, nil
}
if pageBundle.IsPage() {
rootSection := pageBundle.Section()
sectionPageCount[rootSection]++
}
if rebuild {
if pageBundle.IsHome() || pageBundle.IsSection() {
oldDates := pageBundle.m.pageConfig.Dates
// We need to wait until after the walk to determine if any of the dates have changed.
pw.WalkContext.HooksPost().Push(
func() error {
if oldDates != pageBundle.m.pageConfig.Dates {
sa.assembleChanges.Add(pageBundle)
}
return nil
},
)
}
}
const eventName = "dates"
if cnh.isBranchNode(n) {
wasZeroDates := pageBundle.m.pageConfig.Dates.IsAllDatesZero()
if wasZeroDates || pageBundle.IsHome() {
pw.WalkContext.AddEventListener(eventName, keyPage, func(e *doctree.Event[contentNode]) {
sp, ok := e.Source.(*pageState)
if !ok {
return
}
if wasZeroDates {
pageBundle.m.pageConfig.Dates.UpdateDateAndLastmodAndPublishDateIfAfter(sp.m.pageConfig.Dates)
}
if pageBundle.IsHome() {
if pageBundle.m.pageConfig.Dates.Lastmod.After(pageBundle.s.lastmod) {
pageBundle.s.lastmod = pageBundle.m.pageConfig.Dates.Lastmod
}
if sp.m.pageConfig.Dates.Lastmod.After(pageBundle.s.lastmod) {
pageBundle.s.lastmod = sp.m.pageConfig.Dates.Lastmod
}
}
})
}
}
// Send the date info up the tree.
pw.WalkContext.SendEvent(&doctree.Event[contentNode]{Source: n, Path: keyPage, Name: eventName})
isBranch := cnh.isBranchNode(n)
rw.Prefix = keyPage + "/"
rw.IncludeRawFilter = func(s string, n contentNode) bool {
// Only page nodes.
return cnh.isPageNode(n)
}
rw.IncludeFilter = func(s string, n contentNode) bool {
switch n.(type) {
case *pageState:
return true
default:
// We only want to handle page nodes here.
return false
}
}
rw.Handle = func(resourceKey string, n contentNode) (radix.WalkFlag, error) {
if isBranch {
ownerKey, _ := pw.Tree.LongestPrefix(resourceKey, false, nil)
if ownerKey != keyPage {
// Stop walking downwards, someone else owns this resource.
rw.SkipPrefix(ownerKey + "/")
return radix.WalkContinue, nil
}
}
switch rs := n.(type) {
case *pageState:
relPath := rs.m.pathInfo.BaseRel(pageBundle.m.pathInfo)
rs.m.resourcePath = relPath
}
return radix.WalkContinue, nil
}
if err := rw.Walk(sa.ctx); err != nil {
return radix.WalkStop, err
}
return radix.WalkContinue, nil
}
if err := pw.Walk(sa.ctx); err != nil {
return err
}
if err := pw.WalkContext.HandleEventsAndHooks(); err != nil {
return err
}
if !sa.s.conf.C.IsMainSectionsSet() {
var mainSection string
var maxcount int
for section, counter := range sectionPageCount {
if section != "" && counter > maxcount {
mainSection = section
maxcount = counter
}
}
sa.s.conf.C.SetMainSections([]string{mainSection})
}
return nil
}
func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error {
walkContext := &doctree.WalkContext[contentNode]{}
handlePlural := func(key string) error {
var pw *doctree.NodeShiftTreeWalker[contentNode]
pw = &doctree.NodeShiftTreeWalker[contentNode]{
Tree: sa.s.pageMap.treePages,
Prefix: key, // We also want to include the root taxonomy nodes, so no trailing slash.
LockType: doctree.LockTypeRead,
WalkContext: walkContext,
Handle: func(s string, n contentNode) (radix.WalkFlag, error) {
p := n.(*pageState)
if p.Kind() != kinds.KindTerm && p.Kind() != kinds.KindTaxonomy {
// Already handled.
return radix.WalkContinue, nil
}
const eventName = "dates"
if p.Kind() == kinds.KindTerm {
if !p.s.shouldBuild(p) {
sa.s.pageMap.treePages.Delete(s)
sa.s.pageMap.treeTaxonomyEntries.DeletePrefix(paths.AddTrailingSlash(s))
} else if err := sa.s.pageMap.treeTaxonomyEntries.WalkPrefix(
doctree.LockTypeRead,
paths.AddTrailingSlash(s),
func(ss string, wn *weightedContentNode) (bool, error) {
// Send the date info up the tree.
pw.WalkContext.SendEvent(&doctree.Event[contentNode]{Source: wn.n, Path: ss, Name: eventName})
return false, nil
},
); err != nil {
return radix.WalkStop, err
}
}
// Send the date info up the tree.
pw.WalkContext.SendEvent(&doctree.Event[contentNode]{Source: n, Path: s, Name: eventName})
if p.m.pageConfig.Dates.IsAllDatesZero() {
pw.WalkContext.AddEventListener(eventName, s, func(e *doctree.Event[contentNode]) {
sp, ok := e.Source.(*pageState)
if !ok {
return
}
p.m.pageConfig.Dates.UpdateDateAndLastmodAndPublishDateIfAfter(sp.m.pageConfig.Dates)
})
}
return radix.WalkContinue, nil
},
}
if err := pw.Walk(sa.ctx); err != nil {
return err
}
return nil
}
for _, viewName := range sa.s.pageMap.cfg.taxonomyConfig.views {
if err := handlePlural(viewName.pluralTreeKey); err != nil {
return err
}
}
if err := walkContext.HandleEventsAndHooks(); err != nil {
return err
}
return nil
}
func (sa *sitePagesAssembler) assembleTerms() error {
if sa.s.pageMap.cfg.taxonomyTermDisabled {
return nil
}
var (
pages = sa.s.pageMap.treePages
entries = sa.s.pageMap.treeTaxonomyEntries
views = sa.s.pageMap.cfg.taxonomyConfig.views
)
lockType := doctree.LockTypeWrite
w := &doctree.NodeShiftTreeWalker[contentNode]{
Tree: pages,
LockType: lockType,
Handle: func(s string, n contentNode) (radix.WalkFlag, error) {
ps := n.(*pageState)
if ps.m.noLink() {
return radix.WalkContinue, nil
}
for _, viewName := range views {
vals := types.ToStringSlicePreserveString(getParam(ps, viewName.plural, false))
if vals == nil {
continue
}
w := getParamToLower(ps, viewName.plural+"_weight")
weight, err := cast.ToIntE(w)
if err != nil {
sa.s.Log.Warnf("Unable to convert taxonomy weight %#v to int for %q", w, n.Path())
// weight will equal zero, so let the flow continue
}
for i, v := range vals {
if v == "" {
continue
}
viewTermKey := "/" + viewName.plural + "/" + v
pi := sa.s.Conf.PathParser().Parse(files.ComponentFolderContent, viewTermKey+"/_index.md")
termNode := pages.Get(pi.Base())
if termNode == nil {
// This means that the term page has been disabled (e.g. a draft).
continue
}
m := termNode.(*pageState).m
m.term = v
m.singular = viewName.singular
if s == "" {
s = "/"
}
key := pi.Base() + s
entries.Insert(key, &weightedContentNode{
weight: weight,
n: n,
term: &pageWithOrdinal{pageState: termNode.(*pageState), ordinal: i},
})
}
}
return radix.WalkContinue, nil
},
}
if err := w.Walk(sa.ctx); err != nil {
return err
}
return nil
}
func (sa *sitePagesAssembler) assemblePagesStepFinal() error {
if err := sa.assembleResourcesAndSetHome(); err != nil {
return err
}
return nil
}
func (sa *sitePagesAssembler) assembleResourcesAndSetHome() error {
pagesTree := sa.s.pageMap.treePages
lockType := doctree.LockTypeWrite
w := &doctree.NodeShiftTreeWalker[contentNode]{
Tree: pagesTree,
LockType: lockType,
Handle: func(s string, n contentNode) (radix.WalkFlag, error) {
ps := n.(*pageState)
if s == "" {
sa.s.home = ps
} else if ps.s.home == nil {
panic(fmt.Sprintf("[%v] expected home page to be set for %q", sa.s.siteVector, s))
}
// This is a little out of place, but is conveniently put here.
// Check if translationKey is set by user.
// This is to support the manual way of setting the translationKey in front matter.
if ps.m.pageConfig.TranslationKey != "" {
sa.s.h.translationKeyPages.Append(ps.m.pageConfig.TranslationKey, ps)
}
if !sa.s.h.isRebuild() {
if ps.hasRenderableOutput() {
// For multi output pages this will not be complete, but will have to do for now.
sa.s.h.progressReporter.numPagesToRender.Add(1)
}
}
// Prepare resources for this page.
ps.shiftToOutputFormat(true, 0)
targetPaths := ps.targetPaths()
baseTarget := targetPaths.SubResourceBaseTarget
err := sa.s.pageMap.forEachResourceInPage(
ps, lockType,
false,
nil,
func(resourceKey string, n contentNode) (bool, error) {
if _, ok := n.(*pageState); ok {
return false, nil
}
rs := n.(*resourceSource)
relPathOriginal := rs.path.Unnormalized().PathRel(ps.m.pathInfo.Unnormalized())
relPath := rs.path.BaseRel(ps.m.pathInfo)
var targetBasePaths []string
if ps.s.Conf.IsMultihost() {
baseTarget = targetPaths.SubResourceBaseLink
// In multihost we need to publish to the lang sub folder.
targetBasePaths = []string{ps.s.GetTargetLanguageBasePath()} // TODO(bep) we don't need this as a slice anymore.
}
if rs.rc != nil && rs.rc.Content.IsResourceValue() {
if rs.rc.Name == "" {
rs.rc.Name = relPathOriginal
}
r, err := ps.s.ResourceSpec.NewResourceWrapperFromResourceConfig(rs.rc)
if err != nil {
return false, err
}
rs.r = r
return false, nil
}
var mt media.Type
if rs.rc != nil {
mt = rs.rc.ContentMediaType
}
var filename string
if rs.fi != nil {
filename = rs.fi.Meta().Filename
}
rd := resources.ResourceSourceDescriptor{
OpenReadSeekCloser: rs.opener,
Path: rs.path,
GroupIdentity: rs.path,
TargetPath: relPathOriginal, // Use the original path for the target path, so the links can be guessed.
TargetBasePaths: targetBasePaths,
BasePathRelPermalink: targetPaths.SubResourceBaseLink,
BasePathTargetPath: baseTarget,
SourceFilenameOrPath: filename,
NameNormalized: relPath,
NameOriginal: relPathOriginal,
MediaType: mt,
LazyPublish: !ps.m.pageConfig.Build.PublishResources,
}
if rs.rc != nil {
rc := rs.rc
rd.OpenReadSeekCloser = rc.Content.ValueAsOpenReadSeekCloser()
if rc.Name != "" {
rd.NameNormalized = rc.Name
rd.NameOriginal = rc.Name
}
if rc.Title != "" {
rd.Title = rc.Title
}
rd.Params = rc.Params
}
r, err := ps.s.ResourceSpec.NewResource(rd)
if err != nil {
return false, err
}
rs.r = r
return false, nil
},
)
return radix.WalkContinue, err
},
}
return w.Walk(sa.ctx)
}
func (sa *sitePagesAssembler) assemblePagesStep1() error {
if err := sa.applyAggregates(); err != nil {
return err
}
return nil
}
func (sa *sitePagesAssembler) assemblePagesStep2() error {
if err := sa.assembleTerms(); err != nil {
return err
}
if err := sa.applyAggregatesToTaxonomiesAndTerms(); err != nil {
return err
}
return nil
}
// No locking.
func (a *allPagesAssembler) createMissingTaxonomies() error {
if a.m.cfg.taxonomyDisabled && a.m.cfg.taxonomyTermDisabled {
return nil
}
tree := a.m.treePages
viewLanguages := map[viewName][]int{}
for _, s := range a.h.sitesLanguages {
if s.pageMap.cfg.taxonomyDisabled && s.pageMap.cfg.taxonomyTermDisabled {
continue
}
for _, viewName := range s.pageMap.cfg.taxonomyConfig.views {
viewLanguages[viewName] = append(viewLanguages[viewName], s.siteVector.Language())
}
}
for viewName, languages := range viewLanguages {
key := viewName.pluralTreeKey
if a.h.isRebuild() {
if v := tree.Get(key); v != nil {
// Already there.
continue
}
}
matrixAllForLanguages := sitesmatrix.NewIntSetsBuilder(a.h.Conf.ConfiguredDimensions()).WithLanguageIndices(languages...).WithAllIfNotSet().Build()
pi := a.h.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md")
p := &pageMetaSource{
pathInfo: pi,
sitesMatrixBase: matrixAllForLanguages,
pageConfigSource: &pagemeta.PageConfigEarly{
Kind: kinds.KindTaxonomy,
},
}
tree.AppendRaw(key, p)
}
return nil
}
// Create the fixed output pages, e.g. sitemap.xml, if not already there.
// TODO2 revise, especially around disabled and config per language/site.
// No locking.
func (a *allPagesAssembler) createMissingStandalonePages() error {
m := a.m
tree := m.treePages
oneSiteStore := (sitesmatrix.Vectors{sitesmatrix.Vector{0, 0, 0}: struct{}{}}).ToVectorStore()
addStandalone := func(key, kind string, f output.Format) {
if !a.h.Conf.IsKindEnabled(kind) || tree.Has(key) {
return
}
var sitesMatrixBase sitesmatrix.VectorIterator
sitesMatrixBase = a.h.Conf.AllSitesMatrix()
if !a.h.Conf.IsMultihost() {
switch kind {
case kinds.KindSitemapIndex, kinds.KindRobotsTXT:
// First site only.
sitesMatrixBase = oneSiteStore
}
}
p := &pageMetaSource{
pathInfo: a.h.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix),
standaloneOutputFormat: f,
sitesMatrixBase: sitesMatrixBase,
sitesMatrixBaseOnly: true,
pageConfigSource: &pagemeta.PageConfigEarly{
Kind: kind,
},
}
tree.InsertRaw(key, p)
}
addStandalone("/404", kinds.KindStatus404, output.HTTPStatus404HTMLFormat)
if a.h.Configs.Base.EnableRobotsTXT {
if m.i == 0 || a.h.Conf.IsMultihost() {
addStandalone("/_robots", kinds.KindRobotsTXT, output.RobotsTxtFormat)
}
}
sitemapEnabled := false
for _, s := range a.h.Sites {
if s.conf.IsKindEnabled(kinds.KindSitemap) {
sitemapEnabled = true
break
}
}
if sitemapEnabled {
of := output.SitemapFormat
if a.h.Configs.Base.Sitemap.Filename != "" {
of.BaseName = paths.Filename(a.h.Configs.Base.Sitemap.Filename)
}
addStandalone("/_sitemap", kinds.KindSitemap, of)
skipSitemapIndex := a.h.Conf.IsMultihost() || !(a.h.Conf.DefaultContentLanguageInSubdir() || a.h.Conf.IsMultilingual())
if !skipSitemapIndex {
of = output.SitemapIndexFormat
if a.h.Configs.Base.Sitemap.Filename != "" {
of.BaseName = paths.Filename(a.h.Configs.Base.Sitemap.Filename)
}
addStandalone("/_sitemapindex", kinds.KindSitemapIndex, of)
}
}
return nil
}
// No locking.
func (a *allPagesAssembler) createMissingPages() error {
if err := a.createMissingTaxonomies(); err != nil {
return err
}
if !a.h.isRebuild() {
if err := a.createMissingStandalonePages(); err != nil {
return err
}
}
return nil
}