shithub: hugo

Download patch

ref: af72db806f2c1c0bf1dfe5832275c41eeba89906
parent: e951d65771ca299aa899e91bfe00411a5ada8f19
author: Bjørn Erik Pedersen <[email protected]>
date: Sat May 6 16:15:28 EDT 2017

hugolib: Handle shortcode per output format

This commit allows shortcode per output format, a typical use case would be the special AMP media tags.

Note that this will only re-render the "overridden" shortcodes and only  in pages where these are used, so performance in the normal case should not suffer.

Closes #3220

--- a/hugolib/handler_page.go
+++ b/hugolib/handler_page.go
@@ -80,7 +80,7 @@
 	p.createWorkContentCopy()
 
 	if err := p.processShortcodes(); err != nil {
-		return HandledResult{err: err}
+		p.s.Log.ERROR.Println(err)
 	}
 
 	return HandledResult{err: nil}
@@ -131,7 +131,7 @@
 	p.createWorkContentCopy()
 
 	if err := p.processShortcodes(); err != nil {
-		return HandledResult{err: err}
+		p.s.Log.ERROR.Println(err)
 	}
 
 	// TODO(bep) these page handlers need to be re-evaluated, as it is hard to
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -492,13 +492,8 @@
 	}
 }
 
-func (s *Site) preparePagesForRender(outFormatIdx int, cfg *BuildCfg) {
+func (s *Site) preparePagesForRender(cfg *BuildCfg) {
 
-	if outFormatIdx > 0 {
-		// TODO(bep) for now
-		return
-	}
-
 	pageChan := make(chan *Page)
 	wg := &sync.WaitGroup{}
 	numWorkers := getGoMaxProcs() * 4
@@ -508,8 +503,16 @@
 		go func(pages <-chan *Page, wg *sync.WaitGroup) {
 			defer wg.Done()
 			for p := range pages {
+				if !p.shouldRenderTo(s.rc.Format) {
+					// No need to prepare
+					continue
+				}
+				var shortcodeUpdate bool
+				if p.shortcodeState != nil {
+					shortcodeUpdate = p.shortcodeState.updateDelta()
+				}
 
-				if !cfg.whatChanged.other && p.rendered {
+				if !shortcodeUpdate && !cfg.whatChanged.other && p.rendered {
 					// No need to process it again.
 					continue
 				}
@@ -521,10 +524,12 @@
 				// Mark it as rendered
 				p.rendered = true
 
-				// If in watch mode, we need to keep the original so we can
-				// repeat this process on rebuild.
+				// If in watch mode or if we have multiple output formats,
+				// we need to keep the original so we can
+				// potentially repeat this process on rebuild.
+				needsACopy := cfg.Watching || len(p.outputFormats) > 1
 				var workContentCopy []byte
-				if cfg.Watching {
+				if needsACopy {
 					workContentCopy = make([]byte, len(p.workContent))
 					copy(workContentCopy, p.workContent)
 				} else {
@@ -589,15 +594,15 @@
 }
 
 func handleShortcodes(p *Page, rawContentCopy []byte) ([]byte, error) {
-	if p.shortcodeState != nil && len(p.shortcodeState.contentShortCodes) > 0 {
-		p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", len(p.shortcodeState.contentShortCodes), p.BaseFileName())
-		shortcodes, err := executeShortcodeFuncMap(p.shortcodeState.contentShortCodes)
+	if p.shortcodeState != nil && len(p.shortcodeState.contentShortcodes) > 0 {
+		p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", len(p.shortcodeState.contentShortcodes), p.BaseFileName())
+		err := p.shortcodeState.executeShortcodesForDelta(p)
 
 		if err != nil {
 			return rawContentCopy, err
 		}
 
-		rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, shortcodes)
+		rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, p.shortcodeState.renderedShortcodes)
 
 		if err != nil {
 			p.s.Log.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error())
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -213,7 +213,7 @@
 		s.initRenderFormats()
 		for i, rf := range s.renderFormats {
 			s.rc = &siteRenderingContext{Format: rf}
-			s.preparePagesForRender(i, config)
+			s.preparePagesForRender(config)
 
 			if !config.SkipRender {
 				if err := s.render(i); err != nil {
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -1257,6 +1257,11 @@
 	return p.pageMenus
 }
 
+func (p *Page) shouldRenderTo(f output.Format) bool {
+	_, found := p.outputFormats.GetByName(f.Name)
+	return found
+}
+
 func (p *Page) determineMarkupType() string {
 	// Try markup explicitly set in the frontmatter
 	p.Markup = helpers.GuessType(p.Markup)
@@ -1372,8 +1377,8 @@
 }
 
 func (p *Page) processShortcodes() error {
-	p.shortcodeState = newShortcodeHandler()
-	tmpContent, err := p.shortcodeState.extractAndRenderShortcodes(string(p.workContent), p)
+	p.shortcodeState = newShortcodeHandler(p)
+	tmpContent, err := p.shortcodeState.extractShortcodes(string(p.workContent), p)
 	if err != nil {
 		return err
 	}
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Hugo Authors. All rights reserved.
+// Copyright 2017 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.
@@ -24,6 +24,10 @@
 	"strings"
 	"sync"
 
+	"github.com/spf13/hugo/output"
+
+	"github.com/spf13/hugo/media"
+
 	bp "github.com/spf13/hugo/bufferpool"
 	"github.com/spf13/hugo/helpers"
 	"github.com/spf13/hugo/tpl"
@@ -149,10 +153,44 @@
 	return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner)
 }
 
+// We may have special shortcode templates for AMP etc.
+// Note that in the below, OutputFormat may be empty.
+// We will try to look for the most specific shortcode template available.
+type scKey struct {
+	OutputFormat         string
+	Suffix               string
+	ShortcodePlaceholder string
+}
+
+func newScKey(m media.Type, shortcodeplaceholder string) scKey {
+	return scKey{Suffix: m.Suffix, ShortcodePlaceholder: shortcodeplaceholder}
+}
+
+func newScKeyFromOutputFormat(o output.Format, shortcodeplaceholder string) scKey {
+	return scKey{Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder}
+}
+
+func newDefaultScKey(shortcodeplaceholder string) scKey {
+	return newScKey(media.HTMLType, shortcodeplaceholder)
+}
+
 type shortcodeHandler struct {
-	// Maps the shortcodeplaceholder with the shortcode rendering func.
-	contentShortCodes map[string]func() (string, error)
+	init sync.Once
 
+	p *Page
+
+	// This is all shortcode rendering funcs for all potential output formats.
+	contentShortcodes map[scKey]func() (string, error)
+
+	// This map contains the new or changed set of shortcodes that need
+	// to be rendered for the current output format.
+	contentShortcodesDelta map[scKey]func() (string, error)
+
+	// This maps the shorcode placeholders with the rendered content.
+	// We will do (potential) partial re-rendering per output format,
+	// so keep this for the unchanged.
+	renderedShortcodes map[string]string
+
 	// Maps the shortcodeplaceholder with the actual shortcode.
 	shortcodes map[string]shortcode
 
@@ -160,11 +198,13 @@
 	nameSet map[string]bool
 }
 
-func newShortcodeHandler() *shortcodeHandler {
+func newShortcodeHandler(p *Page) *shortcodeHandler {
 	return &shortcodeHandler{
-		contentShortCodes: make(map[string]func() (string, error)),
-		shortcodes:        make(map[string]shortcode),
-		nameSet:           make(map[string]bool),
+		p:                  p,
+		contentShortcodes:  make(map[scKey]func() (string, error)),
+		shortcodes:         make(map[string]shortcode),
+		nameSet:            make(map[string]bool),
+		renderedShortcodes: make(map[string]string),
 	}
 }
 
@@ -208,11 +248,30 @@
 const innerCleanupRegexp = `\A<p>(.*)</p>\n\z`
 const innerCleanupExpand = "$1"
 
-func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string {
-	tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl)
+func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) {
 
+	m := make(map[scKey]func() (string, error))
+
+	for _, f := range p.outputFormats {
+		// The most specific template will win.
+		key := newScKeyFromOutputFormat(f, placeholder)
+		m[key] = func() (string, error) {
+			return renderShortcode(key, sc, nil, p), nil
+		}
+	}
+
+	return m
+}
+
+func renderShortcode(
+	tmplKey scKey,
+	sc shortcode,
+	parent *ShortcodeWithPage,
+	p *Page) string {
+
+	tmpl := getShortcodeTemplateForTemplateKey(tmplKey, sc.name, p.s.Tmpl)
 	if tmpl == nil {
-		p.s.Log.ERROR.Printf("Unable to locate template for shortcode '%s' in page %q", sc.name, p.Path())
+		p.s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path())
 		return ""
 	}
 
@@ -228,7 +287,7 @@
 			case string:
 				inner += innerData.(string)
 			case shortcode:
-				inner += renderShortcode(innerData.(shortcode), data, p)
+				inner += renderShortcode(tmplKey, innerData.(shortcode), data, p)
 			default:
 				p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
 					sc.name, p.Path(), reflect.TypeOf(innerData))
@@ -268,6 +327,7 @@
 				}
 			}
 
+			// TODO(bep) we may have plain text inner templates.
 			data.Inner = template.HTML(newInner)
 		} else {
 			data.Inner = template.HTML(inner)
@@ -278,51 +338,91 @@
 	return renderShortcodeWithPage(tmpl, data)
 }
 
-func (s *shortcodeHandler) extractAndRenderShortcodes(stringToParse string, p *Page) (string, error) {
-	content, err := s.extractShortcodes(stringToParse, p)
+// The delta represents new output format-versions of the shortcodes,
+// which, combined with the ones that do not have alternative representations,
+// builds a complete set ready for a full rebuild of the Page content.
+// This method returns false if there are no new shortcode variants in the
+// current rendering context's output format. This mean we can safely reuse
+// the content from the previous output format, if any.
+func (s *shortcodeHandler) updateDelta() bool {
+	s.init.Do(func() {
+		s.contentShortcodes = createShortcodeRenderers(s.shortcodes, s.p)
+	})
 
-	if err != nil {
-		//  try to render what we have whilst logging the error
-		p.s.Log.ERROR.Println(err.Error())
+	contentShortcodes := s.contentShortcodesForOutputFormat(s.p.s.rc.Format)
+
+	if s.contentShortcodesDelta == nil || len(s.contentShortcodesDelta) == 0 {
+		s.contentShortcodesDelta = contentShortcodes
+		return true
 	}
 
-	s.contentShortCodes = renderShortcodes(s.shortcodes, p)
+	delta := make(map[scKey]func() (string, error))
 
-	return content, err
+	for k, v := range contentShortcodes {
+		if _, found := s.contentShortcodesDelta[k]; !found {
+			delta[k] = v
+		}
+	}
 
+	s.contentShortcodesDelta = delta
+
+	return len(delta) > 0
 }
 
-var emptyShortcodeFn = func() (string, error) { return "", nil }
+func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map[scKey]func() (string, error) {
+	contentShortcodesForOuputFormat := make(map[scKey]func() (string, error))
+	for shortcodePlaceholder := range s.shortcodes {
 
-func executeShortcodeFuncMap(funcs map[string]func() (string, error)) (map[string]string, error) {
-	result := make(map[string]string)
+		key := newScKeyFromOutputFormat(f, shortcodePlaceholder)
+		renderFn, found := s.contentShortcodes[key]
 
-	for k, v := range funcs {
-		s, err := v()
+		if !found {
+			key.OutputFormat = ""
+			renderFn, found = s.contentShortcodes[key]
+		}
+
+		// Fall back to HTML
+		if !found && key.Suffix != "html" {
+			key.Suffix = "html"
+			renderFn, found = s.contentShortcodes[key]
+		}
+
+		if !found {
+			panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder))
+		}
+		contentShortcodesForOuputFormat[newScKeyFromOutputFormat(f, shortcodePlaceholder)] = renderFn
+	}
+
+	return contentShortcodesForOuputFormat
+}
+
+func (s *shortcodeHandler) executeShortcodesForDelta(p *Page) error {
+
+	for k, render := range s.contentShortcodesDelta {
+		renderedShortcode, err := render()
 		if err != nil {
-			return nil, fmt.Errorf("Failed to execute shortcode with key %s: %s", k, err)
+			return fmt.Errorf("Failed to execute shortcode in page %q: %s", p.Path(), err)
 		}
-		result[k] = s
+
+		s.renderedShortcodes[k.ShortcodePlaceholder] = renderedShortcode
 	}
 
-	return result, nil
+	return nil
+
 }
 
-func renderShortcodes(shortcodes map[string]shortcode, p *Page) map[string]func() (string, error) {
+func createShortcodeRenderers(shortcodes map[string]shortcode, p *Page) map[scKey]func() (string, error) {
 
-	renderedShortcodes := make(map[string]func() (string, error))
+	shortcodeRenderers := make(map[scKey]func() (string, error))
 
-	for key, sc := range shortcodes {
-		if sc.err != nil {
-			// need to have something to replace with
-			renderedShortcodes[key] = emptyShortcodeFn
-		} else {
-			shortcode := sc
-			renderedShortcodes[key] = func() (string, error) { return renderShortcode(shortcode, nil, p), nil }
+	for k, v := range shortcodes {
+		prepared := prepareShortcodeForPage(k, v, nil, p)
+		for kk, vv := range prepared {
+			shortcodeRenderers[kk] = vv
 		}
 	}
 
-	return renderedShortcodes
+	return shortcodeRenderers
 }
 
 var errShortCodeIllegalState = errors.New("Illegal shortcode state")
@@ -395,7 +495,9 @@
 			sc.inner = append(sc.inner, currItem.val)
 		case tScName:
 			sc.name = currItem.val
-			tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl)
+			// We pick the first template for an arbitrary output format
+			// if more than one. It is "all inner or no inner".
+			tmpl := getShortcodeTemplateForTemplateKey(scKey{}, sc.name, p.s.Tmpl)
 			if tmpl == nil {
 				return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path())
 			}
@@ -566,17 +668,38 @@
 	return source, nil
 }
 
-func getShortcodeTemplate(name string, t tpl.TemplateFinder) *tpl.TemplateAdapter {
+func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.TemplateFinder) *tpl.TemplateAdapter {
 	isInnerShortcodeCache.RLock()
 	defer isInnerShortcodeCache.RUnlock()
 
-	if x := t.Lookup("shortcodes/" + name + ".html"); x != nil {
-		return x
+	var names []string
+
+	suffix := strings.ToLower(key.Suffix)
+	outFormat := strings.ToLower(key.OutputFormat)
+
+	if outFormat != "" && suffix != "" {
+		names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix))
 	}
-	if x := t.Lookup("theme/shortcodes/" + name + ".html"); x != nil {
-		return x
+
+	if suffix != "" {
+		names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix))
 	}
-	return t.Lookup("_internal/shortcodes/" + name + ".html")
+
+	names = append(names, shortcodeName)
+
+	for _, name := range names {
+
+		if x := t.Lookup("shortcodes/" + name); x != nil {
+			return x
+		}
+		if x := t.Lookup("theme/shortcodes/" + name); x != nil {
+			return x
+		}
+		if x := t.Lookup("_internal/shortcodes/" + name); x != nil {
+			return x
+		}
+	}
+	return nil
 }
 
 func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string {
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -22,6 +22,14 @@
 	"strings"
 	"testing"
 
+	jww "github.com/spf13/jwalterweatherman"
+
+	"github.com/spf13/afero"
+
+	"github.com/spf13/hugo/output"
+
+	"github.com/spf13/hugo/media"
+
 	"github.com/spf13/hugo/deps"
 	"github.com/spf13/hugo/helpers"
 	"github.com/spf13/hugo/source"
@@ -353,7 +361,7 @@
 			return nil
 		})
 
-		s := newShortcodeHandler()
+		s := newShortcodeHandler(p)
 		content, err := s.extractShortcodes(this.input, p)
 
 		if b, ok := this.expect.(bool); ok && !b {
@@ -563,6 +571,150 @@
 
 }
 
+func TestShortcodeMultipleOutputFormats(t *testing.T) {
+	t.Parallel()
+
+	siteConfig := `
+baseURL = "http://example.com/blog"
+
+paginate = 1
+
+disableKinds = ["section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"]
+
+[outputs]
+home = [ "HTML", "AMP", "Calendar" ]
+page =  [ "HTML", "AMP", "JSON" ]
+
+`
+
+	pageTemplate := `---
+title: "%s"
+---
+# Doc
+
+{{< myShort >}}
+{{< noExt >}}
+{{%% onlyHTML %%}}
+
+{{< myInner >}}{{< myShort >}}{{< /myInner >}}
+
+`
+
+	pageTemplateCSVOnly := `---
+title: "%s"
+outputs: ["CSV"]
+---
+# Doc
+
+CSV: {{< myShort >}}
+`
+
+	pageTemplateShortcodeNotFound := `---
+title: "%s"
+outputs: ["CSV"]
+---
+# Doc
+
+NotFound: {{< thisDoesNotExist >}}
+`
+
+	mf := afero.NewMemMapFs()
+
+	th, h := newTestSitesFromConfig(t, mf, siteConfig,
+		"layouts/_default/single.html", `Single HTML: {{ .Title }}|{{ .Content }}`,
+		"layouts/_default/single.json", `Single JSON: {{ .Title }}|{{ .Content }}`,
+		"layouts/_default/single.csv", `Single CSV: {{ .Title }}|{{ .Content }}`,
+		"layouts/index.html", `Home HTML: {{ .Title }}|{{ .Content }}`,
+		"layouts/index.amp.html", `Home AMP: {{ .Title }}|{{ .Content }}`,
+		"layouts/index.ics", `Home Calendar: {{ .Title }}|{{ .Content }}`,
+		"layouts/shortcodes/myShort.html", `ShortHTML`,
+		"layouts/shortcodes/myShort.amp.html", `ShortAMP`,
+		"layouts/shortcodes/myShort.csv", `ShortCSV`,
+		"layouts/shortcodes/myShort.ics", `ShortCalendar`,
+		"layouts/shortcodes/myShort.json", `ShortJSON`,
+		"layouts/shortcodes/noExt", `ShortNoExt`,
+		"layouts/shortcodes/onlyHTML.html", `ShortOnlyHTML`,
+		"layouts/shortcodes/myInner.html", `myInner:--{{- .Inner -}}--`,
+	)
+
+	fs := th.Fs
+
+	writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "Home"))
+	writeSource(t, fs, "content/sect/mypage.md", fmt.Sprintf(pageTemplate, "Single"))
+	writeSource(t, fs, "content/sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV"))
+	writeSource(t, fs, "content/sect/notfound.md", fmt.Sprintf(pageTemplateShortcodeNotFound, "Single CSV"))
+
+	require.NoError(t, h.Build(BuildCfg{}))
+	require.Len(t, h.Sites, 1)
+
+	s := h.Sites[0]
+	home := s.getPage(KindHome)
+	require.NotNil(t, home)
+	require.Len(t, home.outputFormats, 3)
+
+	th.assertFileContent("public/index.html",
+		"Home HTML",
+		"ShortHTML",
+		"ShortNoExt",
+		"ShortOnlyHTML",
+		"myInner:--ShortHTML--",
+	)
+
+	th.assertFileContent("public/amp/index.html",
+		"Home AMP",
+		"ShortAMP",
+		"ShortNoExt",
+		"ShortOnlyHTML",
+		"myInner:--ShortAMP--",
+	)
+
+	th.assertFileContent("public/index.ics",
+		"Home Calendar",
+		"ShortCalendar",
+		"ShortNoExt",
+		"ShortOnlyHTML",
+		"myInner:--ShortCalendar--",
+	)
+
+	th.assertFileContent("public/sect/mypage/index.html",
+		"Single HTML",
+		"ShortHTML",
+		"ShortNoExt",
+		"ShortOnlyHTML",
+		"myInner:--ShortHTML--",
+	)
+
+	th.assertFileContent("public/sect/mypage/index.json",
+		"Single JSON",
+		"ShortJSON",
+		"ShortNoExt",
+		"ShortOnlyHTML",
+		"myInner:--ShortJSON--",
+	)
+
+	th.assertFileContent("public/amp/sect/mypage/index.html",
+		// No special AMP template
+		"Single HTML",
+		"ShortAMP",
+		"ShortNoExt",
+		"ShortOnlyHTML",
+		"myInner:--ShortAMP--",
+	)
+
+	th.assertFileContent("public/sect/mycsvpage/index.csv",
+		"Single CSV",
+		"ShortCSV",
+	)
+
+	th.assertFileContent("public/sect/notfound/index.csv",
+		"NotFound:",
+		"thisDoesNotExist",
+	)
+
+	require.Equal(t, uint64(1), s.Log.LogCountForLevel(jww.LevelError))
+
+}
+
 func collectAndSortShortcodes(shortcodes map[string]shortcode) []string {
 	var asArray []string
 
@@ -679,5 +831,15 @@
 		}
 
 	}
