hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

hugo_sites_build_test.go (39770B)

    1 package hugolib
    2 
    3 import (
    4 	"fmt"
    5 	"path/filepath"
    6 	"strings"
    7 	"testing"
    8 	"time"
    9 
   10 	qt "github.com/frankban/quicktest"
   11 	"github.com/gohugoio/hugo/htesting"
   12 	"github.com/gohugoio/hugo/resources/page"
   13 
   14 	"github.com/fortytw2/leaktest"
   15 	"github.com/fsnotify/fsnotify"
   16 	"github.com/gohugoio/hugo/helpers"
   17 	"github.com/gohugoio/hugo/hugofs"
   18 	"github.com/spf13/afero"
   19 )
   20 
   21 func TestMultiSitesMainLangInRoot(t *testing.T) {
   22 	t.Parallel()
   23 	for _, b := range []bool{false} {
   24 		doTestMultiSitesMainLangInRoot(t, b)
   25 	}
   26 }
   27 
   28 func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
   29 	c := qt.New(t)
   30 
   31 	siteConfig := map[string]any{
   32 		"DefaultContentLanguage":         "fr",
   33 		"DefaultContentLanguageInSubdir": defaultInSubDir,
   34 	}
   35 
   36 	b := newMultiSiteTestBuilder(t, "toml", multiSiteTOMLConfigTemplate, siteConfig)
   37 
   38 	pathMod := func(s string) string {
   39 		return s
   40 	}
   41 
   42 	if !defaultInSubDir {
   43 		pathMod = func(s string) string {
   44 			return strings.Replace(s, "/fr/", "/", -1)
   45 		}
   46 	}
   47 
   48 	b.CreateSites()
   49 	b.Build(BuildCfg{})
   50 
   51 	sites := b.H.Sites
   52 	c.Assert(len(sites), qt.Equals, 4)
   53 
   54 	enSite := sites[0]
   55 	frSite := sites[1]
   56 
   57 	c.Assert(enSite.Info.LanguagePrefix, qt.Equals, "/en")
   58 
   59 	if defaultInSubDir {
   60 		c.Assert(frSite.Info.LanguagePrefix, qt.Equals, "/fr")
   61 	} else {
   62 		c.Assert(frSite.Info.LanguagePrefix, qt.Equals, "")
   63 	}
   64 
   65 	c.Assert(enSite.PathSpec.RelURL("foo", true), qt.Equals, "/blog/en/foo")
   66 
   67 	doc1en := enSite.RegularPages()[0]
   68 	doc1fr := frSite.RegularPages()[0]
   69 
   70 	enPerm := doc1en.Permalink()
   71 	enRelPerm := doc1en.RelPermalink()
   72 	c.Assert(enPerm, qt.Equals, "http://example.com/blog/en/sect/doc1-slug/")
   73 	c.Assert(enRelPerm, qt.Equals, "/blog/en/sect/doc1-slug/")
   74 
   75 	frPerm := doc1fr.Permalink()
   76 	frRelPerm := doc1fr.RelPermalink()
   77 
   78 	b.AssertFileContent(pathMod("public/fr/sect/doc1/index.html"), "Single", "Bonjour")
   79 	b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Hello")
   80 
   81 	if defaultInSubDir {
   82 		c.Assert(frPerm, qt.Equals, "http://example.com/blog/fr/sect/doc1/")
   83 		c.Assert(frRelPerm, qt.Equals, "/blog/fr/sect/doc1/")
   84 
   85 		// should have a redirect on top level.
   86 		b.AssertFileContent("public/index.html", `<meta http-equiv="refresh" content="0; url=http://example.com/blog/fr">`)
   87 	} else {
   88 		// Main language in root
   89 		c.Assert(frPerm, qt.Equals, "http://example.com/blog/sect/doc1/")
   90 		c.Assert(frRelPerm, qt.Equals, "/blog/sect/doc1/")
   91 
   92 		// should have redirect back to root
   93 		b.AssertFileContent("public/fr/index.html", `<meta http-equiv="refresh" content="0; url=http://example.com/blog">`)
   94 	}
   95 	b.AssertFileContent(pathMod("public/fr/index.html"), "Home", "Bonjour")
   96 	b.AssertFileContent("public/en/index.html", "Home", "Hello")
   97 
   98 	// Check list pages
   99 	b.AssertFileContent(pathMod("public/fr/sect/index.html"), "List", "Bonjour")
  100 	b.AssertFileContent("public/en/sect/index.html", "List", "Hello")
  101 	b.AssertFileContent(pathMod("public/fr/plaques/FRtag1/index.html"), "Taxonomy List", "Bonjour")
  102 	b.AssertFileContent("public/en/tags/tag1/index.html", "Taxonomy List", "Hello")
  103 
  104 	// Check sitemaps
  105 	// Sitemaps behaves different: In a multilanguage setup there will always be a index file and
  106 	// one sitemap in each lang folder.
  107 	b.AssertFileContent("public/sitemap.xml",
  108 		"<loc>http://example.com/blog/en/sitemap.xml</loc>",
  109 		"<loc>http://example.com/blog/fr/sitemap.xml</loc>")
  110 
  111 	if defaultInSubDir {
  112 		b.AssertFileContent("public/fr/sitemap.xml", "<loc>http://example.com/blog/fr/</loc>")
  113 	} else {
  114 		b.AssertFileContent("public/fr/sitemap.xml", "<loc>http://example.com/blog/</loc>")
  115 	}
  116 	b.AssertFileContent("public/en/sitemap.xml", "<loc>http://example.com/blog/en/</loc>")
  117 
  118 	// Check rss
  119 	b.AssertFileContent(pathMod("public/fr/index.xml"), pathMod(`<atom:link href="http://example.com/blog/fr/index.xml"`),
  120 		`rel="self" type="application/rss+xml"`)
  121 	b.AssertFileContent("public/en/index.xml", `<atom:link href="http://example.com/blog/en/index.xml"`)
  122 	b.AssertFileContent(
  123 		pathMod("public/fr/sect/index.xml"),
  124 		pathMod(`<atom:link href="http://example.com/blog/fr/sect/index.xml"`))
  125 	b.AssertFileContent("public/en/sect/index.xml", `<atom:link href="http://example.com/blog/en/sect/index.xml"`)
  126 	b.AssertFileContent(
  127 		pathMod("public/fr/plaques/FRtag1/index.xml"),
  128 		pathMod(`<atom:link href="http://example.com/blog/fr/plaques/FRtag1/index.xml"`))
  129 	b.AssertFileContent("public/en/tags/tag1/index.xml", `<atom:link href="http://example.com/blog/en/tags/tag1/index.xml"`)
  130 
  131 	// Check paginators
  132 	b.AssertFileContent(pathMod("public/fr/page/1/index.html"), pathMod(`refresh" content="0; url=http://example.com/blog/fr/"`))
  133 	b.AssertFileContent("public/en/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/"`)
  134 	b.AssertFileContent(pathMod("public/fr/page/2/index.html"), "Home Page 2", "Bonjour", pathMod("http://example.com/blog/fr/"))
  135 	b.AssertFileContent("public/en/page/2/index.html", "Home Page 2", "Hello", "http://example.com/blog/en/")
  136 	b.AssertFileContent(pathMod("public/fr/sect/page/1/index.html"), pathMod(`refresh" content="0; url=http://example.com/blog/fr/sect/"`))
  137 	b.AssertFileContent("public/en/sect/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/sect/"`)
  138 	b.AssertFileContent(pathMod("public/fr/sect/page/2/index.html"), "List Page 2", "Bonjour", pathMod("http://example.com/blog/fr/sect/"))
  139 	b.AssertFileContent("public/en/sect/page/2/index.html", "List Page 2", "Hello", "http://example.com/blog/en/sect/")
  140 	b.AssertFileContent(
  141 		pathMod("public/fr/plaques/FRtag1/page/1/index.html"),
  142 		pathMod(`refresh" content="0; url=http://example.com/blog/fr/plaques/FRtag1/"`))
  143 	b.AssertFileContent("public/en/tags/tag1/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/tags/tag1/"`)
  144 	b.AssertFileContent(
  145 		pathMod("public/fr/plaques/FRtag1/page/2/index.html"), "List Page 2", "Bonjour",
  146 		pathMod("http://example.com/blog/fr/plaques/FRtag1/"))
  147 	b.AssertFileContent("public/en/tags/tag1/page/2/index.html", "List Page 2", "Hello", "http://example.com/blog/en/tags/tag1/")
  148 	// nn (Nynorsk) and nb (Bokmål) have custom pagePath: side ("page" in Norwegian)
  149 	b.AssertFileContent("public/nn/side/1/index.html", `refresh" content="0; url=http://example.com/blog/nn/"`)
  150 	b.AssertFileContent("public/nb/side/1/index.html", `refresh" content="0; url=http://example.com/blog/nb/"`)
  151 }
  152 
  153 func TestMultiSitesWithTwoLanguages(t *testing.T) {
  154 	t.Parallel()
  155 
  156 	c := qt.New(t)
  157 	b := newTestSitesBuilder(t).WithConfigFile("toml", `
  158 
  159 defaultContentLanguage = "nn"
  160 
  161 [languages]
  162 [languages.nn]
  163 languageName = "Nynorsk"
  164 weight = 1
  165 title = "Tittel på Nynorsk"
  166 [languages.nn.params]
  167 p1 = "p1nn"
  168 
  169 [languages.en]
  170 title = "Title in English"
  171 languageName = "English"
  172 weight = 2
  173 [languages.en.params]
  174 p1 = "p1en"
  175 `)
  176 
  177 	b.CreateSites()
  178 	b.Build(BuildCfg{SkipRender: true})
  179 	sites := b.H.Sites
  180 
  181 	c.Assert(len(sites), qt.Equals, 2)
  182 
  183 	nnSite := sites[0]
  184 	nnHome := nnSite.getPage(page.KindHome)
  185 	c.Assert(len(nnHome.AllTranslations()), qt.Equals, 2)
  186 	c.Assert(len(nnHome.Translations()), qt.Equals, 1)
  187 	c.Assert(nnHome.IsTranslated(), qt.Equals, true)
  188 
  189 	enHome := sites[1].getPage(page.KindHome)
  190 
  191 	p1, err := enHome.Param("p1")
  192 	c.Assert(err, qt.IsNil)
  193 	c.Assert(p1, qt.Equals, "p1en")
  194 
  195 	p1, err = nnHome.Param("p1")
  196 	c.Assert(err, qt.IsNil)
  197 	c.Assert(p1, qt.Equals, "p1nn")
  198 }
  199 
  200 func TestMultiSitesBuild(t *testing.T) {
  201 	for _, config := range []struct {
  202 		content string
  203 		suffix  string
  204 	}{
  205 		{multiSiteTOMLConfigTemplate, "toml"},
  206 		{multiSiteYAMLConfigTemplate, "yml"},
  207 		{multiSiteJSONConfigTemplate, "json"},
  208 	} {
  209 		t.Run(config.suffix, func(t *testing.T) {
  210 			t.Parallel()
  211 			doTestMultiSitesBuild(t, config.content, config.suffix)
  212 		})
  213 	}
  214 }
  215 
  216 func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
  217 	c := qt.New(t)
  218 
  219 	b := newMultiSiteTestBuilder(t, configSuffix, configTemplate, nil)
  220 	b.CreateSites()
  221 
  222 	sites := b.H.Sites
  223 	c.Assert(len(sites), qt.Equals, 4)
  224 
  225 	b.Build(BuildCfg{})
  226 
  227 	// Check site config
  228 	for _, s := range sites {
  229 		c.Assert(s.Info.defaultContentLanguageInSubdir, qt.Equals, true)
  230 		c.Assert(s.disabledKinds, qt.Not(qt.IsNil))
  231 	}
  232 
  233 	gp1 := b.H.GetContentPage(filepath.FromSlash("content/sect/doc1.en.md"))
  234 	c.Assert(gp1, qt.Not(qt.IsNil))
  235 	c.Assert(gp1.Title(), qt.Equals, "doc1")
  236 	gp2 := b.H.GetContentPage(filepath.FromSlash("content/dummysect/notfound.md"))
  237 	c.Assert(gp2, qt.IsNil)
  238 
  239 	enSite := sites[0]
  240 	enSiteHome := enSite.getPage(page.KindHome)
  241 	c.Assert(enSiteHome.IsTranslated(), qt.Equals, true)
  242 
  243 	c.Assert(enSite.language.Lang, qt.Equals, "en")
  244 
  245 	// dumpPages(enSite.RegularPages()...)
  246 
  247 	c.Assert(len(enSite.RegularPages()), qt.Equals, 5)
  248 	c.Assert(len(enSite.AllPages()), qt.Equals, 32)
  249 
  250 	// Check 404s
  251 	b.AssertFileContent("public/en/404.html", "404|en|404 Page not found")
  252 	b.AssertFileContent("public/fr/404.html", "404|fr|404 Page not found")
  253 
  254 	// Check robots.txt
  255 	// the domain root is the public directory, so the robots.txt has to be created there and not in the language directories
  256 	b.AssertFileContent("public/robots.txt", "robots")
  257 	b.AssertFileDoesNotExist("public/en/robots.txt")
  258 	b.AssertFileDoesNotExist("public/nn/robots.txt")
  259 
  260 	b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Permalink: http://example.com/blog/en/sect/doc1-slug/")
  261 	b.AssertFileContent("public/en/sect/doc2/index.html", "Permalink: http://example.com/blog/en/sect/doc2/")
  262 	b.AssertFileContent("public/superbob/index.html", "Permalink: http://example.com/blog/superbob/")
  263 
  264 	doc2 := enSite.RegularPages()[1]
  265 	doc3 := enSite.RegularPages()[2]
  266 	c.Assert(doc3, qt.Equals, doc2.Prev())
  267 	doc1en := enSite.RegularPages()[0]
  268 	doc1fr := doc1en.Translations()[0]
  269 	b.AssertFileContent("public/fr/sect/doc1/index.html", "Permalink: http://example.com/blog/fr/sect/doc1/")
  270 
  271 	c.Assert(doc1fr, qt.Equals, doc1en.Translations()[0])
  272 	c.Assert(doc1en, qt.Equals, doc1fr.Translations()[0])
  273 	c.Assert(doc1fr.Language().Lang, qt.Equals, "fr")
  274 
  275 	doc4 := enSite.AllPages()[4]
  276 	c.Assert(len(doc4.Translations()), qt.Equals, 0)
  277 
  278 	// Taxonomies and their URLs
  279 	c.Assert(len(enSite.Taxonomies()), qt.Equals, 1)
  280 	tags := enSite.Taxonomies()["tags"]
  281 	c.Assert(len(tags), qt.Equals, 2)
  282 	c.Assert(doc1en, qt.Equals, tags["tag1"][0].Page)
  283 
  284 	frSite := sites[1]
  285 
  286 	c.Assert(frSite.language.Lang, qt.Equals, "fr")
  287 	c.Assert(len(frSite.RegularPages()), qt.Equals, 4)
  288 	c.Assert(len(frSite.AllPages()), qt.Equals, 32)
  289 
  290 	for _, frenchPage := range frSite.RegularPages() {
  291 		p := frenchPage
  292 		c.Assert(p.Language().Lang, qt.Equals, "fr")
  293 	}
  294 
  295 	// See https://github.com/gohugoio/hugo/issues/4285
  296 	// Before Hugo 0.33 you had to be explicit with the content path to get the correct Page, which
  297 	// isn't ideal in a multilingual setup. You want a way to get the current language version if available.
  298 	// Now you can do lookups with translation base name to get that behaviour.
  299 	// Let us test all the regular page variants:
  300 	getPageDoc1En := enSite.getPage(page.KindPage, filepath.ToSlash(doc1en.File().Path()))
  301 	getPageDoc1EnBase := enSite.getPage(page.KindPage, "sect/doc1")
  302 	getPageDoc1Fr := frSite.getPage(page.KindPage, filepath.ToSlash(doc1fr.File().Path()))
  303 	getPageDoc1FrBase := frSite.getPage(page.KindPage, "sect/doc1")
  304 	c.Assert(getPageDoc1En, qt.Equals, doc1en)
  305 	c.Assert(getPageDoc1Fr, qt.Equals, doc1fr)
  306 	c.Assert(getPageDoc1EnBase, qt.Equals, doc1en)
  307 	c.Assert(getPageDoc1FrBase, qt.Equals, doc1fr)
  308 
  309 	// Check redirect to main language, French
  310 	b.AssertFileContent("public/index.html", "0; url=http://example.com/blog/fr")
  311 
  312 	// check home page content (including data files rendering)
  313 	b.AssertFileContent("public/en/index.html", "Default Home Page 1", "Hello", "Hugo Rocks!")
  314 	b.AssertFileContent("public/fr/index.html", "French Home Page 1", "Bonjour", "Hugo Rocks!")
  315 
  316 	// check single page content
  317 	b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour", "LingoFrench")
  318 	b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello", "LingoDefault")
  319 
  320 	// Check node translations
  321 	homeEn := enSite.getPage(page.KindHome)
  322 	c.Assert(homeEn, qt.Not(qt.IsNil))
  323 	c.Assert(len(homeEn.Translations()), qt.Equals, 3)
  324 	c.Assert(homeEn.Translations()[0].Language().Lang, qt.Equals, "fr")
  325 	c.Assert(homeEn.Translations()[1].Language().Lang, qt.Equals, "nn")
  326 	c.Assert(homeEn.Translations()[1].Title(), qt.Equals, "På nynorsk")
  327 	c.Assert(homeEn.Translations()[2].Language().Lang, qt.Equals, "nb")
  328 	c.Assert(homeEn.Translations()[2].Title(), qt.Equals, "På bokmål")
  329 	c.Assert(homeEn.Translations()[2].Language().LanguageName, qt.Equals, "Bokmål")
  330 
  331 	sectFr := frSite.getPage(page.KindSection, "sect")
  332 	c.Assert(sectFr, qt.Not(qt.IsNil))
  333 
  334 	c.Assert(sectFr.Language().Lang, qt.Equals, "fr")
  335 	c.Assert(len(sectFr.Translations()), qt.Equals, 1)
  336 	c.Assert(sectFr.Translations()[0].Language().Lang, qt.Equals, "en")
  337 	c.Assert(sectFr.Translations()[0].Title(), qt.Equals, "Sects")
  338 
  339 	nnSite := sites[2]
  340 	c.Assert(nnSite.language.Lang, qt.Equals, "nn")
  341 	taxNn := nnSite.getPage(page.KindTaxonomy, "lag")
  342 	c.Assert(taxNn, qt.Not(qt.IsNil))
  343 	c.Assert(len(taxNn.Translations()), qt.Equals, 1)
  344 	c.Assert(taxNn.Translations()[0].Language().Lang, qt.Equals, "nb")
  345 
  346 	taxTermNn := nnSite.getPage(page.KindTerm, "lag", "sogndal")
  347 	c.Assert(taxTermNn, qt.Not(qt.IsNil))
  348 	c.Assert(nnSite.getPage(page.KindTerm, "LAG", "SOGNDAL"), qt.Equals, taxTermNn)
  349 	c.Assert(len(taxTermNn.Translations()), qt.Equals, 1)
  350 	c.Assert(taxTermNn.Translations()[0].Language().Lang, qt.Equals, "nb")
  351 
  352 	// Check sitemap(s)
  353 	b.AssertFileContent("public/sitemap.xml",
  354 		"<loc>http://example.com/blog/en/sitemap.xml</loc>",
  355 		"<loc>http://example.com/blog/fr/sitemap.xml</loc>")
  356 	b.AssertFileContent("public/en/sitemap.xml", "http://example.com/blog/en/sect/doc2/")
  357 	b.AssertFileContent("public/fr/sitemap.xml", "http://example.com/blog/fr/sect/doc1/")
  358 
  359 	// Check taxonomies
  360 	enTags := enSite.Taxonomies()["tags"]
  361 	frTags := frSite.Taxonomies()["plaques"]
  362 	c.Assert(len(enTags), qt.Equals, 2, qt.Commentf("Tags in en: %v", enTags))
  363 	c.Assert(len(frTags), qt.Equals, 2, qt.Commentf("Tags in fr: %v", frTags))
  364 	c.Assert(enTags["tag1"], qt.Not(qt.IsNil))
  365 	c.Assert(frTags["FRtag1"], qt.Not(qt.IsNil))
  366 	b.AssertFileContent("public/fr/plaques/FRtag1/index.html", "FRtag1|Bonjour|http://example.com/blog/fr/plaques/FRtag1/")
  367 
  368 	// en and nn have custom site menus
  369 	c.Assert(len(frSite.Menus()), qt.Equals, 0)
  370 	c.Assert(len(enSite.Menus()), qt.Equals, 1)
  371 	c.Assert(len(nnSite.Menus()), qt.Equals, 1)
  372 
  373 	c.Assert(enSite.Menus()["main"].ByName()[0].Name, qt.Equals, "Home")
  374 	c.Assert(nnSite.Menus()["main"].ByName()[0].Name, qt.Equals, "Heim")
  375 
  376 	// Issue #3108
  377 	prevPage := enSite.RegularPages()[0].Prev()
  378 	c.Assert(prevPage, qt.Not(qt.IsNil))
  379 	c.Assert(prevPage.Kind(), qt.Equals, page.KindPage)
  380 
  381 	for {
  382 		if prevPage == nil {
  383 			break
  384 		}
  385 		c.Assert(prevPage.Kind(), qt.Equals, page.KindPage)
  386 		prevPage = prevPage.Prev()
  387 	}
  388 
  389 	// Check bundles
  390 	b.AssertFileContent("public/fr/bundles/b1/index.html", "RelPermalink: /blog/fr/bundles/b1/|")
  391 	bundleFr := frSite.getPage(page.KindPage, "bundles/b1/index.md")
  392 	c.Assert(bundleFr, qt.Not(qt.IsNil))
  393 	c.Assert(len(bundleFr.Resources()), qt.Equals, 1)
  394 	logoFr := bundleFr.Resources().GetMatch("logo*")
  395 	logoFrGet := bundleFr.Resources().Get("logo.png")
  396 	c.Assert(logoFrGet, qt.Equals, logoFr)
  397 	c.Assert(logoFr, qt.Not(qt.IsNil))
  398 	b.AssertFileContent("public/fr/bundles/b1/index.html", "Resources: image/png: /blog/fr/bundles/b1/logo.png")
  399 	b.AssertFileContent("public/fr/bundles/b1/logo.png", "PNG Data")
  400 
  401 	bundleEn := enSite.getPage(page.KindPage, "bundles/b1/index.en.md")
  402 	c.Assert(bundleEn, qt.Not(qt.IsNil))
  403 	b.AssertFileContent("public/en/bundles/b1/index.html", "RelPermalink: /blog/en/bundles/b1/|")
  404 	c.Assert(len(bundleEn.Resources()), qt.Equals, 1)
  405 	logoEn := bundleEn.Resources().GetMatch("logo*")
  406 	c.Assert(logoEn, qt.Not(qt.IsNil))
  407 	b.AssertFileContent("public/en/bundles/b1/index.html", "Resources: image/png: /blog/en/bundles/b1/logo.png")
  408 	b.AssertFileContent("public/en/bundles/b1/logo.png", "PNG Data")
  409 }
  410 
  411 func TestMultiSitesRebuild(t *testing.T) {
  412 	// t.Parallel() not supported, see https://github.com/fortytw2/leaktest/issues/4
  413 	// This leaktest seems to be a little bit shaky on Travis.
  414 	if !htesting.IsCI() {
  415 		defer leaktest.CheckTimeout(t, 10*time.Second)()
  416 	}
  417 
  418 	c := qt.New(t)
  419 
  420 	b := newMultiSiteTestDefaultBuilder(t).Running().CreateSites().Build(BuildCfg{})
  421 
  422 	sites := b.H.Sites
  423 	fs := b.Fs
  424 
  425 	b.AssertFileContent("public/en/sect/doc2/index.html", "Single: doc2|Hello|en|", "\n\n<h1 id=\"doc2\">doc2</h1>\n\n<p><em>some content</em>")
  426 
  427 	enSite := sites[0]
  428 	frSite := sites[1]
  429 
  430 	c.Assert(len(enSite.RegularPages()), qt.Equals, 5)
  431 	c.Assert(len(frSite.RegularPages()), qt.Equals, 4)
  432 
  433 	// Verify translations
  434 	b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Hello")
  435 	b.AssertFileContent("public/fr/sect/doc1/index.html", "Bonjour")
  436 
  437 	// check single page content
  438 	b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour")
  439 	b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello")
  440 
  441 	homeEn := enSite.getPage(page.KindHome)
  442 	c.Assert(homeEn, qt.Not(qt.IsNil))
  443 	c.Assert(len(homeEn.Translations()), qt.Equals, 3)
  444 
  445 	contentFs := b.H.Fs.Source
  446 
  447 	for i, this := range []struct {
  448 		preFunc    func(t *testing.T)
  449 		events     []fsnotify.Event
  450 		assertFunc func(t *testing.T)
  451 	}{
  452 		// * Remove doc
  453 		// * Add docs existing languages
  454 		// (Add doc new language: TODO(bep) we should load config.toml as part of these so we can add languages).
  455 		// * Rename file
  456 		// * Change doc
  457 		// * Change a template
  458 		// * Change language file
  459 		{
  460 			func(t *testing.T) {
  461 				fs.Source.Remove("content/sect/doc2.en.md")
  462 			},
  463 			[]fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc2.en.md"), Op: fsnotify.Remove}},
  464 			func(t *testing.T) {
  465 				c.Assert(len(enSite.RegularPages()), qt.Equals, 4, qt.Commentf("1 en removed"))
  466 			},
  467 		},
  468 		{
  469 			func(t *testing.T) {
  470 				writeNewContentFile(t, contentFs, "new_en_1", "2016-07-31", "content/new1.en.md", -5)
  471 				writeNewContentFile(t, contentFs, "new_en_2", "1989-07-30", "content/new2.en.md", -10)
  472 				writeNewContentFile(t, contentFs, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10)
  473 			},
  474 			[]fsnotify.Event{
  475 				{Name: filepath.FromSlash("content/new1.en.md"), Op: fsnotify.Create},
  476 				{Name: filepath.FromSlash("content/new2.en.md"), Op: fsnotify.Create},
  477 				{Name: filepath.FromSlash("content/new1.fr.md"), Op: fsnotify.Create},
  478 			},
  479 			func(t *testing.T) {
  480 				c.Assert(len(enSite.RegularPages()), qt.Equals, 6)
  481 				c.Assert(len(enSite.AllPages()), qt.Equals, 34)
  482 				c.Assert(len(frSite.RegularPages()), qt.Equals, 5)
  483 				c.Assert(frSite.RegularPages()[3].Title(), qt.Equals, "new_fr_1")
  484 				c.Assert(enSite.RegularPages()[0].Title(), qt.Equals, "new_en_2")
  485 				c.Assert(enSite.RegularPages()[1].Title(), qt.Equals, "new_en_1")
  486 
  487 				rendered := readWorkingDir(t, fs, "public/en/new1/index.html")
  488 				c.Assert(strings.Contains(rendered, "new_en_1"), qt.Equals, true)
  489 			},
  490 		},
  491 		{
  492 			func(t *testing.T) {
  493 				p := "content/sect/doc1.en.md"
  494 				doc1 := readFileFromFs(t, contentFs, p)
  495 				doc1 += "CHANGED"
  496 				writeToFs(t, contentFs, p, doc1)
  497 			},
  498 			[]fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc1.en.md"), Op: fsnotify.Write}},
  499 			func(t *testing.T) {
  500 				c.Assert(len(enSite.RegularPages()), qt.Equals, 6)
  501 				doc1 := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html")
  502 				c.Assert(strings.Contains(doc1, "CHANGED"), qt.Equals, true)
  503 			},
  504 		},
  505 		// Rename a file
  506 		{
  507 			func(t *testing.T) {
  508 				if err := contentFs.Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil {
  509 					t.Fatalf("Rename failed: %s", err)
  510 				}
  511 			},
  512 			[]fsnotify.Event{
  513 				{Name: filepath.FromSlash("content/new1renamed.en.md"), Op: fsnotify.Rename},
  514 				{Name: filepath.FromSlash("content/new1.en.md"), Op: fsnotify.Rename},
  515 			},
  516 			func(t *testing.T) {
  517 				c.Assert(len(enSite.RegularPages()), qt.Equals, 6, qt.Commentf("Rename"))
  518 				c.Assert(enSite.RegularPages()[1].Title(), qt.Equals, "new_en_1")
  519 				rendered := readWorkingDir(t, fs, "public/en/new1renamed/index.html")
  520 				c.Assert(rendered, qt.Contains, "new_en_1")
  521 			},
  522 		},
  523 		{
  524 			// Change a template
  525 			func(t *testing.T) {
  526 				template := "layouts/_default/single.html"
  527 				templateContent := readSource(t, fs, template)
  528 				templateContent += "{{ print \"Template Changed\"}}"
  529 				writeSource(t, fs, template, templateContent)
  530 			},
  531 			[]fsnotify.Event{{Name: filepath.FromSlash("layouts/_default/single.html"), Op: fsnotify.Write}},
  532 			func(t *testing.T) {
  533 				c.Assert(len(enSite.RegularPages()), qt.Equals, 6)
  534 				c.Assert(len(enSite.AllPages()), qt.Equals, 34)
  535 				c.Assert(len(frSite.RegularPages()), qt.Equals, 5)
  536 				doc1 := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html")
  537 				c.Assert(strings.Contains(doc1, "Template Changed"), qt.Equals, true)
  538 			},
  539 		},
  540 		{
  541 			// Change a language file
  542 			func(t *testing.T) {
  543 				languageFile := "i18n/fr.yaml"
  544 				langContent := readSource(t, fs, languageFile)
  545 				langContent = strings.Replace(langContent, "Bonjour", "Salut", 1)
  546 				writeSource(t, fs, languageFile, langContent)
  547 			},
  548 			[]fsnotify.Event{{Name: filepath.FromSlash("i18n/fr.yaml"), Op: fsnotify.Write}},
  549 			func(t *testing.T) {
  550 				c.Assert(len(enSite.RegularPages()), qt.Equals, 6)
  551 				c.Assert(len(enSite.AllPages()), qt.Equals, 34)
  552 				c.Assert(len(frSite.RegularPages()), qt.Equals, 5)
  553 				docEn := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html")
  554 				c.Assert(strings.Contains(docEn, "Hello"), qt.Equals, true)
  555 				docFr := readWorkingDir(t, fs, "public/fr/sect/doc1/index.html")
  556 				c.Assert(strings.Contains(docFr, "Salut"), qt.Equals, true)
  557 
  558 				homeEn := enSite.getPage(page.KindHome)
  559 				c.Assert(homeEn, qt.Not(qt.IsNil))
  560 				c.Assert(len(homeEn.Translations()), qt.Equals, 3)
  561 				c.Assert(homeEn.Translations()[0].Language().Lang, qt.Equals, "fr")
  562 			},
  563 		},
  564 		// Change a shortcode
  565 		{
  566 			func(t *testing.T) {
  567 				writeSource(t, fs, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}")
  568 			},
  569 			[]fsnotify.Event{
  570 				{Name: filepath.FromSlash("layouts/shortcodes/shortcode.html"), Op: fsnotify.Write},
  571 			},
  572 			func(t *testing.T) {
  573 				c.Assert(len(enSite.RegularPages()), qt.Equals, 6)
  574 				c.Assert(len(enSite.AllPages()), qt.Equals, 34)
  575 				c.Assert(len(frSite.RegularPages()), qt.Equals, 5)
  576 				b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Modified Shortcode: Salut")
  577 				b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Modified Shortcode: Hello")
  578 			},
  579 		},
  580 	} {
  581 
  582 		if this.preFunc != nil {
  583 			this.preFunc(t)
  584 		}
  585 
  586 		err := b.H.Build(BuildCfg{}, this.events...)
  587 		if err != nil {
  588 			t.Fatalf("[%d] Failed to rebuild sites: %s", i, err)
  589 		}
  590 
  591 		this.assertFunc(t)
  592 	}
  593 }
  594 
  595 // https://github.com/gohugoio/hugo/issues/4706
  596 func TestContentStressTest(t *testing.T) {
  597 	b := newTestSitesBuilder(t)
  598 
  599 	numPages := 500
  600 
  601 	contentTempl := `
  602 ---
  603 %s
  604 title: %q
  605 weight: %d
  606 multioutput: %t
  607 ---
  608 
  609 # Header
  610 
  611 CONTENT
  612 
  613 The End.
  614 `
  615 
  616 	contentTempl = strings.Replace(contentTempl, "CONTENT", strings.Repeat(`
  617 	
  618 ## Another header
  619 
  620 Some text. Some more text.
  621 
  622 `, 100), -1)
  623 
  624 	var content []string
  625 	defaultOutputs := `outputs: ["html", "json", "rss" ]`
  626 
  627 	for i := 1; i <= numPages; i++ {
  628 		outputs := defaultOutputs
  629 		multioutput := true
  630 		if i%3 == 0 {
  631 			outputs = `outputs: ["json"]`
  632 			multioutput = false
  633 		}
  634 		section := "s1"
  635 		if i%10 == 0 {
  636 			section = "s2"
  637 		}
  638 		content = append(content, []string{fmt.Sprintf("%s/page%d.md", section, i), fmt.Sprintf(contentTempl, outputs, fmt.Sprintf("Title %d", i), i, multioutput)}...)
  639 	}
  640 
  641 	content = append(content, []string{"_index.md", fmt.Sprintf(contentTempl, defaultOutputs, fmt.Sprintf("Home %d", 0), 0, true)}...)
  642 	content = append(content, []string{"s1/_index.md", fmt.Sprintf(contentTempl, defaultOutputs, fmt.Sprintf("S %d", 1), 1, true)}...)
  643 	content = append(content, []string{"s2/_index.md", fmt.Sprintf(contentTempl, defaultOutputs, fmt.Sprintf("S %d", 2), 2, true)}...)
  644 
  645 	b.WithSimpleConfigFile()
  646 	b.WithTemplates("layouts/_default/single.html", `Single: {{ .Content }}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}`)
  647 	b.WithTemplates("layouts/_default/myview.html", `View: {{ len .Content }}`)
  648 	b.WithTemplates("layouts/_default/single.json", `Single JSON: {{ .Content }}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}`)
  649 	b.WithTemplates("layouts/_default/list.html", `
  650 Page: {{ .Paginator.PageNumber }}
  651 P: {{ with .File }}{{ path.Join .Path }}{{ end }}
  652 List: {{ len .Paginator.Pages }}|List Content: {{ len .Content }}
  653 {{ $shuffled :=  where .Site.RegularPages "Params.multioutput" true | shuffle }}
  654 {{ $first5 := $shuffled | first 5 }}
  655 L1: {{ len .Site.RegularPages }} L2: {{ len $first5 }}
  656 {{ range $i, $e := $first5 }}
  657 Render {{ $i }}: {{ .Render "myview" }}
  658 {{ end }}
  659 END
  660 `)
  661 
  662 	b.WithContent(content...)
  663 
  664 	b.CreateSites().Build(BuildCfg{})
  665 
  666 	contentMatchers := []string{"<h2 id=\"another-header\">Another header</h2>", "<h2 id=\"another-header-99\">Another header</h2>", "<p>The End.</p>"}
  667 
  668 	for i := 1; i <= numPages; i++ {
  669 		if i%3 != 0 {
  670 			section := "s1"
  671 			if i%10 == 0 {
  672 				section = "s2"
  673 			}
  674 			checkContent(b, fmt.Sprintf("public/%s/page%d/index.html", section, i), contentMatchers...)
  675 		}
  676 	}
  677 
  678 	for i := 1; i <= numPages; i++ {
  679 		section := "s1"
  680 		if i%10 == 0 {
  681 			section = "s2"
  682 		}
  683 		checkContent(b, fmt.Sprintf("public/%s/page%d/index.json", section, i), contentMatchers...)
  684 	}
  685 
  686 	checkContent(b, "public/s1/index.html", "P: s1/_index.md\nList: 10|List Content: 8132\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8132\n\nRender 1: View: 8132\n\nRender 2: View: 8132\n\nRender 3: View: 8132\n\nRender 4: View: 8132\n\nEND\n")
  687 	checkContent(b, "public/s2/index.html", "P: s2/_index.md\nList: 10|List Content: 8132", "Render 4: View: 8132\n\nEND")
  688 	checkContent(b, "public/index.html", "P: _index.md\nList: 10|List Content: 8132", "4: View: 8132\n\nEND")
  689 
  690 	// Check paginated pages
  691 	for i := 2; i <= 9; i++ {
  692 		checkContent(b, fmt.Sprintf("public/page/%d/index.html", i), fmt.Sprintf("Page: %d", i), "Content: 8132\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8132", "Render 4: View: 8132\n\nEND")
  693 	}
  694 }
  695 
  696 func checkContent(s *sitesBuilder, filename string, matches ...string) {
  697 	s.T.Helper()
  698 	content := readWorkingDir(s.T, s.Fs, filename)
  699 	for _, match := range matches {
  700 		if !strings.Contains(content, match) {
  701 			s.Fatalf("No match for\n%q\nin content for %s\n%q\nDiff:\n%s", match, filename, content, htesting.DiffStrings(content, match))
  702 		}
  703 	}
  704 }
  705 
  706 func TestTranslationsFromContentToNonContent(t *testing.T) {
  707 	b := newTestSitesBuilder(t)
  708 	b.WithConfigFile("toml", `
  709 
  710 baseURL = "http://example.com/"
  711 
  712 defaultContentLanguage = "en"
  713 
  714 [languages]
  715 [languages.en]
  716 weight = 10
  717 contentDir = "content/en"
  718 [languages.nn]
  719 weight = 20
  720 contentDir = "content/nn"
  721 
  722 
  723 `)
  724 
  725 	b.WithContent("en/mysection/_index.md", `
  726 ---
  727 Title: My Section
  728 ---
  729 
  730 `)
  731 
  732 	b.WithContent("en/_index.md", `
  733 ---
  734 Title: My Home
  735 ---
  736 
  737 `)
  738 
  739 	b.WithContent("en/categories/mycat/_index.md", `
  740 ---
  741 Title: My MyCat
  742 ---
  743 
  744 `)
  745 
  746 	b.WithContent("en/categories/_index.md", `
  747 ---
  748 Title: My categories
  749 ---
  750 
  751 `)
  752 
  753 	for _, lang := range []string{"en", "nn"} {
  754 		b.WithContent(lang+"/mysection/page.md", `
  755 ---
  756 Title: My Page
  757 categories: ["mycat"]
  758 ---
  759 
  760 `)
  761 	}
  762 
  763 	b.Build(BuildCfg{})
  764 
  765 	for _, path := range []string{
  766 		"/",
  767 		"/mysection",
  768 		"/categories",
  769 		"/categories/mycat",
  770 	} {
  771 		t.Run(path, func(t *testing.T) {
  772 			c := qt.New(t)
  773 
  774 			s1, _ := b.H.Sites[0].getPageNew(nil, path)
  775 			s2, _ := b.H.Sites[1].getPageNew(nil, path)
  776 
  777 			c.Assert(s1, qt.Not(qt.IsNil))
  778 			c.Assert(s2, qt.Not(qt.IsNil))
  779 
  780 			c.Assert(len(s1.Translations()), qt.Equals, 1)
  781 			c.Assert(len(s2.Translations()), qt.Equals, 1)
  782 			c.Assert(s1.Translations()[0], qt.Equals, s2)
  783 			c.Assert(s2.Translations()[0], qt.Equals, s1)
  784 
  785 			m1 := s1.Translations().MergeByLanguage(s2.Translations())
  786 			m2 := s2.Translations().MergeByLanguage(s1.Translations())
  787 
  788 			c.Assert(len(m1), qt.Equals, 1)
  789 			c.Assert(len(m2), qt.Equals, 1)
  790 		})
  791 	}
  792 }
  793 
  794 var tocShortcode = `
  795 TOC1: {{ .Page.TableOfContents }}
  796 
  797 TOC2: {{ .Page.TableOfContents }}
  798 `
  799 
  800 func TestSelfReferencedContentInShortcode(t *testing.T) {
  801 	t.Parallel()
  802 
  803 	b := newMultiSiteTestDefaultBuilder(t)
  804 
  805 	var (
  806 		shortcode = `{{- .Page.Content -}}{{- .Page.Summary -}}{{- .Page.Plain -}}{{- .Page.PlainWords -}}{{- .Page.WordCount -}}{{- .Page.ReadingTime -}}`
  807 
  808 		page = `---
  809 title: sctest
  810 ---
  811 Empty:{{< mycontent >}}:
  812 `
  813 	)
  814 
  815 	b.WithTemplatesAdded("layouts/shortcodes/mycontent.html", shortcode)
  816 	b.WithContent("post/simple.en.md", page)
  817 
  818 	b.CreateSites().Build(BuildCfg{})
  819 
  820 	b.AssertFileContent("public/en/post/simple/index.html", "Empty:[]00:")
  821 }
  822 
  823 var tocPageSimple = `---
  824 title: tocTest
  825 publishdate: "2000-01-01"
  826 ---
  827 {{< toc >}}
  828 # Heading 1 {#1}
  829 Some text.
  830 ## Subheading 1.1 {#1-1}
  831 Some more text.
  832 # Heading 2 {#2}
  833 Even more text.
  834 ## Subheading 2.1 {#2-1}
  835 Lorem ipsum...
  836 `
  837 
  838 var tocPageVariants1 = `---
  839 title: tocTest
  840 publishdate: "2000-01-01"
  841 ---
  842 Variant 1:
  843 {{% wrapper %}}
  844 {{< toc >}}
  845 {{% /wrapper %}}
  846 # Heading 1
  847 
  848 Variant 3:
  849 {{% toc %}}
  850 
  851 `
  852 
  853 var tocPageVariants2 = `---
  854 title: tocTest
  855 publishdate: "2000-01-01"
  856 ---
  857 Variant 1:
  858 {{% wrapper %}}
  859 {{< toc >}}
  860 {{% /wrapper %}}
  861 # Heading 1
  862 
  863 Variant 2:
  864 {{< wrapper >}}
  865 {{< toc >}}
  866 {{< /wrapper >}}
  867 
  868 Variant 3:
  869 {{% toc %}}
  870 
  871 `
  872 
  873 var tocPageSimpleExpected = `<nav id="TableOfContents">
  874 <ul>
  875 <li><a href="#1">Heading 1</a>
  876 <ul>
  877 <li><a href="#1-1">Subheading 1.1</a></li>
  878 </ul></li>
  879 <li><a href="#2">Heading 2</a>
  880 <ul>
  881 <li><a href="#2-1">Subheading 2.1</a></li>
  882 </ul></li>
  883 </ul>
  884 </nav>`
  885 
  886 var tocPageWithShortcodesInHeadings = `---
  887 title: tocTest
  888 publishdate: "2000-01-01"
  889 ---
  890 
  891 {{< toc >}}
  892 
  893 # Heading 1 {#1}
  894 
  895 Some text.
  896 
  897 ## Subheading 1.1 {{< shortcode >}} {#1-1}
  898 
  899 Some more text.
  900 
  901 # Heading 2 {{% shortcode %}} {#2}
  902 
  903 Even more text.
  904 
  905 ## Subheading 2.1 {#2-1}
  906 
  907 Lorem ipsum...
  908 `
  909 
  910 var tocPageWithShortcodesInHeadingsExpected = `<nav id="TableOfContents">
  911 <ul>
  912 <li><a href="#1">Heading 1</a>
  913 <ul>
  914 <li><a href="#1-1">Subheading 1.1 Shortcode: Hello</a></li>
  915 </ul></li>
  916 <li><a href="#2">Heading 2 Shortcode: Hello</a>
  917 <ul>
  918 <li><a href="#2-1">Subheading 2.1</a></li>
  919 </ul></li>
  920 </ul>
  921 </nav>`
  922 
  923 var multiSiteTOMLConfigTemplate = `
  924 baseURL = "http://example.com/blog"
  925 
  926 paginate = 1
  927 disablePathToLower = true
  928 defaultContentLanguage = "{{ .DefaultContentLanguage }}"
  929 defaultContentLanguageInSubdir = {{ .DefaultContentLanguageInSubdir }}
  930 enableRobotsTXT = true
  931 
  932 [permalinks]
  933 other = "/somewhere/else/:filename"
  934 
  935 [Taxonomies]
  936 tag = "tags"
  937 
  938 [Languages]
  939 [Languages.en]
  940 weight = 10
  941 title = "In English"
  942 languageName = "English"
  943 [[Languages.en.menu.main]]
  944 url    = "/"
  945 name   = "Home"
  946 weight = 0
  947 
  948 [Languages.fr]
  949 weight = 20
  950 title = "Le Français"
  951 languageName = "Français"
  952 [Languages.fr.Taxonomies]
  953 plaque = "plaques"
  954 
  955 [Languages.nn]
  956 weight = 30
  957 title = "På nynorsk"
  958 languageName = "Nynorsk"
  959 paginatePath = "side"
  960 [Languages.nn.Taxonomies]
  961 lag = "lag"
  962 [[Languages.nn.menu.main]]
  963 url    = "/"
  964 name   = "Heim"
  965 weight = 1
  966 
  967 [Languages.nb]
  968 weight = 40
  969 title = "På bokmål"
  970 languageName = "Bokmål"
  971 paginatePath = "side"
  972 [Languages.nb.Taxonomies]
  973 lag = "lag"
  974 `
  975 
  976 var multiSiteYAMLConfigTemplate = `
  977 baseURL: "http://example.com/blog"
  978 
  979 disablePathToLower: true
  980 paginate: 1
  981 defaultContentLanguage: "{{ .DefaultContentLanguage }}"
  982 defaultContentLanguageInSubdir: {{ .DefaultContentLanguageInSubdir }}
  983 enableRobotsTXT: true
  984 
  985 permalinks:
  986     other: "/somewhere/else/:filename"
  987 
  988 Taxonomies:
  989     tag: "tags"
  990 
  991 Languages:
  992     en:
  993         weight: 10
  994         title: "In English"
  995         languageName: "English"
  996         menu:
  997             main:
  998                 - url: "/"
  999                   name: "Home"
 1000                   weight: 0
 1001     fr:
 1002         weight: 20
 1003         title: "Le Français"
 1004         languageName: "Français"
 1005         Taxonomies:
 1006             plaque: "plaques"
 1007     nn:
 1008         weight: 30
 1009         title: "På nynorsk"
 1010         languageName: "Nynorsk"
 1011         paginatePath: "side"
 1012         Taxonomies:
 1013             lag: "lag"
 1014         menu:
 1015             main:
 1016                 - url: "/"
 1017                   name: "Heim"
 1018                   weight: 1
 1019     nb:
 1020         weight: 40
 1021         title: "På bokmål"
 1022         languageName: "Bokmål"
 1023         paginatePath: "side"
 1024         Taxonomies:
 1025             lag: "lag"
 1026 
 1027 `
 1028 
 1029 // TODO(bep) clean move
 1030 var multiSiteJSONConfigTemplate = `
 1031 {
 1032   "baseURL": "http://example.com/blog",
 1033   "paginate": 1,
 1034   "disablePathToLower": true,
 1035   "defaultContentLanguage": "{{ .DefaultContentLanguage }}",
 1036   "defaultContentLanguageInSubdir": true,
 1037   "enableRobotsTXT": true,
 1038   "permalinks": {
 1039     "other": "/somewhere/else/:filename"
 1040   },
 1041   "Taxonomies": {
 1042     "tag": "tags"
 1043   },
 1044   "Languages": {
 1045     "en": {
 1046       "weight": 10,
 1047       "title": "In English",
 1048       "languageName": "English",
 1049 	  "menu": {
 1050         "main": [
 1051 			{
 1052 			"url": "/",
 1053 			"name": "Home",
 1054 			"weight": 0
 1055 			}
 1056 		]
 1057       }
 1058     },
 1059     "fr": {
 1060       "weight": 20,
 1061       "title": "Le Français",
 1062       "languageName": "Français",
 1063       "Taxonomies": {
 1064         "plaque": "plaques"
 1065       }
 1066     },
 1067     "nn": {
 1068       "weight": 30,
 1069       "title": "På nynorsk",
 1070       "paginatePath": "side",
 1071       "languageName": "Nynorsk",
 1072       "Taxonomies": {
 1073         "lag": "lag"
 1074       },
 1075 	  "menu": {
 1076         "main": [
 1077 			{
 1078         	"url": "/",
 1079 			"name": "Heim",
 1080 			"weight": 1
 1081 			}
 1082       	]
 1083       }
 1084     },
 1085     "nb": {
 1086       "weight": 40,
 1087       "title": "På bokmål",
 1088       "paginatePath": "side",
 1089       "languageName": "Bokmål",
 1090       "Taxonomies": {
 1091         "lag": "lag"
 1092       }
 1093     }
 1094   }
 1095 }
 1096 `
 1097 
 1098 func writeSource(t testing.TB, fs *hugofs.Fs, filename, content string) {
 1099 	t.Helper()
 1100 	writeToFs(t, fs.Source, filename, content)
 1101 }
 1102 
 1103 func writeToFs(t testing.TB, fs afero.Fs, filename, content string) {
 1104 	t.Helper()
 1105 	if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0755); err != nil {
 1106 		t.Fatalf("Failed to write file: %s", err)
 1107 	}
 1108 }
 1109 
 1110 func readWorkingDir(t testing.TB, fs *hugofs.Fs, filename string) string {
 1111 	t.Helper()
 1112 	return readFileFromFs(t, fs.WorkingDirReadOnly, filename)
 1113 }
 1114 
 1115 func workingDirExists(fs *hugofs.Fs, filename string) bool {
 1116 	b, err := helpers.Exists(filename, fs.WorkingDirReadOnly)
 1117 	if err != nil {
 1118 		panic(err)
 1119 	}
 1120 	return b
 1121 }
 1122 
 1123 func readSource(t *testing.T, fs *hugofs.Fs, filename string) string {
 1124 	return readFileFromFs(t, fs.Source, filename)
 1125 }
 1126 
 1127 func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string {
 1128 	t.Helper()
 1129 	filename = filepath.Clean(filename)
 1130 	b, err := afero.ReadFile(fs, filename)
 1131 	if err != nil {
 1132 		// Print some debug info
 1133 		hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator)
 1134 		start := 0
 1135 		if hadSlash {
 1136 			start = 1
 1137 		}
 1138 		end := start + 1
 1139 
 1140 		parts := strings.Split(filename, helpers.FilePathSeparator)
 1141 		if parts[start] == "work" {
 1142 			end++
 1143 		}
 1144 
 1145 		/*
 1146 			root := filepath.Join(parts[start:end]...)
 1147 			if hadSlash {
 1148 				root = helpers.FilePathSeparator + root
 1149 			}
 1150 
 1151 			helpers.PrintFs(fs, root, os.Stdout)
 1152 		*/
 1153 
 1154 		t.Fatalf("Failed to read file: %s", err)
 1155 	}
 1156 	return string(b)
 1157 }
 1158 
 1159 const testPageTemplate = `---
 1160 title: "%s"
 1161 publishdate: "%s"
 1162 weight: %d
 1163 ---
 1164 # Doc %s
 1165 `
 1166 
 1167 func newTestPage(title, date string, weight int) string {
 1168 	return fmt.Sprintf(testPageTemplate, title, date, weight, title)
 1169 }
 1170 
 1171 func writeNewContentFile(t *testing.T, fs afero.Fs, title, date, filename string, weight int) {
 1172 	content := newTestPage(title, date, weight)
 1173 	writeToFs(t, fs, filename, content)
 1174 }
 1175 
 1176 type multiSiteTestBuilder struct {
 1177 	configData   any
 1178 	config       string
 1179 	configFormat string
 1180 
 1181 	*sitesBuilder
 1182 }
 1183 
 1184 func newMultiSiteTestDefaultBuilder(t testing.TB) *multiSiteTestBuilder {
 1185 	return newMultiSiteTestBuilder(t, "", "", nil)
 1186 }
 1187 
 1188 func (b *multiSiteTestBuilder) WithNewConfig(config string) *multiSiteTestBuilder {
 1189 	b.WithConfigTemplate(b.configData, b.configFormat, config)
 1190 	return b
 1191 }
 1192 
 1193 func (b *multiSiteTestBuilder) WithNewConfigData(data any) *multiSiteTestBuilder {
 1194 	b.WithConfigTemplate(data, b.configFormat, b.config)
 1195 	return b
 1196 }
 1197 
 1198 func newMultiSiteTestBuilder(t testing.TB, configFormat, config string, configData any) *multiSiteTestBuilder {
 1199 	if configData == nil {
 1200 		configData = map[string]any{
 1201 			"DefaultContentLanguage":         "fr",
 1202 			"DefaultContentLanguageInSubdir": true,
 1203 		}
 1204 	}
 1205 
 1206 	if config == "" {
 1207 		config = multiSiteTOMLConfigTemplate
 1208 	}
 1209 
 1210 	if configFormat == "" {
 1211 		configFormat = "toml"
 1212 	}
 1213 
 1214 	b := newTestSitesBuilder(t).WithConfigTemplate(configData, configFormat, config)
 1215 	b.WithContent("root.en.md", `---
 1216 title: root
 1217 weight: 10000
 1218 slug: root
 1219 publishdate: "2000-01-01"
 1220 ---
 1221 # root
 1222 `,
 1223 		"sect/doc1.en.md", `---
 1224 title: doc1
 1225 weight: 1
 1226 slug: doc1-slug
 1227 tags:
 1228  - tag1
 1229 publishdate: "2000-01-01"
 1230 ---
 1231 # doc1
 1232 *some "content"*
 1233 
 1234 {{< shortcode >}}
 1235 
 1236 {{< lingo >}}
 1237 
 1238 NOTE: slug should be used as URL
 1239 `,
 1240 		"sect/doc1.fr.md", `---
 1241 title: doc1
 1242 weight: 1
 1243 plaques:
 1244  - FRtag1
 1245  - FRtag2
 1246 publishdate: "2000-01-04"
 1247 ---
 1248 # doc1
 1249 *quelque "contenu"*
 1250 
 1251 {{< shortcode >}}
 1252 
 1253 {{< lingo >}}
 1254 
 1255 NOTE: should be in the 'en' Page's 'Translations' field.
 1256 NOTE: date is after "doc3"
 1257 `,
 1258 		"sect/doc2.en.md", `---
 1259 title: doc2
 1260 weight: 2
 1261 publishdate: "2000-01-02"
 1262 ---
 1263 # doc2
 1264 *some content*
 1265 NOTE: without slug, "doc2" should be used, without ".en" as URL
 1266 `,
 1267 		"sect/doc3.en.md", `---
 1268 title: doc3
 1269 weight: 3
 1270 publishdate: "2000-01-03"
 1271 aliases: [/en/al/alias1,/al/alias2/]
 1272 tags:
 1273  - tag2
 1274  - tag1
 1275 url: /superbob/
 1276 ---
 1277 # doc3
 1278 *some content*
 1279 NOTE: third 'en' doc, should trigger pagination on home page.
 1280 `,
 1281 		"sect/doc4.md", `---
 1282 title: doc4
 1283 weight: 4
 1284 plaques:
 1285  - FRtag1
 1286 publishdate: "2000-01-05"
 1287 ---
 1288 # doc4
 1289 *du contenu francophone*
 1290 NOTE: should use the defaultContentLanguage and mark this doc as 'fr'.
 1291 NOTE: doesn't have any corresponding translation in 'en'
 1292 `,
 1293 		"other/doc5.fr.md", `---
 1294 title: doc5
 1295 weight: 5
 1296 publishdate: "2000-01-06"
 1297 ---
 1298 # doc5
 1299 *autre contenu francophone*
 1300 NOTE: should use the "permalinks" configuration with :filename
 1301 `,
 1302 		// Add some for the stats
 1303 		"stats/expired.fr.md", `---
 1304 title: expired
 1305 publishdate: "2000-01-06"
 1306 expiryDate: "2001-01-06"
 1307 ---
 1308 # Expired
 1309 `,
 1310 		"stats/future.fr.md", `---
 1311 title: future
 1312 weight: 6
 1313 publishdate: "2100-01-06"
 1314 ---
 1315 # Future
 1316 `,
 1317 		"stats/expired.en.md", `---
 1318 title: expired
 1319 weight: 7
 1320 publishdate: "2000-01-06"
 1321 expiryDate: "2001-01-06"
 1322 ---
 1323 # Expired
 1324 `,
 1325 		"stats/future.en.md", `---
 1326 title: future
 1327 weight: 6
 1328 publishdate: "2100-01-06"
 1329 ---
 1330 # Future
 1331 `,
 1332 		"stats/draft.en.md", `---
 1333 title: expired
 1334 publishdate: "2000-01-06"
 1335 draft: true
 1336 ---
 1337 # Draft
 1338 `,
 1339 		"stats/tax.nn.md", `---
 1340 title: Tax NN
 1341 weight: 8
 1342 publishdate: "2000-01-06"
 1343 weight: 1001
 1344 lag:
 1345 - Sogndal
 1346 ---
 1347 # Tax NN
 1348 `,
 1349 		"stats/tax.nb.md", `---
 1350 title: Tax NB
 1351 weight: 8
 1352 publishdate: "2000-01-06"
 1353 weight: 1002
 1354 lag:
 1355 - Sogndal
 1356 ---
 1357 # Tax NB
 1358 `,
 1359 		// Bundle
 1360 		"bundles/b1/index.en.md", `---
 1361 title: Bundle EN
 1362 publishdate: "2000-01-06"
 1363 weight: 2001
 1364 ---
 1365 # Bundle Content EN
 1366 `,
 1367 		"bundles/b1/index.md", `---
 1368 title: Bundle Default
 1369 publishdate: "2000-01-06"
 1370 weight: 2002
 1371 ---
 1372 # Bundle Content Default
 1373 `,
 1374 		"bundles/b1/logo.png", `
 1375 PNG Data
 1376 `)
 1377 
 1378 	i18nContent := func(id, value string) string {
 1379 		return fmt.Sprintf(`
 1380 [%s]
 1381 other = %q
 1382 `, id, value)
 1383 	}
 1384 
 1385 	b.WithSourceFile("i18n/en.toml", i18nContent("hello", "Hello"))
 1386 	b.WithSourceFile("i18n/fr.toml", i18nContent("hello", "Bonjour"))
 1387 	b.WithSourceFile("i18n/nb.toml", i18nContent("hello", "Hallo"))
 1388 	b.WithSourceFile("i18n/nn.toml", i18nContent("hello", "Hallo"))
 1389 
 1390 	return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData}
 1391 }
 1392 
 1393 func TestRebuildOnAssetChange(t *testing.T) {
 1394 	b := newTestSitesBuilder(t).Running()
 1395 	b.WithTemplatesAdded("index.html", `
 1396 {{ (resources.Get "data.json").Content }}
 1397 `)
 1398 	b.WithSourceFile("assets/data.json", "orig data")
 1399 
 1400 	b.Build(BuildCfg{})
 1401 	b.AssertFileContent("public/index.html", `orig data`)
 1402 
 1403 	b.EditFiles("assets/data.json", "changed data")
 1404 
 1405 	b.Build(BuildCfg{})
 1406 	b.AssertFileContent("public/index.html", `changed data`)
 1407 }