1
0
Fork 0
mirror of https://github.com/gohugoio/hugo.git synced 2025-03-31 11:35:02 +00:00

hugolib: Fix some shortcode vs .Content corner cases

This is a follow-up to . There were some assumptions in that implementation that did not hold water in all situations.

This commit simplifies the content lazy initalization making it more robust.

Fixes 
This commit is contained in:
Bjørn Erik Pedersen 2018-04-24 05:57:33 +02:00
parent 44e47478d0
commit 288c396439
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
8 changed files with 175 additions and 109 deletions

View file

@ -559,41 +559,14 @@ func (h *HugoSites) setupTranslations() {
} }
} }
func (s *Site) preparePagesForRender(cfg *BuildCfg) { func (s *Site) preparePagesForRender(start bool) {
pageChan := make(chan *Page)
wg := &sync.WaitGroup{}
numWorkers := getGoMaxProcs() * 4
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(pages <-chan *Page, wg *sync.WaitGroup) {
defer wg.Done()
for p := range pages {
p.setContentInit(cfg)
// In most cases we could delay the content init until rendering time,
// but there could be use cases where the templates would depend
// on state set in the shortcodes (.Page.Scratch.Set), so we
// need to do this early. This will do the needed recursion.
p.initContent()
}
}(pageChan, wg)
}
for _, p := range s.Pages { for _, p := range s.Pages {
pageChan <- p p.setContentInit(start)
} }
for _, p := range s.headlessPages { for _, p := range s.headlessPages {
pageChan <- p p.setContentInit(start)
} }
close(pageChan)
wg.Wait()
} }
// Pages returns all pages for all sites. // Pages returns all pages for all sites.

View file

