Add headless bundle support

This commit adds  support for `headless bundles` for the `index` bundle type.

So:

```toml
headless = true
```

In front matter means that

* It will have no `Permalink` and no rendered HTML in /public
* It will not be part of `.Site.RegularPages` etc.

But you can get it by:

* `.Site.GetPage ...`

The use cases are many:

* Shared media galleries
* Reusable page content "snippets"
* ...

Fixes #4311
This commit is contained in:
Bjørn Erik Pedersen 2018-01-23 14:02:54 +01:00
parent 5a0819b9b5
commit 0432c64dd2
6 changed files with 163 additions and 20 deletions

View file

@ -508,10 +508,14 @@ func (h *HugoSites) setupTranslations() {
shouldBuild := p.shouldBuild() shouldBuild := p.shouldBuild()
s.updateBuildStats(p) s.updateBuildStats(p)
if shouldBuild { if shouldBuild {
if p.headless {
s.headlessPages = append(s.headlessPages, p)
} else {
s.Pages = append(s.Pages, p) s.Pages = append(s.Pages, p)
} }
} }
} }
}
allPages := make(Pages, 0) allPages := make(Pages, 0)
@ -560,6 +564,10 @@ func (s *Site) preparePagesForRender(cfg *BuildCfg) {
pageChan <- p pageChan <- p
} }
for _, p := range s.headlessPages {
pageChan <- p
}
close(pageChan) close(pageChan)
wg.Wait() wg.Wait()

View file

