config_test.go (17460B)
1 // Copyright 2016-present 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 "bytes"
18 "fmt"
19 "path/filepath"
20 "strings"
21 "testing"
22
23 "github.com/gohugoio/hugo/config"
24
25 "github.com/gohugoio/hugo/media"
26 "github.com/google/go-cmp/cmp"
27
28 qt "github.com/frankban/quicktest"
29 "github.com/gohugoio/hugo/common/maps"
30 "github.com/spf13/afero"
31 )
32
33 func TestLoadConfig(t *testing.T) {
34
35 c := qt.New(t)
36
37 loadConfig := func(c *qt.C, configContent string, fromDir bool) config.Provider {
38 mm := afero.NewMemMapFs()
39 filename := "config.toml"
40 descriptor := ConfigSourceDescriptor{Fs: mm}
41 if fromDir {
42 filename = filepath.Join("config", "_default", filename)
43 descriptor.AbsConfigDir = "config"
44 }
45 writeToFs(t, mm, filename, configContent)
46 cfg, _, err := LoadConfig(descriptor)
47 c.Assert(err, qt.IsNil)
48 return cfg
49 }
50
51 c.Run("Basic", func(c *qt.C) {
52 c.Parallel()
53 // Add a random config variable for testing.
54 // side = page in Norwegian.
55 cfg := loadConfig(c, `PaginatePath = "side"`, false)
56 c.Assert(cfg.GetString("paginatePath"), qt.Equals, "side")
57 })
58
59 // Issue #8763
60 for _, fromDir := range []bool{false, true} {
61 testName := "Taxonomy overrides"
62 if fromDir {
63 testName += " from dir"
64 }
65 c.Run(testName, func(c *qt.C) {
66 c.Parallel()
67 cfg := loadConfig(c, `[taxonomies]
68 appellation = "appellations"
69 vigneron = "vignerons"`, fromDir)
70
71 c.Assert(cfg.Get("taxonomies"), qt.DeepEquals, maps.Params{
72 "appellation": "appellations",
73 "vigneron": "vignerons",
74 })
75 })
76 }
77 }
78
79 func TestLoadMultiConfig(t *testing.T) {
80 t.Parallel()
81
82 c := qt.New(t)
83
84 // Add a random config variable for testing.
85 // side = page in Norwegian.
86 configContentBase := `
87 DontChange = "same"
88 PaginatePath = "side"
89 `
90 configContentSub := `
91 PaginatePath = "top"
92 `
93 mm := afero.NewMemMapFs()
94
95 writeToFs(t, mm, "base.toml", configContentBase)
96
97 writeToFs(t, mm, "override.toml", configContentSub)
98
99 cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "base.toml,override.toml"})
100 c.Assert(err, qt.IsNil)
101
102 c.Assert(cfg.GetString("paginatePath"), qt.Equals, "top")
103 c.Assert(cfg.GetString("DontChange"), qt.Equals, "same")
104 }
105
106 func TestLoadConfigFromThemes(t *testing.T) {
107 t.Parallel()
108
109 c := qt.New(t)
110
111 mainConfigTemplate := `
112 theme = "test-theme"
113 baseURL = "https://example.com/"
114
115 [frontmatter]
116 date = ["date","publishDate"]
117
118 [params]
119 MERGE_PARAMS
120 p1 = "p1 main"
121 [params.b]
122 b1 = "b1 main"
123 [params.b.c]
124 bc1 = "bc1 main"
125
126 [mediaTypes]
127 [mediaTypes."text/m1"]
128 suffixes = ["m1main"]
129
130 [outputFormats.o1]
131 mediaType = "text/m1"
132 baseName = "o1main"
133
134 [languages]
135 [languages.en]
136 languageName = "English"
137 [languages.en.params]
138 pl1 = "p1-en-main"
139 [languages.nb]
140 languageName = "Norsk"
141 [languages.nb.params]
142 pl1 = "p1-nb-main"
143
144 [[menu.main]]
145 name = "menu-main-main"
146
147 [[menu.top]]
148 name = "menu-top-main"
149
150 `
151
152 themeConfig := `
153 baseURL = "http://bep.is/"
154
155 # Can not be set in theme.
156 disableKinds = ["taxonomy", "term"]
157
158 # Can not be set in theme.
159 [frontmatter]
160 expiryDate = ["date"]
161
162 [params]
163 p1 = "p1 theme"
164 p2 = "p2 theme"
165 [params.b]
166 b1 = "b1 theme"
167 b2 = "b2 theme"
168 [params.b.c]
169 bc1 = "bc1 theme"
170 bc2 = "bc2 theme"
171 [params.b.c.d]
172 bcd1 = "bcd1 theme"
173
174 [mediaTypes]
175 [mediaTypes."text/m1"]
176 suffixes = ["m1theme"]
177 [mediaTypes."text/m2"]
178 suffixes = ["m2theme"]
179
180 [outputFormats.o1]
181 mediaType = "text/m1"
182 baseName = "o1theme"
183 [outputFormats.o2]
184 mediaType = "text/m2"
185 baseName = "o2theme"
186
187 [languages]
188 [languages.en]
189 languageName = "English2"
190 [languages.en.params]
191 pl1 = "p1-en-theme"
192 pl2 = "p2-en-theme"
193 [[languages.en.menu.main]]
194 name = "menu-lang-en-main"
195 [[languages.en.menu.theme]]
196 name = "menu-lang-en-theme"
197 [languages.nb]
198 languageName = "Norsk2"
199 [languages.nb.params]
200 pl1 = "p1-nb-theme"
201 pl2 = "p2-nb-theme"
202 top = "top-nb-theme"
203 [[languages.nb.menu.main]]
204 name = "menu-lang-nb-main"
205 [[languages.nb.menu.theme]]
206 name = "menu-lang-nb-theme"
207 [[languages.nb.menu.top]]
208 name = "menu-lang-nb-top"
209
210 [[menu.main]]
211 name = "menu-main-theme"
212
213 [[menu.thememenu]]
214 name = "menu-theme"
215
216 `
217
218 buildForConfig := func(t testing.TB, mainConfig, themeConfig string) *sitesBuilder {
219 b := newTestSitesBuilder(t)
220 b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
221 return b.Build(BuildCfg{})
222 }
223
224 buildForStrategy := func(t testing.TB, s string) *sitesBuilder {
225 mainConfig := strings.ReplaceAll(mainConfigTemplate, "MERGE_PARAMS", s)
226 return buildForConfig(t, mainConfig, themeConfig)
227 }
228
229 c.Run("Merge default", func(c *qt.C) {
230 b := buildForStrategy(c, "")
231
232 got := b.Cfg.Get("").(maps.Params)
233
234 // Issue #8866
235 b.Assert(b.Cfg.Get("disableKinds"), qt.IsNil)
236
237 b.Assert(got["params"], qt.DeepEquals, maps.Params{
238 "b": maps.Params{
239 "b1": "b1 main",
240 "c": maps.Params{
241 "bc1": "bc1 main",
242 "bc2": "bc2 theme",
243 "d": maps.Params{"bcd1": string("bcd1 theme")},
244 },
245 "b2": "b2 theme",
246 },
247 "p2": "p2 theme",
248 "p1": "p1 main",
249 })
250
251 b.Assert(got["mediatypes"], qt.DeepEquals, maps.Params{
252 "text/m2": maps.Params{
253 "suffixes": []any{
254 "m2theme",
255 },
256 },
257 "text/m1": maps.Params{
258 "suffixes": []any{
259 "m1main",
260 },
261 },
262 })
263
264 var eq = qt.CmpEquals(
265 cmp.Comparer(func(m1, m2 media.Type) bool {
266 if m1.SubType != m2.SubType {
267 return false
268 }
269 return m1.FirstSuffix == m2.FirstSuffix
270 }),
271 )
272
273 mediaTypes := b.H.Sites[0].mediaTypesConfig
274 m1, _ := mediaTypes.GetByType("text/m1")
275 m2, _ := mediaTypes.GetByType("text/m2")
276
277 b.Assert(got["outputformats"], eq, maps.Params{
278 "o1": maps.Params{
279 "mediatype": m1,
280 "basename": "o1main",
281 },
282 "o2": maps.Params{
283 "basename": "o2theme",
284 "mediatype": m2,
285 },
286 })
287
288 b.Assert(got["languages"], qt.DeepEquals, maps.Params{
289 "en": maps.Params{
290 "languagename": "English",
291 "params": maps.Params{
292 "pl2": "p2-en-theme",
293 "pl1": "p1-en-main",
294 },
295 "menus": maps.Params{
296 "main": []any{
297 map[string]any{
298 "name": "menu-lang-en-main",
299 },
300 },
301 "theme": []any{
302 map[string]any{
303 "name": "menu-lang-en-theme",
304 },
305 },
306 },
307 },
308 "nb": maps.Params{
309 "languagename": "Norsk",
310 "params": maps.Params{
311 "top": "top-nb-theme",
312 "pl1": "p1-nb-main",
313 "pl2": "p2-nb-theme",
314 },
315 "menus": maps.Params{
316 "main": []any{
317 map[string]any{
318 "name": "menu-lang-nb-main",
319 },
320 },
321 "theme": []any{
322 map[string]any{
323 "name": "menu-lang-nb-theme",
324 },
325 },
326 "top": []any{
327 map[string]any{
328 "name": "menu-lang-nb-top",
329 },
330 },
331 },
332 },
333 })
334
335 c.Assert(got["baseurl"], qt.Equals, "https://example.com/")
336 })
337
338 c.Run("Merge shallow", func(c *qt.C) {
339 b := buildForStrategy(c, fmt.Sprintf("_merge=%q", "shallow"))
340
341 got := b.Cfg.Get("").(maps.Params)
342
343 // Shallow merge, only add new keys to params.
344 b.Assert(got["params"], qt.DeepEquals, maps.Params{
345 "p1": "p1 main",
346 "b": maps.Params{
347 "b1": "b1 main",
348 "c": maps.Params{
349 "bc1": "bc1 main",
350 },
351 },
352 "p2": "p2 theme",
353 })
354 })
355
356 c.Run("Merge no params in project", func(c *qt.C) {
357 b := buildForConfig(
358 c,
359 "baseURL=\"https://example.org\"\ntheme = \"test-theme\"\n",
360 "[params]\np1 = \"p1 theme\"\n",
361 )
362
363 got := b.Cfg.Get("").(maps.Params)
364
365 b.Assert(got["params"], qt.DeepEquals, maps.Params{
366 "p1": "p1 theme",
367 })
368 })
369
370 c.Run("Merge language no menus or params in project", func(c *qt.C) {
371 b := buildForConfig(
372 c,
373 `
374 theme = "test-theme"
375 baseURL = "https://example.com/"
376
377 [languages]
378 [languages.en]
379 languageName = "English"
380
381 `,
382 `
383 [languages]
384 [languages.en]
385 languageName = "EnglishTheme"
386
387 [languages.en.params]
388 p1="themep1"
389
390 [[languages.en.menus.main]]
391 name = "menu-theme"
392 `,
393 )
394
395 got := b.Cfg.Get("").(maps.Params)
396
397 b.Assert(got["languages"], qt.DeepEquals,
398 maps.Params{
399 "en": maps.Params{
400 "languagename": "English",
401 "menus": maps.Params{
402 "main": []any{
403 map[string]any{
404 "name": "menu-theme",
405 },
406 },
407 },
408 "params": maps.Params{
409 "p1": "themep1",
410 },
411 },
412 },
413 )
414 })
415
416 // Issue #8724
417 for _, mergeStrategy := range []string{"none", "shallow"} {
418 c.Run(fmt.Sprintf("Merge with sitemap config in theme, mergestrategy %s", mergeStrategy), func(c *qt.C) {
419
420 smapConfigTempl := `[sitemap]
421 changefreq = %q
422 filename = "sitemap.xml"
423 priority = 0.5`
424
425 b := buildForConfig(
426 c,
427 fmt.Sprintf("_merge=%q\nbaseURL=\"https://example.org\"\ntheme = \"test-theme\"\n", mergeStrategy),
428 "baseURL=\"http://example.com\"\n"+fmt.Sprintf(smapConfigTempl, "monthly"),
429 )
430
431 got := b.Cfg.Get("").(maps.Params)
432
433 if mergeStrategy == "none" {
434 b.Assert(got["sitemap"], qt.DeepEquals, maps.Params{
435 "priority": int(-1),
436 "filename": "sitemap.xml",
437 })
438
439 b.AssertFileContent("public/sitemap.xml", "schemas/sitemap")
440 } else {
441 b.Assert(got["sitemap"], qt.DeepEquals, maps.Params{
442 "priority": int(-1),
443 "filename": "sitemap.xml",
444 "changefreq": "monthly",
445 })
446
447 b.AssertFileContent("public/sitemap.xml", "<changefreq>monthly</changefreq>")
448 }
449
450 })
451 }
452
453 }
454
455 func TestLoadConfigFromThemeDir(t *testing.T) {
456 t.Parallel()
457
458 mainConfig := `
459 theme = "test-theme"
460
461 [params]
462 m1 = "mv1"
463 `
464
465 themeConfig := `
466 [params]
467 t1 = "tv1"
468 t2 = "tv2"
469 `
470
471 themeConfigDir := filepath.Join("themes", "test-theme", "config")
472 themeConfigDirDefault := filepath.Join(themeConfigDir, "_default")
473 themeConfigDirProduction := filepath.Join(themeConfigDir, "production")
474
475 projectConfigDir := "config"
476
477 b := newTestSitesBuilder(t)
478 b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
479 b.Assert(b.Fs.Source.MkdirAll(themeConfigDirDefault, 0777), qt.IsNil)
480 b.Assert(b.Fs.Source.MkdirAll(themeConfigDirProduction, 0777), qt.IsNil)
481 b.Assert(b.Fs.Source.MkdirAll(projectConfigDir, 0777), qt.IsNil)
482
483 b.WithSourceFile(filepath.Join(projectConfigDir, "config.toml"), `[params]
484 m2 = "mv2"
485 `)
486 b.WithSourceFile(filepath.Join(themeConfigDirDefault, "config.toml"), `[params]
487 t2 = "tv2d"
488 t3 = "tv3d"
489 `)
490
491 b.WithSourceFile(filepath.Join(themeConfigDirProduction, "config.toml"), `[params]
492 t3 = "tv3p"
493 `)
494
495 b.Build(BuildCfg{})
496
497 got := b.Cfg.Get("params").(maps.Params)
498
499 b.Assert(got, qt.DeepEquals, maps.Params{
500 "t3": "tv3p",
501 "m1": "mv1",
502 "t1": "tv1",
503 "t2": "tv2d",
504 })
505
506 }
507
508 func TestPrivacyConfig(t *testing.T) {
509 t.Parallel()
510
511 c := qt.New(t)
512
513 tomlConfig := `
514
515 someOtherValue = "foo"
516
517 [privacy]
518 [privacy.youtube]
519 privacyEnhanced = true
520 `
521
522 b := newTestSitesBuilder(t)
523 b.WithConfigFile("toml", tomlConfig)
524 b.Build(BuildCfg{SkipRender: true})
525
526 c.Assert(b.H.Sites[0].Info.Config().Privacy.YouTube.PrivacyEnhanced, qt.Equals, true)
527 }
528
529 func TestLoadConfigModules(t *testing.T) {
530 t.Parallel()
531
532 c := qt.New(t)
533
534 // https://github.com/gohugoio/hugoThemes#themetoml
535
536 const (
537 // Before Hugo 0.56 each theme/component could have its own theme.toml
538 // with some settings, mostly used on the Hugo themes site.
539 // To preserve combability we read these files into the new "modules"
540 // section in config.toml.
541 o1t = `
542 name = "Component o1"
543 license = "MIT"
544 min_version = 0.38
545 `
546 // This is the component's config.toml, using the old theme syntax.
547 o1c = `
548 theme = ["n2"]
549 `
550
551 n1 = `
552 title = "Component n1"
553
554 [module]
555 description = "Component n1 description"
556 [module.hugoVersion]
557 min = "0.40.0"
558 max = "0.50.0"
559 extended = true
560 [[module.imports]]
561 path="o1"
562 [[module.imports]]
563 path="n3"
564
565
566 `
567
568 n2 = `
569 title = "Component n2"
570 `
571
572 n3 = `
573 title = "Component n3"
574 `
575
576 n4 = `
577 title = "Component n4"
578 `
579 )
580
581 b := newTestSitesBuilder(t)
582
583 writeThemeFiles := func(name, configTOML, themeTOML string) {
584 b.WithSourceFile(filepath.Join("themes", name, "data", "module.toml"), fmt.Sprintf("name=%q", name))
585 if configTOML != "" {
586 b.WithSourceFile(filepath.Join("themes", name, "config.toml"), configTOML)
587 }
588 if themeTOML != "" {
589 b.WithSourceFile(filepath.Join("themes", name, "theme.toml"), themeTOML)
590 }
591 }
592
593 writeThemeFiles("n1", n1, "")
594 writeThemeFiles("n2", n2, "")
595 writeThemeFiles("n3", n3, "")
596 writeThemeFiles("n4", n4, "")
597 writeThemeFiles("o1", o1c, o1t)
598
599 b.WithConfigFile("toml", `
600 [module]
601 [[module.imports]]
602 path="n1"
603 [[module.imports]]
604 path="n4"
605
606 `)
607
608 b.Build(BuildCfg{})
609
610 modulesClient := b.H.Paths.ModulesClient
611 var graphb bytes.Buffer
612 modulesClient.Graph(&graphb)
613
614 expected := `project n1
615 n1 o1
616 o1 n2
617 n1 n3
618 project n4
619 `
620
621 c.Assert(graphb.String(), qt.Equals, expected)
622 }
623
624 func TestLoadConfigWithOsEnvOverrides(t *testing.T) {
625 c := qt.New(t)
626
627 baseConfig := `
628
629 theme = "mytheme"
630 environment = "production"
631 enableGitInfo = true
632 intSlice = [5,7,9]
633 floatSlice = [3.14, 5.19]
634 stringSlice = ["a", "b"]
635
636 [outputFormats]
637 [outputFormats.ofbase]
638 mediaType = "text/plain"
639
640 [params]
641 paramWithNoEnvOverride="nooverride"
642 [params.api_config]
643 api_key="default_key"
644 another_key="default another_key"
645
646 [imaging]
647 anchor = "smart"
648 quality = 75
649 `
650
651 newB := func(t testing.TB) *sitesBuilder {
652 b := newTestSitesBuilder(t).WithConfigFile("toml", baseConfig)
653
654 b.WithSourceFile("themes/mytheme/config.toml", `
655
656 [outputFormats]
657 [outputFormats.oftheme]
658 mediaType = "text/plain"
659 [outputFormats.ofbase]
660 mediaType = "application/xml"
661
662 [params]
663 [params.mytheme_section]
664 theme_param="themevalue"
665 theme_param_nooverride="nooverride"
666 [params.mytheme_section2]
667 theme_param="themevalue2"
668
669 `)
670
671 return b
672 }
673
674 c.Run("Variations", func(c *qt.C) {
675
676 b := newB(c)
677
678 b.WithEnviron(
679 "HUGO_ENVIRONMENT", "test",
680 "HUGO_NEW", "new", // key not in config.toml
681 "HUGO_ENABLEGITINFO", "false",
682 "HUGO_IMAGING_ANCHOR", "top",
683 "HUGO_IMAGING_RESAMPLEFILTER", "CatmullRom",
684 "HUGO_STRINGSLICE", `["c", "d"]`,
685 "HUGO_INTSLICE", `[5, 8, 9]`,
686 "HUGO_FLOATSLICE", `[5.32]`,
687 // Issue #7829
688 "HUGOxPARAMSxAPI_CONFIGxAPI_KEY", "new_key",
689 // Delimiters are case sensitive.
690 "HUGOxPARAMSxAPI_CONFIGXANOTHER_KEY", "another_key",
691 // Issue #8346
692 "HUGOxPARAMSxMYTHEME_SECTIONxTHEME_PARAM", "themevalue_changed",
693 "HUGOxPARAMSxMYTHEME_SECTION2xTHEME_PARAM", "themevalue2_changed",
694 "HUGO_PARAMS_EMPTY", ``,
695 "HUGO_PARAMS_HTML", `<a target="_blank" />`,
696 // Issue #8618
697 "HUGO_SERVICES_GOOGLEANALYTICS_ID", `gaid`,
698 "HUGO_PARAMS_A_B_C", "abc",
699 )
700
701 b.Build(BuildCfg{})
702
703 cfg := b.H.Cfg
704 s := b.H.Sites[0]
705 scfg := s.siteConfigConfig.Services
706
707 c.Assert(cfg.Get("environment"), qt.Equals, "test")
708 c.Assert(cfg.GetBool("enablegitinfo"), qt.Equals, false)
709 c.Assert(cfg.Get("new"), qt.Equals, "new")
710 c.Assert(cfg.Get("imaging.anchor"), qt.Equals, "top")
711 c.Assert(cfg.Get("imaging.quality"), qt.Equals, int64(75))
712 c.Assert(cfg.Get("imaging.resamplefilter"), qt.Equals, "CatmullRom")
713 c.Assert(cfg.Get("stringSlice"), qt.DeepEquals, []any{"c", "d"})
714 c.Assert(cfg.Get("floatSlice"), qt.DeepEquals, []any{5.32})
715 c.Assert(cfg.Get("intSlice"), qt.DeepEquals, []any{5, 8, 9})
716 c.Assert(cfg.Get("params.api_config.api_key"), qt.Equals, "new_key")
717 c.Assert(cfg.Get("params.api_config.another_key"), qt.Equals, "default another_key")
718 c.Assert(cfg.Get("params.mytheme_section.theme_param"), qt.Equals, "themevalue_changed")
719 c.Assert(cfg.Get("params.mytheme_section.theme_param_nooverride"), qt.Equals, "nooverride")
720 c.Assert(cfg.Get("params.mytheme_section2.theme_param"), qt.Equals, "themevalue2_changed")
721 c.Assert(cfg.Get("params.empty"), qt.Equals, ``)
722 c.Assert(cfg.Get("params.html"), qt.Equals, `<a target="_blank" />`)
723
724 params := cfg.Get("params").(maps.Params)
725 c.Assert(params["paramwithnoenvoverride"], qt.Equals, "nooverride")
726 c.Assert(cfg.Get("params.paramwithnoenvoverride"), qt.Equals, "nooverride")
727 c.Assert(scfg.GoogleAnalytics.ID, qt.Equals, "gaid")
728 c.Assert(cfg.Get("params.a.b"), qt.DeepEquals, maps.Params{
729 "c": "abc",
730 })
731
732 ofBase, _ := s.outputFormatsConfig.GetByName("ofbase")
733 ofTheme, _ := s.outputFormatsConfig.GetByName("oftheme")
734
735 c.Assert(ofBase.MediaType, qt.Equals, media.TextType)
736 c.Assert(ofTheme.MediaType, qt.Equals, media.TextType)
737
738 })
739
740 // Issue #8709
741 c.Run("Set in string", func(c *qt.C) {
742 b := newB(c)
743
744 b.WithEnviron(
745 "HUGO_ENABLEGITINFO", "false",
746 // imaging.anchor is a string, and it's not possible
747 // to set a child attribute.
748 "HUGO_IMAGING_ANCHOR_FOO", "top",
749 )
750
751 b.Build(BuildCfg{})
752
753 cfg := b.H.Cfg
754 c.Assert(cfg.Get("imaging.anchor"), qt.Equals, "smart")
755
756 })
757
758 }
759
760 func TestInvalidDefaultMarkdownHandler(t *testing.T) {
761 t.Parallel()
762
763 files := `
764 -- config.toml --
765 [markup]
766 defaultMarkdownHandler = 'blackfriday'
767 -- content/_index.md --
768 ## Foo
769 -- layouts/index.html --
770 {{ .Content }}
771
772 `
773
774 b, err := NewIntegrationTestBuilder(
775 IntegrationTestConfig{
776 T: t,
777 TxtarString: files,
778 },
779 ).BuildE()
780
781 b.Assert(err, qt.IsNotNil)
782 b.Assert(err.Error(), qt.Contains, "Configured defaultMarkdownHandler \"blackfriday\" not found. Did you mean to use goldmark? Blackfriday was removed in Hugo v0.100.0.")
783
784 }