Allow headless bundles to list pages via $page.Pages and $page.RegularPages

Fixes #7075
This commit is contained in:
Bjørn Erik Pedersen 2020-03-20 09:37:21 +01:00
parent 1d91d8e14b
commit 99958f90fe
10 changed files with 288 additions and 72 deletions

View file

@ -21,7 +21,7 @@ They are stored in a reserved Front Matter object named `_build` with the follow
```yaml ```yaml
_build: _build:
render: true render: true
list: true list: always
publishResources: true publishResources: true
``` ```
@ -29,6 +29,20 @@ _build:
If true, the page will be treated as a published page, holding its dedicated output files (`index.html`, etc...) and permalink. If true, the page will be treated as a published page, holding its dedicated output files (`index.html`, etc...) and permalink.
#### list #### list
Note that we extended this property from a boolean to an enum in Hugo 0.58.0.
Valid values are:
never
: The page will not be incldued in any page collection.
always (default)
: The page will be included in all page collections, e.g. `site.RegularPages`, `$page.Pages`.
local
: The page will be included in any _local_ page collection, e.g. `$page.RegularPages`, `$page.Pages`. One use case for this would be to create fully navigable, but headless content sections. {{< new-in "0.58.0" >}}
If true, the page will be treated as part of the project's collections and, when appropriate, returned by Hugo's listing methods (`.Pages`, `.RegularPages` etc...). If true, the page will be treated as part of the project's collections and, when appropriate, returned by Hugo's listing methods (`.Pages`, `.RegularPages` etc...).
#### publishResources #### publishResources

View file

