Skip to content

Commit aa6b1b9

Browse files
committed
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
1 parent a1d260b commit aa6b1b9

File tree

7 files changed

+109
-40
lines changed

7 files changed

+109
-40
lines changed

‎hugolib/hugo_sites_build_test.go‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,12 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
305305
require.True(t, strings.Contains(languageRedirect, "0; url=http://example.com/blog/fr"), languageRedirect)
306306

307307
// check home page content (including data files rendering)
308-
th.assertFileContent("public/en/index.html", "Home Page 1", "Hello", "Hugo Rocks!")
309-
th.assertFileContent("public/fr/index.html", "Home Page 1", "Bonjour", "Hugo Rocks!")
308+
th.assertFileContent("public/en/index.html", "Default Home Page 1", "Hello", "Hugo Rocks!")
309+
th.assertFileContent("public/fr/index.html", "French Home Page 1", "Bonjour", "Hugo Rocks!")
310310

311311
// check single page content
312-
th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour")
313-
th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello")
312+
th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour", "LingoFrench")
313+
th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello", "LingoDefault")
314314

315315
// Check node translations
316316
homeEn := enSite.getPage(KindHome)
@@ -1042,7 +1042,14 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf
10421042

10431043
if err := afero.WriteFile(mf,
10441044
filepath.Join("layouts", "index.html"),
1045-
[]byte("{{ $p := .Paginator }}Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"),
1045+
[]byte("{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"),
1046+
0755); err != nil {
1047+
t.Fatalf("Failed to write layout file: %s", err)
1048+
}
1049+
1050+
if err := afero.WriteFile(mf,
1051+
filepath.Join("layouts", "index.fr.html"),
1052+
[]byte("{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"),
10461053
0755); err != nil {
10471054
t.Fatalf("Failed to write layout file: %s", err)
10481055
}
@@ -1055,6 +1062,21 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf
10551062
t.Fatalf("Failed to write layout file: %s", err)
10561063
}
10571064

