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 }