@ -789,6 +789,12 @@ func (t contentTrees) DeletePrefix(prefix string) int {
type contentTreeNodeCallback func(s string, n *contentNode) bool type contentTreeNodeCallback func(s string, n *contentNode) bool
func newContentTreeFilter(fn func(n *contentNode) bool) contentTreeNodeCallback {
return func(s string, n *contentNode) bool {
return fn(n)
}
}
var ( var (
contentTreeNoListFilter = func(s string, n *contentNode) bool { contentTreeNoListFilter = func(s string, n *contentNode) bool {
if n.p == nil { if n.p == nil {
@ -805,43 +811,36 @@ var (
} }
) )
func (c *contentTree) WalkPrefixListable(prefix string, fn contentTreeNodeCallback) { func (c *contentTree) WalkQuery(query pageMapQuery, walkFn contentTreeNodeCallback) {
c.WalkPrefixFilter(prefix, contentTreeNoListFilter, fn) filter := query.Filter
} if filter == nil {
filter = contentTreeNoListFilter
}
if query.Prefix != "" {
c.WalkPrefix(query.Prefix, func(s string, v interface{}) bool {
n := v.(*contentNode)
if filter != nil && filter(s, n) {
return false
}
return walkFn(s, n)
})
func (c *contentTree) WalkPrefixFilter(prefix string, filter, walkFn contentTreeNodeCallback) { return
c.WalkPrefix(prefix, func(s string, v interface{}) bool { }
n := v.(*contentNode)
if filter(s, n) {
return false
}
return walkFn(s, n)
})
}
func (c *contentTree) WalkListable(fn contentTreeNodeCallback) {
c.WalkFilter(contentTreeNoListFilter, fn)
}
func (c *contentTree) WalkFilter(filter, walkFn contentTreeNodeCallback) {
c.Walk(func(s string, v interface{}) bool { c.Walk(func(s string, v interface{}) bool {
n := v.(*contentNode) n := v.(*contentNode)
if filter(s, n) { if filter != nil && filter(s, n) {
return false return false
} }
return walkFn(s, n) return walkFn(s, n)
}) })
} }
func (c contentTrees) WalkListable(fn contentTreeNodeCallback) {
for _, tree := range c {
tree.WalkListable(fn)
}
}
func (c contentTrees) WalkRenderable(fn contentTreeNodeCallback) { func (c contentTrees) WalkRenderable(fn contentTreeNodeCallback) {
query := pageMapQuery{Filter: contentTreeNoRenderFilter}
for _, tree := range c { for _, tree := range c {
tree.WalkFilter(contentTreeNoRenderFilter, fn) tree.WalkQuery(query, fn)
} }
} }
@ -931,44 +930,73 @@ func (c *contentTreeRef) getSection() (string, *contentNode) {
return c.m.getSection(c.key) return c.m.getSection(c.key)
} }
func (c *contentTreeRef) collectPages() page.Pages { func (c *contentTreeRef) getPages() page.Pages {
var pas page.Pages var pas page.Pages
c.m.collectPages(c.key+cmBranchSeparator, func(c *contentNode) { c.m.collectPages(
pas = append(pas, c.p) pageMapQuery{
}) Prefix: c.key + cmBranchSeparator,
Filter: c.n.p.m.getListFilter(true),
},
func(c *contentNode) {
pas = append(pas, c.p)
},
)
page.SortByDefault(pas) page.SortByDefault(pas)
return pas return pas
} }
func (c *contentTreeRef) collectPagesRecursive() page.Pages { func (c *contentTreeRef) getPagesRecursive() page.Pages {
var pas page.Pages var pas page.Pages
c.m.collectPages(c.key+cmBranchSeparator, func(c *contentNode) {
query := pageMapQuery{
Filter: c.n.p.m.getListFilter(true),
}
query.Prefix = c.key + cmBranchSeparator
c.m.collectPages(query, func(c *contentNode) {
pas = append(pas, c.p) pas = append(pas, c.p)
}) })
c.m.collectPages(c.key+"/", func(c *contentNode) {
query.Prefix = c.key + "/"
c.m.collectPages(query, func(c *contentNode) {
pas = append(pas, c.p) pas = append(pas, c.p)
}) })
page.SortByDefault(pas) page.SortByDefault(pas)
return pas return pas
} }
func (c *contentTreeRef) collectPagesAndSections() page.Pages { func (c *contentTreeRef) getPagesAndSections() page.Pages {
var pas page.Pages var pas page.Pages
c.m.collectPagesAndSections(c.key, func(c *contentNode) {
query := pageMapQuery{
Filter: c.n.p.m.getListFilter(true),
Prefix: c.key,
}
c.m.collectPagesAndSections(query, func(c *contentNode) {
pas = append(pas, c.p) pas = append(pas, c.p)
}) })
page.SortByDefault(pas) page.SortByDefault(pas)
return pas return pas
} }
func (c *contentTreeRef) collectSections() page.Pages { func (c *contentTreeRef) getSections() page.Pages {
var pas page.Pages var pas page.Pages
c.m.collectSections(c.key, func(c *contentNode) {
query := pageMapQuery{
Filter: c.n.p.m.getListFilter(true),
Prefix: c.key,
}
c.m.collectSections(query, func(c *contentNode) {
pas = append(pas, c.p) pas = append(pas, c.p)
}) })
page.SortByDefault(pas) page.SortByDefault(pas)
return pas return pas

View file

@ -606,36 +606,47 @@ func (m *pageMap) attachPageToViews(s string, b *contentNode) {
} }
} }
func (m *pageMap) collectPages(prefix string, fn func(c *contentNode)) error { type pageMapQuery struct {
m.pages.WalkPrefixListable(prefix, func(s string, n *contentNode) bool { Prefix string
Filter contentTreeNodeCallback
}
func (m *pageMap) collectPages(query pageMapQuery, fn func(c *contentNode)) error {
if query.Filter == nil {
query.Filter = contentTreeNoListFilter
}
m.pages.WalkQuery(query, func(s string, n *contentNode) bool {
fn(n) fn(n)
return false return false
}) })
return nil return nil
} }
func (m *pageMap) collectPagesAndSections(prefix string, fn func(c *contentNode)) error { func (m *pageMap) collectPagesAndSections(query pageMapQuery, fn func(c *contentNode)) error {
if err := m.collectSections(prefix, fn); err != nil { if err := m.collectSections(query, fn); err != nil {
return err return err
} }
if err := m.collectPages(prefix+cmBranchSeparator, fn); err != nil { query.Prefix = query.Prefix + cmBranchSeparator
if err := m.collectPages(query, fn); err != nil {
return err return err
} }
return nil return nil
} }
func (m *pageMap) collectSections(prefix string, fn func(c *contentNode)) error { func (m *pageMap) collectSections(query pageMapQuery, fn func(c *contentNode)) error {
var level int var level int
isHome := prefix == "/" isHome := query.Prefix == "/"
if !isHome { if !isHome {
level = strings.Count(prefix, "/") level = strings.Count(query.Prefix, "/")
} }
return m.collectSectionsFn(prefix, func(s string, c *contentNode) bool { return m.collectSectionsFn(query, func(s string, c *contentNode) bool {
if s == prefix { if s == query.Prefix {
return false return false
} }
@ -649,27 +660,28 @@ func (m *pageMap) collectSections(prefix string, fn func(c *contentNode)) error
}) })
} }
func (m *pageMap) collectSectionsFn(prefix string, fn func(s string, c *contentNode) bool) error { func (m *pageMap) collectSectionsFn(query pageMapQuery, fn func(s string, c *contentNode) bool) error {
if !strings.HasSuffix(prefix, "/") {
prefix += "/" if !strings.HasSuffix(query.Prefix, "/") {
query.Prefix += "/"
} }
m.sections.WalkPrefixListable(prefix, func(s string, n *contentNode) bool { m.sections.WalkQuery(query, func(s string, n *contentNode) bool {
return fn(s, n) return fn(s, n)
}) })
return nil return nil
} }
func (m *pageMap) collectSectionsRecursiveIncludingSelf(prefix string, fn func(c *contentNode)) error { func (m *pageMap) collectSectionsRecursiveIncludingSelf(query pageMapQuery, fn func(c *contentNode)) error {
return m.collectSectionsFn(prefix, func(s string, c *contentNode) bool { return m.collectSectionsFn(query, func(s string, c *contentNode) bool {
fn(c) fn(c)
return false return false
}) })
} }
func (m *pageMap) collectTaxonomies(prefix string, fn func(c *contentNode)) error { func (m *pageMap) collectTaxonomies(prefix string, fn func(c *contentNode)) error {
m.taxonomies.WalkPrefixListable(prefix, func(s string, n *contentNode) bool { m.taxonomies.WalkQuery(pageMapQuery{Prefix: prefix}, func(s string, n *contentNode) bool {
fn(n) fn(n)
return false return false
}) })
@ -797,21 +809,21 @@ type pagesMapBucket struct {
func (b *pagesMapBucket) getPages() page.Pages { func (b *pagesMapBucket) getPages() page.Pages {
b.pagesInit.Do(func() { b.pagesInit.Do(func() {
b.pages = b.owner.treeRef.collectPages() b.pages = b.owner.treeRef.getPages()
page.SortByDefault(b.pages) page.SortByDefault(b.pages)
}) })
return b.pages return b.pages
} }
func (b *pagesMapBucket) getPagesRecursive() page.Pages { func (b *pagesMapBucket) getPagesRecursive() page.Pages {
pages := b.owner.treeRef.collectPagesRecursive() pages := b.owner.treeRef.getPagesRecursive()
page.SortByDefault(pages) page.SortByDefault(pages)
return pages return pages
} }
func (b *pagesMapBucket) getPagesAndSections() page.Pages { func (b *pagesMapBucket) getPagesAndSections() page.Pages {
b.pagesAndSectionsInit.Do(func() { b.pagesAndSectionsInit.Do(func() {
b.pagesAndSections = b.owner.treeRef.collectPagesAndSections() b.pagesAndSections = b.owner.treeRef.getPagesAndSections()
}) })
return b.pagesAndSections return b.pagesAndSections
} }
@ -821,7 +833,7 @@ func (b *pagesMapBucket) getSections() page.Pages {
if b.owner.treeRef == nil { if b.owner.treeRef == nil {
return return
} }
b.sections = b.owner.treeRef.collectSections() b.sections = b.owner.treeRef.getSections()
}) })
return b.sections return b.sections

View file

@ -66,7 +66,26 @@ title: Headless
headless: true headless: true
--- ---
`)
`, "headless-local/_index.md", `
---
title: Headless Local Lists
cascade:
_build:
render: false
list: local
publishResources: false
---
`, "headless-local/headless-local-page.md", "---\ntitle: Headless Local Page\n---",
"headless-local/sub/_index.md", `
---
title: Headless Local Lists Sub
---
`, "headless-local/sub/headless-local-sub-page.md", "---\ntitle: Headless Local Sub Page\n---",
)
b.WithSourceFile("content/sect/headlessbundle/data.json", "DATA") b.WithSourceFile("content/sect/headlessbundle/data.json", "DATA")
b.WithSourceFile("content/sect/no-publishresources/data.json", "DATA") b.WithSourceFile("content/sect/no-publishresources/data.json", "DATA")
@ -93,8 +112,11 @@ headless: true
return nil return nil
} }
getPageInPagePages := func(p page.Page, ref string) page.Page { getPageInPagePages := func(p page.Page, ref string, pageCollections ...page.Pages) page.Page {
for _, pages := range []page.Pages{p.Pages(), p.RegularPages(), p.Sections()} { if len(pageCollections) == 0 {
pageCollections = []page.Pages{p.Pages(), p.RegularPages(), p.RegularPagesRecursive(), p.Sections()}
}
for _, pages := range pageCollections {
for _, p := range pages { for _, p := range pages {
if ref == p.(*pageState).sourceRef() { if ref == p.(*pageState).sourceRef() {
return p return p
@ -240,6 +262,33 @@ headless: true
}) })
c.Run("Build config, local list", func(c *qt.C) {
b := newSitesBuilder(c, disableKind)
b.Build(BuildCfg{})
ref := "/headless-local"
sect := getPage(b, ref)
b.Assert(sect, qt.Not(qt.IsNil))
b.Assert(getPageInSitePages(b, ref), qt.IsNil)
b.Assert(getPageInSitePages(b, ref+"/headless-local-page"), qt.IsNil)
for i, p := range sect.RegularPages() {
fmt.Println("REG", i, p.(*pageState).sourceRef())
}
localPageRef := ref + "/headless-local-page.md"
b.Assert(getPageInPagePages(sect, localPageRef, sect.RegularPages()), qt.Not(qt.IsNil))
b.Assert(getPageInPagePages(sect, localPageRef, sect.RegularPagesRecursive()), qt.Not(qt.IsNil))
b.Assert(getPageInPagePages(sect, localPageRef, sect.Pages()), qt.Not(qt.IsNil))
ref = "/headless-local/sub"
sect = getPage(b, ref)
b.Assert(sect, qt.Not(qt.IsNil))
localPageRef = ref + "/headless-local-sub-page.md"
b.Assert(getPageInPagePages(sect, localPageRef), qt.Not(qt.IsNil))
})
c.Run("Build config, no render", func(c *qt.C) { c.Run("Build config, no render", func(c *qt.C) {
b := newSitesBuilder(c, disableKind) b := newSitesBuilder(c, disableKind)
b.Build(BuildCfg{}) b.Build(BuildCfg{})

View file

@ -147,7 +147,7 @@ func (p *pageState) GetTerms(taxonomy string) page.Pages {
var pas page.Pages var pas page.Pages
m.taxonomies.WalkPrefixListable(prefix, func(s string, n *contentNode) bool { m.taxonomies.WalkQuery(pageMapQuery{Prefix: prefix}, func(s string, n *contentNode) bool {
if _, found := m.taxonomyEntries.Get(s + self); found { if _, found := m.taxonomyEntries.Get(s + self); found {
pas = append(pas, n.p) pas = append(pas, n.p)
} }

View file

@ -460,7 +460,7 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron
isHeadless := cast.ToBool(v) isHeadless := cast.ToBool(v)
pm.params[loki] = isHeadless pm.params[loki] = isHeadless
if p.File().TranslationBaseName() == "index" && isHeadless { if p.File().TranslationBaseName() == "index" && isHeadless {
pm.buildConfig.List = false pm.buildConfig.List = pagemeta.Never
pm.buildConfig.Render = false pm.buildConfig.Render = false
} }
case "outputs": case "outputs":
@ -613,7 +613,28 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron
} }
func (p *pageMeta) noList() bool { func (p *pageMeta) noList() bool {
return !p.buildConfig.List return !p.buildConfig.ShouldList()
}
func (p *pageMeta) getListFilter(local bool) contentTreeNodeCallback {
return newContentTreeFilter(func(n *contentNode) bool {
if n == nil {
return true
}
var shouldList bool
switch n.p.m.buildConfig.List {
case pagemeta.Always:
shouldList = true
case pagemeta.Never:
shouldList = false
case pagemeta.ListLocally:
shouldList = local
}
return !shouldList
})
} }
func (p *pageMeta) noRender() bool { func (p *pageMeta) noRender() bool {

View file

@ -248,7 +248,7 @@ func (s *Site) prepareInits() {
s.init.prevNextInSection = init.Branch(func() (interface{}, error) { s.init.prevNextInSection = init.Branch(func() (interface{}, error) {
var sections page.Pages var sections page.Pages
s.home.treeRef.m.collectSectionsRecursiveIncludingSelf(s.home.treeRef.key, func(n *contentNode) { s.home.treeRef.m.collectSectionsRecursiveIncludingSelf(pageMapQuery{Prefix: s.home.treeRef.key}, func(n *contentNode) {
sections = append(sections, n.p) sections = append(sections, n.p)
}) })
@ -281,7 +281,7 @@ func (s *Site) prepareInits() {
treeRef := sect.(treeRefProvider).getTreeRef() treeRef := sect.(treeRefProvider).getTreeRef()
var pas page.Pages var pas page.Pages
treeRef.m.collectPages(treeRef.key+cmBranchSeparator, func(c *contentNode) { treeRef.m.collectPages(pageMapQuery{Prefix: treeRef.key + cmBranchSeparator}, func(c *contentNode) {
pas = append(pas, c.p) pas = append(pas, c.p)
}) })
page.SortByDefault(pas) page.SortByDefault(pas)
@ -293,7 +293,7 @@ func (s *Site) prepareInits() {
treeRef := s.home.getTreeRef() treeRef := s.home.getTreeRef()
var pas page.Pages var pas page.Pages
treeRef.m.collectPages(treeRef.key+cmBranchSeparator, func(c *contentNode) { treeRef.m.collectPages(pageMapQuery{Prefix: treeRef.key + cmBranchSeparator}, func(c *contentNode) {
pas = append(pas, c.p) pas = append(pas, c.p)
}) })
page.SortByDefault(pas) page.SortByDefault(pas)

View file

@ -1021,7 +1021,7 @@ func printStringIndexes(s string) {
} }
func isCI() bool { func isCI() bool {
return os.Getenv("CI") != "" && os.Getenv("CIRCLE_BRANCH") == "" return (os.Getenv("CI") != "" || os.Getenv("CI_LOCAL") != "") && os.Getenv("CIRCLE_BRANCH") == ""
} }
// See https://github.com/golang/go/issues/19280 // See https://github.com/golang/go/issues/19280

View file

@ -24,8 +24,14 @@ type URLPath struct {
Section string Section string
} }
const (
Never = "never"
Always = "always"
ListLocally = "local"
)
var defaultBuildConfig = BuildConfig{ var defaultBuildConfig = BuildConfig{
List: true, List: Always,
Render: true, Render: true,
PublishResources: true, PublishResources: true,
set: true, set: true,
@ -35,8 +41,12 @@ var defaultBuildConfig = BuildConfig{
// build process. // build process.
type BuildConfig struct { type BuildConfig struct {
// Whether to add it to any of the page collections. // Whether to add it to any of the page collections.
// Note that the page can still be found with .Site.GetPage. // Note that the page can always be found with .Site.GetPage.
List bool // Valid values: never, always, local.
// Setting it to 'local' means they will be available via the local
// page collections, e.g. $section.Pages.
// Note: before 0.57.2 this was a bool, so we accept those too.
List string
// Whether to render it. // Whether to render it.
Render bool Render bool
@ -51,7 +61,7 @@ type BuildConfig struct {
// Disable sets all options to their off value. // Disable sets all options to their off value.
func (b *BuildConfig) Disable() { func (b *BuildConfig) Disable() {
b.List = false b.List = Never
b.Render = false b.Render = false
b.PublishResources = false b.PublishResources = false
b.set = true b.set = true
@ -61,11 +71,29 @@ func (b BuildConfig) IsZero() bool {
return !b.set return !b.set
} }
func (b *BuildConfig) ShouldList() bool {
return b.List == Always || b.List == ListLocally
}
func DecodeBuildConfig(m interface{}) (BuildConfig, error) { func DecodeBuildConfig(m interface{}) (BuildConfig, error) {
b := defaultBuildConfig b := defaultBuildConfig
if m == nil { if m == nil {
return b, nil return b, nil
} }
err := mapstructure.WeakDecode(m, &b) err := mapstructure.WeakDecode(m, &b)
// In 0.67.1 we changed the list attribute from a bool to a string (enum).
// Bool values will become 0 or 1.
switch b.List {
case "0":
b.List = Never
case "1":
b.List = Always
case Always, Never, ListLocally:
default:
b.List = Always
}
return b, err return b, err
} }

View file

@ -0,0 +1,64 @@
// Copyright 2020 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 pagemeta
import (
"fmt"
"testing"
"github.com/gohugoio/hugo/htesting/hqt"
"github.com/gohugoio/hugo/config"
qt "github.com/frankban/quicktest"
)
func TestDecodeBuildConfig(t *testing.T) {
t.Parallel()
c := qt.New(t)
configTempl := `
[_build]
render = true
list = %s
publishResources = true`
for _, test := range []struct {
list interface{}
expect string
}{
{"true", Always},
{"false", Never},
{`"always"`, Always},
{`"local"`, ListLocally},
{`"asdfadf"`, Always},
} {
cfg, err := config.FromConfigString(fmt.Sprintf(configTempl, test.list), "toml")
c.Assert(err, qt.IsNil)
bcfg, err := DecodeBuildConfig(cfg.Get("_build"))
c.Assert(err, qt.IsNil)
eq := qt.CmpEquals(hqt.DeepAllowUnexported(BuildConfig{}))
c.Assert(bcfg, eq, BuildConfig{
Render: true,
List: test.expect,
PublishResources: true,
set: true,
})
}
}