+
+}
+
+func TestScKey(t *testing.T) {
+	require.Equal(t, scKey{Suffix: "xml", ShortcodePlaceholder: "ABCD"},
+		newScKey(media.XMLType, "ABCD"))
+	require.Equal(t, scKey{Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"},
+		newScKeyFromOutputFormat(output.AMPFormat, "EFGH"))
+	require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"},
+		newDefaultScKey("IJKL"))
 
 }
--- a/hugolib/site_output_test.go
+++ b/hugolib/site_output_test.go
@@ -99,6 +99,8 @@
 outputs: %s
 ---
 # Doc
+
+{{< myShort >}}
 `
 
 	mf := afero.NewMemMapFs()
@@ -118,6 +120,8 @@
 		"layouts/partials/GoHugo.html", `Go Hugo Partial`,
 		"layouts/_default/baseof.json", `START JSON:{{block "main" .}}default content{{ end }}:END JSON`,
 		"layouts/_default/baseof.html", `START HTML:{{block "main" .}}default content{{ end }}:END HTML`,
+		"layouts/shortcodes/myShort.html", `ShortHTML`,
+		"layouts/shortcodes/myShort.json", `ShortJSON`,
 
 		"layouts/_default/list.json", `{{ define "main" }}
 List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}|
@@ -141,6 +145,7 @@
 {{ .Site.Language.Lang }}: {{ T "elbow" -}}
 Partial Hugo 1: {{ partial "GoHugo.html" . }}
 Partial Hugo 2: {{ partial "GoHugo" . -}}
