site_output_test.go (17604B)
1 // Copyright 2019 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package hugolib
15
16 import (
17 "fmt"
18 "strings"
19 "testing"
20
21 qt "github.com/frankban/quicktest"
22 "github.com/gohugoio/hugo/config"
23 "github.com/gohugoio/hugo/resources/page"
24
25 "github.com/spf13/afero"
26
27 "github.com/gohugoio/hugo/helpers"
28 "github.com/gohugoio/hugo/output"
29 )
30
31 func TestSiteWithPageOutputs(t *testing.T) {
32 for _, outputs := range [][]string{{"html", "json", "calendar"}, {"json"}} {
33 outputs := outputs
34 t.Run(fmt.Sprintf("%v", outputs), func(t *testing.T) {
35 t.Parallel()
36 doTestSiteWithPageOutputs(t, outputs)
37 })
38 }
39 }
40
41 func doTestSiteWithPageOutputs(t *testing.T, outputs []string) {
42 outputsStr := strings.Replace(fmt.Sprintf("%q", outputs), " ", ", ", -1)
43
44 siteConfig := `
45 baseURL = "http://example.com/blog"
46
47 paginate = 1
48 defaultContentLanguage = "en"
49
50 disableKinds = ["section", "term", "taxonomy", "RSS", "sitemap", "robotsTXT", "404"]
51
52 [Taxonomies]
53 tag = "tags"
54 category = "categories"
55
56 defaultContentLanguage = "en"
57
58
59 [languages]
60
61 [languages.en]
62 title = "Title in English"
63 languageName = "English"
64 weight = 1
65
66 [languages.nn]
67 languageName = "Nynorsk"
68 weight = 2
69 title = "Tittel på Nynorsk"
70
71 `
72
73 pageTemplate := `---
74 title: "%s"
75 outputs: %s
76 ---
77 # Doc
78
79 {{< myShort >}}
80
81 {{< myOtherShort >}}
82
83 `
84
85 b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig)
86 b.WithI18n("en.toml", `
87 [elbow]
88 other = "Elbow"
89 `, "nn.toml", `
90 [elbow]
91 other = "Olboge"
92 `)
93
94 b.WithTemplates(
95 // Case issue partials #3333
96 "layouts/partials/GoHugo.html", `Go Hugo Partial`,
97 "layouts/_default/baseof.json", `START JSON:{{block "main" .}}default content{{ end }}:END JSON`,
98 "layouts/_default/baseof.html", `START HTML:{{block "main" .}}default content{{ end }}:END HTML`,
99 "layouts/shortcodes/myOtherShort.html", `OtherShort: {{ "<h1>Hi!</h1>" | safeHTML }}`,
100 "layouts/shortcodes/myShort.html", `ShortHTML`,
101 "layouts/shortcodes/myShort.json", `ShortJSON`,
102
103 "layouts/_default/list.json", `{{ define "main" }}
104 List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}|
105 {{- range .AlternativeOutputFormats -}}
106 Alt Output: {{ .Name -}}|
107 {{- end -}}|
108 {{- range .OutputFormats -}}
109 Output/Rel: {{ .Name -}}/{{ .Rel }}|{{ .MediaType }}
110 {{- end -}}
111 {{ with .OutputFormats.Get "JSON" }}
112 <atom:link href={{ .Permalink }} rel="self" type="{{ .MediaType }}" />
113 {{ end }}
114 {{ .Site.Language.Lang }}: {{ T "elbow" -}}
115 {{ end }}
116 `,
117 "layouts/_default/list.html", `{{ define "main" }}
118 List HTML|{{.Title }}|
119 {{- with .OutputFormats.Get "HTML" -}}
120 <atom:link href={{ .Permalink }} rel="self" type="{{ .MediaType }}" />
121 {{- end -}}
122 {{ .Site.Language.Lang }}: {{ T "elbow" -}}
123 Partial Hugo 1: {{ partial "GoHugo.html" . }}
124 Partial Hugo 2: {{ partial "GoHugo" . -}}
125 Content: {{ .Content }}
126 Len Pages: {{ .Kind }} {{ len .Site.RegularPages }} Page Number: {{ .Paginator.PageNumber }}
127 {{ end }}
128 `,
129 "layouts/_default/single.html", `{{ define "main" }}{{ .Content }}{{ end }}`,
130 )
131
132 b.WithContent("_index.md", fmt.Sprintf(pageTemplate, "JSON Home", outputsStr))
133 b.WithContent("_index.nn.md", fmt.Sprintf(pageTemplate, "JSON Nynorsk Heim", outputsStr))
134
135 for i := 1; i <= 10; i++ {
136 b.WithContent(fmt.Sprintf("p%d.md", i), fmt.Sprintf(pageTemplate, fmt.Sprintf("Page %d", i), outputsStr))
137 }
138
139 b.Build(BuildCfg{})
140
141 s := b.H.Sites[0]
142 b.Assert(s.language.Lang, qt.Equals, "en")
143
144 home := s.getPage(page.KindHome)
145
146 b.Assert(home, qt.Not(qt.IsNil))
147
148 lenOut := len(outputs)
149
150 b.Assert(len(home.OutputFormats()), qt.Equals, lenOut)
151
152 // There is currently always a JSON output to make it simpler ...
153 altFormats := lenOut - 1
154 hasHTML := helpers.InStringArray(outputs, "html")
155 b.AssertFileContent("public/index.json",
156 "List JSON",
157 fmt.Sprintf("Alt formats: %d", altFormats),
158 )
159
160 if hasHTML {
161 b.AssertFileContent("public/index.json",
162 "Alt Output: HTML",
163 "Output/Rel: JSON/alternate|",
164 "Output/Rel: HTML/canonical|",
165 "en: Elbow",
166 "ShortJSON",
167 "OtherShort: <h1>Hi!</h1>",
168 )
169
170 b.AssertFileContent("public/index.html",
171 // The HTML entity is a deliberate part of this test: The HTML templates are
172 // parsed with html/template.
173 `List HTML|JSON Home|<atom:link href=http://example.com/blog/ rel="self" type="text/html" />`,
174 "en: Elbow",
175 "ShortHTML",
176 "OtherShort: <h1>Hi!</h1>",
177 "Len Pages: home 10",
178 )
179 b.AssertFileContent("public/page/2/index.html", "Page Number: 2")
180 b.Assert(b.CheckExists("public/page/2/index.json"), qt.Equals, false)
181
182 b.AssertFileContent("public/nn/index.html",
183 "List HTML|JSON Nynorsk Heim|",
184 "nn: Olboge")
185 } else {
186 b.AssertFileContent("public/index.json",
187 "Output/Rel: JSON/canonical|",
188 // JSON is plain text, so no need to safeHTML this and that
189 `<atom:link href=http://example.com/blog/index.json rel="self" type="application/json" />`,
190 "ShortJSON",
191 "OtherShort: <h1>Hi!</h1>",
192 )
193 b.AssertFileContent("public/nn/index.json",
194 "List JSON|JSON Nynorsk Heim|",
195 "nn: Olboge",
196 "ShortJSON",
197 )
198 }
199
200 of := home.OutputFormats()
201
202 json := of.Get("JSON")
203 b.Assert(json, qt.Not(qt.IsNil))
204 b.Assert(json.RelPermalink(), qt.Equals, "/blog/index.json")
205 b.Assert(json.Permalink(), qt.Equals, "http://example.com/blog/index.json")
206
207 if helpers.InStringArray(outputs, "cal") {
208 cal := of.Get("calendar")
209 b.Assert(cal, qt.Not(qt.IsNil))
210 b.Assert(cal.RelPermalink(), qt.Equals, "/blog/index.ics")
211 b.Assert(cal.Permalink(), qt.Equals, "webcal://example.com/blog/index.ics")
212 }
213
214 b.Assert(home.HasShortcode("myShort"), qt.Equals, true)
215 b.Assert(home.HasShortcode("doesNotExist"), qt.Equals, false)
216 }
217
218 // Issue #3447
219 func TestRedefineRSSOutputFormat(t *testing.T) {
220 siteConfig := `
221 baseURL = "http://example.com/blog"
222
223 paginate = 1
224 defaultContentLanguage = "en"
225
226 disableKinds = ["page", "section", "term", "taxonomy", "sitemap", "robotsTXT", "404"]
227
228 [outputFormats]
229 [outputFormats.RSS]
230 mediatype = "application/rss"
231 baseName = "feed"
232
233 `
234
235 c := qt.New(t)
236
237 mf := afero.NewMemMapFs()
238 writeToFs(t, mf, "content/foo.html", `foo`)
239
240 th, h := newTestSitesFromConfig(t, mf, siteConfig)
241
242 err := h.Build(BuildCfg{})
243
244 c.Assert(err, qt.IsNil)
245
246 th.assertFileContent("public/feed.xml", "Recent content on")
247
248 s := h.Sites[0]
249
250 // Issue #3450
251 c.Assert(s.Info.RSSLink, qt.Equals, "http://example.com/blog/feed.xml")
252 }
253
254 // Issue #3614
255 func TestDotLessOutputFormat(t *testing.T) {
256 siteConfig := `
257 baseURL = "http://example.com/blog"
258
259 paginate = 1
260 defaultContentLanguage = "en"
261
262 disableKinds = ["page", "section", "term", "taxonomy", "sitemap", "robotsTXT", "404"]
263
264 [mediaTypes]
265 [mediaTypes."text/nodot"]
266 delimiter = ""
267 [mediaTypes."text/defaultdelim"]
268 suffixes = ["defd"]
269 [mediaTypes."text/nosuffix"]
270 [mediaTypes."text/customdelim"]
271 suffixes = ["del"]
272 delimiter = "_"
273
274 [outputs]
275 home = [ "DOTLESS", "DEF", "NOS", "CUS" ]
276
277 [outputFormats]
278 [outputFormats.DOTLESS]
279 mediatype = "text/nodot"
280 baseName = "_redirects" # This is how Netlify names their redirect files.
281 [outputFormats.DEF]
282 mediatype = "text/defaultdelim"
283 baseName = "defaultdelimbase"
284 [outputFormats.NOS]
285 mediatype = "text/nosuffix"
286 baseName = "nosuffixbase"
287 [outputFormats.CUS]
288 mediatype = "text/customdelim"
289 baseName = "customdelimbase"
290
291 `
292
293 c := qt.New(t)
294
295 mf := afero.NewMemMapFs()
296 writeToFs(t, mf, "content/foo.html", `foo`)
297 writeToFs(t, mf, "layouts/_default/list.dotless", `a dotless`)
298 writeToFs(t, mf, "layouts/_default/list.def.defd", `default delimim`)
299 writeToFs(t, mf, "layouts/_default/list.nos", `no suffix`)
300 writeToFs(t, mf, "layouts/_default/list.cus.del", `custom delim`)
301
302 th, h := newTestSitesFromConfig(t, mf, siteConfig)
303
304 err := h.Build(BuildCfg{})
305
306 c.Assert(err, qt.IsNil)
307
308 s := h.Sites[0]
309
310 th.assertFileContent("public/_redirects", "a dotless")
311 th.assertFileContent("public/defaultdelimbase.defd", "default delimim")
312 // This looks weird, but the user has chosen this definition.
313 th.assertFileContent("public/nosuffixbase", "no suffix")
314 th.assertFileContent("public/customdelimbase_del", "custom delim")
315
316 home := s.getPage(page.KindHome)
317 c.Assert(home, qt.Not(qt.IsNil))
318
319 outputs := home.OutputFormats()
320
321 c.Assert(outputs.Get("DOTLESS").RelPermalink(), qt.Equals, "/blog/_redirects")
322 c.Assert(outputs.Get("DEF").RelPermalink(), qt.Equals, "/blog/defaultdelimbase.defd")
323 c.Assert(outputs.Get("NOS").RelPermalink(), qt.Equals, "/blog/nosuffixbase")
324 c.Assert(outputs.Get("CUS").RelPermalink(), qt.Equals, "/blog/customdelimbase_del")
325 }
326
327 // Issue 8030
328 func TestGetOutputFormatRel(t *testing.T) {
329 b := newTestSitesBuilder(t).
330 WithSimpleConfigFileAndSettings(map[string]any{
331 "outputFormats": map[string]any{
332 "humansTXT": map[string]any{
333 "name": "HUMANS",
334 "mediaType": "text/plain",
335 "baseName": "humans",
336 "isPlainText": true,
337 "rel": "author",
338 },
339 },
340 }).WithTemplates("index.html", `
341 {{- with ($.Site.GetPage "humans").OutputFormats.Get "humans" -}}
342 <link rel="{{ .Rel }}" type="{{ .MediaType.String }}" href="{{ .Permalink }}">
343 {{- end -}}
344 `).WithContent("humans.md", `---
345 outputs:
346 - HUMANS
347 ---
348 This is my content.
349 `)
350
351 b.Build(BuildCfg{})
352 b.AssertFileContent("public/index.html", `
353 <link rel="author" type="text/plain" href="/humans.txt">
354 `)
355 }
356
357 func TestCreateSiteOutputFormats(t *testing.T) {
358 t.Run("Basic", func(t *testing.T) {
359 c := qt.New(t)
360
361 outputsConfig := map[string]any{
362 page.KindHome: []string{"HTML", "JSON"},
363 page.KindSection: []string{"JSON"},
364 }
365
366 cfg := config.NewWithTestDefaults()
367 cfg.Set("outputs", outputsConfig)
368
369 outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
370 c.Assert(err, qt.IsNil)
371 c.Assert(outputs[page.KindSection], deepEqualsOutputFormats, output.Formats{output.JSONFormat})
372 c.Assert(outputs[page.KindHome], deepEqualsOutputFormats, output.Formats{output.HTMLFormat, output.JSONFormat})
373
374 // Defaults
375 c.Assert(outputs[page.KindTerm], deepEqualsOutputFormats, output.Formats{output.HTMLFormat, output.RSSFormat})
376 c.Assert(outputs[page.KindTaxonomy], deepEqualsOutputFormats, output.Formats{output.HTMLFormat, output.RSSFormat})
377 c.Assert(outputs[page.KindPage], deepEqualsOutputFormats, output.Formats{output.HTMLFormat})
378
379 // These aren't (currently) in use when rendering in Hugo,
380 // but the pages needs to be assigned an output format,
381 // so these should also be correct/sensible.
382 c.Assert(outputs[kindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat})
383 c.Assert(outputs[kindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat})
384 c.Assert(outputs[kindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat})
385 c.Assert(outputs[kind404], deepEqualsOutputFormats, output.Formats{output.HTMLFormat})
386 })
387
388 // Issue #4528
389 t.Run("Mixed case", func(t *testing.T) {
390 c := qt.New(t)
391 cfg := config.NewWithTestDefaults()
392
393 outputsConfig := map[string]any{
394 // Note that we in Hugo 0.53.0 renamed this Kind to "taxonomy",
395 // but keep this test to test the legacy mapping.
396 "taxonomyterm": []string{"JSON"},
397 }
398 cfg.Set("outputs", outputsConfig)
399
400 outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
401 c.Assert(err, qt.IsNil)
402 c.Assert(outputs[page.KindTaxonomy], deepEqualsOutputFormats, output.Formats{output.JSONFormat})
403 })
404 }
405
406 func TestCreateSiteOutputFormatsInvalidConfig(t *testing.T) {
407 c := qt.New(t)
408
409 outputsConfig := map[string]any{
410 page.KindHome: []string{"FOO", "JSON"},
411 }
412
413 cfg := config.NewWithTestDefaults()
414 cfg.Set("outputs", outputsConfig)
415
416 _, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
417 c.Assert(err, qt.Not(qt.IsNil))
418 }
419
420 func TestCreateSiteOutputFormatsEmptyConfig(t *testing.T) {
421 c := qt.New(t)
422
423 outputsConfig := map[string]any{
424 page.KindHome: []string{},
425 }
426
427 cfg := config.NewWithTestDefaults()
428 cfg.Set("outputs", outputsConfig)
429
430 outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false)
431 c.Assert(err, qt.IsNil)
432 c.Assert(outputs[page.KindHome], deepEqualsOutputFormats, output.Formats{output.HTMLFormat, output.RSSFormat})
433 }
434
435 func TestCreateSiteOutputFormatsCustomFormats(t *testing.T) {
436 c := qt.New(t)
437
438 outputsConfig := map[string]any{
439 page.KindHome: []string{},
440 }
441
442 cfg := config.NewWithTestDefaults()
443 cfg.Set("outputs", outputsConfig)
444
445 var (
446 customRSS = output.Format{Name: "RSS", BaseName: "customRSS"}
447 customHTML = output.Format{Name: "HTML", BaseName: "customHTML"}
448 )
449
450 outputs, err := createSiteOutputFormats(output.Formats{customRSS, customHTML}, cfg.GetStringMap("outputs"), false)
451 c.Assert(err, qt.IsNil)
452 c.Assert(outputs[page.KindHome], deepEqualsOutputFormats, output.Formats{customHTML, customRSS})
453 }
454
455 // https://github.com/gohugoio/hugo/issues/5849
456 func TestOutputFormatPermalinkable(t *testing.T) {
457 config := `
458 baseURL = "https://example.com"
459
460
461
462 # DAMP is similar to AMP, but not permalinkable.
463 [outputFormats]
464 [outputFormats.damp]
465 mediaType = "text/html"
466 path = "damp"
467 [outputFormats.ramp]
468 mediaType = "text/html"
469 path = "ramp"
470 permalinkable = true
471 [outputFormats.base]
472 mediaType = "text/html"
473 isHTML = true
474 baseName = "that"
475 permalinkable = true
476 [outputFormats.nobase]
477 mediaType = "application/json"
478 permalinkable = true
479
480 `
481
482 b := newTestSitesBuilder(t).WithConfigFile("toml", config)
483 b.WithContent("_index.md", `
484 ---
485 Title: Home Sweet Home
486 outputs: [ "html", "amp", "damp", "base" ]
487 ---
488
489 `)
490
491 b.WithContent("blog/html-amp.md", `
492 ---
493 Title: AMP and HTML
494 outputs: [ "html", "amp" ]
495 ---
496
497 `)
498
499 b.WithContent("blog/html-damp.md", `
500 ---
501 Title: DAMP and HTML
502 outputs: [ "html", "damp" ]
503 ---
504
505 `)
506
507 b.WithContent("blog/html-ramp.md", `
508 ---
509 Title: RAMP and HTML
510 outputs: [ "html", "ramp" ]
511 ---
512
513 `)
514
515 b.WithContent("blog/html.md", `
516 ---
517 Title: HTML only
518 outputs: [ "html" ]
519 ---
520
521 `)
522
523 b.WithContent("blog/amp.md", `
524 ---
525 Title: AMP only
526 outputs: [ "amp" ]
527 ---
528
529 `)
530
531 b.WithContent("blog/html-base-nobase.md", `
532 ---
533 Title: HTML, Base and Nobase
534 outputs: [ "html", "base", "nobase" ]
535 ---
536
537 `)
538
539 const commonTemplate = `
540 This RelPermalink: {{ .RelPermalink }}
541 Output Formats: {{ len .OutputFormats }};{{ range .OutputFormats }}{{ .Name }};{{ .RelPermalink }}|{{ end }}
542
543 `
544
545 b.WithTemplatesAdded("index.html", commonTemplate)
546 b.WithTemplatesAdded("_default/single.html", commonTemplate)
547 b.WithTemplatesAdded("_default/single.json", commonTemplate)
548
549 b.Build(BuildCfg{})
550
551 b.AssertFileContent("public/index.html",
552 "This RelPermalink: /",
553 "Output Formats: 4;HTML;/|AMP;/amp/|damp;/damp/|base;/that.html|",
554 )
555
556 b.AssertFileContent("public/amp/index.html",
557 "This RelPermalink: /amp/",
558 "Output Formats: 4;HTML;/|AMP;/amp/|damp;/damp/|base;/that.html|",
559 )
560
561 b.AssertFileContent("public/blog/html-amp/index.html",
562 "Output Formats: 2;HTML;/blog/html-amp/|AMP;/amp/blog/html-amp/|",
563 "This RelPermalink: /blog/html-amp/")
564
565 b.AssertFileContent("public/amp/blog/html-amp/index.html",
566 "Output Formats: 2;HTML;/blog/html-amp/|AMP;/amp/blog/html-amp/|",
567 "This RelPermalink: /amp/blog/html-amp/")
568
569 // Damp is not permalinkable
570 b.AssertFileContent("public/damp/blog/html-damp/index.html",
571 "This RelPermalink: /blog/html-damp/",
572 "Output Formats: 2;HTML;/blog/html-damp/|damp;/damp/blog/html-damp/|")
573
574 b.AssertFileContent("public/blog/html-ramp/index.html",
575 "This RelPermalink: /blog/html-ramp/",
576 "Output Formats: 2;HTML;/blog/html-ramp/|ramp;/ramp/blog/html-ramp/|")
577
578 b.AssertFileContent("public/ramp/blog/html-ramp/index.html",
579 "This RelPermalink: /ramp/blog/html-ramp/",
580 "Output Formats: 2;HTML;/blog/html-ramp/|ramp;/ramp/blog/html-ramp/|")
581
582 // https://github.com/gohugoio/hugo/issues/5877
583 outputFormats := "Output Formats: 3;HTML;/blog/html-base-nobase/|base;/blog/html-base-nobase/that.html|nobase;/blog/html-base-nobase/index.json|"
584
585 b.AssertFileContent("public/blog/html-base-nobase/index.json",
586 "This RelPermalink: /blog/html-base-nobase/index.json",
587 outputFormats,
588 )
589
590 b.AssertFileContent("public/blog/html-base-nobase/that.html",
591 "This RelPermalink: /blog/html-base-nobase/that.html",
592 outputFormats,
593 )
594
595 b.AssertFileContent("public/blog/html-base-nobase/index.html",
596 "This RelPermalink: /blog/html-base-nobase/",
597 outputFormats,
598 )
599 }
600
601 func TestSiteWithPageNoOutputs(t *testing.T) {
602 t.Parallel()
603
604 b := newTestSitesBuilder(t)
605 b.WithConfigFile("toml", `
606 baseURL = "https://example.com"
607
608 [outputFormats.o1]
609 mediaType = "text/html"
610
611
612
613 `)
614 b.WithContent("outputs-empty.md", `---
615 title: "Empty Outputs"
616 outputs: []
617 ---
618
619 Word1. Word2.
620
621 `,
622 "outputs-string.md", `---
623 title: "Outputs String"
624 outputs: "o1"
625 ---
626
627 Word1. Word2.
628
629 `)
630
631 b.WithTemplates("index.html", `
632 {{ range .Site.RegularPages }}
633 WordCount: {{ .WordCount }}
634 {{ end }}
635 `)
636
637 b.WithTemplates("_default/single.html", `HTML: {{ .Content }}`)
638 b.WithTemplates("_default/single.o1.html", `O1: {{ .Content }}`)
639
640 b.Build(BuildCfg{})
641
642 b.AssertFileContent(
643 "public/index.html",
644 " WordCount: 2")
645
646 b.AssertFileContent("public/outputs-empty/index.html", "HTML:", "Word1. Word2.")
647 b.AssertFileContent("public/outputs-string/index.html", "O1:", "Word1. Word2.")
648 }