From: Bjørn Erik Pedersen Date: Fri, 11 Nov 2016 10:35:55 +0000 (+0100) Subject: node to page: Handle Date and Lastmod X-Git-Tag: v0.18~98 X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=487b210fb8a31b3636030ea960f6565b9e6b3c54;p=brevno-suite%2Fhugo node to page: Handle Date and Lastmod Updates #2297 --- diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index ba743d4f..bf5f2775 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -31,11 +31,6 @@ import ( jww "github.com/spf13/jwalterweatherman" ) -// Temporary feature flag to ease the refactoring of node vs page, see -// https://github.com/spf13/hugo/issues/2297 -// TODO(bep) eventually remove -var nodePageFeatureFlag bool = true - // HugoSites represents the sites to build. Each site represents a language. type HugoSites struct { Sites []*Site @@ -313,8 +308,6 @@ func (s *Site) newNodePage(typ PageType) *Page { return &Page{ PageType: typ, Node: Node{ - Date: s.Info.LastChange, - Lastmod: s.Info.LastChange, Data: make(map[string]interface{}), Site: &s.Info, language: s.Language, diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go new file mode 100644 index 00000000..05743a80 --- /dev/null +++ b/hugolib/hugo_sites_build_test.go @@ -0,0 +1,1265 @@ +package hugolib + +import ( + "bytes" + "fmt" + "regexp" + "strings" + "testing" + + "os" + "path/filepath" + "text/template" + + "github.com/fortytw2/leaktest" + "github.com/fsnotify/fsnotify" + "github.com/spf13/afero" + "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/source" + // jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testSiteConfig struct { + DefaultContentLanguage string +} + +func init() { + testCommonResetState() +} + +func testCommonResetState() { + hugofs.InitMemFs() + viper.Reset() + viper.SetFs(hugofs.Source()) + helpers.ResetConfigProvider() + loadDefaultSettings() + + // Default is false, but true is easier to use as default in tests + viper.Set("defaultContentLanguageInSubdir", true) + + if err := hugofs.Source().Mkdir("content", 0755); err != nil { + panic("Content folder creation failed.") + } + +} + +func TestMultiSitesMainLangInRoot(t *testing.T) { + //jww.SetStdoutThreshold(jww.LevelDebug) + + for _, b := range []bool{true, false} { + doTestMultiSitesMainLangInRoot(t, b) + } +} + +func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { + testCommonResetState() + viper.Set("defaultContentLanguageInSubdir", defaultInSubDir) + siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} + + sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) + + err := sites.Build(BuildCfg{}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + require.Len(t, sites.Sites, 4) + + enSite := sites.Sites[0] + frSite := sites.Sites[1] + + require.Equal(t, "/en", enSite.Info.LanguagePrefix) + + if defaultInSubDir { + require.Equal(t, "/fr", frSite.Info.LanguagePrefix) + } else { + require.Equal(t, "", frSite.Info.LanguagePrefix) + } + + require.Equal(t, "/blog/en/foo", enSite.Info.pathSpec.RelURL("foo", true)) + + doc1en := enSite.regularPages[0] + doc1fr := frSite.regularPages[0] + + enPerm, _ := doc1en.Permalink() + enRelPerm, _ := doc1en.RelPermalink() + require.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", enPerm) + require.Equal(t, "/blog/en/sect/doc1-slug/", enRelPerm) + + frPerm, _ := doc1fr.Permalink() + frRelPerm, _ := doc1fr.RelPermalink() + // Main language in root + require.Equal(t, replaceDefaultContentLanguageValue("http://example.com/blog/fr/sect/doc1/", defaultInSubDir), frPerm) + require.Equal(t, replaceDefaultContentLanguageValue("/blog/fr/sect/doc1/", defaultInSubDir), frRelPerm) + + assertFileContent(t, "public/fr/sect/doc1/index.html", defaultInSubDir, "Single", "Bonjour") + assertFileContent(t, "public/en/sect/doc1-slug/index.html", defaultInSubDir, "Single", "Hello") + + // Check home + if defaultInSubDir { + // should have a redirect on top level. + assertFileContent(t, "public/index.html", true, ``) + } else { + // should have redirect back to root + assertFileContent(t, "public/fr/index.html", true, ``) + } + assertFileContent(t, "public/fr/index.html", defaultInSubDir, "Home", "Bonjour") + assertFileContent(t, "public/en/index.html", defaultInSubDir, "Home", "Hello") + + // Check list pages + assertFileContent(t, "public/fr/sect/index.html", defaultInSubDir, "List", "Bonjour") + assertFileContent(t, "public/en/sect/index.html", defaultInSubDir, "List", "Hello") + assertFileContent(t, "public/fr/plaques/frtag1/index.html", defaultInSubDir, "List", "Bonjour") + assertFileContent(t, "public/en/tags/tag1/index.html", defaultInSubDir, "List", "Hello") + + // Check sitemaps + // Sitemaps behaves different: In a multilanguage setup there will always be a index file and + // one sitemap in each lang folder. + assertFileContent(t, "public/sitemap.xml", true, + "http://example.com/blog/en/sitemap.xml", + "http://example.com/blog/fr/sitemap.xml") + + if defaultInSubDir { + assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/fr/") + } else { + assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/") + } + assertFileContent(t, "public/en/sitemap.xml", true, "http://example.com/blog/en/") + + // Check rss + assertFileContent(t, "public/fr/index.xml", defaultInSubDir, `http://example.com/blog/en/sitemap.xml"), sitemapIndex) + require.True(t, strings.Contains(sitemapIndex, "http://example.com/blog/fr/sitemap.xml"), sitemapIndex) + sitemapEn := readDestination(t, "public/en/sitemap.xml") + sitemapFr := readDestination(t, "public/fr/sitemap.xml") + require.True(t, strings.Contains(sitemapEn, "http://example.com/blog/en/sect/doc2/"), sitemapEn) + require.True(t, strings.Contains(sitemapFr, "http://example.com/blog/fr/sect/doc1/"), sitemapFr) + + // Check taxonomies + enTags := enSite.Taxonomies["tags"] + frTags := frSite.Taxonomies["plaques"] + require.Len(t, enTags, 2, fmt.Sprintf("Tags in en: %v", enTags)) + require.Len(t, frTags, 2, fmt.Sprintf("Tags in fr: %v", frTags)) + require.NotNil(t, enTags["tag1"]) + require.NotNil(t, frTags["frtag1"]) + readDestination(t, "public/fr/plaques/frtag1/index.html") + readDestination(t, "public/en/tags/tag1/index.html") + + // Check Blackfriday config + assert.True(t, strings.Contains(string(doc1fr.Content), "«"), string(doc1fr.Content)) + assert.False(t, strings.Contains(string(doc1en.Content), "«"), string(doc1en.Content)) + assert.True(t, strings.Contains(string(doc1en.Content), "“"), string(doc1en.Content)) + + // Check that the drafts etc. are not built/processed/rendered. + assertShouldNotBuild(t, sites) + + // en and nn have custom site menus + require.Len(t, frSite.Menus, 0, "fr: "+configSuffix) + require.Len(t, enSite.Menus, 1, "en: "+configSuffix) + require.Len(t, nnSite.Menus, 1, "nn: "+configSuffix) + + require.Equal(t, "Home", enSite.Menus["main"].ByName()[0].Name) + require.Equal(t, "Heim", nnSite.Menus["main"].ByName()[0].Name) + +} + +func TestMultiSitesRebuild(t *testing.T) { + + defer leaktest.Check(t)() + testCommonResetState() + siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} + sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) + cfg := BuildCfg{Watching: true} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + _, err = hugofs.Destination().Open("public/en/sect/doc2/index.html") + + if err != nil { + t.Fatalf("Unable to locate file") + } + + enSite := sites.Sites[0] + frSite := sites.Sites[1] + + require.Len(t, enSite.regularPages, 4) + require.Len(t, frSite.regularPages, 3) + + // Verify translations + assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Hello") + assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Bonjour") + + // check single page content + assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Shortcode: Bonjour") + assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Single", "Shortcode: Hello") + + for i, this := range []struct { + preFunc func(t *testing.T) + events []fsnotify.Event + assertFunc func(t *testing.T) + }{ + // * Remove doc + // * Add docs existing languages + // (Add doc new language: TODO(bep) we should load config.toml as part of these so we can add languages). + // * Rename file + // * Change doc + // * Change a template + // * Change language file + { + nil, + []fsnotify.Event{{Name: "content/sect/doc2.en.md", Op: fsnotify.Remove}}, + func(t *testing.T) { + require.Len(t, enSite.regularPages, 3, "1 en removed") + + // Check build stats + require.Equal(t, 1, enSite.draftCount, "Draft") + require.Equal(t, 1, enSite.futureCount, "Future") + require.Equal(t, 1, enSite.expiredCount, "Expired") + require.Equal(t, 0, frSite.draftCount, "Draft") + require.Equal(t, 1, frSite.futureCount, "Future") + require.Equal(t, 1, frSite.expiredCount, "Expired") + }, + }, + { + func(t *testing.T) { + writeNewContentFile(t, "new_en_1", "2016-07-31", "content/new1.en.md", -5) + writeNewContentFile(t, "new_en_2", "1989-07-30", "content/new2.en.md", -10) + writeNewContentFile(t, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10) + }, + []fsnotify.Event{ + {Name: "content/new1.en.md", Op: fsnotify.Create}, + {Name: "content/new2.en.md", Op: fsnotify.Create}, + {Name: "content/new1.fr.md", Op: fsnotify.Create}, + }, + func(t *testing.T) { + require.Len(t, enSite.regularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.regularPages, 4) + require.Equal(t, "new_fr_1", frSite.regularPages[3].Title) + require.Equal(t, "new_en_2", enSite.regularPages[0].Title) + require.Equal(t, "new_en_1", enSite.regularPages[1].Title) + + rendered := readDestination(t, "public/en/new1/index.html") + require.True(t, strings.Contains(rendered, "new_en_1"), rendered) + }, + }, + { + func(t *testing.T) { + p := "content/sect/doc1.en.md" + doc1 := readSource(t, p) + doc1 += "CHANGED" + writeSource(t, p, doc1) + }, + []fsnotify.Event{{Name: "content/sect/doc1.en.md", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.regularPages, 5) + doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(doc1, "CHANGED"), doc1) + + }, + }, + // Rename a file + { + func(t *testing.T) { + if err := hugofs.Source().Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil { + t.Fatalf("Rename failed: %s", err) + } + }, + []fsnotify.Event{ + {Name: "content/new1renamed.en.md", Op: fsnotify.Rename}, + {Name: "content/new1.en.md", Op: fsnotify.Rename}, + }, + func(t *testing.T) { + require.Len(t, enSite.regularPages, 5, "Rename") + require.Equal(t, "new_en_1", enSite.regularPages[1].Title) + rendered := readDestination(t, "public/en/new1renamed/index.html") + require.True(t, strings.Contains(rendered, "new_en_1"), rendered) + }}, + { + // Change a template + func(t *testing.T) { + template := "layouts/_default/single.html" + templateContent := readSource(t, template) + templateContent += "{{ print \"Template Changed\"}}" + writeSource(t, template, templateContent) + }, + []fsnotify.Event{{Name: "layouts/_default/single.html", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.regularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.regularPages, 4) + doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(doc1, "Template Changed"), doc1) + }, + }, + { + // Change a language file + func(t *testing.T) { + languageFile := "i18n/fr.yaml" + langContent := readSource(t, languageFile) + langContent = strings.Replace(langContent, "Bonjour", "Salut", 1) + writeSource(t, languageFile, langContent) + }, + []fsnotify.Event{{Name: "i18n/fr.yaml", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.regularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.regularPages, 4) + docEn := readDestination(t, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(docEn, "Hello"), "No Hello") + docFr := readDestination(t, "public/fr/sect/doc1/index.html") + require.True(t, strings.Contains(docFr, "Salut"), "No Salut") + + homeEn := enSite.getPage(PageHome) + require.NotNil(t, homeEn) + require.Len(t, homeEn.Translations(), 3) + require.Equal(t, "fr", homeEn.Translations()[0].Lang()) + + }, + }, + // Change a shortcode + { + func(t *testing.T) { + writeSource(t, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}") + }, + []fsnotify.Event{ + {Name: "layouts/shortcodes/shortcode.html", Op: fsnotify.Write}, + }, + func(t *testing.T) { + require.Len(t, enSite.regularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.regularPages, 4) + assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Modified Shortcode: Salut") + assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Single", "Modified Shortcode: Hello") + }, + }, + } { + + if this.preFunc != nil { + this.preFunc(t) + } + + err = sites.Build(cfg, this.events...) + + if err != nil { + t.Fatalf("[%d] Failed to rebuild sites: %s", i, err) + } + + this.assertFunc(t) + } + + // Check that the drafts etc. are not built/processed/rendered. + assertShouldNotBuild(t, sites) + +} + +func assertShouldNotBuild(t *testing.T, sites *HugoSites) { + s := sites.Sites[0] + + for _, p := range s.rawAllPages { + // No HTML when not processed + require.Equal(t, p.shouldBuild(), bytes.Contains(p.rawContent, []byte("}} + +# Heading 1 {#1} + +Some text. + +## Subheading 1.1 {#1-1} + +Some more text. + +# Heading 2 {#2} + +Even more text. + +## Subheading 2.1 {#2-1} + +Lorem ipsum... +` + +var tocPageSimpleExpected = `` + +var tocPageWithShortcodesInHeadings = `--- +title: tocTest +publishdate: "2000-01-01" +--- + +{{< toc >}} + +# Heading 1 {#1} + +Some text. + +## Subheading 1.1 {{< shortcode >}} {#1-1} + +Some more text. + +# Heading 2 {{% shortcode %}} {#2} + +Even more text. + +## Subheading 2.1 {#2-1} + +Lorem ipsum... +` + +var tocPageWithShortcodesInHeadingsExpected = `` + +var multiSiteTOMLConfigTemplate = ` +defaultExtension = "html" +baseURL = "http://example.com/blog" +disableSitemap = false +disableRSS = false +rssURI = "index.xml" + +paginate = 1 +defaultContentLanguage = "{{ .DefaultContentLanguage }}" + +[permalinks] +other = "/somewhere/else/:filename" + +[blackfriday] +angledQuotes = true + +[Taxonomies] +tag = "tags" + +[Languages] +[Languages.en] +weight = 10 +title = "In English" +languageName = "English" +[Languages.en.blackfriday] +angledQuotes = false +[[Languages.en.menu.main]] +url = "/" +name = "Home" +weight = 0 + +[Languages.fr] +weight = 20 +title = "Le Français" +languageName = "Français" +[Languages.fr.Taxonomies] +plaque = "plaques" + +[Languages.nn] +weight = 30 +title = "PÃ¥ nynorsk" +languageName = "Nynorsk" +paginatePath = "side" +[Languages.nn.Taxonomies] +lag = "lag" +[[Languages.nn.menu.main]] +url = "/" +name = "Heim" +weight = 1 + +[Languages.nb] +weight = 40 +title = "PÃ¥ bokmÃ¥l" +languageName = "BokmÃ¥l" +paginatePath = "side" +[Languages.nb.Taxonomies] +lag = "lag" +` + +var multiSiteYAMLConfig = ` +defaultExtension: "html" +baseURL: "http://example.com/blog" +disableSitemap: false +disableRSS: false +rssURI: "index.xml" + +paginate: 1 +defaultContentLanguage: "fr" + +permalinks: + other: "/somewhere/else/:filename" + +blackfriday: + angledQuotes: true + +Taxonomies: + tag: "tags" + +Languages: + en: + weight: 10 + title: "In English" + languageName: "English" + blackfriday: + angledQuotes: false + menu: + main: + - url: "/" + name: "Home" + weight: 0 + fr: + weight: 20 + title: "Le Français" + languageName: "Français" + Taxonomies: + plaque: "plaques" + nn: + weight: 30 + title: "PÃ¥ nynorsk" + languageName: "Nynorsk" + paginatePath: "side" + Taxonomies: + lag: "lag" + menu: + main: + - url: "/" + name: "Heim" + weight: 1 + nb: + weight: 40 + title: "PÃ¥ bokmÃ¥l" + languageName: "BokmÃ¥l" + paginatePath: "side" + Taxonomies: + lag: "lag" + +` + +var multiSiteJSONConfig = ` +{ + "defaultExtension": "html", + "baseURL": "http://example.com/blog", + "disableSitemap": false, + "disableRSS": false, + "rssURI": "index.xml", + "paginate": 1, + "defaultContentLanguage": "fr", + "permalinks": { + "other": "/somewhere/else/:filename" + }, + "blackfriday": { + "angledQuotes": true + }, + "Taxonomies": { + "tag": "tags" + }, + "Languages": { + "en": { + "weight": 10, + "title": "In English", + "languageName": "English", + "blackfriday": { + "angledQuotes": false + }, + "menu": { + "main": [ + { + "url": "/", + "name": "Home", + "weight": 0 + } + ] + } + }, + "fr": { + "weight": 20, + "title": "Le Français", + "languageName": "Français", + "Taxonomies": { + "plaque": "plaques" + } + }, + "nn": { + "weight": 30, + "title": "PÃ¥ nynorsk", + "paginatePath": "side", + "languageName": "Nynorsk", + "Taxonomies": { + "lag": "lag" + }, + "menu": { + "main": [ + { + "url": "/", + "name": "Heim", + "weight": 1 + } + ] + } + }, + "nb": { + "weight": 40, + "title": "PÃ¥ bokmÃ¥l", + "paginatePath": "side", + "languageName": "BokmÃ¥l", + "Taxonomies": { + "lag": "lag" + } + } + } +} +` + +func createMultiTestSites(t *testing.T, siteConfig testSiteConfig, tomlConfigTemplate string) *HugoSites { + return createMultiTestSitesForConfig(t, siteConfig, tomlConfigTemplate, "toml") +} + +func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, configTemplate, configSuffix string) *HugoSites { + + configContent := createConfig(t, siteConfig, configTemplate) + + // Add some layouts + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("layouts", "_default/single.html"), + []byte("Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Lang}}|{{ .Content }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("layouts", "_default/list.html"), + []byte("{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("layouts", "index.html"), + []byte("{{ $p := .Paginator }}Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + // Add a shortcode + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("layouts", "shortcodes", "shortcode.html"), + []byte("Shortcode: {{ i18n \"hello\" }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + // Add some language files + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("i18n", "en.yaml"), + []byte(` +- id: hello + translation: "Hello" +`), + 0755); err != nil { + t.Fatalf("Failed to write language file: %s", err) + } + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("i18n", "fr.yaml"), + []byte(` +- id: hello + translation: "Bonjour" +`), + 0755); err != nil { + t.Fatalf("Failed to write language file: %s", err) + } + + // Sources + sources := []source.ByteSource{ + {Name: filepath.FromSlash("root.en.md"), Content: []byte(`--- +title: root +weight: 10000 +slug: root +publishdate: "2000-01-01" +--- +# root +`)}, + {Name: filepath.FromSlash("sect/doc1.en.md"), Content: []byte(`--- +title: doc1 +weight: 1 +slug: doc1-slug +tags: + - tag1 +publishdate: "2000-01-01" +--- +# doc1 +*some "content"* + +{{< shortcode >}} + +NOTE: slug should be used as URL +`)}, + {Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`--- +title: doc1 +weight: 1 +plaques: + - frtag1 + - frtag2 +publishdate: "2000-01-04" +--- +# doc1 +*quelque "contenu"* + +{{< shortcode >}} + +NOTE: should be in the 'en' Page's 'Translations' field. +NOTE: date is after "doc3" +`)}, + {Name: filepath.FromSlash("sect/doc2.en.md"), Content: []byte(`--- +title: doc2 +weight: 2 +publishdate: "2000-01-02" +--- +# doc2 +*some content* +NOTE: without slug, "doc2" should be used, without ".en" as URL +`)}, + {Name: filepath.FromSlash("sect/doc3.en.md"), Content: []byte(`--- +title: doc3 +weight: 3 +publishdate: "2000-01-03" +tags: + - tag2 + - tag1 +url: /superbob +--- +# doc3 +*some content* +NOTE: third 'en' doc, should trigger pagination on home page. +`)}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte(`--- +title: doc4 +weight: 4 +plaques: + - frtag1 +publishdate: "2000-01-05" +--- +# doc4 +*du contenu francophone* +NOTE: should use the defaultContentLanguage and mark this doc as 'fr'. +NOTE: doesn't have any corresponding translation in 'en' +`)}, + {Name: filepath.FromSlash("other/doc5.fr.md"), Content: []byte(`--- +title: doc5 +weight: 5 +publishdate: "2000-01-06" +--- +# doc5 +*autre contenu francophone* +NOTE: should use the "permalinks" configuration with :filename +`)}, + // Add some for the stats + {Name: filepath.FromSlash("stats/expired.fr.md"), Content: []byte(`--- +title: expired +publishdate: "2000-01-06" +expiryDate: "2001-01-06" +--- +# Expired +`)}, + {Name: filepath.FromSlash("stats/future.fr.md"), Content: []byte(`--- +title: future +weight: 6 +publishdate: "2100-01-06" +--- +# Future +`)}, + {Name: filepath.FromSlash("stats/expired.en.md"), Content: []byte(`--- +title: expired +weight: 7 +publishdate: "2000-01-06" +expiryDate: "2001-01-06" +--- +# Expired +`)}, + {Name: filepath.FromSlash("stats/future.en.md"), Content: []byte(`--- +title: future +weight: 6 +publishdate: "2100-01-06" +--- +# Future +`)}, + {Name: filepath.FromSlash("stats/draft.en.md"), Content: []byte(`--- +title: expired +publishdate: "2000-01-06" +draft: true +--- +# Draft +`)}, + {Name: filepath.FromSlash("stats/tax.nn.md"), Content: []byte(`--- +title: Tax NN +weight: 8 +publishdate: "2000-01-06" +weight: 1001 +lag: +- Sogndal +--- +# Tax NN +`)}, + {Name: filepath.FromSlash("stats/tax.nb.md"), Content: []byte(`--- +title: Tax NB +weight: 8 +publishdate: "2000-01-06" +weight: 1002 +lag: +- Sogndal +--- +# Tax NB +`)}, + } + + configFile := "multilangconfig." + configSuffix + writeSource(t, configFile, configContent) + if err := LoadGlobalConfig("", configFile); err != nil { + t.Fatalf("Failed to load config: %s", err) + } + + // Hugo support using ByteSource's directly (for testing), + // but to make it more real, we write them to the mem file system. + for _, s := range sources { + if err := afero.WriteFile(hugofs.Source(), filepath.Join("content", s.Name), s.Content, 0755); err != nil { + t.Fatalf("Failed to write file: %s", err) + } + } + + // Add some data + writeSource(t, "data/hugo.toml", "slogan = \"Hugo Rocks!\"") + + sites, err := NewHugoSitesFromConfiguration() + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + if len(sites.Sites) != 4 { + t.Fatalf("Got %d sites", len(sites.Sites)) + } + + return sites +} + +func writeSource(t *testing.T, filename, content string) { + if err := afero.WriteFile(hugofs.Source(), filepath.FromSlash(filename), []byte(content), 0755); err != nil { + t.Fatalf("Failed to write file: %s", err) + } +} + +func readDestination(t *testing.T, filename string) string { + return readFileFromFs(t, hugofs.Destination(), filename) +} + +func destinationExists(filename string) bool { + b, err := helpers.Exists(filename, hugofs.Destination()) + if err != nil { + panic(err) + } + return b +} + +func readSource(t *testing.T, filename string) string { + return readFileFromFs(t, hugofs.Source(), filename) +} + +func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { + filename = filepath.FromSlash(filename) + b, err := afero.ReadFile(fs, filename) + if err != nil { + // Print some debug info + root := strings.Split(filename, helpers.FilePathSeparator)[0] + afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error { + if info != nil && !info.IsDir() { + fmt.Println(" ", path) + } + + return nil + }) + t.Fatalf("Failed to read file: %s", err) + } + return string(b) +} + +const testPageTemplate = `--- +title: "%s" +publishdate: "%s" +weight: %d +--- +# Doc %s +` + +func newTestPage(title, date string, weight int) string { + return fmt.Sprintf(testPageTemplate, title, date, weight, title) +} + +func writeNewContentFile(t *testing.T, title, date, filename string, weight int) { + content := newTestPage(title, date, weight) + writeSource(t, filename, content) +} + +func createConfig(t *testing.T, config testSiteConfig, configTemplate string) string { + templ, err := template.New("test").Parse(configTemplate) + if err != nil { + t.Fatal("Template parse failed:", err) + } + var b bytes.Buffer + templ.Execute(&b, config) + return b.String() +} diff --git a/hugolib/hugo_sites_test.go b/hugolib/hugo_sites_test.go deleted file mode 100644 index a6118b5c..00000000 --- a/hugolib/hugo_sites_test.go +++ /dev/null @@ -1,1266 +0,0 @@ -package hugolib - -import ( - "bytes" - "fmt" - "regexp" - "strings" - "testing" - - "os" - "path/filepath" - "text/template" - - "github.com/fortytw2/leaktest" - "github.com/fsnotify/fsnotify" - "github.com/spf13/afero" - "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/hugofs" - "github.com/spf13/hugo/source" - // jww "github.com/spf13/jwalterweatherman" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type testSiteConfig struct { - DefaultContentLanguage string -} - -func init() { - nodePageFeatureFlag = true - testCommonResetState() -} - -func testCommonResetState() { - hugofs.InitMemFs() - viper.Reset() - viper.SetFs(hugofs.Source()) - helpers.ResetConfigProvider() - loadDefaultSettings() - - // Default is false, but true is easier to use as default in tests - viper.Set("defaultContentLanguageInSubdir", true) - - if err := hugofs.Source().Mkdir("content", 0755); err != nil { - panic("Content folder creation failed.") - } - -} - -func TestMultiSitesMainLangInRoot(t *testing.T) { - //jww.SetStdoutThreshold(jww.LevelDebug) - - for _, b := range []bool{true, false} { - doTestMultiSitesMainLangInRoot(t, b) - } -} - -func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { - testCommonResetState() - viper.Set("defaultContentLanguageInSubdir", defaultInSubDir) - siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} - - sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) - - err := sites.Build(BuildCfg{}) - - if err != nil { - t.Fatalf("Failed to build sites: %s", err) - } - - require.Len(t, sites.Sites, 4) - - enSite := sites.Sites[0] - frSite := sites.Sites[1] - - require.Equal(t, "/en", enSite.Info.LanguagePrefix) - - if defaultInSubDir { - require.Equal(t, "/fr", frSite.Info.LanguagePrefix) - } else { - require.Equal(t, "", frSite.Info.LanguagePrefix) - } - - require.Equal(t, "/blog/en/foo", enSite.Info.pathSpec.RelURL("foo", true)) - - doc1en := enSite.regularPages[0] - doc1fr := frSite.regularPages[0] - - enPerm, _ := doc1en.Permalink() - enRelPerm, _ := doc1en.RelPermalink() - require.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", enPerm) - require.Equal(t, "/blog/en/sect/doc1-slug/", enRelPerm) - - frPerm, _ := doc1fr.Permalink() - frRelPerm, _ := doc1fr.RelPermalink() - // Main language in root - require.Equal(t, replaceDefaultContentLanguageValue("http://example.com/blog/fr/sect/doc1/", defaultInSubDir), frPerm) - require.Equal(t, replaceDefaultContentLanguageValue("/blog/fr/sect/doc1/", defaultInSubDir), frRelPerm) - - assertFileContent(t, "public/fr/sect/doc1/index.html", defaultInSubDir, "Single", "Bonjour") - assertFileContent(t, "public/en/sect/doc1-slug/index.html", defaultInSubDir, "Single", "Hello") - - // Check home - if defaultInSubDir { - // should have a redirect on top level. - assertFileContent(t, "public/index.html", true, ``) - } else { - // should have redirect back to root - assertFileContent(t, "public/fr/index.html", true, ``) - } - assertFileContent(t, "public/fr/index.html", defaultInSubDir, "Home", "Bonjour") - assertFileContent(t, "public/en/index.html", defaultInSubDir, "Home", "Hello") - - // Check list pages - assertFileContent(t, "public/fr/sect/index.html", defaultInSubDir, "List", "Bonjour") - assertFileContent(t, "public/en/sect/index.html", defaultInSubDir, "List", "Hello") - assertFileContent(t, "public/fr/plaques/frtag1/index.html", defaultInSubDir, "List", "Bonjour") - assertFileContent(t, "public/en/tags/tag1/index.html", defaultInSubDir, "List", "Hello") - - // Check sitemaps - // Sitemaps behaves different: In a multilanguage setup there will always be a index file and - // one sitemap in each lang folder. - assertFileContent(t, "public/sitemap.xml", true, - "http://example.com/blog/en/sitemap.xml", - "http://example.com/blog/fr/sitemap.xml") - - if defaultInSubDir { - assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/fr/") - } else { - assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/") - } - assertFileContent(t, "public/en/sitemap.xml", true, "http://example.com/blog/en/") - - // Check rss - assertFileContent(t, "public/fr/index.xml", defaultInSubDir, `http://example.com/blog/en/sitemap.xml"), sitemapIndex) - require.True(t, strings.Contains(sitemapIndex, "http://example.com/blog/fr/sitemap.xml"), sitemapIndex) - sitemapEn := readDestination(t, "public/en/sitemap.xml") - sitemapFr := readDestination(t, "public/fr/sitemap.xml") - require.True(t, strings.Contains(sitemapEn, "http://example.com/blog/en/sect/doc2/"), sitemapEn) - require.True(t, strings.Contains(sitemapFr, "http://example.com/blog/fr/sect/doc1/"), sitemapFr) - - // Check taxonomies - enTags := enSite.Taxonomies["tags"] - frTags := frSite.Taxonomies["plaques"] - require.Len(t, enTags, 2, fmt.Sprintf("Tags in en: %v", enTags)) - require.Len(t, frTags, 2, fmt.Sprintf("Tags in fr: %v", frTags)) - require.NotNil(t, enTags["tag1"]) - require.NotNil(t, frTags["frtag1"]) - readDestination(t, "public/fr/plaques/frtag1/index.html") - readDestination(t, "public/en/tags/tag1/index.html") - - // Check Blackfriday config - assert.True(t, strings.Contains(string(doc1fr.Content), "«"), string(doc1fr.Content)) - assert.False(t, strings.Contains(string(doc1en.Content), "«"), string(doc1en.Content)) - assert.True(t, strings.Contains(string(doc1en.Content), "“"), string(doc1en.Content)) - - // Check that the drafts etc. are not built/processed/rendered. - assertShouldNotBuild(t, sites) - - // en and nn have custom site menus - require.Len(t, frSite.Menus, 0, "fr: "+configSuffix) - require.Len(t, enSite.Menus, 1, "en: "+configSuffix) - require.Len(t, nnSite.Menus, 1, "nn: "+configSuffix) - - require.Equal(t, "Home", enSite.Menus["main"].ByName()[0].Name) - require.Equal(t, "Heim", nnSite.Menus["main"].ByName()[0].Name) - -} - -func TestMultiSitesRebuild(t *testing.T) { - - defer leaktest.Check(t)() - testCommonResetState() - siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} - sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) - cfg := BuildCfg{Watching: true} - - err := sites.Build(cfg) - - if err != nil { - t.Fatalf("Failed to build sites: %s", err) - } - - _, err = hugofs.Destination().Open("public/en/sect/doc2/index.html") - - if err != nil { - t.Fatalf("Unable to locate file") - } - - enSite := sites.Sites[0] - frSite := sites.Sites[1] - - require.Len(t, enSite.regularPages, 4) - require.Len(t, frSite.regularPages, 3) - - // Verify translations - assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Hello") - assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Bonjour") - - // check single page content - assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Shortcode: Bonjour") - assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Single", "Shortcode: Hello") - - for i, this := range []struct { - preFunc func(t *testing.T) - events []fsnotify.Event - assertFunc func(t *testing.T) - }{ - // * Remove doc - // * Add docs existing languages - // (Add doc new language: TODO(bep) we should load config.toml as part of these so we can add languages). - // * Rename file - // * Change doc - // * Change a template - // * Change language file - { - nil, - []fsnotify.Event{{Name: "content/sect/doc2.en.md", Op: fsnotify.Remove}}, - func(t *testing.T) { - require.Len(t, enSite.regularPages, 3, "1 en removed") - - // Check build stats - require.Equal(t, 1, enSite.draftCount, "Draft") - require.Equal(t, 1, enSite.futureCount, "Future") - require.Equal(t, 1, enSite.expiredCount, "Expired") - require.Equal(t, 0, frSite.draftCount, "Draft") - require.Equal(t, 1, frSite.futureCount, "Future") - require.Equal(t, 1, frSite.expiredCount, "Expired") - }, - }, - { - func(t *testing.T) { - writeNewContentFile(t, "new_en_1", "2016-07-31", "content/new1.en.md", -5) - writeNewContentFile(t, "new_en_2", "1989-07-30", "content/new2.en.md", -10) - writeNewContentFile(t, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10) - }, - []fsnotify.Event{ - {Name: "content/new1.en.md", Op: fsnotify.Create}, - {Name: "content/new2.en.md", Op: fsnotify.Create}, - {Name: "content/new1.fr.md", Op: fsnotify.Create}, - }, - func(t *testing.T) { - require.Len(t, enSite.regularPages, 5) - require.Len(t, enSite.AllPages, 30) - require.Len(t, frSite.regularPages, 4) - require.Equal(t, "new_fr_1", frSite.regularPages[3].Title) - require.Equal(t, "new_en_2", enSite.regularPages[0].Title) - require.Equal(t, "new_en_1", enSite.regularPages[1].Title) - - rendered := readDestination(t, "public/en/new1/index.html") - require.True(t, strings.Contains(rendered, "new_en_1"), rendered) - }, - }, - { - func(t *testing.T) { - p := "content/sect/doc1.en.md" - doc1 := readSource(t, p) - doc1 += "CHANGED" - writeSource(t, p, doc1) - }, - []fsnotify.Event{{Name: "content/sect/doc1.en.md", Op: fsnotify.Write}}, - func(t *testing.T) { - require.Len(t, enSite.regularPages, 5) - doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") - require.True(t, strings.Contains(doc1, "CHANGED"), doc1) - - }, - }, - // Rename a file - { - func(t *testing.T) { - if err := hugofs.Source().Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil { - t.Fatalf("Rename failed: %s", err) - } - }, - []fsnotify.Event{ - {Name: "content/new1renamed.en.md", Op: fsnotify.Rename}, - {Name: "content/new1.en.md", Op: fsnotify.Rename}, - }, - func(t *testing.T) { - require.Len(t, enSite.regularPages, 5, "Rename") - require.Equal(t, "new_en_1", enSite.regularPages[1].Title) - rendered := readDestination(t, "public/en/new1renamed/index.html") - require.True(t, strings.Contains(rendered, "new_en_1"), rendered) - }}, - { - // Change a template - func(t *testing.T) { - template := "layouts/_default/single.html" - templateContent := readSource(t, template) - templateContent += "{{ print \"Template Changed\"}}" - writeSource(t, template, templateContent) - }, - []fsnotify.Event{{Name: "layouts/_default/single.html", Op: fsnotify.Write}}, - func(t *testing.T) { - require.Len(t, enSite.regularPages, 5) - require.Len(t, enSite.AllPages, 30) - require.Len(t, frSite.regularPages, 4) - doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") - require.True(t, strings.Contains(doc1, "Template Changed"), doc1) - }, - }, - { - // Change a language file - func(t *testing.T) { - languageFile := "i18n/fr.yaml" - langContent := readSource(t, languageFile) - langContent = strings.Replace(langContent, "Bonjour", "Salut", 1) - writeSource(t, languageFile, langContent) - }, - []fsnotify.Event{{Name: "i18n/fr.yaml", Op: fsnotify.Write}}, - func(t *testing.T) { - require.Len(t, enSite.regularPages, 5) - require.Len(t, enSite.AllPages, 30) - require.Len(t, frSite.regularPages, 4) - docEn := readDestination(t, "public/en/sect/doc1-slug/index.html") - require.True(t, strings.Contains(docEn, "Hello"), "No Hello") - docFr := readDestination(t, "public/fr/sect/doc1/index.html") - require.True(t, strings.Contains(docFr, "Salut"), "No Salut") - - homeEn := enSite.getPage(PageHome) - require.NotNil(t, homeEn) - require.Len(t, homeEn.Translations(), 3) - require.Equal(t, "fr", homeEn.Translations()[0].Lang()) - - }, - }, - // Change a shortcode - { - func(t *testing.T) { - writeSource(t, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}") - }, - []fsnotify.Event{ - {Name: "layouts/shortcodes/shortcode.html", Op: fsnotify.Write}, - }, - func(t *testing.T) { - require.Len(t, enSite.regularPages, 5) - require.Len(t, enSite.AllPages, 30) - require.Len(t, frSite.regularPages, 4) - assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Modified Shortcode: Salut") - assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Single", "Modified Shortcode: Hello") - }, - }, - } { - - if this.preFunc != nil { - this.preFunc(t) - } - - err = sites.Build(cfg, this.events...) - - if err != nil { - t.Fatalf("[%d] Failed to rebuild sites: %s", i, err) - } - - this.assertFunc(t) - } - - // Check that the drafts etc. are not built/processed/rendered. - assertShouldNotBuild(t, sites) - -} - -func assertShouldNotBuild(t *testing.T, sites *HugoSites) { - s := sites.Sites[0] - - for _, p := range s.rawAllPages { - // No HTML when not processed - require.Equal(t, p.shouldBuild(), bytes.Contains(p.rawContent, []byte("}} - -# Heading 1 {#1} - -Some text. - -## Subheading 1.1 {#1-1} - -Some more text. - -# Heading 2 {#2} - -Even more text. - -## Subheading 2.1 {#2-1} - -Lorem ipsum... -` - -var tocPageSimpleExpected = `` - -var tocPageWithShortcodesInHeadings = `--- -title: tocTest -publishdate: "2000-01-01" ---- - -{{< toc >}} - -# Heading 1 {#1} - -Some text. - -## Subheading 1.1 {{< shortcode >}} {#1-1} - -Some more text. - -# Heading 2 {{% shortcode %}} {#2} - -Even more text. - -## Subheading 2.1 {#2-1} - -Lorem ipsum... -` - -var tocPageWithShortcodesInHeadingsExpected = `` - -var multiSiteTOMLConfigTemplate = ` -defaultExtension = "html" -baseURL = "http://example.com/blog" -disableSitemap = false -disableRSS = false -rssURI = "index.xml" - -paginate = 1 -defaultContentLanguage = "{{ .DefaultContentLanguage }}" - -[permalinks] -other = "/somewhere/else/:filename" - -[blackfriday] -angledQuotes = true - -[Taxonomies] -tag = "tags" - -[Languages] -[Languages.en] -weight = 10 -title = "In English" -languageName = "English" -[Languages.en.blackfriday] -angledQuotes = false -[[Languages.en.menu.main]] -url = "/" -name = "Home" -weight = 0 - -[Languages.fr] -weight = 20 -title = "Le Français" -languageName = "Français" -[Languages.fr.Taxonomies] -plaque = "plaques" - -[Languages.nn] -weight = 30 -title = "PÃ¥ nynorsk" -languageName = "Nynorsk" -paginatePath = "side" -[Languages.nn.Taxonomies] -lag = "lag" -[[Languages.nn.menu.main]] -url = "/" -name = "Heim" -weight = 1 - -[Languages.nb] -weight = 40 -title = "PÃ¥ bokmÃ¥l" -languageName = "BokmÃ¥l" -paginatePath = "side" -[Languages.nb.Taxonomies] -lag = "lag" -` - -var multiSiteYAMLConfig = ` -defaultExtension: "html" -baseURL: "http://example.com/blog" -disableSitemap: false -disableRSS: false -rssURI: "index.xml" - -paginate: 1 -defaultContentLanguage: "fr" - -permalinks: - other: "/somewhere/else/:filename" - -blackfriday: - angledQuotes: true - -Taxonomies: - tag: "tags" - -Languages: - en: - weight: 10 - title: "In English" - languageName: "English" - blackfriday: - angledQuotes: false - menu: - main: - - url: "/" - name: "Home" - weight: 0 - fr: - weight: 20 - title: "Le Français" - languageName: "Français" - Taxonomies: - plaque: "plaques" - nn: - weight: 30 - title: "PÃ¥ nynorsk" - languageName: "Nynorsk" - paginatePath: "side" - Taxonomies: - lag: "lag" - menu: - main: - - url: "/" - name: "Heim" - weight: 1 - nb: - weight: 40 - title: "PÃ¥ bokmÃ¥l" - languageName: "BokmÃ¥l" - paginatePath: "side" - Taxonomies: - lag: "lag" - -` - -var multiSiteJSONConfig = ` -{ - "defaultExtension": "html", - "baseURL": "http://example.com/blog", - "disableSitemap": false, - "disableRSS": false, - "rssURI": "index.xml", - "paginate": 1, - "defaultContentLanguage": "fr", - "permalinks": { - "other": "/somewhere/else/:filename" - }, - "blackfriday": { - "angledQuotes": true - }, - "Taxonomies": { - "tag": "tags" - }, - "Languages": { - "en": { - "weight": 10, - "title": "In English", - "languageName": "English", - "blackfriday": { - "angledQuotes": false - }, - "menu": { - "main": [ - { - "url": "/", - "name": "Home", - "weight": 0 - } - ] - } - }, - "fr": { - "weight": 20, - "title": "Le Français", - "languageName": "Français", - "Taxonomies": { - "plaque": "plaques" - } - }, - "nn": { - "weight": 30, - "title": "PÃ¥ nynorsk", - "paginatePath": "side", - "languageName": "Nynorsk", - "Taxonomies": { - "lag": "lag" - }, - "menu": { - "main": [ - { - "url": "/", - "name": "Heim", - "weight": 1 - } - ] - } - }, - "nb": { - "weight": 40, - "title": "PÃ¥ bokmÃ¥l", - "paginatePath": "side", - "languageName": "BokmÃ¥l", - "Taxonomies": { - "lag": "lag" - } - } - } -} -` - -func createMultiTestSites(t *testing.T, siteConfig testSiteConfig, tomlConfigTemplate string) *HugoSites { - return createMultiTestSitesForConfig(t, siteConfig, tomlConfigTemplate, "toml") -} - -func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, configTemplate, configSuffix string) *HugoSites { - - configContent := createConfig(t, siteConfig, configTemplate) - - // Add some layouts - if err := afero.WriteFile(hugofs.Source(), - filepath.Join("layouts", "_default/single.html"), - []byte("Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Lang}}|{{ .Content }}"), - 0755); err != nil { - t.Fatalf("Failed to write layout file: %s", err) - } - - if err := afero.WriteFile(hugofs.Source(), - filepath.Join("layouts", "_default/list.html"), - []byte("{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}"), - 0755); err != nil { - t.Fatalf("Failed to write layout file: %s", err) - } - - if err := afero.WriteFile(hugofs.Source(), - filepath.Join("layouts", "index.html"), - []byte("{{ $p := .Paginator }}Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), - 0755); err != nil { - t.Fatalf("Failed to write layout file: %s", err) - } - - // Add a shortcode - if err := afero.WriteFile(hugofs.Source(), - filepath.Join("layouts", "shortcodes", "shortcode.html"), - []byte("Shortcode: {{ i18n \"hello\" }}"), - 0755); err != nil { - t.Fatalf("Failed to write layout file: %s", err) - } - - // Add some language files - if err := afero.WriteFile(hugofs.Source(), - filepath.Join("i18n", "en.yaml"), - []byte(` -- id: hello - translation: "Hello" -`), - 0755); err != nil { - t.Fatalf("Failed to write language file: %s", err) - } - if err := afero.WriteFile(hugofs.Source(), - filepath.Join("i18n", "fr.yaml"), - []byte(` -- id: hello - translation: "Bonjour" -`), - 0755); err != nil { - t.Fatalf("Failed to write language file: %s", err) - } - - // Sources - sources := []source.ByteSource{ - {Name: filepath.FromSlash("root.en.md"), Content: []byte(`--- -title: root -weight: 10000 -slug: root -publishdate: "2000-01-01" ---- -# root -`)}, - {Name: filepath.FromSlash("sect/doc1.en.md"), Content: []byte(`--- -title: doc1 -weight: 1 -slug: doc1-slug -tags: - - tag1 -publishdate: "2000-01-01" ---- -# doc1 -*some "content"* - -{{< shortcode >}} - -NOTE: slug should be used as URL -`)}, - {Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`--- -title: doc1 -weight: 1 -plaques: - - frtag1 - - frtag2 -publishdate: "2000-01-04" ---- -# doc1 -*quelque "contenu"* - -{{< shortcode >}} - -NOTE: should be in the 'en' Page's 'Translations' field. -NOTE: date is after "doc3" -`)}, - {Name: filepath.FromSlash("sect/doc2.en.md"), Content: []byte(`--- -title: doc2 -weight: 2 -publishdate: "2000-01-02" ---- -# doc2 -*some content* -NOTE: without slug, "doc2" should be used, without ".en" as URL -`)}, - {Name: filepath.FromSlash("sect/doc3.en.md"), Content: []byte(`--- -title: doc3 -weight: 3 -publishdate: "2000-01-03" -tags: - - tag2 - - tag1 -url: /superbob ---- -# doc3 -*some content* -NOTE: third 'en' doc, should trigger pagination on home page. -`)}, - {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte(`--- -title: doc4 -weight: 4 -plaques: - - frtag1 -publishdate: "2000-01-05" ---- -# doc4 -*du contenu francophone* -NOTE: should use the defaultContentLanguage and mark this doc as 'fr'. -NOTE: doesn't have any corresponding translation in 'en' -`)}, - {Name: filepath.FromSlash("other/doc5.fr.md"), Content: []byte(`--- -title: doc5 -weight: 5 -publishdate: "2000-01-06" ---- -# doc5 -*autre contenu francophone* -NOTE: should use the "permalinks" configuration with :filename -`)}, - // Add some for the stats - {Name: filepath.FromSlash("stats/expired.fr.md"), Content: []byte(`--- -title: expired -publishdate: "2000-01-06" -expiryDate: "2001-01-06" ---- -# Expired -`)}, - {Name: filepath.FromSlash("stats/future.fr.md"), Content: []byte(`--- -title: future -weight: 6 -publishdate: "2100-01-06" ---- -# Future -`)}, - {Name: filepath.FromSlash("stats/expired.en.md"), Content: []byte(`--- -title: expired -weight: 7 -publishdate: "2000-01-06" -expiryDate: "2001-01-06" ---- -# Expired -`)}, - {Name: filepath.FromSlash("stats/future.en.md"), Content: []byte(`--- -title: future -weight: 6 -publishdate: "2100-01-06" ---- -# Future -`)}, - {Name: filepath.FromSlash("stats/draft.en.md"), Content: []byte(`--- -title: expired -publishdate: "2000-01-06" -draft: true ---- -# Draft -`)}, - {Name: filepath.FromSlash("stats/tax.nn.md"), Content: []byte(`--- -title: Tax NN -weight: 8 -publishdate: "2000-01-06" -weight: 1001 -lag: -- Sogndal ---- -# Tax NN -`)}, - {Name: filepath.FromSlash("stats/tax.nb.md"), Content: []byte(`--- -title: Tax NB -weight: 8 -publishdate: "2000-01-06" -weight: 1002 -lag: -- Sogndal ---- -# Tax NB -`)}, - } - - configFile := "multilangconfig." + configSuffix - writeSource(t, configFile, configContent) - if err := LoadGlobalConfig("", configFile); err != nil { - t.Fatalf("Failed to load config: %s", err) - } - - // Hugo support using ByteSource's directly (for testing), - // but to make it more real, we write them to the mem file system. - for _, s := range sources { - if err := afero.WriteFile(hugofs.Source(), filepath.Join("content", s.Name), s.Content, 0755); err != nil { - t.Fatalf("Failed to write file: %s", err) - } - } - - // Add some data - writeSource(t, "data/hugo.toml", "slogan = \"Hugo Rocks!\"") - - sites, err := NewHugoSitesFromConfiguration() - - if err != nil { - t.Fatalf("Failed to create sites: %s", err) - } - - if len(sites.Sites) != 4 { - t.Fatalf("Got %d sites", len(sites.Sites)) - } - - return sites -} - -func writeSource(t *testing.T, filename, content string) { - if err := afero.WriteFile(hugofs.Source(), filepath.FromSlash(filename), []byte(content), 0755); err != nil { - t.Fatalf("Failed to write file: %s", err) - } -} - -func readDestination(t *testing.T, filename string) string { - return readFileFromFs(t, hugofs.Destination(), filename) -} - -func destinationExists(filename string) bool { - b, err := helpers.Exists(filename, hugofs.Destination()) - if err != nil { - panic(err) - } - return b -} - -func readSource(t *testing.T, filename string) string { - return readFileFromFs(t, hugofs.Source(), filename) -} - -func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { - filename = filepath.FromSlash(filename) - b, err := afero.ReadFile(fs, filename) - if err != nil { - // Print some debug info - root := strings.Split(filename, helpers.FilePathSeparator)[0] - afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error { - if info != nil && !info.IsDir() { - fmt.Println(" ", path) - } - - return nil - }) - t.Fatalf("Failed to read file: %s", err) - } - return string(b) -} - -const testPageTemplate = `--- -title: "%s" -publishdate: "%s" -weight: %d ---- -# Doc %s -` - -func newTestPage(title, date string, weight int) string { - return fmt.Sprintf(testPageTemplate, title, date, weight, title) -} - -func writeNewContentFile(t *testing.T, title, date, filename string, weight int) { - content := newTestPage(title, date, weight) - writeSource(t, filename, content) -} - -func createConfig(t *testing.T, config testSiteConfig, configTemplate string) string { - templ, err := template.New("test").Parse(configTemplate) - if err != nil { - t.Fatal("Template parse failed:", err) - } - var b bytes.Buffer - templ.Execute(&b, config) - return b.String() -} diff --git a/hugolib/node_as_page_test.go b/hugolib/node_as_page_test.go index 78ccc6ce..50451749 100644 --- a/hugolib/node_as_page_test.go +++ b/hugolib/node_as_page_test.go @@ -17,6 +17,7 @@ import ( "fmt" "path/filepath" "testing" + "time" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" @@ -49,22 +50,7 @@ func TestNodesAsPage(t *testing.T) { writeLayoutsForNodeAsPageTests(t) writeNodePagesForNodeAsPageTests("", t) - // Add some regular pages - for i := 1; i <= 4; i++ { - sect := "sect1" - if i > 2 { - sect = "sect2" - } - writeSource(t, filepath.Join("content", sect, fmt.Sprintf("regular%d.md", i)), fmt.Sprintf(`--- -title: Page %02d -categories: [ - "Hugo", - "Web" -] ---- -Content Page %02d -`, i, i)) - } + writeRegularPagesForNodeAsPageTests(t) viper.Set("paginate", 1) viper.Set("title", "Hugo Rocks") @@ -76,40 +62,56 @@ Content Page %02d t.Fatalf("Failed to build site: %s", err) } + // date order: home, sect1, sect2, cat/hugo, cat/web, categories + assertFileContent(t, filepath.Join("public", "index.html"), false, "Index Title: Home Sweet Home!", "Home Content!", - "# Pages: 9") + "# Pages: 9", + "Date: 2009-01-02", + "Lastmod: 2009-01-03", + ) assertFileContent(t, filepath.Join("public", "sect1", "regular1", "index.html"), false, "Single Title: Page 01", "Content Page 01") h := s.owner - nodes := h.findAllPagesByNodeType(PageHome) - require.Len(t, nodes, 1) + nodes := h.findAllPagesByNodeTypeNotIn(PagePage) + require.Len(t, nodes, 6) - home := nodes[0] + home := nodes[5] // oldest require.True(t, home.IsHome()) require.True(t, home.IsNode()) require.False(t, home.IsPage()) + section2 := nodes[3] + require.Equal(t, "Section2", section2.Title) + pages := h.findAllPagesByNodeType(PagePage) require.Len(t, pages, 4) first := pages[0] + require.False(t, first.IsHome()) require.False(t, first.IsNode()) require.True(t, first.IsPage()) - first.Paginator() - // Check Home paginator assertFileContent(t, filepath.Join("public", "page", "2", "index.html"), false, "Pag: Page 02") // Check Sections - assertFileContent(t, filepath.Join("public", "sect1", "index.html"), false, "Section Title: Section", "Section1 Content!") - assertFileContent(t, filepath.Join("public", "sect2", "index.html"), false, "Section Title: Section", "Section2 Content!") + assertFileContent(t, filepath.Join("public", "sect1", "index.html"), false, + "Section Title: Section", "Section1 Content!", + "Date: 2009-01-04", + "Lastmod: 2009-01-05", + ) + + assertFileContent(t, filepath.Join("public", "sect2", "index.html"), false, + "Section Title: Section", "Section2 Content!", + "Date: 2009-01-06", + "Lastmod: 2009-01-07", + ) // Check Sections paginator assertFileContent(t, filepath.Join("public", "sect1", "page", "2", "index.html"), false, @@ -121,10 +123,17 @@ Content Page %02d // Check taxonomy lists assertFileContent(t, filepath.Join("public", "categories", "hugo", "index.html"), false, - "Taxonomy Title: Taxonomy Hugo", "Taxonomy Hugo Content!") + "Taxonomy Title: Taxonomy Hugo", "Taxonomy Hugo Content!", + "Date: 2009-01-08", + "Lastmod: 2009-01-09", + ) assertFileContent(t, filepath.Join("public", "categories", "web", "index.html"), false, - "Taxonomy Title: Taxonomy Web", "Taxonomy Web Content!") + "Taxonomy Title: Taxonomy Web", + "Taxonomy Web Content!", + "Date: 2009-01-10", + "Lastmod: 2009-01-11", + ) // Check taxonomy list paginator assertFileContent(t, filepath.Join("public", "categories", "hugo", "page", "2", "index.html"), false, @@ -133,7 +142,10 @@ Content Page %02d // Check taxonomy terms assertFileContent(t, filepath.Join("public", "categories", "index.html"), false, - "Taxonomy Terms Title: Taxonomy Term Categories", "Taxonomy Term Categories Content!", "k/v: hugo") + "Taxonomy Terms Title: Taxonomy Term Categories", "Taxonomy Term Categories Content!", "k/v: hugo", + "Date: 2009-01-12", + "Lastmod: 2009-01-13", + ) // There are no pages to paginate over in the taxonomy terms. @@ -174,21 +186,35 @@ func TestNodesWithNoContentFile(t *testing.T) { require.Len(t, homePage.Pages, 9) // Alias assertFileContent(t, filepath.Join("public", "index.html"), false, - "Index Title: Hugo Rocks!") + "Index Title: Hugo Rocks!", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) // Taxonomy list assertFileContent(t, filepath.Join("public", "categories", "hugo", "index.html"), false, - "Taxonomy Title: Hugo") + "Taxonomy Title: Hugo", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) // Taxonomy terms assertFileContent(t, filepath.Join("public", "categories", "index.html"), false, - "Taxonomy Terms Title: Categories") + "Taxonomy Terms Title: Categories", + ) // Sections assertFileContent(t, filepath.Join("public", "sect1", "index.html"), false, - "Section Title: Sect1s") + "Section Title: Sect1s", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) + assertFileContent(t, filepath.Join("public", "sect2", "index.html"), false, - "Section Title: Sect2s") + "Section Title: Sect2s", + "Date: 2008-07-06", + "Lastmod: 2008-07-09", + ) // RSS assertFileContent(t, filepath.Join("public", "customrss.xml"), false, "Recent content in Hugo Rocks! on Hugo Rocks!", " 2 { sect = "sect2" + + date, _ = time.Parse(format, "2008-07-15") // Nodes are placed in 2009 + } + date = date.Add(-24 * time.Duration(i) * time.Hour) writeSource(t, filepath.Join("content", sect, fmt.Sprintf("regular%d.%smd", i, langStr)), fmt.Sprintf(`--- title: Page %02d +lastMod : %q +date : %q categories: [ "Hugo", "Web" ] --- Content Page %02d -`, i, i)) +`, i, date.Add(time.Duration(i)*-24*time.Hour).Format(time.RFC822), date.Add(time.Duration(i)*-2*24*time.Hour).Format(time.RFC822), i)) } } @@ -440,41 +476,56 @@ func writeNodePagesForNodeAsPageTests(lang string, t *testing.T) { filename = fmt.Sprintf("_index.%s.md", lang) } - writeSource(t, filepath.Join("content", filename), `--- + format := "2006-01-02" + + date, _ := time.Parse(format, "2009-01-01") + + writeSource(t, filepath.Join("content", filename), fmt.Sprintf(`--- title: Home Sweet Home! +date : %q +lastMod : %q --- Home **Content!** -`) +`, date.Add(1*24*time.Hour).Format(time.RFC822), date.Add(2*24*time.Hour).Format(time.RFC822))) - writeSource(t, filepath.Join("content", "sect1", filename), `--- + writeSource(t, filepath.Join("content", "sect1", filename), fmt.Sprintf(`--- title: Section1 +date : %q +lastMod : %q --- Section1 **Content!** -`) - - writeSource(t, filepath.Join("content", "sect2", filename), `--- +`, date.Add(3*24*time.Hour).Format(time.RFC822), date.Add(4*24*time.Hour).Format(time.RFC822))) + writeSource(t, filepath.Join("content", "sect2", filename), fmt.Sprintf(`--- title: Section2 +date : %q +lastMod : %q --- Section2 **Content!** -`) +`, date.Add(5*24*time.Hour).Format(time.RFC822), date.Add(6*24*time.Hour).Format(time.RFC822))) - writeSource(t, filepath.Join("content", "categories", "hugo", filename), `--- + writeSource(t, filepath.Join("content", "categories", "hugo", filename), fmt.Sprintf(`--- title: Taxonomy Hugo +date : %q +lastMod : %q --- Taxonomy Hugo **Content!** -`) +`, date.Add(7*24*time.Hour).Format(time.RFC822), date.Add(8*24*time.Hour).Format(time.RFC822))) - writeSource(t, filepath.Join("content", "categories", "web", filename), `--- + writeSource(t, filepath.Join("content", "categories", "web", filename), fmt.Sprintf(`--- title: Taxonomy Web +date : %q +lastMod : %q --- Taxonomy Web **Content!** -`) +`, date.Add(9*24*time.Hour).Format(time.RFC822), date.Add(10*24*time.Hour).Format(time.RFC822))) - writeSource(t, filepath.Join("content", "categories", filename), `--- + writeSource(t, filepath.Join("content", "categories", filename), fmt.Sprintf(`--- title: Taxonomy Term Categories +date : %q +lastMod : %q --- Taxonomy Term Categories **Content!** -`) +`, date.Add(11*24*time.Hour).Format(time.RFC822), date.Add(12*24*time.Hour).Format(time.RFC822))) } func writeLayoutsForNodeAsPageTests(t *testing.T) { @@ -490,11 +541,15 @@ Index Content: {{ .Content }} Menu Item: {{ .Name }} {{ end }} {{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} `) writeSource(t, filepath.Join("layouts", "_default", "single.html"), ` Single Title: {{ .Title }} Single Content: {{ .Content }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} `) writeSource(t, filepath.Join("layouts", "_default", "section.html"), ` @@ -504,6 +559,8 @@ Section Content: {{ .Content }} {{ range .Paginator.Pages }} Pag: {{ .Title }} {{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} `) // Taxonomy lists @@ -514,6 +571,8 @@ Taxonomy Content: {{ .Content }} {{ range .Paginator.Pages }} Pag: {{ .Title }} {{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} `) // Taxonomy terms @@ -523,5 +582,7 @@ Taxonomy Terms Content: {{ .Content }} {{ range $key, $value := .Data.Terms }} k/v: {{ $key }} / {{ printf "%=v" $value }} {{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} `) } diff --git a/hugolib/page.go b/hugolib/page.go index 28d4f711..b885e7eb 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1365,9 +1365,51 @@ func (p *Page) prepareData(s *Site) error { p.Data["Pages"] = pages p.Pages = pages + // Now we know enough to set missing dates on home page etc. + p.updatePageDates() + return nil } +func (p *Page) updatePageDates() { + // TODO(bep) np there is a potential issue with page sorting for home pages + // etc. without front matter dates set, but let us wrap the head around + // that in another time. + if !p.PageType.IsNode() { + return + } + + if !p.Date.IsZero() { + if p.Lastmod.IsZero() { + p.Lastmod = p.Date + } + return + } else if !p.Lastmod.IsZero() { + if p.Date.IsZero() { + p.Date = p.Lastmod + } + return + } + + // Set it to the first non Zero date in children + var foundDate, foundLastMod bool + + for _, child := range p.Pages { + if !child.Date.IsZero() { + p.Date = child.Date + foundDate = true + } + if !child.Lastmod.IsZero() { + p.Lastmod = child.Lastmod + foundLastMod = true + } + + if foundDate && foundLastMod { + break + } + } +} + // Page constains some sync.Once which have a mutex, so we cannot just // copy the Page by value. So for the situations where we need a copy, // the paginators etc., we do it manually here.