1065+
// A shortcode in multiple languages
1066+
if err := afero.WriteFile(mf,
1067+
filepath.Join("layouts", "shortcodes", "lingo.html"),
1068+
[]byte("LingoDefault"),
1069+
0755); err != nil {
1070+
t.Fatalf("Failed to write layout file: %s", err)
1071+
}
1072+
1073+
if err := afero.WriteFile(mf,
1074+
filepath.Join("layouts", "shortcodes", "lingo.fr.html"),
1075+
[]byte("LingoFrench"),
1076+
0755); err != nil {
1077+
t.Fatalf("Failed to write layout file: %s", err)
1078+
}
1079+
10581080
// Add some language files
10591081
if err := afero.WriteFile(mf,
10601082
filepath.Join("i18n", "en.yaml"),
@@ -1098,6 +1120,8 @@ publishdate: "2000-01-01"
10981120
10991121
{{< shortcode >}}
11001122
1123+
{{< lingo >}}
1124+
11011125
NOTE: slug should be used as URL
11021126
`)},
11031127
{Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`---
@@ -1113,6 +1137,8 @@ publishdate: "2000-01-04"
11131137
11141138
{{< shortcode >}}
11151139
1140+
{{< lingo >}}
1141+
11161142
NOTE: should be in the 'en' Page's 'Translations' field.
11171143
NOTE: date is after "doc3"
11181144
`)},

‎hugolib/page.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ func (p *Page) createLayoutDescriptor() output.LayoutDescriptor {
250250
return output.LayoutDescriptor{
251251
Kind: p.Kind,
252252
Type: p.Type(),
253+
Lang: p.Lang(),
253254
Layout: p.Layout,
254255
Section: section,
255256
}

‎hugolib/shortcode.go‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ func (sc shortcode) String() string {
157157
// Note that in the below, OutputFormat may be empty.
158158
// We will try to look for the most specific shortcode template available.
159159
type scKey struct {
160+
Lang string
160161
OutputFormat string
161162
Suffix string
162163
ShortcodePlaceholder string
@@ -166,8 +167,8 @@ func newScKey(m media.Type, shortcodeplaceholder string) scKey {
166167
return scKey{Suffix: m.Suffix, ShortcodePlaceholder: shortcodeplaceholder}
167168
}
168169

169-
func newScKeyFromOutputFormat(o output.Format, shortcodeplaceholder string) scKey {
170-
return scKey{Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder}
170+
func newScKeyFromLangAndOutputFormat(lang string, o output.Format, shortcodeplaceholder string) scKey {
171+
return scKey{Lang: lang, Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder}
171172
}
172173

173174
func newDefaultScKey(shortcodeplaceholder string) scKey {
@@ -251,10 +252,11 @@ const innerCleanupExpand = "$1"
251252
func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) {
252253

253254
m := make(map[scKey]func() (string, error))
255+
lang := p.Lang()
254256

255257
for _, f := range p.outputFormats {
256258
// The most specific template will win.
257-
key := newScKeyFromOutputFormat(f, placeholder)
259+
key := newScKeyFromLangAndOutputFormat(lang, f, placeholder)
258260
m[key] = func() (string, error) {
259261
return renderShortcode(key, sc, nil, p), nil
260262
}
@@ -371,9 +373,11 @@ func (s *shortcodeHandler) updateDelta() bool {
371373

372374
func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map[scKey]func() (string, error) {
373375
contentShortcodesForOuputFormat := make(map[scKey]func() (string, error))
376+
lang := s.p.Lang()
377+
374378
for shortcodePlaceholder := range s.shortcodes {
375379

376-
key := newScKeyFromOutputFormat(f, shortcodePlaceholder)
380+
key := newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)
377381
renderFn, found := s.contentShortcodes[key]
378382

379383
if !found {
@@ -390,7 +394,7 @@ func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map
390394
if !found {
391395
panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder))
392396
}
393-
contentShortcodesForOuputFormat[newScKeyFromOutputFormat(f, shortcodePlaceholder)] = renderFn
397+
contentShortcodesForOuputFormat[newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)] = renderFn
394398
}
395399

396400
return contentShortcodesForOuputFormat
@@ -676,12 +680,19 @@ func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.T
676680

677681
suffix := strings.ToLower(key.Suffix)
678682
outFormat := strings.ToLower(key.OutputFormat)
683+
lang := strings.ToLower(key.Lang)
679684

680685
if outFormat != "" && suffix != "" {
686+
if lang != "" {
687+
names = append(names, fmt.Sprintf("%s.%s.%s.%s", shortcodeName, lang, outFormat, suffix))
688+
}
681689
names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix))
682690
}
683691

684692
if suffix != "" {
693+
if lang != "" {
694+
names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, lang, suffix))
695+
}
685696
names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix))
686697
}
687698

‎hugolib/shortcode_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -837,8 +837,8 @@ func TestReplaceShortcodeTokens(t *testing.T) {
837837
func TestScKey(t *testing.T) {
838838
require.Equal(t, scKey{Suffix: "xml", ShortcodePlaceholder: "ABCD"},
839839
newScKey(media.XMLType, "ABCD"))
840-
require.Equal(t, scKey{Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"},
841-
newScKeyFromOutputFormat(output.AMPFormat, "EFGH"))
840+
require.Equal(t, scKey{Lang: "en", Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"},
841+
newScKeyFromLangAndOutputFormat("en", output.AMPFormat, "EFGH"))
842842
require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"},
843843
newDefaultScKey("IJKL"))
844844

‎output/docshelper.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func createLayoutExamples() interface{} {
4444
f Format
4545
}{
4646
{`AMP home, with theme "demoTheme".`, LayoutDescriptor{Kind: "home"}, true, "", AMPFormat},
47+
{`AMP home, French language".`, LayoutDescriptor{Kind: "home", Lang: "fr"}, false, "", AMPFormat},
4748
{"JSON home, no theme.", LayoutDescriptor{Kind: "home"}, false, "", JSONFormat},
4849
{fmt.Sprintf(`CSV regular, "layout: %s" in front matter.`, demoLayout), LayoutDescriptor{Kind: "page", Layout: demoLayout}, false, "", CSVFormat},
4950
{fmt.Sprintf(`JSON regular, "type: %s" in front matter.`, demoType), LayoutDescriptor{Kind: "page", Type: demoType}, false, "", JSONFormat},

‎output/layout.go‎

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type LayoutDescriptor struct {
2626
Type string
2727
Section string
2828
Kind string
29+
Lang string
2930
Layout string
3031
}
3132

@@ -55,31 +56,33 @@ func NewLayoutHandler(hasTheme bool) *LayoutHandler {
5556

5657
const (
5758

59+
// TODO(bep) variations reduce to 1 "."
60+
5861
// The RSS templates doesn't map easily into the regular pages.
59-
layoutsRSSHome = `NAME.SUFFIX _default/NAME.SUFFIX _internal/_default/rss.xml`
60-
layoutsRSSSection = `section/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
61-
layoutsRSSTaxonomy = `taxonomy/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
62-
layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
62+
layoutsRSSHome = `VARIATIONS _default/VARIATIONS _internal/_default/rss.xml`
63+
layoutsRSSSection = `section/SECTION.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
64+
layoutsRSSTaxonomy = `taxonomy/SECTION.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
65+
layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml`
6366

64-
layoutsHome = "index.NAME.SUFFIX index.SUFFIX _default/list.NAME.SUFFIX _default/list.SUFFIX"
67+
layoutsHome = "index.VARIATIONS _default/list.VARIATIONS"
6568
layoutsSection = `
66-
section/SECTION.NAME.SUFFIX section/SECTION.SUFFIX
67-
SECTION/list.NAME.SUFFIX SECTION/list.SUFFIX
68-
_default/section.NAME.SUFFIX _default/section.SUFFIX
69-
_default/list.NAME.SUFFIX _default/list.SUFFIX
70-
indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX
71-
_default/indexes.NAME.SUFFIX _default/indexes.SUFFIX
69+
section/SECTION.VARIATIONS
70+
SECTION/list.VARIATIONS
71+
_default/section.VARIATIONS
72+
_default/list.VARIATIONS
73+
indexes/SECTION.VARIATIONS
74+
_default/indexes.VARIATIONS
7275
`
7376
layoutsTaxonomy = `
74-
taxonomy/SECTION.NAME.SUFFIX taxonomy/SECTION.SUFFIX
75-
indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX
76-
_default/taxonomy.NAME.SUFFIX _default/taxonomy.SUFFIX
77-
_default/list.NAME.SUFFIX _default/list.SUFFIX
77+
taxonomy/SECTION.VARIATIONS
78+
indexes/SECTION.VARIATIONS
79+
_default/taxonomy.VARIATIONS
80+
_default/list.VARIATIONS
7881
`
7982
layoutsTaxonomyTerm = `
80-
taxonomy/SECTION.terms.NAME.SUFFIX taxonomy/SECTION.terms.SUFFIX
81-
_default/terms.NAME.SUFFIX _default/terms.SUFFIX
82-
indexes/indexes.NAME.SUFFIX indexes/indexes.SUFFIX
83+
taxonomy/SECTION.terms.VARIATIONS
84+
_default/terms.VARIATIONS
85+
indexes/indexes.VARIATIONS
8386
`
8487
)
8588

@@ -185,14 +188,41 @@ func resolveListTemplate(d LayoutDescriptor, f Format,
185188
}
186189

187190
func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string {
188-
delim := "."
189-
if f.MediaType.Delimiter == "" {
190-
delim = ""
191+
192+
// VARIATIONS will be replaced with
193+
// .lang.name.suffix
194+
// .name.suffix
195+
// .lang.suffix
196+
// .suffix
197+
var replacementValues []string
198+
199+
name := strings.ToLower(f.Name)
200+
201+
if d.Lang != "" {
202+
replacementValues = append(replacementValues, fmt.Sprintf("%s.%s.%s", d.Lang, name, f.MediaType.Suffix))
203+
}
204+
205+
replacementValues = append(replacementValues, fmt.Sprintf("%s.%s", name, f.MediaType.Suffix))
206+
207+
if d.Lang != "" {
208+
replacementValues = append(replacementValues, fmt.Sprintf("%s.%s", d.Lang, f.MediaType.Suffix))
209+
}
210+
211+
isRSS := f.Name == RSSFormat.Name
212+
213+
if !isRSS {
214+
replacementValues = append(replacementValues, f.MediaType.Suffix)
215+
}
216+
217+
var layouts []string
218+
219+
templFields := strings.Fields(templ)
220+
221+
for _, field := range templFields {
222+
for _, replacements := range replacementValues {
223+
layouts = append(layouts, replaceKeyValues(field, "VARIATIONS", replacements, "SECTION", d.Section))
224+
}
191225
}
192-
layouts := strings.Fields(replaceKeyValues(templ,
193-
".SUFFIX", delim+f.MediaType.Suffix,
194-
"NAME", strings.ToLower(f.Name),
195-
"SECTION", d.Section))
196226

197227
return filterDotLess(layouts)
198228
}
@@ -201,9 +231,7 @@ func filterDotLess(layouts []string) []string {
201231
var filteredLayouts []string
202232

203233
for _, l := range layouts {
204-
// This may be constructed, but media types can be suffix-less, but can contain
205-
// a delimiter.
206-
l = strings.TrimSuffix(l, ".")
234+
l = strings.Trim(l, ".")
207235
// If media type has no suffix, we have "index" type of layouts in this list, which
208236
// doesn't make much sense.
209237
if strings.Contains(l, ".") {

‎output/layout_test.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ func TestLayout(t *testing.T) {
5959
}{
6060
{"Home", LayoutDescriptor{Kind: "home"}, true, "", ampType,
6161
[]string{"index.amp.html", "index.html", "_default/list.amp.html", "_default/list.html", "theme/index.amp.html", "theme/index.html"}},
62+
{"Home, french language", LayoutDescriptor{Kind: "home", Lang: "fr"}, true, "", ampType,
63+
[]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"}},
6264
{"Home, no ext or delim", LayoutDescriptor{Kind: "home"}, true, "", noExtDelimFormat,
6365
[]string{"index.nem", "_default/list.nem"}},
6466
{"Home, no ext", LayoutDescriptor{Kind: "home"}, true, "", noExt,

0 commit comments

Comments
 (0)