shithub: hugo

Download patch

ref: 0432c64dd22e4610302162678bb93661ba68d758
parent: 5a0819b9b5eb9e79826cfa0a65f235d9821b1ac4
author: Bjørn Erik Pedersen <[email protected]>
date: Tue Jan 23 09:02:54 EST 2018

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

--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -508,7 +508,11 @@
 			shouldBuild := p.shouldBuild()
 			s.updateBuildStats(p)
 			if shouldBuild {
-				s.Pages = append(s.Pages, p)
+				if p.headless {
+					s.headlessPages = append(s.headlessPages, p)
+				} else {
+					s.Pages = append(s.Pages, p)
+				}
 			}
 		}
 	}
@@ -557,6 +561,10 @@
 	}
 
 	for _, p := range s.Pages {
+		pageChan <- p
+	}
+
+	for _, p := range s.headlessPages {
 		pageChan <- p
 	}
 
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -179,19 +179,26 @@
 	}
 
 	for _, s := range h.Sites {
-		for _, p := range s.Pages {
-			// May have been set in front matter
-			if len(p.outputFormats) == 0 {
-				p.outputFormats = s.outputFormats[p.Kind]
-			}
-			for _, r := range p.Resources.ByType(pageResourceType) {
-				r.(*Page).outputFormats = p.outputFormats
-			}
+		for _, pages := range []Pages{s.Pages, s.headlessPages} {
+			for _, p := range pages {
+				// May have been set in front matter
+				if len(p.outputFormats) == 0 {
+					p.outputFormats = s.outputFormats[p.Kind]
+				}
 
-			if err := p.initPaths(); err != nil {
-				return err
-			}
+				if p.headless {
+					// headless = 1 output format only
+					p.outputFormats = p.outputFormats[:1]
+				}
+				for _, r := range p.Resources.ByType(pageResourceType) {
+					r.(*Page).outputFormats = p.outputFormats
+				}
 
+				if err := p.initPaths(); err != nil {
+					return err
+				}
+
+			}
 		}
 		s.assembleMenus()
 		s.refreshPageCaches()
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -237,6 +237,13 @@
 	// Is set to a forward slashed path if this is a Page resources living in a folder below its owner.
 	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
 
 	scratch *Scratch
@@ -986,11 +993,17 @@
 
 // Permalink returns the absolute URL to this Page.
 func (p *Page) Permalink() string {
+	if p.headless {
+		return ""
+	}
 	return p.permalink
 }
 
 // RelPermalink gets a URL to the resource relative to the host.
 func (p *Page) RelPermalink() string {
+	if p.headless {
+		return ""
+	}
 	return p.relPermalink
 }
 
@@ -1150,6 +1163,13 @@
 				p.s.Log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path())
 			}
 			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":
 			p.Lastmod, err = cast.ToTimeE(v)
 			if err != nil {
--- a/hugolib/page_bundler_test.go
+++ b/hugolib/page_bundler_test.go
@@ -224,6 +224,87 @@
 
 }
 
+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) {
 	cfg, fs := newTestCfg()
 	assert := require.New(t)
--- a/hugolib/page_collections.go
+++ b/hugolib/page_collections.go
@@ -43,6 +43,9 @@
 	// Includes absolute all pages (of all types), including drafts etc.
 	rawAllPages Pages
 
+	// Includes headless bundles, i.e. bundles that produce no output for its content page.
+	headlessPages Pages
+
 	pageCache *cache.PartitionedLazyCache
 }
 
@@ -66,15 +69,17 @@
 				// 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
 				// what he/she is looking for.
-				for _, p := range c.AllRegularPages {
-					cache[filepath.ToSlash(p.Source.Path())] = p
-					// Ref/Relref supports this potentially ambiguous lookup.
-					cache[p.Source.LogicalName()] = p
+				for _, pageCollection := range []Pages{c.AllRegularPages, c.headlessPages} {
+					for _, p := range pageCollection {
+						cache[filepath.ToSlash(p.Source.Path())] = p
+						// Ref/Relref supports this potentially ambiguous lookup.
+						cache[p.Source.LogicalName()] = p
 
-					if s != nil && p.s == s {
-						// We need a way to get to the current language version.
-						pathWithNoExtensions := path.Join(filepath.ToSlash(p.Source.Dir()), p.Source.TranslationBaseName())
-						cache[pathWithNoExtensions] = p
+						if s != nil && p.s == s {
+							// We need a way to get to the current language version.
+							pathWithNoExtensions := path.Join(filepath.ToSlash(p.Source.Dir()), p.Source.TranslationBaseName())
+							cache[pathWithNoExtensions] = p
+						}
 					}
 
 				}
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -45,6 +45,12 @@
 		go pageRenderer(s, pages, results, wg)
 	}
 
+	if len(s.headlessPages) > 0 {
+		wg.Add(1)
+		go headlessPagesPublisher(s, wg)
+
+	}
+
 	hasFilter := filter != nil && len(filter) > 0
 
 	for _, page := range s.Pages {
@@ -65,6 +71,22 @@
 		return fmt.Errorf("Error(s) rendering pages: %s", err)
 	}
 	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) {