@ -179,11 +179,17 @@ func (h *HugoSites) assemble(config *BuildCfg) error {
} }
for _, s := range h.Sites { for _, s := range h.Sites {
for _, p := range s.Pages { for _, pages := range []Pages{s.Pages, s.headlessPages} {
for _, p := range pages {
// May have been set in front matter // May have been set in front matter
if len(p.outputFormats) == 0 { if len(p.outputFormats) == 0 {
p.outputFormats = s.outputFormats[p.Kind] p.outputFormats = s.outputFormats[p.Kind]
} }
if p.headless {
// headless = 1 output format only
p.outputFormats = p.outputFormats[:1]
}
for _, r := range p.Resources.ByType(pageResourceType) { for _, r := range p.Resources.ByType(pageResourceType) {
r.(*Page).outputFormats = p.outputFormats r.(*Page).outputFormats = p.outputFormats
} }
@ -193,6 +199,7 @@ func (h *HugoSites) assemble(config *BuildCfg) error {
} }
} }
}
s.assembleMenus() s.assembleMenus()
s.refreshPageCaches() s.refreshPageCaches()
s.setupSitePages() s.setupSitePages()

View file

@ -237,6 +237,13 @@ type Page struct {
// Is set to a forward slashed path if this is a Page resources living in a folder below its owner. // Is set to a forward slashed path if this is a Page resources living in a folder below its owner.
resourcePath string resourcePath string
// This is enabled if it is a leaf bundle (the "index.md" type) and it is marked as headless in front matter.
// Being headless means that
// 1. The page itself is not rendered to disk
// 2. It is not available in .Site.Pages etc.
// 3. But you can get it via .Site.GetPage
headless bool
layoutDescriptor output.LayoutDescriptor layoutDescriptor output.LayoutDescriptor
scratch *Scratch scratch *Scratch
@ -986,11 +993,17 @@ func (p *Page) URL() string {
// Permalink returns the absolute URL to this Page. // Permalink returns the absolute URL to this Page.
func (p *Page) Permalink() string { func (p *Page) Permalink() string {
if p.headless {
return ""
}
return p.permalink return p.permalink
} }
// RelPermalink gets a URL to the resource relative to the host. // RelPermalink gets a URL to the resource relative to the host.
func (p *Page) RelPermalink() string { func (p *Page) RelPermalink() string {
if p.headless {
return ""
}
return p.relPermalink return p.relPermalink
} }
@ -1150,6 +1163,13 @@ func (p *Page) update(f interface{}) error {
p.s.Log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path()) p.s.Log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path())
} }
p.params[loki] = p.Date p.params[loki] = p.Date
case "headless":
// For now, only the leaf bundles ("index.md") can be headless (i.e. produce no output).
// We may expand on this in the future, but that gets more complex pretty fast.
if p.TranslationBaseName() == "index" {
p.headless = cast.ToBool(v)
}
p.params[loki] = p.headless
case "lastmod": case "lastmod":
p.Lastmod, err = cast.ToTimeE(v) p.Lastmod, err = cast.ToTimeE(v)
if err != nil { if err != nil {

View file

@ -224,6 +224,87 @@ func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) {
} }
func TestPageBundlerHeadless(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()
assert := require.New(t)
workDir := "/work"
cfg.Set("workingDir", workDir)
cfg.Set("contentDir", "base")
cfg.Set("baseURL", "https://example.com")
pageContent := `---
title: "Bundle Galore"
slug: s1
date: 2017-01-23
---
TheContent.
{{< myShort >}}
`
writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), "single {{ .Content }}")
writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), "list")
writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), "SHORTCODE")
writeSource(t, fs, filepath.Join(workDir, "base", "a", "index.md"), pageContent)
writeSource(t, fs, filepath.Join(workDir, "base", "a", "l1.png"), "PNG image")
writeSource(t, fs, filepath.Join(workDir, "base", "a", "l2.png"), "PNG image")
writeSource(t, fs, filepath.Join(workDir, "base", "b", "index.md"), `---
title: "Headless Bundle in Topless Bar"
slug: s2
headless: true
date: 2017-01-23
---
TheContent.
HEADLESS {{< myShort >}}
`)
writeSource(t, fs, filepath.Join(workDir, "base", "b", "l1.png"), "PNG image")
writeSource(t, fs, filepath.Join(workDir, "base", "b", "l2.png"), "PNG image")
writeSource(t, fs, filepath.Join(workDir, "base", "b", "p1.md"), pageContent)
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
assert.Equal(1, len(s.RegularPages))
assert.Equal(1, len(s.headlessPages))
regular := s.getPage(KindPage, "a/index")
assert.Equal("/a/s1/", regular.RelPermalink())
headless := s.getPage(KindPage, "b/index")
assert.NotNil(headless)
assert.True(headless.headless)
assert.Equal("Headless Bundle in Topless Bar", headless.Title())
assert.Equal("", headless.RelPermalink())
assert.Equal("", headless.Permalink())
assert.Contains(headless.Content, "HEADLESS SHORTCODE")
headlessResources := headless.Resources
assert.Equal(3, len(headlessResources))
assert.Equal(2, len(headlessResources.Match("l*")))
pageResource := headlessResources.GetMatch("p*")
assert.NotNil(pageResource)
assert.IsType(&Page{}, pageResource)
p := pageResource.(*Page)
assert.Contains(p.Content, "SHORTCODE")
assert.Equal("p1.md", p.Name())
th := testHelper{s.Cfg, s.Fs, t}
th.assertFileContent(filepath.FromSlash(workDir+"/public/a/s1/index.html"), "TheContent")
th.assertFileContent(filepath.FromSlash(workDir+"/public/a/s1/l1.png"), "PNG")
th.assertFileNotExist(workDir + "/public/b/s2/index.html")
// But the bundled resources needs to be published
th.assertFileContent(filepath.FromSlash(workDir+"/public/b/s2/l1.png"), "PNG")
}
func newTestBundleSources(t *testing.T) (*viper.Viper, *hugofs.Fs) { func newTestBundleSources(t *testing.T) (*viper.Viper, *hugofs.Fs) {
cfg, fs := newTestCfg() cfg, fs := newTestCfg()
assert := require.New(t) assert := require.New(t)

View file

@ -43,6 +43,9 @@ type PageCollections struct {
// Includes absolute all pages (of all types), including drafts etc. // Includes absolute all pages (of all types), including drafts etc.
rawAllPages Pages rawAllPages Pages
// Includes headless bundles, i.e. bundles that produce no output for its content page.
headlessPages Pages
pageCache *cache.PartitionedLazyCache pageCache *cache.PartitionedLazyCache
} }
@ -66,7 +69,8 @@ func (c *PageCollections) refreshPageCaches() {
// in this cache, as we intend to use this in the ref and relref // in this cache, as we intend to use this in the ref and relref
// shortcodes. If the user says "sect/doc1.en.md", he/she knows // shortcodes. If the user says "sect/doc1.en.md", he/she knows
// what he/she is looking for. // what he/she is looking for.
for _, p := range c.AllRegularPages { for _, pageCollection := range []Pages{c.AllRegularPages, c.headlessPages} {
for _, p := range pageCollection {
cache[filepath.ToSlash(p.Source.Path())] = p cache[filepath.ToSlash(p.Source.Path())] = p
// Ref/Relref supports this potentially ambiguous lookup. // Ref/Relref supports this potentially ambiguous lookup.
cache[p.Source.LogicalName()] = p cache[p.Source.LogicalName()] = p
@ -76,6 +80,7 @@ func (c *PageCollections) refreshPageCaches() {
pathWithNoExtensions := path.Join(filepath.ToSlash(p.Source.Dir()), p.Source.TranslationBaseName()) pathWithNoExtensions := path.Join(filepath.ToSlash(p.Source.Dir()), p.Source.TranslationBaseName())
cache[pathWithNoExtensions] = p cache[pathWithNoExtensions] = p
} }
}
} }
default: default:

View file

@ -45,6 +45,12 @@ func (s *Site) renderPages(filter map[string]bool) error {
go pageRenderer(s, pages, results, wg) go pageRenderer(s, pages, results, wg)
} }
if len(s.headlessPages) > 0 {
wg.Add(1)
go headlessPagesPublisher(s, wg)
}
hasFilter := filter != nil && len(filter) > 0 hasFilter := filter != nil && len(filter) > 0
for _, page := range s.Pages { for _, page := range s.Pages {
@ -67,6 +73,22 @@ func (s *Site) renderPages(filter map[string]bool) error {
return nil return nil
} }
func headlessPagesPublisher(s *Site, wg *sync.WaitGroup) {
defer wg.Done()
for _, page := range s.headlessPages {
outFormat := page.outputFormats[0] // There is only one
pageOutput, err := newPageOutput(page, false, outFormat)
if err == nil {
page.mainPageOutput = pageOutput
err = pageOutput.renderResources()
}
if err != nil {
s.Log.ERROR.Printf("Failed to render resources for headless page %q: %s", page, err)
}
}
}
func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.WaitGroup) { func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()