Permalink
Browse files

output: Support templates per site/language

This applies to both regular templates and shortcodes. So, if the site language is French and the output format is AMP, this is the (start) of the lookup order for the home page:

1. index.fr.amp.html
2. index.amp.html
3. index.fr.html
4. index.html
5. ...

Fixes #3360
  • Loading branch information...
bep committed Jul 2, 2017
1 parent a1d260b commit aa6b1b9be7c9d7322333893b642aaf8c7a5f2c2e
Showing with 109 additions and 40 deletions.
  1. +31 −5 hugolib/hugo_sites_build_test.go
  2. +1 −0 hugolib/page.go
  3. +16 −5 hugolib/shortcode.go
  4. +2 −2 hugolib/shortcode_test.go
  5. +1 −0 output/docshelper.go
  6. +56 −28 output/layout.go
  7. +2 −0 output/layout_test.go
@@ -305,12 +305,12 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
require.True(t, strings.Contains(languageRedirect, "0; url=http://example.com/blog/fr"), languageRedirect)
// check home page content (including data files rendering)
th.assertFileContent("public/en/index.html", "Home Page 1", "Hello", "Hugo Rocks!")
th.assertFileContent("public/fr/index.html", "Home Page 1", "Bonjour", "Hugo Rocks!")
th.assertFileContent("public/en/index.html", "Default Home Page 1", "Hello", "Hugo Rocks!")
th.assertFileContent("public/fr/index.html", "French Home Page 1", "Bonjour", "Hugo Rocks!")
// check single page content
th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour")
th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello")
th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour", "LingoFrench")
th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello", "LingoDefault")
// Check node translations
homeEn := enSite.getPage(KindHome)
@@ -1042,7 +1042,14 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf
if err := afero.WriteFile(mf,
filepath.Join("layouts", "index.html"),
[]byte("{{ $p := .Paginator }}Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"),
[]byte("{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"),
0755); err != nil {
t.Fatalf("Failed to write layout file: %s", err)
}
if err := afero.WriteFile(mf,
filepath.Join("layouts", "index.fr.html"),
[]byte("{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"),
0755); err != nil {
t.Fatalf("Failed to write layout file: %s", err)
}
@@ -1055,6 +1062,21 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf
t.Fatalf("Failed to write layout file: %s", err)
}
// A shortcode in multiple languages
if err := afero.WriteFile(mf,
filepath.Join("layouts", "shortcodes", "lingo.html"),
[]byte("LingoDefault"),
0755); err != nil {
t.Fatalf("Failed to write layout file: %s", err)
}
if err := afero.WriteFile(mf,
filepath.Join("layouts", "shortcodes", "lingo.fr.html"),
[]byte("LingoFrench"),
0755); err != nil {
t.Fatalf("Failed to write layout file: %s", err)
}
// Add some language files
if err := afero.WriteFile(mf,
filepath.Join("i18n", "en.yaml"),
@@ -1098,6 +1120,8 @@ publishdate: "2000-01-01"
{{< shortcode >}}
{{< lingo >}}
NOTE: slug should be used as URL
`)},
{Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`---
@@ -1113,6 +1137,8 @@ publishdate: "2000-01-04"
{{< shortcode >}}
{{< lingo >}}
NOTE: should be in the 'en' Page's 'Translations' field.
NOTE: date is after "doc3"
`)},
View
@@ -250,6 +250,7 @@ func (p *Page) createLayoutDescriptor() output.LayoutDescriptor {
return output.LayoutDescriptor{
Kind: p.Kind,
Type: p.Type(),
Lang: p.Lang(),
Layout: p.Layout,
Section: section,
}
View
@@ -157,6 +157,7 @@ func (sc shortcode) String() string {
// Note that in the below, OutputFormat may be empty.
// We will try to look for the most specific shortcode template available.
type scKey struct {
Lang string
OutputFormat string
Suffix string
ShortcodePlaceholder string
@@ -166,8 +167,8 @@ 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 newScKeyFromLangAndOutputFormat(lang string, o output.Format, shortcodeplaceholder string) scKey {
return scKey{Lang: lang, Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder}
}
func newDefaultScKey(shortcodeplaceholder string) scKey {
@@ -251,10 +252,11 @@ const innerCleanupExpand = "$1"
func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) {
m := make(map[scKey]func() (string, error))
lang := p.Lang()
for _, f := range p.outputFormats {
// The most specific template will win.
key := newScKeyFromOutputFormat(f, placeholder)
key := newScKeyFromLangAndOutputFormat(lang, f, placeholder)
m[key] = func() (string, error) {
return renderShortcode(key, sc, nil, p), nil
}
@@ -371,9 +373,11 @@ func (s *shortcodeHandler) updateDelta() bool {
func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map[scKey]func() (string, error) {
contentShortcodesForOuputFormat := make(map[scKey]func() (string, error))
lang := s.p.Lang()
for shortcodePlaceholder := range s.shortcodes {
key := newScKeyFromOutputFormat(f, shortcodePlaceholder)
key := newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)
renderFn, found := s.contentShortcodes[key]
if !found {
@@ -390,7 +394,7 @@ func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map
if !found {
panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder))
}
contentShortcodesForOuputFormat[newScKeyFromOutputFormat(f, shortcodePlaceholder)] = renderFn
contentShortcodesForOuputFormat[newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)] = renderFn
}
return contentShortcodesForOuputFormat
@@ -676,12 +680,19 @@ func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.T
suffix := strings.ToLower(key.Suffix)
outFormat := strings.ToLower(key.OutputFormat)
lang := strings.ToLower(key.Lang)
if outFormat != "" && suffix != "" {
if lang != "" {
names = append(names, fmt.Sprintf("%s.%s.%s.%s", shortcodeName, lang, outFormat, suffix))
}
names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix))
}
if suffix != "" {
if lang != "" {
names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, lang, suffix))
}
names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix))
}
@@ -837,8 +837,8 @@ func TestReplaceShortcodeTokens(t *testing.T) {
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{Lang: "en", Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"},
newScKeyFromLangAndOutputFormat("en", output.AMPFormat, "EFGH"))
require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"},
newDefaultScKey("IJKL"))
View
@@ -44,6 +44,7 @@ func createLayoutExamples() interface{} {
f Format
}{
{`AMP home, with theme "demoTheme".`, LayoutDescriptor{Kind: "home"}, true, "", AMPFormat},
{`AMP home, French language".`, LayoutDescriptor{Kind: "home", Lang: "fr"}, false, "", AMPFormat},
{"JSON home, no theme.", LayoutDescriptor{Kind: "home"}, false, "", JSONFormat},
{fmt.Sprintf(`CSV regular, "layout: %s" in front matter.`, demoLayout), LayoutDescriptor{Kind: "page", Layout: demoLayout}, false, "", CSVFormat},
{fmt.Sprintf(`JSON regular, "type: %s" in front matter.`, demoType), LayoutDescriptor{Kind: "page", Type: demoType}, false, "", JSONFormat},
View
@@ -26,6 +26,7 @@ type LayoutDescriptor struct {
Type string
Section string
Kind string
Lang string
Layout string
}
@@ -55,31 +56,33 @@ func NewLayoutHandler(hasTheme bool) *LayoutHandler {
const (
// TODO(bep) variations reduce to 1 "."
// The RSS templates doesn't map easily into the regular pages.
layoutsRSSHome = `NAME.SUFFIX _default/NAME.SUFFIX _internal/_default/rss.xml`
layoutsRSSSection = `section/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
layoutsRSSTaxonomy = `taxonomy/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
layoutsRSSHome = `VARIATIONS _default/VARIATIONS _internal/_default/rss.xml`
layoutsRSSSection = `section/SECTION.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
layoutsRSSTaxonomy = `taxonomy/SECTION.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
layoutsHome = "index.NAME.SUFFIX index.SUFFIX _default/list.NAME.SUFFIX _default/list.SUFFIX"
layoutsHome = "index.VARIATIONS _default/list.VARIATIONS"
layoutsSection = `
section/SECTION.NAME.SUFFIX section/SECTION.SUFFIX
SECTION/list.NAME.SUFFIX SECTION/list.SUFFIX
_default/section.NAME.SUFFIX _default/section.SUFFIX
_default/list.NAME.SUFFIX _default/list.SUFFIX
indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX
_default/indexes.NAME.SUFFIX _default/indexes.SUFFIX
section/SECTION.VARIATIONS
SECTION/list.VARIATIONS
_default/section.VARIATIONS
_default/list.VARIATIONS
indexes/SECTION.VARIATIONS
_default/indexes.VARIATIONS
`
layoutsTaxonomy = `
taxonomy/SECTION.NAME.SUFFIX taxonomy/SECTION.SUFFIX
indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX
_default/taxonomy.NAME.SUFFIX _default/taxonomy.SUFFIX
_default/list.NAME.SUFFIX _default/list.SUFFIX
taxonomy/SECTION.VARIATIONS
indexes/SECTION.VARIATIONS
_default/taxonomy.VARIATIONS
_default/list.VARIATIONS
`
layoutsTaxonomyTerm = `
taxonomy/SECTION.terms.NAME.SUFFIX taxonomy/SECTION.terms.SUFFIX
_default/terms.NAME.SUFFIX _default/terms.SUFFIX
indexes/indexes.NAME.SUFFIX indexes/indexes.SUFFIX
taxonomy/SECTION.terms.VARIATIONS
_default/terms.VARIATIONS
indexes/indexes.VARIATIONS
`
)
@@ -185,14 +188,41 @@ func resolveListTemplate(d LayoutDescriptor, f Format,
}
func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string {
delim := "."
if f.MediaType.Delimiter == "" {
delim = ""
// VARIATIONS will be replaced with
// .lang.name.suffix
// .name.suffix
// .lang.suffix
// .suffix
var replacementValues []string
name := strings.ToLower(f.Name)
if d.Lang != "" {
replacementValues = append(replacementValues, fmt.Sprintf("%s.%s.%s", d.Lang, name, f.MediaType.Suffix))
}
replacementValues = append(replacementValues, fmt.Sprintf("%s.%s", name, f.MediaType.Suffix))
if d.Lang != "" {
replacementValues = append(replacementValues, fmt.Sprintf("%s.%s", d.Lang, f.MediaType.Suffix))
}
isRSS := f.Name == RSSFormat.Name
if !isRSS {
replacementValues = append(replacementValues, f.MediaType.Suffix)
}
var layouts []string
templFields := strings.Fields(templ)
for _, field := range templFields {
for _, replacements := range replacementValues {
layouts = append(layouts, replaceKeyValues(field, "VARIATIONS", replacements, "SECTION", d.Section))
}
}
layouts := strings.Fields(replaceKeyValues(templ,
".SUFFIX", delim+f.MediaType.Suffix,
"NAME", strings.ToLower(f.Name),
"SECTION", d.Section))
return filterDotLess(layouts)
}
@@ -201,9 +231,7 @@ func filterDotLess(layouts []string) []string {
var filteredLayouts []string
for _, l := range layouts {
// This may be constructed, but media types can be suffix-less, but can contain
// a delimiter.
l = strings.TrimSuffix(l, ".")
l = strings.Trim(l, ".")
// If media type has no suffix, we have "index" type of layouts in this list, which
// doesn't make much sense.
if strings.Contains(l, ".") {
View
@@ -59,6 +59,8 @@ func TestLayout(t *testing.T) {
}{
{"Home", LayoutDescriptor{Kind: "home"}, true, "", ampType,
[]string{"index.amp.html", "index.html", "_default/list.amp.html", "_default/list.html", "theme/index.amp.html", "theme/index.html"}},
{"Home, french language", LayoutDescriptor{Kind: "home", Lang: "fr"}, true, "", ampType,
[]string{"index.fr.amp.html", "index.amp.html", "index.fr.html", "index.html", "_default/list.fr.amp.html", "_default/list.amp.html", "_default/list.fr.html", "_default/list.html", "theme/index.fr.amp.html", "theme/index.amp.html", "theme/index.fr.html"}},
{"Home, no ext or delim", LayoutDescriptor{Kind: "home"}, true, "", noExtDelimFormat,
[]string{"index.nem", "_default/list.nem"}},
{"Home, no ext", LayoutDescriptor{Kind: "home"}, true, "", noExt,

0 comments on commit aa6b1b9

Please sign in to comment.