+Content: {{ .Content }}
 {{ end }}
 `,
 	)
@@ -180,6 +185,7 @@
 			"Output/Rel: JSON/alternate|",
 			"Output/Rel: HTML/canonical|",
 			"en: Elbow",
+			"ShortJSON",
 		)
 
 		th.assertFileContent("public/index.html",
@@ -187,6 +193,7 @@
 			// parsed with html/template.
 			`List HTML|JSON Home|<atom:link href=http://example.com/blog/ rel="self" type="text/html&#43;html" />`,
 			"en: Elbow",
+			"ShortHTML",
 		)
 		th.assertFileContent("public/nn/index.html",
 			"List HTML|JSON Nynorsk Heim|",
@@ -196,10 +203,12 @@
 			"Output/Rel: JSON/canonical|",
 			// JSON is plain text, so no need to safeHTML this and that
 			`<atom:link href=http://example.com/blog/index.json rel="self" type="application/json+json" />`,
+			"ShortJSON",
 		)
 		th.assertFileContent("public/nn/index.json",
 			"List JSON|JSON Nynorsk Heim|",
 			"nn: Olboge",
+			"ShortJSON",
 		)
 	}
 
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -77,13 +77,15 @@
 			if i == 0 {
 				pageOutput, err = newPageOutput(page, false, outFormat)
 				page.mainPageOutput = pageOutput
-			} else {
-				pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat)
 			}
 
 			if outFormat != page.s.rc.Format {
 				// Will be rendered  ... later.
 				continue
+			}
+
+			if pageOutput == nil {
+				pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat)
 			}
 
 			if err != nil {
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -58,6 +58,11 @@
 	Tree() string
 }
 
