summaryrefslogtreecommitdiffhomepage
path: root/hugolib/site_render.go
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2019-01-02 12:33:26 +0100
committerBjørn Erik Pedersen <[email protected]>2019-03-23 18:51:22 +0100
commit597e418cb02883418f2cebb41400e8e61413f651 (patch)
tree177ad9c540b2583b6dab138c9f0490d28989c7f7 /hugolib/site_render.go
parent44f5c1c14cb1f42cc5f01739c289e9cfc83602af (diff)
downloadhugo-597e418cb02883418f2cebb41400e8e61413f651.tar.gz
hugo-597e418cb02883418f2cebb41400e8e61413f651.zip
Make Page an interface
The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct. This is all a preparation step for issue #5074, "pages from other data sources". But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes. Most notable changes: * The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday. This means that any markdown will partake in the global ToC and footnote context etc. * The Custom Output formats are now "fully virtualized". This removes many of the current limitations. * The taxonomy list type now has a reference to the `Page` object. This improves the taxonomy template `.Title` situation and make common template constructs much simpler. See #5074 Fixes #5763 Fixes #5758 Fixes #5090 Fixes #5204 Fixes #4695 Fixes #5607 Fixes #5707 Fixes #5719 Fixes #3113 Fixes #5706 Fixes #5767 Fixes #5723 Fixes #5769 Fixes #5770 Fixes #5771 Fixes #5759 Fixes #5776 Fixes #5777 Fixes #5778
Diffstat (limited to 'hugolib/site_render.go')
-rw-r--r--hugolib/site_render.go444
1 files changed, 176 insertions, 268 deletions
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
index 4ce2b4c53..a6cf4bafa 100644
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Hugo Authors. All rights reserved.
+// Copyright 2019 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.
@@ -19,17 +19,44 @@ import (
"strings"
"sync"
+ "github.com/gohugoio/hugo/output"
"github.com/pkg/errors"
- "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
)
+type siteRenderContext struct {
+ cfg *BuildCfg
+
+ // Zero based index for all output formats combined.
+ sitesOutIdx int
+
+ // Zero based index of the output formats configured within a Site.
+ outIdx int
+
+ multihost bool
+}
+
+// Whether to render 404.html, robotsTXT.txt which usually is rendered
+// once only in the site root.
+func (s siteRenderContext) renderSingletonPages() bool {
+ if s.multihost {
+ // 1 per site
+ return s.outIdx == 0
+ }
+
+ // 1 for all sites
+ return s.sitesOutIdx == 0
+
+}
+
// renderPages renders pages each corresponding to a markdown file.
// TODO(bep np doc
-func (s *Site) renderPages(cfg *BuildCfg) error {
+func (s *Site) renderPages(ctx *siteRenderContext) error {
results := make(chan error)
- pages := make(chan *Page)
+ pages := make(chan *pageState)
errs := make(chan error)
go s.errorCollator(results, errs)
@@ -40,17 +67,25 @@ func (s *Site) renderPages(cfg *BuildCfg) error {
for i := 0; i < numWorkers; i++ {
wg.Add(1)
- go pageRenderer(s, pages, results, wg)
+ go pageRenderer(ctx, s, pages, results, wg)
}
- if !cfg.PartialReRender && len(s.headlessPages) > 0 {
+ cfg := ctx.cfg
+
+ if !cfg.PartialReRender && ctx.outIdx == 0 && len(s.headlessPages) > 0 {
wg.Add(1)
go headlessPagesPublisher(s, wg)
}
- for _, page := range s.Pages {
+L:
+ for _, page := range s.workAllPages {
if cfg.shouldRender(page) {
- pages <- page
+ select {
+ case <-s.h.Done():
+ break L
+ default:
+ pages <- page
+ }
}
}
@@ -69,207 +104,99 @@ func (s *Site) renderPages(cfg *BuildCfg) error {
func headlessPagesPublisher(s *Site, wg *sync.WaitGroup) {
defer wg.Done()
- for _, page := range s.headlessPages {
- outFormat := page.outputFormats[0] // There is only one
- if outFormat.Name != s.rc.Format.Name {
- // Avoid double work.
- continue
- }
- pageOutput, err := newPageOutput(page, false, 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)
+ for _, p := range s.headlessPages {
+ if err := p.renderResources(); err != nil {
+ s.SendError(p.errorf(err, "failed to render page resources"))
}
}
}
-func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.WaitGroup) {
- defer wg.Done()
-
- for page := range pages {
-
- for i, outFormat := range page.outputFormats {
+func pageRenderer(
+ ctx *siteRenderContext,
+ s *Site,
+ pages <-chan *pageState,
+ results chan<- error,
+ wg *sync.WaitGroup) {
- if outFormat.Name != page.s.rc.Format.Name {
- // Will be rendered ... later.
- continue
- }
+ defer wg.Done()
- var (
- pageOutput *PageOutput
- err error
- )
+ for p := range pages {
+ f := p.outputFormat()
- if i == 0 {
- pageOutput = page.mainPageOutput
- } else {
- pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat, true)
- }
+ // TODO(bep) get rid of this odd construct. RSS is an output format.
+ if f.Name == "RSS" && !s.isEnabled(kindRSS) {
+ continue
+ }
- if err != nil {
- s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, page, err)
+ if ctx.outIdx == 0 {
+ if err := p.renderResources(); err != nil {
+ s.SendError(p.errorf(err, "failed to render page resources"))
continue
}
+ }
- if pageOutput == nil {
- panic("no pageOutput")
- }
-
- // We only need to re-publish the resources if the output format is different
- // from all of the previous (e.g. the "amp" use case).
- shouldRender := i == 0
- if i > 0 {
- for j := i; j >= 0; j-- {
- if outFormat.Path != page.outputFormats[j].Path {
- shouldRender = true
- } else {
- shouldRender = false
- }
- }
- }
-
- if shouldRender {
- if err := pageOutput.renderResources(); err != nil {
- s.SendError(page.errorf(err, "failed to render page resources"))
- continue
- }
- }
-
- var layouts []string
-
- if page.selfLayout != "" {
- layouts = []string{page.selfLayout}
- } else {
- layouts, err = s.layouts(pageOutput)
- if err != nil {
- s.Log.ERROR.Printf("Failed to resolve layout for output %q for page %q: %s", outFormat.Name, page, err)
- continue
- }
- }
-
- switch pageOutput.outputFormat.Name {
+ layouts, err := p.getLayouts()
+ if err != nil {
+ s.Log.ERROR.Printf("Failed to resolve layout for output %q for page %q: %s", f.Name, p, err)
+ continue
+ }
- case "RSS":
- if err := s.renderRSS(pageOutput); err != nil {
- results <- err
- }
- default:
- targetPath, err := pageOutput.targetPath()
- if err != nil {
- s.Log.ERROR.Printf("Failed to create target path for output %q for page %q: %s", outFormat.Name, page, err)
- continue
- }
+ targetPath := p.targetPaths().TargetFilename
- s.Log.DEBUG.Printf("Render %s to %q with layouts %q", pageOutput.Kind, targetPath, layouts)
+ if targetPath == "" {
+ s.Log.ERROR.Printf("Failed to create target path for output %q for page %q: %s", f.Name, p, err)
+ continue
+ }
- if err := s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "page "+pageOutput.FullFilePath(), targetPath, pageOutput, layouts...); err != nil {
- results <- err
- }
+ if err := s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "page "+p.Title(), targetPath, p, layouts...); err != nil {
+ results <- err
+ }
- // Only render paginators for the main output format
- if i == 0 && pageOutput.IsNode() {
- if err := s.renderPaginator(pageOutput); err != nil {
- results <- err
- }
- }
+ if p.paginator != nil && p.paginator.current != nil {
+ if err := s.renderPaginator(p, layouts); err != nil {
+ results <- err
}
-
}
}
}
// renderPaginator must be run after the owning Page has been rendered.
-func (s *Site) renderPaginator(p *PageOutput) error {
- if p.paginator != nil {
- s.Log.DEBUG.Printf("Render paginator for page %q", p.Path())
- paginatePath := s.Cfg.GetString("paginatePath")
-
- // write alias for page 1
- addend := fmt.Sprintf("/%s/%d", paginatePath, 1)
- target, err := p.createTargetPath(p.outputFormat, false, addend)
- if err != nil {
- return err
- }
+func (s *Site) renderPaginator(p *pageState, layouts []string) error {
- // TODO(bep) do better
- link := newOutputFormat(p.Page, p.outputFormat).Permalink()
- if err := s.writeDestAlias(target, link, p.outputFormat, nil); err != nil {
- return err
- }
+ paginatePath := s.Cfg.GetString("paginatePath")
- pagers := p.paginator.Pagers()
-
- for i, pager := range pagers {
- if i == 0 {
- // already created
- continue
- }
+ d := p.targetPathDescriptor
+ f := p.s.rc.Format
+ d.Type = f
- pagerNode, err := p.copy()
- if err != nil {
- return err
- }
+ // Rewind
+ p.paginator.current = p.paginator.current.First()
- pagerNode.origOnCopy = p.Page
+ // Write alias for page 1
+ d.Addends = fmt.Sprintf("/%s/%d", paginatePath, 1)
+ targetPaths := page.CreateTargetPaths(d)
- pagerNode.paginator = pager
- if pager.TotalPages() > 0 {
- first, _ := pager.page(0)
- pagerNode.Date = first.Date
- pagerNode.Lastmod = first.Lastmod
- }
-
- pageNumber := i + 1
- addend := fmt.Sprintf("/%s/%d", paginatePath, pageNumber)
- targetPath, _ := p.targetPath(addend)
- layouts, err := p.layouts()
-
- if err != nil {
- return err
- }
-
- if err := s.renderAndWritePage(
- &s.PathSpec.ProcessingStats.PaginatorPages,
- pagerNode.title,
- targetPath, pagerNode, layouts...); err != nil {
- return err
- }
-
- }
+ if err := s.writeDestAlias(targetPaths.TargetFilename, p.Permalink(), f, nil); err != nil {
+ return err
}
- return nil
-}
-func (s *Site) renderRSS(p *PageOutput) error {
+ // Render pages for the rest
+ for current := p.paginator.current.Next(); current != nil; current = current.Next() {
- if !s.isEnabled(kindRSS) {
- return nil
- }
-
- limit := s.Cfg.GetInt("rssLimit")
- if limit >= 0 && len(p.Pages) > limit {
- p.Pages = p.Pages[:limit]
- p.data["Pages"] = p.Pages
- }
+ p.paginator.current = current
+ d.Addends = fmt.Sprintf("/%s/%d", paginatePath, current.PageNumber())
+ targetPaths := page.CreateTargetPaths(d)
- layouts, err := s.layoutHandler.For(
- p.layoutDescriptor,
- p.outputFormat)
- if err != nil {
- return err
- }
+ if err := s.renderAndWritePage(
+ &s.PathSpec.ProcessingStats.PaginatorPages,
+ p.Title(),
+ targetPaths.TargetFilename, p, layouts...); err != nil {
+ return err
+ }
- targetPath, err := p.targetPath()
- if err != nil {
- return err
}
- return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Pages, p.title,
- targetPath, p, layouts...)
+ return nil
}
func (s *Site) render404() error {
@@ -277,33 +204,29 @@ func (s *Site) render404() error {
return nil
}
- p := s.newNodePage(kind404)
+ p, err := newPageStandalone(&pageMeta{
+ s: s,
+ kind: kind404,
+ urlPaths: pagemeta.URLPath{
+ URL: path.Join(s.GetURLLanguageBasePath(), "404.html"),
+ },
+ },
+ output.HTMLFormat,
+ )
- p.title = "404 Page not found"
- p.data["Pages"] = s.Pages
- p.Pages = s.Pages
- p.URLPath.URL = "404.html"
-
- if err := p.initTargetPathDescriptor(); err != nil {
+ if err != nil {
return err
}
nfLayouts := []string{"404.html"}
- htmlOut := output.HTMLFormat
- htmlOut.BaseName = "404"
-
- pageOutput, err := newPageOutput(p, false, false, htmlOut)
- if err != nil {
- return err
- }
+ targetPath := p.targetPaths().TargetFilename
- targetPath, err := pageOutput.targetPath()
- if err != nil {
- s.Log.ERROR.Printf("Failed to create target path for page %q: %s", p, err)
+ if targetPath == "" {
+ return errors.New("failed to create targetPath for 404 page")
}
- return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "404 page", targetPath, pageOutput, nfLayouts...)
+ return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "404 page", targetPath, p, nfLayouts...)
}
func (s *Site) renderSitemap() error {
@@ -311,50 +234,28 @@ func (s *Site) renderSitemap() error {
return nil
}
- sitemapDefault := parseSitemap(s.Cfg.GetStringMap("sitemap"))
-
- n := s.newNodePage(kindSitemap)
-
- // Include all pages (regular, home page, taxonomies etc.)
- pages := s.Pages
-
- page := s.newNodePage(kindSitemap)
- page.URLPath.URL = ""
- if err := page.initTargetPathDescriptor(); err != nil {
- return err
- }
- page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq
- page.Sitemap.Priority = sitemapDefault.Priority
- page.Sitemap.Filename = sitemapDefault.Filename
-
- n.data["Pages"] = pages
- n.Pages = pages
+ p, err := newPageStandalone(&pageMeta{
+ s: s,
+ kind: kindSitemap,
+ urlPaths: pagemeta.URLPath{
+ URL: s.siteCfg.sitemap.Filename,
+ }},
+ output.HTMLFormat,
+ )
- // TODO(bep) we have several of these
- if err := page.initTargetPathDescriptor(); err != nil {
+ if err != nil {
return err
}
- // TODO(bep) this should be done somewhere else
- for _, page := range pages {
- if page.Sitemap.ChangeFreq == "" {
- page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq
- }
-
- if page.Sitemap.Priority == -1 {
- page.Sitemap.Priority = sitemapDefault.Priority
- }
+ targetPath := p.targetPaths().TargetFilename
- if page.Sitemap.Filename == "" {
- page.Sitemap.Filename = sitemapDefault.Filename
- }
+ if targetPath == "" {
+ return errors.New("failed to create targetPath for sitemap")
}
smLayouts := []string{"sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml"}
- addLanguagePrefix := n.Site.IsMultiLingual()
- return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemap",
- n.addLangPathPrefixIfFlagSet(page.Sitemap.Filename, addLanguagePrefix), n, smLayouts...)
+ return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemap", targetPath, p, smLayouts...)
}
func (s *Site) renderRobotsTXT() error {
@@ -366,53 +267,50 @@ func (s *Site) renderRobotsTXT() error {
return nil
}
- p := s.newNodePage(kindRobotsTXT)
- if err := p.initTargetPathDescriptor(); err != nil {
- return err
- }
- p.data["Pages"] = s.Pages
- p.Pages = s.Pages
-
- rLayouts := []string{"robots.txt", "_default/robots.txt", "_internal/_default/robots.txt"}
+ p, err := newPageStandalone(&pageMeta{
+ s: s,
+ kind: kindRobotsTXT,
+ urlPaths: pagemeta.URLPath{
+ URL: path.Join(s.GetURLLanguageBasePath(), "robots.txt"),
+ },
+ },
+ output.RobotsTxtFormat)
- pageOutput, err := newPageOutput(p, false, false, output.RobotsTxtFormat)
if err != nil {
return err
}
- targetPath, err := pageOutput.targetPath()
- if err != nil {
- s.Log.ERROR.Printf("Failed to create target path for page %q: %s", p, err)
- }
+ rLayouts := []string{"robots.txt", "_default/robots.txt", "_internal/_default/robots.txt"}
- return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", targetPath, pageOutput, rLayouts...)
+ return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", p.targetPaths().TargetFilename, p, rLayouts...)
}
// renderAliases renders shell pages that simply have a redirect in the header.
func (s *Site) renderAliases() error {
- for _, p := range s.Pages {
- if len(p.Aliases) == 0 {
+ for _, p := range s.workAllPages {
+
+ if len(p.Aliases()) == 0 {
continue
}
- for _, f := range p.outputFormats {
- if !f.IsHTML {
+ for _, of := range p.OutputFormats() {
+ if !of.Format.IsHTML {
continue
}
- o := newOutputFormat(p, f)
- plink := o.Permalink()
+ plink := of.Permalink()
+ f := of.Format
- for _, a := range p.Aliases {
+ for _, a := range p.Aliases() {
if f.Path != "" {
// Make sure AMP and similar doesn't clash with regular aliases.
a = path.Join(a, f.Path)
}
- lang := p.Lang()
+ lang := p.Language().Lang
- if s.owner.multihost && !strings.HasPrefix(a, "/"+lang) {
+ if s.h.multihost && !strings.HasPrefix(a, "/"+lang) {
// These need to be in its language root.
a = path.Join(lang, a)
}
@@ -424,22 +322,32 @@ func (s *Site) renderAliases() error {
}
}
- if s.owner.multilingual.enabled() && !s.owner.IsMultihost() {
- html, found := s.outputFormatsConfig.GetByName("HTML")
- if found {
- mainLang := s.owner.multilingual.DefaultLang
- if s.Info.defaultContentLanguageInSubdir {
- mainLangURL := s.PathSpec.AbsURL(mainLang.Lang, false)
- s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
- if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil {
- return err
- }
- } else {
- mainLangURL := s.PathSpec.AbsURL("", false)
- s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
- if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, html, nil); err != nil {
- return err
- }
+ return nil
+}
+
+// renderMainLanguageRedirect creates a redirect to the main language home,
+// depending on if it lives in sub folder (e.g. /en) or not.
+func (s *Site) renderMainLanguageRedirect() error {
+
+ if !s.h.multilingual.enabled() || s.h.IsMultihost() {
+ // No need for a redirect
+ return nil
+ }
+
+ html, found := s.outputFormatsConfig.GetByName("HTML")
+ if found {
+ mainLang := s.h.multilingual.DefaultLang
+ if s.Info.defaultContentLanguageInSubdir {
+ mainLangURL := s.PathSpec.AbsURL(mainLang.Lang, false)
+ s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
+ if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil {
+ return err
+ }
+ } else {
+ mainLangURL := s.PathSpec.AbsURL("", false)
+ s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
+ if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, html, nil); err != nil {
+ return err
}
}
}