@ -222,10 +222,21 @@ func (h *HugoSites) assemble(config *BuildCfg) error {
func (h *HugoSites) render(config *BuildCfg) error { func (h *HugoSites) render(config *BuildCfg) error {
for _, s := range h.Sites { for _, s := range h.Sites {
s.initRenderFormats() s.initRenderFormats()
for i, rf := range s.renderFormats { }
s.rc = &siteRenderingContext{Format: rf}
s.preparePagesForRender(config) for _, s := range h.Sites {
for i, rf := range s.renderFormats {
for _, s2 := range h.Sites {
// We render site by site, but since the content is lazily rendered
// and a site can "borrow" content from other sites, every site
// needs this set.
s2.rc = &siteRenderingContext{Format: rf}
isRenderingSite := s == s2
s2.preparePagesForRender(isRenderingSite && i == 0)
}
if !config.SkipRender { if !config.SkipRender {
if err := s.render(config, i); err != nil { if err := s.render(config, i); err != nil {

View file

@ -38,6 +38,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -129,9 +130,6 @@ type Page struct {
// Params contains configuration defined in the params section of page frontmatter. // Params contains configuration defined in the params section of page frontmatter.
params map[string]interface{} params map[string]interface{}
// Called when needed to init the content (render shortcodes etc.).
contentInitFn func(p *Page) func()
// Content sections // Content sections
contentv template.HTML contentv template.HTML
summary template.HTML summary template.HTML
@ -270,7 +268,14 @@ type Page struct {
targetPathDescriptorPrototype *targetPathDescriptor targetPathDescriptorPrototype *targetPathDescriptor
} }
func stackTrace() string {
trace := make([]byte, 2000)
runtime.Stack(trace, true)
return string(trace)
}
func (p *Page) initContent() { func (p *Page) initContent() {
p.contentInit.Do(func() { p.contentInit.Do(func() {
// This careful dance is here to protect against circular loops in shortcode/content // This careful dance is here to protect against circular loops in shortcode/content
// constructs. // constructs.
@ -284,9 +289,12 @@ func (p *Page) initContent() {
p.contentInitMu.Lock() p.contentInitMu.Lock()
defer p.contentInitMu.Unlock() defer p.contentInitMu.Unlock()
if p.contentInitFn != nil { err = p.prepareForRender()
p.contentInitFn(p)() if err != nil {
p.s.Log.ERROR.Printf("Failed to prepare page %q for render: %s", p.Path(), err)
return
} }
if len(p.summary) == 0 { if len(p.summary) == 0 {
if err = p.setAutoSummary(); err != nil { if err = p.setAutoSummary(); err != nil {
err = fmt.Errorf("Failed to set user auto summary for page %q: %s", p.pathOrTitle(), err) err = fmt.Errorf("Failed to set user auto summary for page %q: %s", p.pathOrTitle(), err)
@ -297,7 +305,7 @@ func (p *Page) initContent() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
p.s.Log.WARN.Printf(`WARNING: Timed out creating content for page %q (.Content will be empty). This is most likely a circular shortcode content loop that should be fixed. If this is just a shortcode calling a slow remote service, try to set "timeout=20000" (or higher, value is in milliseconds) in config.toml.`, p.pathOrTitle()) p.s.Log.WARN.Printf(`WARNING: Timed out creating content for page %q (.Content will be empty). This is most likely a circular shortcode content loop that should be fixed. If this is just a shortcode calling a slow remote service, try to set "timeout=20000" (or higher, value is in milliseconds) in config.toml.\n`, p.pathOrTitle())
case err := <-c: case err := <-c:
if err != nil { if err != nil {
p.s.Log.ERROR.Println(err) p.s.Log.ERROR.Println(err)
@ -420,14 +428,8 @@ type pageContentInit struct {
plainWordsInit sync.Once plainWordsInit sync.Once
} }
func (p *Page) resetContent(init func(page *Page) func()) { func (p *Page) resetContent() {
p.pageContentInit = &pageContentInit{} p.pageContentInit = &pageContentInit{}
if init == nil {
init = func(page *Page) func() {
return func() {}
}
}
p.contentInitFn = init
} }
// IsNode returns whether this is an item of one of the list types in Hugo, // IsNode returns whether this is an item of one of the list types in Hugo,
@ -1165,49 +1167,40 @@ func (p *Page) subResourceTargetPathFactory(base string) string {
return path.Join(p.relTargetPathBase, base) return path.Join(p.relTargetPathBase, base)
} }
func (p *Page) setContentInit(cfg *BuildCfg) error { func (p *Page) setContentInit(start bool) error {
if !p.shouldRenderTo(p.s.rc.Format) {
// No need to prepare
return nil
}
var shortcodeUpdate bool if start {
// This is a new language.
p.shortcodeState.clearDelta()
}
updated := true
if p.shortcodeState != nil { if p.shortcodeState != nil {
shortcodeUpdate = p.shortcodeState.updateDelta() updated = p.shortcodeState.updateDelta()
} }
resetFunc := func(page *Page) func() { if updated {
return func() { p.resetContent()
err := page.prepareForRender(cfg)
if err != nil {
p.s.Log.ERROR.Printf("Failed to prepare page %q for render: %s", page.Path(), err)
}
}
} }
if shortcodeUpdate || cfg.whatChanged.other {
p.resetContent(resetFunc)
}
// Handle bundled pages.
for _, r := range p.Resources.ByType(pageResourceType) { for _, r := range p.Resources.ByType(pageResourceType) {
shortcodeUpdate = false p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages)
bp := r.(*Page) bp := r.(*Page)
if start {
if bp.shortcodeState != nil { bp.shortcodeState.clearDelta()
shortcodeUpdate = bp.shortcodeState.updateDelta()
} }
if bp.shortcodeState != nil {
if shortcodeUpdate || cfg.whatChanged.other { updated = bp.shortcodeState.updateDelta()
p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages) }
bp.resetContent(resetFunc) if updated {
bp.resetContent()
} }
} }
return nil return nil
} }
func (p *Page) prepareForRender(cfg *BuildCfg) error { func (p *Page) prepareForRender() error {
s := p.s s := p.s
// If we got this far it means that this is either a new Page pointer // If we got this far it means that this is either a new Page pointer
@ -1217,7 +1210,7 @@ func (p *Page) prepareForRender(cfg *BuildCfg) error {
// If in watch mode or if we have multiple output formats, // If in watch mode or if we have multiple output formats,
// we need to keep the original so we can // we need to keep the original so we can
// potentially repeat this process on rebuild. // potentially repeat this process on rebuild.
needsACopy := p.s.running() || len(p.outputFormats) > 1 needsACopy := s.running() || len(p.outputFormats) > 1
var workContentCopy []byte var workContentCopy []byte
if needsACopy { if needsACopy {
workContentCopy = make([]byte, len(p.workContent)) workContentCopy = make([]byte, len(p.workContent))

View file

@ -15,7 +15,6 @@ package hugolib
import ( import (
"fmt" "fmt"
"html/template"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
@ -168,11 +167,16 @@ func setSortVals(dates [4]time.Time, titles [4]string, weights [4]int, pages Pag
pages[len(dates)-1-i].linkTitle = pages[i].title + "l" pages[len(dates)-1-i].linkTitle = pages[i].title + "l"
pages[len(dates)-1-i].PublishDate = dates[i] pages[len(dates)-1-i].PublishDate = dates[i]
pages[len(dates)-1-i].ExpiryDate = dates[i] pages[len(dates)-1-i].ExpiryDate = dates[i]
pages[len(dates)-1-i].contentv = template.HTML(titles[i] + "_content") pages[len(dates)-1-i].workContent = []byte(titles[i] + "_content")
} }
lastLastMod := pages[2].Lastmod lastLastMod := pages[2].Lastmod
pages[2].Lastmod = pages[1].Lastmod pages[2].Lastmod = pages[1].Lastmod
pages[1].Lastmod = lastLastMod pages[1].Lastmod = lastLastMod
for _, p := range pages {
p.resetContent()
}
} }
func createSortTestPages(s *Site, num int) Pages { func createSortTestPages(s *Site, num int) Pages {

View file

@ -525,7 +525,7 @@ func checkPageDate(t *testing.T, page *Page, time time.Time) {
} }
func checkTruncation(t *testing.T, page *Page, shouldBe bool, msg string) { func checkTruncation(t *testing.T, page *Page, shouldBe bool, msg string) {
if page.summary == "" { if page.Summary() == "" {
t.Fatal("page has no summary, can not check truncation") t.Fatal("page has no summary, can not check truncation")
} }
if page.truncated != shouldBe { if page.truncated != shouldBe {
@ -722,8 +722,8 @@ func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) {
p := s.RegularPages[0] p := s.RegularPages[0]
if p.summary != template.HTML("<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a href=\"#fn:1\">1</a></sup>\n</p>") { if p.Summary() != template.HTML("<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a href=\"#fn:1\">1</a></sup>\n</p>") {
t.Fatalf("Got summary:\n%q", p.summary) t.Fatalf("Got summary:\n%q", p.Summary())
} }
if p.content() != template.HTML("<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a href=\"#fn:1\">1</a></sup>\n</p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:1\">Many people say so.\n <a class=\"footnote-return\" href=\"#fnref:1\"><sup>[return]</sup></a></li>\n</ol>\n</div>") { if p.content() != template.HTML("<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a href=\"#fn:1\">1</a></sup>\n</p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:1\">Many people say so.\n <a class=\"footnote-return\" href=\"#fnref:1\"><sup>[return]</sup></a></li>\n</ol>\n</div>") {
@ -876,8 +876,8 @@ func TestSummaryWithHTMLTagsOnNextLine(t *testing.T) {
assertFunc := func(t *testing.T, ext string, pages Pages) { assertFunc := func(t *testing.T, ext string, pages Pages) {
p := pages[0] p := pages[0]
require.Contains(t, p.summary, "Happy new year everyone!") require.Contains(t, p.Summary(), "Happy new year everyone!")
require.NotContains(t, p.summary, "User interface") require.NotContains(t, p.Summary(), "User interface")
} }
testAllMarkdownEnginesForPages(t, assertFunc, nil, `--- testAllMarkdownEnginesForPages(t, assertFunc, nil, `---
@ -1511,8 +1511,8 @@ func TestPageSimpleMethods(t *testing.T) {
} { } {
p, _ := s.NewPage("Test") p, _ := s.NewPage("Test")
p.contentv = "<h1>Do Be Do Be Do</h1>" p.workContent = []byte("<h1>Do Be Do Be Do</h1>")
p.initContent() p.resetContent()
if !this.assertFunc(p) { if !this.assertFunc(p) {
t.Errorf("[%d] Page method error", i) t.Errorf("[%d] Page method error", i)
} }

View file

@ -366,7 +366,12 @@ func (s *shortcodeHandler) updateDelta() bool {
s.contentShortcodes = createShortcodeRenderers(s.shortcodes, s.p.withoutContent()) s.contentShortcodes = createShortcodeRenderers(s.shortcodes, s.p.withoutContent())
}) })
contentShortcodes := s.contentShortcodesForOutputFormat(s.p.s.rc.Format) if !s.p.shouldRenderTo(s.p.s.rc.Format) {
// TODO(bep) add test for this re translations
return false
}
of := s.p.s.rc.Format
contentShortcodes := s.contentShortcodesForOutputFormat(of)
if s.contentShortcodesDelta == nil || s.contentShortcodesDelta.Len() == 0 { if s.contentShortcodesDelta == nil || s.contentShortcodesDelta.Len() == 0 {
s.contentShortcodesDelta = contentShortcodes s.contentShortcodesDelta = contentShortcodes
@ -387,13 +392,19 @@ func (s *shortcodeHandler) updateDelta() bool {
return delta.Len() > 0 return delta.Len() > 0
} }
func (s *shortcodeHandler) clearDelta() {
if s == nil {
return
}
s.contentShortcodesDelta = newOrderedMap()
}
func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) *orderedMap { func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) *orderedMap {
contentShortcodesForOuputFormat := newOrderedMap() contentShortcodesForOuputFormat := newOrderedMap()
lang := s.p.Lang() lang := s.p.Lang()
for _, key := range s.shortcodes.Keys() { for _, key := range s.shortcodes.Keys() {
shortcodePlaceholder := key.(string) shortcodePlaceholder := key.(string)
// shortcodePlaceholder := s.shortcodes.getShortcode(key)
key := newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder) key := newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)
renderFn, found := s.contentShortcodes.Get(key) renderFn, found := s.contentShortcodes.Get(key)

View file

@ -22,6 +22,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/spf13/viper"
jww "github.com/spf13/jwalterweatherman" jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -884,7 +886,83 @@ func TestScKey(t *testing.T) {
} }
func TestPreserveShortcodeOrder(t *testing.T) { func TestShortcodeGetContent(t *testing.T) {
t.Parallel()
assert := require.New(t)
contentShortcode := `
{{- $t := .Get 0 -}}
{{- $p := .Get 1 -}}
{{- $k := .Get 2 -}}
{{- $page := $.Page.Site.GetPage "page" $p -}}
{{ if $page }}
{{- if eq $t "bundle" -}}
{{- .Scratch.Set "p" ($page.Resources.GetMatch (printf "%s*" $k)) -}}
{{- else -}}
{{- $.Scratch.Set "p" $page -}}
{{- end -}}P1:{{ .Page.Content }}|P2:{{ $p := ($.Scratch.Get "p") }}{{ $p.Title }}/{{ $p.Content }}|
{{- else -}}
{{- errorf "Page %s is nil" $p -}}
{{- end -}}
`
var templates []string
var content []string
contentWithShortcodeTemplate := `---
title: doc%s
weight: %d
---
Logo:{{< c "bundle" "b1" "logo.png" >}}:P1: {{< c "page" "section1/p1" "" >}}:BP1:{{< c "bundle" "b1" "bp1" >}}`
simpleContentTemplate := `---
title: doc%s
weight: %d
---
C-%s`
v := viper.New()
v.Set("timeout", 500)
templates = append(templates, []string{"shortcodes/c.html", contentShortcode}...)
templates = append(templates, []string{"_default/single.html", "Single Content: {{ .Content }}"}...)
templates = append(templates, []string{"_default/list.html", "List Content: {{ .Content }}"}...)
content = append(content, []string{"b1/index.md", fmt.Sprintf(contentWithShortcodeTemplate, "b1", 1)}...)
content = append(content, []string{"b1/logo.png", "PNG logo"}...)
content = append(content, []string{"b1/bp1.md", fmt.Sprintf(simpleContentTemplate, "bp1", 1, "bp1")}...)
content = append(content, []string{"section1/_index.md", fmt.Sprintf(contentWithShortcodeTemplate, "s1", 2)}...)
content = append(content, []string{"section1/p1.md", fmt.Sprintf(simpleContentTemplate, "s1p1", 2, "s1p1")}...)
content = append(content, []string{"section2/_index.md", fmt.Sprintf(simpleContentTemplate, "b1", 1, "b1")}...)
content = append(content, []string{"section2/s2p1.md", fmt.Sprintf(contentWithShortcodeTemplate, "bp1", 1)}...)
builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
builder.WithViper(v).WithContent(content...).WithTemplates(templates...).CreateSites().Build(BuildCfg{})
s := builder.H.Sites[0]
assert.Equal(3, len(s.RegularPages))
builder.AssertFileContent("public/section1/index.html",
"List Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
"BP1:P1:|P2:docbp1/<p>C-bp1</p>",
)
builder.AssertFileContent("public/b1/index.html",
"Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
"P2:docbp1/<p>C-bp1</p>",
)
builder.AssertFileContent("public/section2/s2p1/index.html",
"Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
"P2:docbp1/<p>C-bp1</p>",
)
}
func TestShortcodePreserveOrder(t *testing.T) {
t.Parallel() t.Parallel()
assert := require.New(t) assert := require.New(t)
@ -897,28 +975,30 @@ weight: %d
{{< s1 >}}{{< s2 >}}{{< s3 >}}{{< s4 >}}{{< s5 >}} {{< s1 >}}{{< s2 >}}{{< s3 >}}{{< s4 >}}{{< s5 >}}
{{< nested >}} {{< nested >}}
{{< ordinal >}} {{< ordinal >}} {{< scratch >}}
{{< ordinal >}} {{< ordinal >}} {{< scratch >}}
{{< ordinal >}} {{< ordinal >}} {{< scratch >}}
{{< /nested >}} {{< /nested >}}
` `
ordinalShortcodeTemplate := `ordinal: {{ .Ordinal }}` ordinalShortcodeTemplate := `ordinal: {{ .Ordinal }}{{ .Page.Scratch.Set "ordinal" .Ordinal }}`
nestedShortcode := `outer ordinal: {{ .Ordinal }} inner: {{ .Inner }}` nestedShortcode := `outer ordinal: {{ .Ordinal }} inner: {{ .Inner }}`
scratchGetShortcode := `scratch ordinal: {{ .Ordinal }} scratch get ordinal: {{ .Page.Scratch.Get "ordinal" }}`
shortCodeTemplate := `v%d: {{ .Ordinal }}|` shortcodeTemplate := `v%d: {{ .Ordinal }} sgo: {{ .Page.Scratch.Get "o2" }}{{ .Page.Scratch.Set "o2" .Ordinal }}|`
var shortcodes []string var shortcodes []string
var content []string var content []string
shortcodes = append(shortcodes, []string{"shortcodes/nested.html", nestedShortcode}...) shortcodes = append(shortcodes, []string{"shortcodes/nested.html", nestedShortcode}...)
shortcodes = append(shortcodes, []string{"shortcodes/ordinal.html", ordinalShortcodeTemplate}...) shortcodes = append(shortcodes, []string{"shortcodes/ordinal.html", ordinalShortcodeTemplate}...)
shortcodes = append(shortcodes, []string{"shortcodes/scratch.html", scratchGetShortcode}...)
for i := 1; i <= 5; i++ { for i := 1; i <= 5; i++ {
shortcodes = append(shortcodes, []string{fmt.Sprintf("shortcodes/s%d.html", i), fmt.Sprintf(shortCodeTemplate, i)}...) sc := fmt.Sprintf(shortcodeTemplate, i)
sc = strings.Replace(sc, "%%", "%", -1)
shortcodes = append(shortcodes, []string{fmt.Sprintf("shortcodes/s%d.html", i), sc}...)
} }
for i := 1; i <= 3; i++ { for i := 1; i <= 3; i++ {
@ -932,18 +1012,10 @@ weight: %d
s := builder.H.Sites[0] s := builder.H.Sites[0]
assert.Equal(3, len(s.RegularPages)) assert.Equal(3, len(s.RegularPages))
p1 := s.RegularPages[0] builder.AssertFileContent("public/en/p1/index.html", `v1: 0 sgo: |v2: 1 sgo: 0|v3: 2 sgo: 1|v4: 3 sgo: 2|v5: 4 sgo: 3`)
builder.AssertFileContent("public/en/p1/index.html", `outer ordinal: 5 inner:
if !strings.Contains(string(p1.content()), `v1: 0|v2: 1|v3: 2|v4: 3|v5: 4|`) { ordinal: 0 scratch ordinal: 1 scratch get ordinal: 0
t.Fatal(p1.content()) ordinal: 2 scratch ordinal: 3 scratch get ordinal: 2
} ordinal: 4 scratch ordinal: 5 scratch get ordinal: 4`)
// Check nested behaviour
if !strings.Contains(string(p1.content()), `outer ordinal: 5 inner:
ordinal: 0
ordinal: 1
ordinal: 2`) {
t.Fatal(p1.content())
}
} }

View file

@ -180,6 +180,7 @@ func (s *Site) reset() *Site {
titleFunc: s.titleFunc, titleFunc: s.titleFunc,
relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg), relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg),
outputFormats: s.outputFormats, outputFormats: s.outputFormats,
rc: s.rc,
outputFormatsConfig: s.outputFormatsConfig, outputFormatsConfig: s.outputFormatsConfig,
frontmatterHandler: s.frontmatterHandler, frontmatterHandler: s.frontmatterHandler,
mediaTypesConfig: s.mediaTypesConfig, mediaTypesConfig: s.mediaTypesConfig,
@ -266,6 +267,7 @@ func newSite(cfg deps.DepsCfg) (*Site, error) {
titleFunc: titleFunc, titleFunc: titleFunc,
relatedDocsHandler: newSearchIndexHandler(relatedContentConfig), relatedDocsHandler: newSearchIndexHandler(relatedContentConfig),
outputFormats: outputFormats, outputFormats: outputFormats,
rc: &siteRenderingContext{output.HTMLFormat},
outputFormatsConfig: siteOutputFormatsConfig, outputFormatsConfig: siteOutputFormatsConfig,
mediaTypesConfig: siteMediaTypesConfig, mediaTypesConfig: siteMediaTypesConfig,
frontmatterHandler: frontMatterHandler, frontmatterHandler: frontMatterHandler,
@ -546,7 +548,7 @@ func (s *SiteInfo) RelRef(ref string, page *Page, options ...string) (string, er
} }
func (s *Site) running() bool { func (s *Site) running() bool {
return s.owner.running return s.owner != nil && s.owner.running
} }
func init() { func init() {