+// TemplateDebugger prints some debug info to stdoud.
+type TemplateDebugger interface {
+	Debug()
+}
+
 // TemplateAdapter implements the TemplateExecutor interface.
 type TemplateAdapter struct {
 	Template
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -14,7 +14,9 @@
 package tplimpl
 
 import (
+	"fmt"
 	"html/template"
+	"path"
 	"strings"
 	texttemplate "text/template"
 
@@ -39,6 +41,7 @@
 
 var (
 	_ tpl.TemplateHandler       = (*templateHandler)(nil)
+	_ tpl.TemplateDebugger      = (*templateHandler)(nil)
 	_ tpl.TemplateFuncsGetter   = (*templateHandler)(nil)
 	_ tpl.TemplateTestMocker    = (*templateHandler)(nil)
 	_ tpl.TemplateFinder        = (*htmlTemplates)(nil)
@@ -88,6 +91,11 @@
 	t.errors = append(t.errors, &templateErr{name, err})
 }
 
+func (t *templateHandler) Debug() {
+	fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates())
+	fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates())
+}
+
 // PrintErrors prints the accumulated errors as ERROR to the log.
 func (t *templateHandler) PrintErrors() {
 	for _, e := range t.errors {
@@ -293,6 +301,13 @@
 		return err
 	}
 
+	if strings.Contains(name, "shortcodes") {
+		// We need to keep track of one ot the output format's shortcode template
+		// without knowing the rendering context.
+		withoutExt := strings.TrimSuffix(name, path.Ext(name))
+		tt.AddParseTree(withoutExt, templ.Tree)
+	}
+
 	return nil
 }
 
@@ -313,6 +328,13 @@
 
 	if err := applyTemplateTransformersToTextTemplate(templ); err != nil {
 		return err
+	}
+
+	if strings.Contains(name, "shortcodes") {
+		// We need to keep track of one ot the output format's shortcode template
+		// without knowing the rendering context.
+		withoutExt := strings.TrimSuffix(name, path.Ext(name))
+		tt.AddParseTree(withoutExt, templ.Tree)
 	}
 
 	return nil