hugo/hugolib/site_render.go
Bjørn Erik Pedersen 51dd462c39 layout: Respect Type and Layout for list template selection
This commit also has some other nice side-effects:

* The layout logic is unified for all page types, which should make it less surprising
* Page.Render now supports all types
* The legacy "indexes" type is removed from the template lookup order. This is an undocumented type from early Hugo days. This means that having a template in, say, `/layouts/indexes/list.html` will no longer work.
* The theme override logic is improved. As an example, an `index.html` in theme will now wn over a `_default/list.html` in the project, which most will expect.

Fixes #3005
Fixes #3245
2018-01-14 19:59:51 +01:00

436 lines
10 KiB
Go

// Copyright 2016 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 (
"fmt"
"path"
"strings"
"sync"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/output"
bp "github.com/gohugoio/hugo/bufferpool"
)
// renderPages renders pages each corresponding to a markdown file.
// TODO(bep np doc
func (s *Site) renderPages(filter map[string]bool) error {
results := make(chan error)
pages := make(chan *Page)
errs := make(chan error)
go errorCollator(results, errs)
numWorkers := getGoMaxProcs() * 4
wg := &sync.WaitGroup{}
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go pageRenderer(s, pages, results, wg)
}
hasFilter := filter != nil && len(filter) > 0
for _, page := range s.Pages {
if hasFilter && !filter[page.RelPermalink()] {
continue
}
pages <- page
}
close(pages)
wg.Wait()
close(results)
err := <-errs
if err != nil {
return fmt.Errorf("Error(s) rendering pages: %s", err)
}
return nil
}
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 {
var (
pageOutput *PageOutput
err error
)
if i == 0 {
pageOutput, err = newPageOutput(page, false, outFormat)
page.mainPageOutput = pageOutput
}
if outFormat != page.s.rc.Format {
// Will be rendered ... later.
continue
}
if pageOutput == nil {
pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat)
}
if err != nil {
s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, page, err)
continue
}
// 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.Log.ERROR.Printf("Failed to render resources for page %q: %s", page, err)
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 output %q for page %q: %s", outFormat.Name, page, err)
continue
}
}
switch pageOutput.outputFormat.Name {
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
}
s.Log.DEBUG.Printf("Render %s to %q with layouts %q", pageOutput.Kind, targetPath, layouts)
if err := s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "page "+pageOutput.FullFilePath(), targetPath, pageOutput, layouts...); err != nil {
results <- err
}
if pageOutput.IsNode() {
if err := s.renderPaginator(pageOutput); 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
}
// TODO(bep) do better
link := newOutputFormat(p.Page, p.outputFormat).Permalink()
if err := s.writeDestAlias(target, link, nil); err != nil {
return err
}
pagers := p.paginator.Pagers()
for i, pager := range pagers {
if i == 0 {
// already created
continue
}
pagerNode, err := p.copy()
if err != nil {
return err
}
pagerNode.origOnCopy = p.Page
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
}
}
}
return nil
}
func (s *Site) renderRSS(p *PageOutput) error {
if !s.isEnabled(kindRSS) {
return nil
}
if s.Cfg.GetBool("disableRSS") {
return nil
}
p.Kind = kindRSS
limit := s.Cfg.GetInt("rssLimit")
if limit >= 0 && len(p.Pages) > limit {
p.Pages = p.Pages[:limit]
p.Data["Pages"] = p.Pages
}
layouts, err := s.layoutHandler.For(
p.layoutDescriptor,
p.outputFormat)
if 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...)
}
func (s *Site) render404() error {
if !s.isEnabled(kind404) {
return nil
}
if s.Cfg.GetBool("disable404") {
return nil
}
p := s.newNodePage(kind404)
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 {
return err
}
nfLayouts := []string{"404.html"}
htmlOut := output.HTMLFormat
htmlOut.BaseName = "404"
pageOutput, err := newPageOutput(p, false, htmlOut)
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)
}
return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "404 page", targetPath, pageOutput, s.appendThemeTemplates(nfLayouts)...)
}
func (s *Site) renderSitemap() error {
if !s.isEnabled(kindSitemap) {
return nil
}
if s.Cfg.GetBool("disableSitemap") {
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
// TODO(bep) we have several of these
if err := page.initTargetPathDescriptor(); 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
}
if page.Sitemap.Filename == "" {
page.Sitemap.Filename = sitemapDefault.Filename
}
}
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, s.appendThemeTemplates(smLayouts)...)
}
func (s *Site) renderRobotsTXT() error {
if !s.isEnabled(kindRobotsTXT) {
return nil
}
if !s.Cfg.GetBool("enableRobotsTXT") {
return nil
}
n := s.newNodePage(kindRobotsTXT)
if err := n.initTargetPathDescriptor(); err != nil {
return err
}
n.Data["Pages"] = s.Pages
n.Pages = s.Pages
rLayouts := []string{"robots.txt", "_default/robots.txt", "_internal/_default/robots.txt"}
outBuffer := bp.GetBuffer()
defer bp.PutBuffer(outBuffer)
if err := s.renderForLayouts("robots", n, outBuffer, s.appendThemeTemplates(rLayouts)...); err != nil {
helpers.DistinctWarnLog.Println(err)
return nil
}
if outBuffer.Len() == 0 {
return nil
}
return s.publish(&s.PathSpec.ProcessingStats.Pages, "robots.txt", outBuffer)
}
// 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 {
continue
}
for _, f := range p.outputFormats {
if !f.IsHTML {
continue
}
o := newOutputFormat(p, f)
plink := o.Permalink()
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()
if s.owner.multihost && !strings.HasPrefix(a, "/"+lang) {
// These need to be in its language root.
a = path.Join(lang, a)
}
if err := s.writeDestAlias(a, plink, p); err != nil {
return err
}
}
}
}
if s.owner.multilingual.enabled() && !s.owner.IsMultihost() {
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, 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, nil); err != nil {
return err
}
}
}
return nil
}