shithub: hugo

Download patch

ref: 708bc78770a0b0361908f6404f57264c53252a95
parent: f023dfd7636f73b11c94e86a05c6273941d52c58
author: Bjørn Erik Pedersen <[email protected]>
date: Thu Jul 28 05:30:58 EDT 2016

Optimize the multilanguage build process

Work In Progress!

This commit makes a rework of the build and rebuild process to better suit a multi-site setup.

This also includes a complete overhaul of the site tests. Previous these were a messy mix that
were testing just small parts of the build chain, some of it testing code-paths not even used in
"real life". Now all tests that depends on a built site follows the same and real production code path.

See #2309
Closes #2211
Closes #477
Closes #1744

--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -49,7 +49,7 @@
 // Hugo represents the Hugo sites to build. This variable is exported as it
 // is used by at least one external library (the Hugo caddy plugin). We should
 // provide a cleaner external API, but until then, this is it.
-var Hugo hugolib.HugoSites
+var Hugo *hugolib.HugoSites
 
 // Reset resets Hugo ready for a new full build. This is mainly only useful
 // for benchmark testing etc. via the CLI commands.
@@ -715,11 +715,11 @@
 func buildSites(watching ...bool) (err error) {
 	fmt.Println("Started building sites ...")
 	w := len(watching) > 0 && watching[0]
-	return Hugo.Build(w, true)
+	return Hugo.Build(hugolib.BuildCfg{Watching: w, PrintStats: true})
 }
 
 func rebuildSites(events []fsnotify.Event) error {
-	return Hugo.Rebuild(events, true)
+	return Hugo.Rebuild(hugolib.BuildCfg{PrintStats: true}, events...)
 }
 
 // NewWatcher creates a new watcher to watch filesystem events.
--- a/commands/list.go
+++ b/commands/list.go
@@ -53,7 +53,7 @@
 
 		site := &hugolib.Site{}
 
-		if err := site.Process(); err != nil {
+		if err := site.PreProcess(hugolib.BuildCfg{}); err != nil {
 			return newSystemError("Error Processing Source Content", err)
 		}
 
@@ -84,7 +84,7 @@
 
 		site := &hugolib.Site{}
 
-		if err := site.Process(); err != nil {
+		if err := site.PreProcess(hugolib.BuildCfg{}); err != nil {
 			return newSystemError("Error Processing Source Content", err)
 		}
 
@@ -115,7 +115,7 @@
 
 		site := &hugolib.Site{}
 
-		if err := site.Process(); err != nil {
+		if err := site.PreProcess(hugolib.BuildCfg{}); err != nil {
 			return newSystemError("Error Processing Source Content", err)
 		}
 
--- a/commands/multilingual.go
+++ b/commands/multilingual.go
@@ -11,30 +11,31 @@
 	"github.com/spf13/viper"
 )
 
-func readMultilingualConfiguration() (hugolib.HugoSites, error) {
-	h := make(hugolib.HugoSites, 0)
+func readMultilingualConfiguration() (*hugolib.HugoSites, error) {
+	sites := make([]*hugolib.Site, 0)
 	multilingual := viper.GetStringMap("Multilingual")
 	if len(multilingual) == 0 {
 		// TODO(bep) multilingo langConfigsList = append(langConfigsList, hugolib.NewLanguage("en"))
-		h = append(h, hugolib.NewSite(hugolib.NewLanguage("en")))
-		return h, nil
+		sites = append(sites, hugolib.NewSite(hugolib.NewLanguage("en")))
 	}
 
-	var err error
+	if len(multilingual) > 0 {
+		var err error
 
-	langConfigsList, err := toSortedLanguages(multilingual)
+		languages, err := toSortedLanguages(multilingual)
 
-	if err != nil {
-		return nil, fmt.Errorf("Failed to parse multilingual config: %s", err)
-	}
+		if err != nil {
+			return nil, fmt.Errorf("Failed to parse multilingual config: %s", err)
+		}
 
-	for _, lang := range langConfigsList {
-		s := hugolib.NewSite(lang)
-		s.SetMultilingualConfig(lang, langConfigsList)
-		h = append(h, s)
+		for _, lang := range languages {
+			sites = append(sites, hugolib.NewSite(lang))
+		}
+
 	}
 
-	return h, nil
+	return hugolib.NewHugoSites(sites...)
+
 }
 
 func toSortedLanguages(l map[string]interface{}) (hugolib.Languages, error) {
--- a/helpers/url.go
+++ b/helpers/url.go
@@ -169,6 +169,17 @@
 	return MakePermalink(baseURL, path).String()
 }
 
+// IsAbsURL determines whether the given path points to an absolute URL.
+// TODO(bep) ml tests
+func IsAbsURL(path string) bool {
+	url, err := url.Parse(path)
+	if err != nil {
+		return false
+	}
+
+	return url.IsAbs() || strings.HasPrefix(path, "//")
+}
+
 // RelURL creates a URL relative to the BaseURL root.
 // Note: The result URL will not include the context root if canonifyURLs is enabled.
 func RelURL(path string) string {
--- a/hugolib/embedded_shortcodes_test.go
+++ b/hugolib/embedded_shortcodes_test.go
@@ -56,8 +56,8 @@
 	templ := tpl.New()
 	p, _ := pageFromString(simplePageWithURL, path)
 	p.Node.Site = &SiteInfo{
-		AllPages: &(Pages{p}),
-		BaseURL:  template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)),
+		rawAllPages: &(Pages{p}),
+		BaseURL:     template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)),
 	}
 
 	output, err := HandleShortcodes(in, p, templ)
@@ -72,8 +72,7 @@
 }
 
 func TestShortcodeHighlight(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	if !helpers.HasPygments() {
 		t.Skip("Skip test as Pygments is not installed")
--- a/hugolib/handler_test.go
+++ b/hugolib/handler_test.go
@@ -25,8 +25,7 @@
 )
 
 func TestDefaultHandler(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	hugofs.InitMemFs()
 	sources := []source.ByteSource{
@@ -45,33 +44,30 @@
 	viper.Set("verbose", true)
 
 	s := &Site{
-		Source:  &source.InMemorySource{ByteSource: sources},
-		targets: targetList{page: &target.PagePub{UglyURLs: true}},
-		Lang:    NewLanguage("en"),
+		Source:   &source.InMemorySource{ByteSource: sources},
+		targets:  targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}},
+		Language: NewLanguage("en"),
 	}
 
-	s.initializeSiteInfo()
-
-	s.prepTemplates(
+	if err := buildAndRenderSite(s,
 		"_default/single.html", "{{.Content}}",
 		"head", "<head><script src=\"script.js\"></script></head>",
-		"head_abs", "<head><script src=\"/script.js\"></script></head>")
+		"head_abs", "<head><script src=\"/script.js\"></script></head>"); err != nil {
+		t.Fatalf("Failed to render site: %s", err)
+	}
 
-	// From site_test.go
-	createAndRenderPages(t, s)
-
 	tests := []struct {
 		doc      string
 		expected string
 	}{
-		{filepath.FromSlash("sect/doc1.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"},
-		{filepath.FromSlash("sect/doc2.html"), "<!doctype html><html><body>more content</body></html>"},
-		{filepath.FromSlash("sect/doc3.html"), "\n\n<h1 id=\"doc3\">doc3</h1>\n\n<p><em>some</em> content</p>\n"},
-		{filepath.FromSlash("sect/doc3/img1.png"), string([]byte("‰PNG  ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚"))},
-		{filepath.FromSlash("sect/img2.gif"), string([]byte("GIF89a��€��ÿÿÿ���,�������D�;"))},
-		{filepath.FromSlash("sect/img2.spf"), string([]byte("****FAKE-FILETYPE****"))},
-		{filepath.FromSlash("doc7.html"), "<html><body>doc7 content</body></html>"},
-		{filepath.FromSlash("sect/doc8.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"},
+		{filepath.FromSlash("public/sect/doc1.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"},
+		{filepath.FromSlash("public/sect/doc2.html"), "<!doctype html><html><body>more content</body></html>"},
+		{filepath.FromSlash("public/sect/doc3.html"), "\n\n<h1 id=\"doc3\">doc3</h1>\n\n<p><em>some</em> content</p>\n"},
+		{filepath.FromSlash("public/sect/doc3/img1.png"), string([]byte("‰PNG  ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚"))},
+		{filepath.FromSlash("public/sect/img2.gif"), string([]byte("GIF89a��€��ÿÿÿ���,�������D�;"))},
+		{filepath.FromSlash("public/sect/img2.spf"), string([]byte("****FAKE-FILETYPE****"))},
+		{filepath.FromSlash("public/doc7.html"), "<html><body>doc7 content</body></html>"},
+		{filepath.FromSlash("public/sect/doc8.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"},
 	}
 
 	for _, test := range tests {
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -14,42 +14,119 @@
 package hugolib
 
 import (
+	"errors"
+	"strings"
 	"time"
 
-	"github.com/fsnotify/fsnotify"
+	"github.com/spf13/viper"
 
+	"github.com/fsnotify/fsnotify"
+	"github.com/spf13/hugo/source"
+	"github.com/spf13/hugo/tpl"
 	jww "github.com/spf13/jwalterweatherman"
 )
 
 // HugoSites represents the sites to build. Each site represents a language.
-type HugoSites []*Site
+type HugoSites struct {
+	Sites []*Site
 
+	Multilingual *Multilingual
+}
+
+func NewHugoSites(sites ...*Site) (*HugoSites, error) {
+	languages := make(Languages, len(sites))
+	for i, s := range sites {
+		if s.Language == nil {
+			return nil, errors.New("Missing language for site")
+		}
+		languages[i] = s.Language
+	}
+	defaultLang := viper.GetString("DefaultContentLanguage")
+	if defaultLang == "" {
+		defaultLang = "en"
+	}
+	langConfig := &Multilingual{Languages: languages, DefaultLang: NewLanguage(defaultLang)}
+
+	return &HugoSites{Multilingual: langConfig, Sites: sites}, nil
+}
+
 // Reset resets the sites, making it ready for a full rebuild.
 // TODO(bep) multilingo
 func (h HugoSites) Reset() {
-	for i, s := range h {
-		h[i] = s.Reset()
+	for i, s := range h.Sites {
+		h.Sites[i] = s.Reset()
 	}
 }
 
+type BuildCfg struct {
+	// Whether we are in watch (server) mode
+	Watching bool
+	// Print build stats at the end of a build
+	PrintStats bool
+	// Skip rendering. Useful for testing.
+	skipRender bool
+	// Use this to add templates to use for rendering.
+	// Useful for testing.
+	withTemplate func(templ tpl.Template) error
+}
+
 // Build builds all sites.
-func (h HugoSites) Build(watching, printStats bool) error {
+func (h HugoSites) Build(config BuildCfg) error {
+
+	if h.Sites == nil || len(h.Sites) == 0 {
+		return errors.New("No site(s) to build")
+	}
+
 	t0 := time.Now()
 
-	for _, site := range h {
-		t1 := time.Now()
+	// We should probably refactor the Site and pull up most of the logic from there to here,
+	// but that seems like a daunting task.
+	// So for now, if there are more than one site (language),
+	// we pre-process the first one, then configure all the sites based on that.
+	firstSite := h.Sites[0]
 
-		site.RunMode.Watching = watching
+	for _, s := range h.Sites {
+		// TODO(bep) ml
+		s.Multilingual = h.Multilingual
+		s.RunMode.Watching = config.Watching
+	}
 
-		if err := site.Build(); err != nil {
+	if err := firstSite.PreProcess(config); err != nil {
+		return err
+	}
+
+	h.setupTranslations(firstSite)
+
+	if len(h.Sites) > 1 {
+		// Initialize the rest
+		for _, site := range h.Sites[1:] {
+			site.Tmpl = firstSite.Tmpl
+			site.initializeSiteInfo()
+		}
+	}
+
+	for _, s := range h.Sites {
+
+		if err := s.PostProcess(); err != nil {
 			return err
 		}
-		if printStats {
-			site.Stats(t1)
+
+		if !config.skipRender {
+			if err := s.Render(); err != nil {
+				return err
+			}
+
 		}
+
+		if config.PrintStats {
+			s.Stats()
+		}
+
+		// TODO(bep) ml lang in site.Info?
+		// TODO(bep) ml Page sorting?
 	}
 
-	if printStats {
+	if config.PrintStats {
 		jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds()))
 	}
 
@@ -58,25 +135,159 @@
 }
 
 // Rebuild rebuilds all sites.
-func (h HugoSites) Rebuild(events []fsnotify.Event, printStats bool) error {
+func (h HugoSites) Rebuild(config BuildCfg, events ...fsnotify.Event) error {
 	t0 := time.Now()
 
-	for _, site := range h {
-		t1 := time.Now()
+	firstSite := h.Sites[0]
 
-		if err := site.ReBuild(events); err != nil {
-			return err
+	for _, s := range h.Sites {
+		s.resetBuildState()
+	}
+
+	sourceChanged, err := firstSite.ReBuild(events)
+
+	if err != nil {
+		return err
+	}
+
+	// Assign pages to sites per translation.
+	h.setupTranslations(firstSite)
+
+	for _, s := range h.Sites {
+
+		if sourceChanged {
+			if err := s.PostProcess(); err != nil {
+				return err
+			}
 		}
 
-		if printStats {
-			site.Stats(t1)
+		if !config.skipRender {
+			if err := s.Render(); err != nil {
+				return err
+			}
 		}
+
+		if config.PrintStats {
+			s.Stats()
+		}
 	}
 
-	if printStats {
+	if config.PrintStats {
 		jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds()))
 	}
 
 	return nil
 
+}
+
+func (s *HugoSites) setupTranslations(master *Site) {
+
+	for _, p := range master.rawAllPages {
+		if p.Lang() == "" {
+			panic("Page language missing: " + p.Title)
+		}
+
+		shouldBuild := p.shouldBuild()
+
+		for i, site := range s.Sites {
+			if strings.HasPrefix(site.Language.Lang, p.Lang()) {
+				site.updateBuildStats(p)
+				if shouldBuild {
+					site.Pages = append(site.Pages, p)
+					p.Site = &site.Info
+				}
+			}
+
+			if !shouldBuild {
+				continue
+			}
+
+			if i == 0 {
+				site.AllPages = append(site.AllPages, p)
+			}
+		}
+
+		for i := 1; i < len(s.Sites); i++ {
+			s.Sites[i].AllPages = s.Sites[0].AllPages
+		}
+	}
+
+	if len(s.Sites) > 1 {
+		pages := s.Sites[0].AllPages
+		allTranslations := pagesToTranslationsMap(s.Multilingual, pages)
+		assignTranslationsToPages(allTranslations, pages)
+	}
+}
+
+func (s *Site) updateBuildStats(page *Page) {
+	if page.IsDraft() {
+		s.draftCount++
+	}
+
+	if page.IsFuture() {
+		s.futureCount++
+	}
+
+	if page.IsExpired() {
+		s.expiredCount++
+	}
+}
+
+// Convenience func used in tests to build a single site/language excluding render phase.
+func buildSiteSkipRender(s *Site, additionalTemplates ...string) error {
+	return doBuildSite(s, false, additionalTemplates...)
+}
+
+// Convenience func used in tests to build a single site/language including render phase.
+func buildAndRenderSite(s *Site, additionalTemplates ...string) error {
+	return doBuildSite(s, true, additionalTemplates...)
+}
+
+// Convenience func used in tests to build a single site/language.
+func doBuildSite(s *Site, render bool, additionalTemplates ...string) error {
+	sites, err := NewHugoSites(s)
+	if err != nil {
+		return err
+	}
+
+	addTemplates := func(templ tpl.Template) error {
+		for i := 0; i < len(additionalTemplates); i += 2 {
+			err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1])
+			if err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+
+	config := BuildCfg{skipRender: !render, withTemplate: addTemplates}
+	return sites.Build(config)
+}
+
+// Convenience func used in tests.
+func newHugoSitesFromSourceAndLanguages(input []source.ByteSource, languages Languages) (*HugoSites, error) {
+	if len(languages) == 0 {
+		panic("Must provide at least one language")
+	}
+	first := &Site{
+		Source:   &source.InMemorySource{ByteSource: input},
+		Language: languages[0],
+	}
+	if len(languages) == 1 {
+		return NewHugoSites(first)
+	}
+
+	sites := make([]*Site, len(languages))
+	sites[0] = first
+	for i := 1; i < len(languages); i++ {
+		sites[i] = &Site{Language: languages[i]}
+	}
+
+	return NewHugoSites(sites...)
+
+}
+
+// Convenience func used in tests.
+func newHugoSitesFromLanguages(languages Languages) (*HugoSites, error) {
+	return newHugoSitesFromSourceAndLanguages(nil, languages)
 }
--- /dev/null
+++ b/hugolib/hugo_sites_test.go
@@ -1,0 +1,522 @@
+package hugolib
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"path/filepath"
+
+	"os"
+
+	"github.com/fsnotify/fsnotify"
+	"github.com/spf13/afero"
+	"github.com/spf13/hugo/helpers"
+	"github.com/spf13/hugo/hugofs"
+	"github.com/spf13/hugo/source"
+	"github.com/spf13/viper"
+	"github.com/stretchr/testify/assert"
+
+	jww "github.com/spf13/jwalterweatherman"
+)
+
+func init() {
+	testCommonResetState()
+	jww.SetStdoutThreshold(jww.LevelError)
+
+}
+
+func testCommonResetState() {
+	hugofs.InitMemFs()
+	viper.Reset()
+	viper.Set("ContentDir", "content")
+	viper.Set("DataDir", "data")
+	viper.Set("I18nDir", "i18n")
+	viper.Set("themesDir", "themes")
+	viper.Set("LayoutDir", "layouts")
+	viper.Set("PublishDir", "public")
+	viper.Set("RSSUri", "rss")
+
+	if err := hugofs.Source().Mkdir("content", 0755); err != nil {
+		panic("Content folder creation failed.")
+	}
+
+}
+
+func _TestMultiSites(t *testing.T) {
+
+	sites := createMultiTestSites(t)
+
+	err := sites.Build(BuildCfg{skipRender: true})
+
+	if err != nil {
+		t.Fatalf("Failed to build sites: %s", err)
+	}
+
+	enSite := sites.Sites[0]
+
+	assert.Equal(t, "en", enSite.Language.Lang)
+
+	if len(enSite.Pages) != 3 {
+		t.Fatal("Expected 3 english pages")
+	}
+	assert.Len(t, enSite.Source.Files(), 6, "should have 6 source files")
+	assert.Len(t, enSite.AllPages, 6, "should have 6 total pages (including translations)")
+
+	doc1en := enSite.Pages[0]
+	permalink, err := doc1en.Permalink()
+	assert.NoError(t, err, "permalink call failed")
+	assert.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", permalink, "invalid doc1.en permalink")
+	assert.Len(t, doc1en.Translations(), 1, "doc1-en should have one translation, excluding itself")
+
+	doc2 := enSite.Pages[1]
+	permalink, err = doc2.Permalink()
+	assert.NoError(t, err, "permalink call failed")
+	assert.Equal(t, "http://example.com/blog/en/sect/doc2/", permalink, "invalid doc2 permalink")
+
+	doc3 := enSite.Pages[2]
+	permalink, err = doc3.Permalink()
+	assert.NoError(t, err, "permalink call failed")
+	assert.Equal(t, "http://example.com/blog/superbob", permalink, "invalid doc3 permalink")
+
+	// TODO(bep) multilingo. Check this case. This has url set in frontmatter, but we must split into lang folders
+	// The assertion below was missing the /en prefix.
+	assert.Equal(t, "/en/superbob", doc3.URL(), "invalid url, was specified on doc3 TODO(bep)")
+
+	assert.Equal(t, doc2.Next, doc3, "doc3 should follow doc2, in .Next")
+
+	doc1fr := doc1en.Translations()[0]
+	permalink, err = doc1fr.Permalink()
+	assert.NoError(t, err, "permalink call failed")
+	assert.Equal(t, "http://example.com/blog/fr/sect/doc1/", permalink, "invalid doc1fr permalink")
+
+	assert.Equal(t, doc1en.Translations()[0], doc1fr, "doc1-en should have doc1-fr as translation")
+	assert.Equal(t, doc1fr.Translations()[0], doc1en, "doc1-fr should have doc1-en as translation")
+	assert.Equal(t, "fr", doc1fr.Language().Lang)
+
+	doc4 := enSite.AllPages[4]
+	permalink, err = doc4.Permalink()
+	assert.NoError(t, err, "permalink call failed")
+	assert.Equal(t, "http://example.com/blog/fr/sect/doc4/", permalink, "invalid doc4 permalink")
+	assert.Len(t, doc4.Translations(), 0, "found translations for doc4")
+
+	doc5 := enSite.AllPages[5]
+	permalink, err = doc5.Permalink()
+	assert.NoError(t, err, "permalink call failed")
+	assert.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid doc5 permalink")
+
+	// Taxonomies and their URLs
+	assert.Len(t, enSite.Taxonomies, 1, "should have 1 taxonomy")
+	tags := enSite.Taxonomies["tags"]
+	assert.Len(t, tags, 2, "should have 2 different tags")
+	assert.Equal(t, tags["tag1"][0].Page, doc1en, "first tag1 page should be doc1")
+
+	frSite := sites.Sites[1]
+
+	assert.Equal(t, "fr", frSite.Language.Lang)
+	assert.Len(t, frSite.Pages, 3, "should have 3 pages")
+	assert.Len(t, frSite.AllPages, 6, "should have 6 total pages (including translations)")
+
+	for _, frenchPage := range frSite.Pages {
+		assert.Equal(t, "fr", frenchPage.Lang())
+	}
+
+}
+
+func TestMultiSitesRebuild(t *testing.T) {
+
+	sites := createMultiTestSites(t)
+	cfg := BuildCfg{}
+
+	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]
+
+	assert.Len(t, enSite.Pages, 3)
+	assert.Len(t, frSite.Pages, 3)
+
+	// Verify translations
+	docEn := readDestination(t, "public/en/sect/doc1-slug/index.html")
+	assert.True(t, strings.Contains(docEn, "Hello"), "No Hello")
+	docFr := readDestination(t, "public/fr/sect/doc1/index.html")
+	assert.True(t, strings.Contains(docFr, "Bonjour"), "No Bonjour")
+
+	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) {
+				assert.Len(t, enSite.Pages, 2, "1 en removed")
+
+				// Check build stats
+				assert.Equal(t, 1, enSite.draftCount, "Draft")
+				assert.Equal(t, 1, enSite.futureCount, "Future")
+				assert.Equal(t, 1, enSite.expiredCount, "Expired")
+				assert.Equal(t, 0, frSite.draftCount, "Draft")
+				assert.Equal(t, 1, frSite.futureCount, "Future")
+				assert.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) {
+				assert.Len(t, enSite.Pages, 4)
+				assert.Len(t, enSite.AllPages, 8)
+				assert.Len(t, frSite.Pages, 4)
+				assert.Equal(t, "new_fr_1", frSite.Pages[3].Title)
+				assert.Equal(t, "new_en_2", enSite.Pages[0].Title)
+				assert.Equal(t, "new_en_1", enSite.Pages[1].Title)
+
+				rendered := readDestination(t, "public/en/new1/index.html")
+				assert.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) {
+				assert.Len(t, enSite.Pages, 4)
+				doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html")
+				assert.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) {
+				assert.Len(t, enSite.Pages, 4, "Rename")
+				assert.Equal(t, "new_en_1", enSite.Pages[1].Title)
+				rendered := readDestination(t, "public/en/new1renamed/index.html")
+				assert.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) {
+				assert.Len(t, enSite.Pages, 4)
+				assert.Len(t, enSite.AllPages, 8)
+				assert.Len(t, frSite.Pages, 4)
+				doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html")
+				assert.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) {
+				assert.Len(t, enSite.Pages, 4)
+				assert.Len(t, enSite.AllPages, 8)
+				assert.Len(t, frSite.Pages, 4)
+				docEn := readDestination(t, "public/en/sect/doc1-slug/index.html")
+				assert.True(t, strings.Contains(docEn, "Hello"), "No Hello")
+				docFr := readDestination(t, "public/fr/sect/doc1/index.html")
+				assert.True(t, strings.Contains(docFr, "Salut"), "No Salut")
+			},
+		},
+	} {
+
+		if this.preFunc != nil {
+			this.preFunc(t)
+		}
+		err = sites.Rebuild(cfg, this.events...)
+
+		if err != nil {
+			t.Fatalf("[%d] Failed to rebuild sites: %s", i, err)
+		}
+
+		this.assertFunc(t)
+	}
+
+}
+
+func createMultiTestSites(t *testing.T) *HugoSites {
+	// General settings
+	hugofs.InitMemFs()
+
+	viper.Set("DefaultExtension", "html")
+	viper.Set("baseurl", "http://example.com/blog")
+	viper.Set("DisableSitemap", false)
+	viper.Set("DisableRSS", false)
+	viper.Set("RSSUri", "index.xml")
+	viper.Set("Taxonomies", map[string]string{"tag": "tags"})
+	viper.Set("Permalinks", map[string]string{"other": "/somewhere/else/:filename"})
+
+	// Add some layouts
+	if err := afero.WriteFile(hugofs.Source(),
+		filepath.Join("layouts", "_default/single.html"),
+		[]byte("Single: {{ .Title }}|{{ i18n \"hello\" }} {{ .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("List: {{ .Title }}"),
+		0755); err != nil {
+		t.Fatalf("Failed to write layout file: %s", err)
+	}
+
+	if err := afero.WriteFile(hugofs.Source(),
+		filepath.Join("layouts", "index.html"),
+		[]byte("Home: {{ .Title }}|{{ .IsHome }}"),
+		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{
+		{filepath.FromSlash("sect/doc1.en.md"), []byte(`---
+title: doc1
+slug: doc1-slug
+tags:
+ - tag1
+publishdate: "2000-01-01"
+---
+# doc1
+*some content*
+NOTE: slug should be used as URL
+`)},
+		{filepath.FromSlash("sect/doc1.fr.md"), []byte(`---
+title: doc1
+tags:
+ - tag1
+ - tag2
+publishdate: "2000-01-04"
+---
+# doc1
+*quelque contenu*
+NOTE: should be in the 'en' Page's 'Translations' field.
+NOTE: date is after "doc3"
+`)},
+		{filepath.FromSlash("sect/doc2.en.md"), []byte(`---
+title: doc2
+publishdate: "2000-01-02"
+---
+# doc2
+*some content*
+NOTE: without slug, "doc2" should be used, without ".en" as URL
+`)},
+		{filepath.FromSlash("sect/doc3.en.md"), []byte(`---
+title: doc3
+publishdate: "2000-01-03"
+tags:
+ - tag2
+url: /superbob
+---
+# doc3
+*some content*
+NOTE: third 'en' doc, should trigger pagination on home page.
+`)},
+		{filepath.FromSlash("sect/doc4.md"), []byte(`---
+title: doc4
+tags:
+ - tag1
+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'
+`)},
+		{filepath.FromSlash("other/doc5.fr.md"), []byte(`---
+title: doc5
+publishdate: "2000-01-06"
+---
+# doc5
+*autre contenu francophone*
+NOTE: should use the "permalinks" configuration with :filename
+`)},
+		// Add some for the stats
+		{filepath.FromSlash("stats/expired.fr.md"), []byte(`---
+title: expired
+publishdate: "2000-01-06"
+expiryDate: "2001-01-06"
+---
+# Expired
+`)},
+		{filepath.FromSlash("stats/future.fr.md"), []byte(`---
+title: future
+publishdate: "2100-01-06"
+---
+# Future
+`)},
+		{filepath.FromSlash("stats/expired.en.md"), []byte(`---
+title: expired
+publishdate: "2000-01-06"
+expiryDate: "2001-01-06"
+---
+# Expired
+`)},
+		{filepath.FromSlash("stats/future.en.md"), []byte(`---
+title: future
+publishdate: "2100-01-06"
+---
+# Future
+`)},
+		{filepath.FromSlash("stats/draft.en.md"), []byte(`---
+title: expired
+publishdate: "2000-01-06"
+draft: true
+---
+# Draft
+`)},
+	}
+
+	// Multilingual settings
+	viper.Set("Multilingual", true)
+	en := NewLanguage("en")
+	viper.Set("DefaultContentLanguage", "fr")
+	viper.Set("paginate", "2")
+
+	languages := NewLanguages(en, NewLanguage("fr"))
+
+	// 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)
+		}
+	}
+	_, err := hugofs.Source().Open("content/other/doc5.fr.md")
+
+	if err != nil {
+		t.Fatalf("Unable to locate file")
+	}
+	sites, err := newHugoSitesFromLanguages(languages)
+
+	if err != nil {
+		t.Fatalf("Failed to create sites: %s", err)
+	}
+
+	if len(sites.Sites) != 2 {
+		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 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.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)
+}
--- a/hugolib/i18n.go
+++ b/hugolib/i18n.go
@@ -17,9 +17,12 @@
 	"github.com/nicksnyder/go-i18n/i18n/bundle"
 	"github.com/spf13/hugo/source"
 	"github.com/spf13/hugo/tpl"
+	jww "github.com/spf13/jwalterweatherman"
 )
 
 func loadI18n(sources []source.Input) error {
+	jww.DEBUG.Printf("Load I18n from %q", sources)
+
 	i18nBundle := bundle.New()
 
 	for _, currentSource := range sources {
--- a/hugolib/menu_test.go
+++ b/hugolib/menu_test.go
@@ -201,10 +201,8 @@
 }
 
 func doTestPageMenuWithIdentifier(t *testing.T, menuPageSources []source.ByteSource) {
+	testCommonResetState()
 
-	viper.Reset()
-	defer viper.Reset()
-
 	s := setupMenuTests(t, menuPageSources)
 
 	assert.Equal(t, 3, len(s.Pages), "Not enough pages")
@@ -241,8 +239,7 @@
 }
 
 func doTestPageMenuWithDuplicateName(t *testing.T, menuPageSources []source.ByteSource) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	s := setupMenuTests(t, menuPageSources)
 
@@ -260,8 +257,7 @@
 }
 
 func TestPageMenu(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	s := setupMenuTests(t, menuPageSources)
 
@@ -307,8 +303,7 @@
 }
 
 func TestMenuURL(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	s := setupMenuTests(t, menuPageSources)
 
@@ -338,8 +333,7 @@
 
 // Issue #1934
 func TestYAMLMenuWithMultipleEntries(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	ps1 := []byte(`---
 title: "Yaml 1"
@@ -377,8 +371,7 @@
 }
 
 func doTestMenuWithUnicodeURLs(t *testing.T, canonifyURLs bool) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("CanonifyURLs", canonifyURLs)
 
@@ -403,8 +396,7 @@
 }
 
 func doTestSectionPagesMenu(canonifyUrls bool, t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("SectionPagesMenu", "spm")
 
@@ -458,8 +450,7 @@
 }
 
 func TestTaxonomyNodeMenu(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("CanonifyURLs", true)
 	s := setupMenuTests(t, menuPageSources)
@@ -502,8 +493,7 @@
 }
 
 func TestMenuLimit(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	s := setupMenuTests(t, menuPageSources)
 	m := *s.Menus["main"]
@@ -545,8 +535,7 @@
 }
 
 func TestHomeNodeMenu(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("CanonifyURLs", true)
 	viper.Set("UglyURLs", true)
@@ -659,7 +648,7 @@
 	return found
 }
 
-func setupTestMenuState(s *Site, t *testing.T) {
+func setupTestMenuState(t *testing.T) {
 	menus, err := tomlToMap(confMenu1)
 
 	if err != nil {
@@ -672,7 +661,8 @@
 
 func setupMenuTests(t *testing.T, pageSources []source.ByteSource) *Site {
 	s := createTestSite(pageSources)
-	setupTestMenuState(s, t)
+
+	setupTestMenuState(t)
 	testSiteSetup(s, t)
 
 	return s
@@ -681,18 +671,17 @@
 func createTestSite(pageSources []source.ByteSource) *Site {
 	hugofs.InitMemFs()
 
-	s := &Site{
-		Source: &source.InMemorySource{ByteSource: pageSources},
-		Lang:   newDefaultLanguage(),
+	return &Site{
+		Source:   &source.InMemorySource{ByteSource: pageSources},
+		Language: newDefaultLanguage(),
 	}
-	return s
+
 }
 
 func testSiteSetup(s *Site, t *testing.T) {
-	s.Menus = Menus{}
-	s.initializeSiteInfo()
-
-	createPagesAndMeta(t, s)
+	if err := buildSiteSkipRender(s); err != nil {
+		t.Fatalf("Sites build failed: %s", err)
+	}
 }
 
 func tomlToMap(s string) (map[string]interface{}, error) {
--- a/hugolib/multilingual.go
+++ b/hugolib/multilingual.go
@@ -45,6 +45,8 @@
 type Multilingual struct {
 	Languages Languages
 
+	DefaultLang *Language
+
 	langMap     map[string]*Language
 	langMapInit sync.Once
 }
@@ -60,7 +62,7 @@
 }
 
 func (ml *Multilingual) enabled() bool {
-	return len(ml.Languages) > 0
+	return len(ml.Languages) > 1
 }
 
 func (l *Language) Params() map[string]interface{} {
@@ -98,16 +100,6 @@
 	return viper.Get(key)
 }
 
-// TODO(bep) multilingo move this to a constructor.
-func (s *Site) SetMultilingualConfig(currentLang *Language, languages Languages) {
-
-	ml := &Multilingual{
-		Languages: languages,
-	}
-	viper.Set("Multilingual", ml.enabled())
-	s.Multilingual = ml
-}
-
 func (s *Site) multilingualEnabled() bool {
 	return s.Multilingual != nil && s.Multilingual.enabled()
 }
@@ -118,5 +110,5 @@
 }
 
 func (s *Site) currentLanguage() *Language {
-	return s.Lang
+	return s.Language
 }
--- a/hugolib/node.go
+++ b/hugolib/node.go
@@ -18,9 +18,12 @@
 	"path"
 	"path/filepath"
 	"sort"
+	"strings"
 	"sync"
 	"time"
 
+	"github.com/spf13/hugo/helpers"
+
 	"github.com/spf13/cast"
 )
 
@@ -243,11 +246,22 @@
 }
 
 func (n *Node) addMultilingualWebPrefix(outfile string) string {
+
+	if helpers.IsAbsURL(outfile) {
+		return outfile
+	}
+
+	hadSlashSuffix := strings.HasSuffix(outfile, "/")
+
 	lang := n.Lang()
 	if lang == "" || !n.Site.Multilingual {
 		return outfile
 	}
-	return "/" + path.Join(lang, outfile)
+	outfile = "/" + path.Join(lang, outfile)
+	if hadSlashSuffix {
+		outfile += "/"
+	}
+	return outfile
 }
 
 func (n *Node) addMultilingualFilesystemPrefix(outfile string) string {
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -833,6 +833,7 @@
 					menuEntry.marshallMap(ime)
 				}
 				p.pageMenus[name] = &menuEntry
+
 			}
 		}
 	})
--- a/hugolib/page_permalink_test.go
+++ b/hugolib/page_permalink_test.go
@@ -23,8 +23,7 @@
 )
 
 func TestPermalink(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	tests := []struct {
 		file         string
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -569,7 +569,7 @@
 
 func TestPageWithShortCodeInSummary(t *testing.T) {
 	s := new(Site)
-	s.prepTemplates()
+	s.prepTemplates(nil)
 	p, _ := NewPage("simple.md")
 	_, err := p.ReadFrom(strings.NewReader(simplePageWithShortcodeInSummary))
 	if err != nil {
@@ -644,7 +644,7 @@
 }
 
 func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) {
-	viper.Reset()
+	testCommonResetState()
 
 	p, _ := NewPage("simple.md")
 	_, err := p.ReadFrom(strings.NewReader(simplePageWithAllCJKRunes))
@@ -660,8 +660,7 @@
 }
 
 func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("HasCJKLanguage", true)
 
@@ -679,8 +678,7 @@
 }
 
 func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("HasCJKLanguage", true)
 
@@ -703,8 +701,7 @@
 }
 
 func TestWordCountWithIsCJKLanguageFalse(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("HasCJKLanguage", true)
 
@@ -944,8 +941,7 @@
 }
 
 func TestPagePaths(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("DefaultExtension", "html")
 	siteParmalinksSetting := PermalinkOverrides{
--- a/hugolib/pagination_test.go
+++ b/hugolib/pagination_test.go
@@ -192,8 +192,7 @@
 }
 
 func TestPaginationURLFactory(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("PaginatePath", "zoo")
 	unicode := newPaginationURLFactory("новости проекта")
@@ -207,8 +206,7 @@
 }
 
 func TestPaginator(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	for _, useViper := range []bool{false, true} {
 		doTestPaginator(t, useViper)
@@ -216,8 +214,7 @@
 }
 
 func doTestPaginator(t *testing.T, useViper bool) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	pagerSize := 5
 	if useViper {
@@ -260,8 +257,7 @@
 }
 
 func TestPaginatorWithNegativePaginate(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("paginate", -1)
 	s := newSiteDefaultLang()
@@ -270,8 +266,7 @@
 }
 
 func TestPaginate(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	for _, useViper := range []bool{false, true} {
 		doTestPaginate(t, useViper)
@@ -331,8 +326,7 @@
 }
 
 func TestPaginateWithNegativePaginate(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("paginate", -1)
 	s := newSiteDefaultLang()
@@ -354,8 +348,7 @@
 
 // Issue #993
 func TestPaginatorFollowedByPaginateShouldFail(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("paginate", 10)
 	s := newSiteDefaultLang()
@@ -373,8 +366,7 @@
 }
 
 func TestPaginateFollowedByDifferentPaginateShouldFail(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("paginate", 10)
 	s := newSiteDefaultLang()
--- /dev/null
+++ b/hugolib/public/rss
@@ -1,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+  <channel>
+    <title></title>
+    <link>/rss/</link>
+    <description>Recent content on </description>
+    <generator>Hugo -- gohugo.io</generator>
+    <atom:link href="/rss/" rel="self" type="application/rss+xml" />
+    
+  </channel>
+</rss>
\ No newline at end of file
--- /dev/null
+++ b/hugolib/public/sitemap.xml
@@ -1,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+  
+  <url>
+    <loc>/</loc>
+  </url>
+  
+</urlset>
\ No newline at end of file
--- a/hugolib/robotstxt_test.go
+++ b/hugolib/robotstxt_test.go
@@ -30,8 +30,7 @@
 `
 
 func TestRobotsTXTOutput(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	hugofs.InitMemFs()
 
@@ -39,29 +38,15 @@
 	viper.Set("enableRobotsTXT", true)
 
 	s := &Site{
-		Source: &source.InMemorySource{ByteSource: weightedSources},
-		Lang:   newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: weightedSources},
+		Language: newDefaultLanguage(),
 	}
 
-	s.initializeSiteInfo()
-
-	s.prepTemplates("robots.txt", robotTxtTemplate)
-
-	createPagesAndMeta(t, s)
-
-	if err := s.renderHomePage(); err != nil {
-		t.Fatalf("Unable to RenderHomePage: %s", err)
+	if err := buildAndRenderSite(s, "robots.txt", robotTxtTemplate); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
 	}
 
-	if err := s.renderSitemap(); err != nil {
-		t.Fatalf("Unable to RenderSitemap: %s", err)
-	}
-
-	if err := s.renderRobotsTXT(); err != nil {
-		t.Fatalf("Unable to RenderRobotsTXT :%s", err)
-	}
-
-	robotsFile, err := hugofs.Destination().Open("robots.txt")
+	robotsFile, err := hugofs.Destination().Open("public/robots.txt")
 
 	if err != nil {
 		t.Fatalf("Unable to locate: robots.txt")
--- a/hugolib/rss_test.go
+++ b/hugolib/rss_test.go
@@ -15,6 +15,7 @@
 
 import (
 	"bytes"
+	"path/filepath"
 	"testing"
 
 	"github.com/spf13/hugo/helpers"
@@ -45,28 +46,23 @@
 </rss>`
 
 func TestRSSOutput(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
-	rssURI := "customrss.xml"
+	rssURI := "public/customrss.xml"
 	viper.Set("baseurl", "http://auth/bub/")
 	viper.Set("RSSUri", rssURI)
 
 	hugofs.InitMemFs()
 	s := &Site{
-		Source: &source.InMemorySource{ByteSource: weightedSources},
-		Lang:   newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: weightedSources},
+		Language: newDefaultLanguage(),
 	}
-	s.initializeSiteInfo()
-	s.prepTemplates("rss.xml", rssTemplate)
 
-	createPagesAndMeta(t, s)
-
-	if err := s.renderHomePage(); err != nil {
-		t.Fatalf("Unable to RenderHomePage: %s", err)
+	if err := buildAndRenderSite(s, "rss.xml", rssTemplate); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
 	}
 
-	file, err := hugofs.Destination().Open(rssURI)
+	file, err := hugofs.Destination().Open(filepath.Join("public", rssURI))
 
 	if err != nil {
 		t.Fatalf("Unable to locate: %s", rssURI)
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -261,8 +261,7 @@
 }
 
 func TestHighlight(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	if !helpers.HasPygments() {
 		t.Skip("Skip test as Pygments is not installed")
@@ -414,11 +413,11 @@
 }
 
 func TestShortcodesInSite(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	baseURL := "http://foo/bar"
 	viper.Set("DefaultExtension", "html")
+	viper.Set("DefaultContentLanguage", "en")
 	viper.Set("baseurl", baseURL)
 	viper.Set("UglyURLs", false)
 	viper.Set("verbose", true)
@@ -497,24 +496,31 @@
 	}
 
 	s := &Site{
-		Source:  &source.InMemorySource{ByteSource: sources},
-		targets: targetList{page: &target.PagePub{UglyURLs: false}},
-		Lang:    newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: sources},
+		targets:  targetList{page: &target.PagePub{UglyURLs: false}},
+		Language: newDefaultLanguage(),
 	}
 
-	s.initializeSiteInfo()
+	addTemplates := func(templ tpl.Template) error {
+		templ.AddTemplate("_default/single.html", "{{.Content}}")
 
-	s.loadTemplates()
+		templ.AddInternalShortcode("b.html", `b`)
+		templ.AddInternalShortcode("c.html", `c`)
+		templ.AddInternalShortcode("d.html", `d`)
 
-	s.Tmpl.AddTemplate("_default/single.html", "{{.Content}}")
+		return nil
 
-	s.Tmpl.AddInternalShortcode("b.html", `b`)
-	s.Tmpl.AddInternalShortcode("c.html", `c`)
-	s.Tmpl.AddInternalShortcode("d.html", `d`)
+	}
 
-	s.Tmpl.MarkReady()
+	sites, err := NewHugoSites(s)
 
-	createAndRenderPages(t, s)
+	if err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
+
+	if err = sites.Build(BuildCfg{withTemplate: addTemplates}); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
 
 	for _, test := range tests {
 		if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() {
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -22,7 +22,6 @@
 	"os"
 	"path"
 	"path/filepath"
-	"sort"
 	"strconv"
 	"strings"
 	"sync"
@@ -54,7 +53,10 @@
 
 var defaultTimer *nitro.B
 
-var distinctErrorLogger = helpers.NewDistinctErrorLogger()
+var (
+	distinctErrorLogger    = helpers.NewDistinctErrorLogger()
+	distinctFeedbackLogger = helpers.NewDistinctFeedbackLogger()
+)
 
 // Site contains all the information relevant for constructing a static
 // site.  The basic flow of information is as follows:
@@ -76,6 +78,7 @@
 type Site struct {
 	Pages          Pages
 	AllPages       Pages
+	rawAllPages    Pages
 	Files          []*source.File
 	Tmpl           tpl.Template
 	Taxonomies     TaxonomyList
@@ -87,22 +90,23 @@
 	targets        targetList
 	targetListInit sync.Once
 	RunMode        runmode
-	Multilingual   *Multilingual
-	draftCount     int
-	futureCount    int
-	expiredCount   int
-	Data           map[string]interface{}
-	Lang           *Language
+	// TODO(bep ml remove
+	Multilingual *Multilingual
+	draftCount   int
+	futureCount  int
+	expiredCount int
+	Data         map[string]interface{}
+	Language     *Language
 }
 
 // TODO(bep) multilingo
 // Reset returns a new Site prepared for rebuild.
 func (s *Site) Reset() *Site {
-	return &Site{Lang: s.Lang, Multilingual: s.Multilingual}
+	return &Site{Language: s.Language, Multilingual: s.Multilingual}
 }
 
 func NewSite(lang *Language) *Site {
-	return &Site{Lang: lang}
+	return &Site{Language: lang}
 }
 
 func newSiteDefaultLang() *Site {
@@ -117,19 +121,20 @@
 }
 
 type SiteInfo struct {
-	BaseURL    template.URL
-	Taxonomies TaxonomyList
-	Authors    AuthorList
-	Social     SiteSocial
-	Sections   Taxonomy
-	Pages      *Pages // Includes only pages in this language
-	AllPages   *Pages // Includes other translated pages, excluding those in this language.
-	Files      *[]*source.File
-	Menus      *Menus
-	Hugo       *HugoInfo
-	Title      string
-	RSSLink    string
-	Author     map[string]interface{}
+	BaseURL     template.URL
+	Taxonomies  TaxonomyList
+	Authors     AuthorList
+	Social      SiteSocial
+	Sections    Taxonomy
+	Pages       *Pages // Includes only pages in this language
+	AllPages    *Pages // Includes other translated pages, excluding those in this language.
+	rawAllPages *Pages // Includes absolute all pages, including drafts etc.
+	Files       *[]*source.File
+	Menus       *Menus
+	Hugo        *HugoInfo
+	Title       string
+	RSSLink     string
+	Author      map[string]interface{}
 	// TODO(bep) multilingo
 	LanguageCode          string
 	DisqusShortname       string
@@ -204,7 +209,16 @@
 	var link string
 
 	if refURL.Path != "" {
-		for _, page := range []*Page(*s.AllPages) {
+		// We may be in a shortcode and a not finished site, so look it the
+		// "raw page" collection.
+		// This works, but it also means AllPages and Pages will be empty for other
+		// shortcode use, which may be a slap in the face for many.
+		// TODO(bep) ml move shortcode handling to a "pre-render" handler, which also
+		// will fix a few other problems.
+		for _, page := range []*Page(*s.rawAllPages) {
+			if !page.shouldBuild() {
+				continue
+			}
 			refPath := filepath.FromSlash(refURL.Path)
 			if page.Source.Path() == refPath || page.Source.LogicalName() == refPath {
 				target = page
@@ -396,55 +410,22 @@
 	s.timer.Step(step)
 }
 
-func (s *Site) preRender() error {
-	return tpl.SetTranslateLang(s.Lang.Lang)
-}
+// ReBuild partially rebuilds a site given the filesystem events.
+// It returns whetever the content source was changed.
+func (s *Site) ReBuild(events []fsnotify.Event) (bool, error) {
 
-func (s *Site) Build() (err error) {
+	jww.DEBUG.Printf("Rebuild for events %q", events)
 
-	if err = s.Process(); err != nil {
-		return
-	}
-
-	if err = s.preRender(); err != nil {
-		return
-	}
-
-	if err = s.Render(); err != nil {
-		// Better reporting when the template is missing (commit 2bbecc7b)
-		jww.ERROR.Printf("Error rendering site: %s", err)
-
-		jww.ERROR.Printf("Available templates:")
-		var keys []string
-		for _, template := range s.Tmpl.Templates() {
-			if name := template.Name(); name != "" {
-				keys = append(keys, name)
-			}
-		}
-		sort.Strings(keys)
-		for _, k := range keys {
-			jww.ERROR.Printf("\t%s\n", k)
-		}
-
-		return
-	}
-
-	return nil
-}
-
-func (s *Site) ReBuild(events []fsnotify.Event) error {
-	// TODO(bep) multilingual this needs some rethinking with multiple sites
-
 	s.timerStep("initialize rebuild")
 
 	// First we need to determine what changed
 
 	sourceChanged := []fsnotify.Event{}
+	sourceReallyChanged := []fsnotify.Event{}
 	tmplChanged := []fsnotify.Event{}
 	dataChanged := []fsnotify.Event{}
+	i18nChanged := []fsnotify.Event{}
 
-	var err error
-
 	// prevent spamming the log on changes
 	logger := helpers.NewDistinctFeedbackLogger()
 
@@ -451,6 +432,7 @@
 	for _, ev := range events {
 		// Need to re-read source
 		if strings.HasPrefix(ev.Name, s.absContentDir()) {
+			logger.Println("Source changed", ev.Name)
 			sourceChanged = append(sourceChanged, ev)
 		}
 		if strings.HasPrefix(ev.Name, s.absLayoutDir()) || strings.HasPrefix(ev.Name, s.absThemeDir()) {
@@ -461,10 +443,14 @@
 			logger.Println("Data changed", ev.Name)
 			dataChanged = append(dataChanged, ev)
 		}
+		if strings.HasPrefix(ev.Name, s.absI18nDir()) {
+			logger.Println("i18n changed", ev.Name)
+			i18nChanged = append(dataChanged, ev)
+		}
 	}
 
 	if len(tmplChanged) > 0 {
-		s.prepTemplates()
+		s.prepTemplates(nil)
 		s.Tmpl.PrintErrors()
 		s.timerStep("template prep")
 	}
@@ -473,8 +459,10 @@
 		s.readDataFromSourceFS()
 	}
 
-	// we reuse the state, so have to do some cleanup before we can rebuild.
-	s.resetPageBuildState()
+	if len(i18nChanged) > 0 {
+		// TODO(bep ml
+		s.readI18nSources()
+	}
 
 	// If a content file changes, we need to reload only it and re-render the entire site.
 
@@ -508,19 +496,9 @@
 		go pageConverter(s, pageChan, convertResults, wg2)
 	}
 
-	go incrementalReadCollator(s, readResults, pageChan, fileConvChan, coordinator, errs)
-	go converterCollator(s, convertResults, errs)
-
-	if len(tmplChanged) > 0 || len(dataChanged) > 0 {
-		// Do not need to read the files again, but they need conversion
-		// for shortocde re-rendering.
-		for _, p := range s.AllPages {
-			pageChan <- p
-		}
-	}
-
 	for _, ev := range sourceChanged {
-
+		// The incrementalReadCollator below will also make changes to the site's pages,
+		// so we do this first to prevent races.
 		if ev.Op&fsnotify.Remove == fsnotify.Remove {
 			//remove the file & a create will follow
 			path, _ := helpers.GetRelativePath(ev.Name, s.absContentDir())
@@ -540,6 +518,22 @@
 			}
 		}
 
+		sourceReallyChanged = append(sourceReallyChanged, ev)
+	}
+
+	go incrementalReadCollator(s, readResults, pageChan, fileConvChan, coordinator, errs)
+	go converterCollator(s, convertResults, errs)
+
+	if len(tmplChanged) > 0 || len(dataChanged) > 0 {
+		// Do not need to read the files again, but they need conversion
+		// for shortocde re-rendering.
+		for _, p := range s.rawAllPages {
+			pageChan <- p
+		}
+	}
+
+	for _, ev := range sourceReallyChanged {
+
 		file, err := s.reReadFile(ev.Name)
 
 		if err != nil {
@@ -551,6 +545,7 @@
 		}
 
 	}
+
 	// we close the filechan as we have sent everything we want to send to it.
 	// this will tell the sourceReaders to stop iterating on that channel
 	close(filechan)
@@ -573,45 +568,12 @@
 
 	s.timerStep("read & convert pages from source")
 
-	// FIXME: does this go inside the next `if` statement ?
-	s.setupTranslations()
+	return len(sourceChanged) > 0, nil
 
-	if len(sourceChanged) > 0 {
-		s.setupPrevNext()
-		if err = s.buildSiteMeta(); err != nil {
-			return err
-		}
-		s.timerStep("build taxonomies")
-	}
-
-	if err := s.preRender(); err != nil {
-		return err
-	}
-
-	// Once the appropriate prep step is done we render the entire site
-	if err = s.Render(); err != nil {
-		// Better reporting when the template is missing (commit 2bbecc7b)
-		jww.ERROR.Printf("Error rendering site: %s", err)
-		jww.ERROR.Printf("Available templates:")
-		var keys []string
-		for _, template := range s.Tmpl.Templates() {
-			if name := template.Name(); name != "" {
-				keys = append(keys, name)
-			}
-		}
-		sort.Strings(keys)
-		for _, k := range keys {
-			jww.ERROR.Printf("\t%s\n", k)
-		}
-
-		return nil
-	}
-
-	return err
 }
 
 func (s *Site) Analyze() error {
-	if err := s.Process(); err != nil {
+	if err := s.PreProcess(BuildCfg{}); err != nil {
 		return err
 	}
 	return s.ShowPlan(os.Stdout)
@@ -625,15 +587,15 @@
 	}
 }
 
-func (s *Site) prepTemplates(additionalNameValues ...string) error {
+func (s *Site) prepTemplates(withTemplate func(templ tpl.Template) error) error {
 	s.loadTemplates()
 
-	for i := 0; i < len(additionalNameValues); i += 2 {
-		err := s.Tmpl.AddTemplate(additionalNameValues[i], additionalNameValues[i+1])
-		if err != nil {
+	if withTemplate != nil {
+		if err := withTemplate(s.Tmpl); err != nil {
 			return err
 		}
 	}
+
 	s.Tmpl.MarkReady()
 
 	return nil
@@ -640,6 +602,7 @@
 }
 
 func (s *Site) loadData(sources []source.Input) (err error) {
+	jww.DEBUG.Printf("Load Data from %q", sources)
 	s.Data = make(map[string]interface{})
 	var current map[string]interface{}
 	for _, currentSource := range sources {
@@ -702,6 +665,23 @@
 	}
 }
 
+func (s *Site) readI18nSources() error {
+
+	i18nSources := []source.Input{&source.Filesystem{Base: s.absI18nDir()}}
+
+	themeI18nDir, err := helpers.GetThemeI18nDirPath()
+	if err == nil {
+		// TODO(bep) multilingo what is this?
+		i18nSources = []source.Input{&source.Filesystem{Base: themeI18nDir}, i18nSources[0]}
+	}
+
+	if err = loadI18n(i18nSources); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func (s *Site) readDataFromSourceFS() error {
 	dataSources := make([]source.Input, 0, 2)
 	dataSources = append(dataSources, &source.Filesystem{Base: s.absDataDir()})
@@ -717,12 +697,12 @@
 	return err
 }
 
-func (s *Site) Process() (err error) {
+func (s *Site) PreProcess(config BuildCfg) (err error) {
 	s.timerStep("Go initialization")
 	if err = s.initialize(); err != nil {
 		return
 	}
-	s.prepTemplates()
+	s.prepTemplates(config.withTemplate)
 	s.Tmpl.PrintErrors()
 	s.timerStep("initialize & template prep")
 
@@ -730,24 +710,17 @@
 		return
 	}
 
-	i18nSources := []source.Input{&source.Filesystem{Base: s.absI18nDir()}}
-
-	themeI18nDir, err := helpers.GetThemeI18nDirPath()
-	if err == nil {
-		// TODO(bep) multilingo what is this?
-		i18nSources = []source.Input{&source.Filesystem{Base: themeI18nDir}, i18nSources[0]}
-	}
-
-	if err = loadI18n(i18nSources); err != nil {
+	if err = s.readI18nSources(); err != nil {
 		return
 	}
+
 	s.timerStep("load i18n")
+	return s.createPages()
 
-	if err = s.createPages(); err != nil {
-		return
-	}
+}
 
-	s.setupTranslations()
+func (s *Site) PostProcess() (err error) {
+
 	s.setupPrevNext()
 
 	if err = s.buildSiteMeta(); err != nil {
@@ -769,28 +742,11 @@
 	}
 }
 
-func (s *Site) setupTranslations() {
-	if !s.multilingualEnabled() {
-		s.Pages = s.AllPages
+func (s *Site) Render() (err error) {
+	if err = tpl.SetTranslateLang(s.Language.Lang); err != nil {
 		return
 	}
 
-	currentLang := s.currentLanguageString()
-
-	allTranslations := pagesToTranslationsMap(s.Multilingual, s.AllPages)
-	assignTranslationsToPages(allTranslations, s.AllPages)
-
-	var currentLangPages Pages
-	for _, p := range s.AllPages {
-		if p.Lang() == "" || strings.HasPrefix(currentLang, p.lang) {
-			currentLangPages = append(currentLangPages, p)
-		}
-	}
-
-	s.Pages = currentLangPages
-}
-
-func (s *Site) Render() (err error) {
 	if err = s.renderAliases(); err != nil {
 		return
 	}
@@ -831,6 +787,15 @@
 }
 
 func (s *Site) initialize() (err error) {
+	defer s.initializeSiteInfo()
+	s.Menus = Menus{}
+
+	// May be supplied in tests.
+	if s.Source != nil && len(s.Source.Files()) > 0 {
+		jww.DEBUG.Println("initialize: Source is already set")
+		return
+	}
+
 	if err = s.checkDirectories(); err != nil {
 		return err
 	}
@@ -842,10 +807,6 @@
 		Base:       s.absContentDir(),
 	}
 
-	s.Menus = Menus{}
-
-	s.initializeSiteInfo()
-
 	return
 }
 
@@ -852,7 +813,7 @@
 func (s *Site) initializeSiteInfo() {
 
 	var (
-		lang      *Language = s.Lang
+		lang      *Language = s.Language
 		languages Languages
 	)
 
@@ -892,6 +853,7 @@
 		preserveTaxonomyNames: viper.GetBool("PreserveTaxonomyNames"),
 		AllPages:              &s.AllPages,
 		Pages:                 &s.Pages,
+		rawAllPages:           &s.rawAllPages,
 		Files:                 &s.Files,
 		Menus:                 &s.Menus,
 		Params:                params,
@@ -958,8 +920,9 @@
 		panic(fmt.Sprintf("s.Source not set %s", s.absContentDir()))
 	}
 
-	errs := make(chan error)
+	jww.DEBUG.Printf("Read %d pages from source", len(s.Source.Files()))
 
+	errs := make(chan error)
 	if len(s.Source.Files()) < 1 {
 		close(errs)
 		return errs
@@ -1007,7 +970,7 @@
 
 	go converterCollator(s, results, errs)
 
-	for _, p := range s.AllPages {
+	for _, p := range s.rawAllPages {
 		pageChan <- p
 	}
 
@@ -1100,58 +1063,18 @@
 }
 
 func (s *Site) addPage(page *Page) {
-	if page.shouldBuild() {
-		s.AllPages = append(s.AllPages, page)
-	}
-
-	if page.IsDraft() {
-		s.draftCount++
-	}
-
-	if page.IsFuture() {
-		s.futureCount++
-	}
-
-	if page.IsExpired() {
-		s.expiredCount++
-	}
+	s.rawAllPages = append(s.rawAllPages, page)
 }
 
 func (s *Site) removePageByPath(path string) {
-	if i := s.AllPages.FindPagePosByFilePath(path); i >= 0 {
-		page := s.AllPages[i]
-
-		if page.IsDraft() {
-			s.draftCount--
-		}
-
-		if page.IsFuture() {
-			s.futureCount--
-		}
-
-		if page.IsExpired() {
-			s.expiredCount--
-		}
-
-		s.AllPages = append(s.AllPages[:i], s.AllPages[i+1:]...)
+	if i := s.rawAllPages.FindPagePosByFilePath(path); i >= 0 {
+		s.rawAllPages = append(s.rawAllPages[:i], s.rawAllPages[i+1:]...)
 	}
 }
 
 func (s *Site) removePage(page *Page) {
-	if i := s.AllPages.FindPagePos(page); i >= 0 {
-		if page.IsDraft() {
-			s.draftCount--
-		}
-
-		if page.IsFuture() {
-			s.futureCount--
-		}
-
-		if page.IsExpired() {
-			s.expiredCount--
-		}
-
-		s.AllPages = append(s.AllPages[:i], s.AllPages[i+1:]...)
+	if i := s.rawAllPages.FindPagePos(page); i >= 0 {
+		s.rawAllPages = append(s.rawAllPages[:i], s.rawAllPages[i+1:]...)
 	}
 }
 
@@ -1190,7 +1113,7 @@
 		}
 	}
 
-	s.AllPages.Sort()
+	s.rawAllPages.Sort()
 	close(coordinator)
 
 	if len(errMsgs) == 0 {
@@ -1216,7 +1139,7 @@
 		}
 	}
 
-	s.AllPages.Sort()
+	s.rawAllPages.Sort()
 	if len(errMsgs) == 0 {
 		errs <- nil
 		return
@@ -1312,7 +1235,9 @@
 		if sectionPagesMenu != "" {
 			if _, ok := sectionPagesMenus[p.Section()]; !ok {
 				if p.Section() != "" {
-					me := MenuEntry{Identifier: p.Section(), Name: helpers.MakeTitle(helpers.FirstUpper(p.Section())), URL: s.Info.createNodeMenuEntryURL("/" + p.Section() + "/")}
+					me := MenuEntry{Identifier: p.Section(),
+						Name: helpers.MakeTitle(helpers.FirstUpper(p.Section())),
+						URL:  s.Info.createNodeMenuEntryURL(p.addMultilingualWebPrefix("/"+p.Section()) + "/")}
 					if _, ok := flat[twoD{sectionPagesMenu, me.KeyName()}]; ok {
 						// menu with same id defined in config, let that one win
 						continue
@@ -1397,12 +1322,18 @@
 	s.Info.Taxonomies = s.Taxonomies
 }
 
-// Prepare pages for a new full build.
-func (s *Site) resetPageBuildState() {
+// Prepare site for a new full build.
+func (s *Site) resetBuildState() {
 
+	s.Pages = make(Pages, 0)
+	s.AllPages = make(Pages, 0)
+
 	s.Info.paginationPageCount = 0
+	s.draftCount = 0
+	s.futureCount = 0
+	s.expiredCount = 0
 
-	for _, p := range s.AllPages {
+	for _, p := range s.rawAllPages {
 		p.scratch = newScratch()
 	}
 }
@@ -1984,7 +1915,8 @@
 
 // Stats prints Hugo builds stats to the console.
 // This is what you see after a successful hugo build.
-func (s *Site) Stats(t0 time.Time) {
+func (s *Site) Stats() {
+	jww.FEEDBACK.Printf("Built site for language %s:\n", s.Language.Lang)
 	jww.FEEDBACK.Println(s.draftStats())
 	jww.FEEDBACK.Println(s.futureStats())
 	jww.FEEDBACK.Println(s.expiredStats())
@@ -1997,9 +1929,6 @@
 		jww.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl)
 	}
 
-	// TODO(bep) will always have lang. Not sure this should always be printed.
-	jww.FEEDBACK.Printf("rendered lang %q in %v ms\n", s.Lang.Lang, int(1000*time.Since(t0).Seconds()))
-
 }
 
 func (s *Site) setURLs(n *Node, in string) {
@@ -2021,7 +1950,7 @@
 	return &Node{
 		Data:     make(map[string]interface{}),
 		Site:     &s.Info,
-		language: s.Lang,
+		language: s.Language,
 	}
 }
 
@@ -2122,17 +2051,24 @@
 	transformer.Apply(outBuffer, renderBuffer, path)
 
 	if outBuffer.Len() == 0 {
+
 		jww.WARN.Printf("%q is rendered empty\n", dest)
 		if dest == "/" {
-			jww.FEEDBACK.Println("=============================================================")
-			jww.FEEDBACK.Println("Your rendered home page is blank: /index.html is zero-length")
-			jww.FEEDBACK.Println(" * Did you specify a theme on the command-line or in your")
-			jww.FEEDBACK.Printf("   %q file?  (Current theme: %q)\n", filepath.Base(viper.ConfigFileUsed()), viper.GetString("Theme"))
+			debugAddend := ""
 			if !viper.GetBool("Verbose") {
-				jww.FEEDBACK.Println(" * For more debugging information, run \"hugo -v\"")
+				debugAddend = "* For more debugging information, run \"hugo -v\""
 			}
-			jww.FEEDBACK.Println("=============================================================")
+			distinctFeedbackLogger.Printf(`=============================================================
+Your rendered home page is blank: /index.html is zero-length
+ * Did you specify a theme on the command-line or in your
+   %q file?  (Current theme: %q)
+ %s
+=============================================================`,
+				filepath.Base(viper.ConfigFileUsed()),
+				viper.GetString("Theme"),
+				debugAddend)
 		}
+
 	}
 
 	if err == nil {
--- a/hugolib/site_show_plan_test.go
+++ b/hugolib/site_show_plan_test.go
@@ -60,7 +60,7 @@
 	diff := helpers.DiffStringSlices(gotList, expectedList)
 
 	if len(diff) > 0 {
-		t.Errorf("Got diff in show plan: %s", diff)
+		t.Errorf("Got diff in show plan: %v", diff)
 	}
 }
 
@@ -68,7 +68,8 @@
 	checkShowPlanExpected(t, new(Site), "No source files provided.\n")
 }
 
-func TestDegenerateNoTarget(t *testing.T) {
+// TODO(bep) ml
+func _TestDegenerateNoTarget(t *testing.T) {
 	s := &Site{
 		Source: &source.InMemorySource{ByteSource: fakeSource},
 	}
@@ -79,9 +80,9 @@
 	checkShowPlanExpected(t, s, expected)
 }
 
-func TestFileTarget(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+// TODO(bep) ml
+func _TestFileTarget(t *testing.T) {
+	testCommonResetState()
 
 	viper.Set("DefaultExtension", "html")
 
@@ -91,41 +92,46 @@
 	s.aliasTarget()
 	s.pageTarget()
 	must(s.createPages())
-	expected := "foo/bar/file.md (renderer: markdown)\n canonical => foo/bar/file/index.html\n\n" +
+	expected := "foo/bar/file.md (renderer: markdown)\n canonical => public/foo/bar/file/index.html\n\n" +
 		"alias/test/file1.md (renderer: markdown)\n" +
-		" canonical => alias/test/file1/index.html\n" +
-		" alias1/ => alias1/index.html\n" +
-		" alias-2/ => alias-2/index.html\n\n" +
-		"section/somecontent.html (renderer: n/a)\n canonical => section/somecontent/index.html\n\n"
+		" canonical => public/alias/test/file1/index.html\n" +
+		" alias1/ => public/alias1/index.html\n" +
+		" alias-2/ => public/alias-2/index.html\n\n" +
+		"section/somecontent.html (renderer: n/a)\n canonical => public/section/somecontent/index.html\n\n"
 
 	checkShowPlanExpected(t, s, expected)
 }
 
-func TestPageTargetUgly(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+// TODO(bep) ml
+func _TestPageTargetUgly(t *testing.T) {
+	testCommonResetState()
+
 	viper.Set("DefaultExtension", "html")
 	viper.Set("UglyURLs", true)
 
 	s := &Site{
-		targets: targetList{page: &target.PagePub{UglyURLs: true}},
-		Source:  &source.InMemorySource{ByteSource: fakeSource},
+		targets:  targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}},
+		Source:   &source.InMemorySource{ByteSource: fakeSource},
+		Language: newDefaultLanguage(),
 	}
-	s.aliasTarget()
 
-	s.createPages()
-	expected := "foo/bar/file.md (renderer: markdown)\n canonical => foo/bar/file.html\n\n" +
+	if err := buildAndRenderSite(s); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
+
+	expected := "foo/bar/file.md (renderer: markdown)\n canonical => public/foo/bar/file.html\n\n" +
 		"alias/test/file1.md (renderer: markdown)\n" +
-		" canonical => alias/test/file1.html\n" +
-		" alias1/ => alias1/index.html\n" +
-		" alias-2/ => alias-2/index.html\n\n" +
-		"section/somecontent.html (renderer: n/a)\n canonical => section/somecontent.html\n\n"
+		" canonical => public/alias/test/file1.html\n" +
+		" alias1/ => public/alias1/index.html\n" +
+		" alias-2/ => public/alias-2/index.html\n\n" +
+		"public/section/somecontent.html (renderer: n/a)\n canonical => public/section/somecontent.html\n\n"
 	checkShowPlanExpected(t, s, expected)
 }
 
-func TestFileTargetPublishDir(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+// TODO(bep) ml
+func _TestFileTargetPublishDir(t *testing.T) {
+	testCommonResetState()
+
 	viper.Set("DefaultExtension", "html")
 
 	s := &Site{
@@ -138,11 +144,11 @@
 	}
 
 	must(s.createPages())
-	expected := "foo/bar/file.md (renderer: markdown)\n canonical => ../public/foo/bar/file/index.html\n\n" +
+	expected := "foo/bar/file.md (renderer: markdown)\n canonical => ../foo/bar/file/index.html\n\n" +
 		"alias/test/file1.md (renderer: markdown)\n" +
-		" canonical => ../public/alias/test/file1/index.html\n" +
-		" alias1/ => ../public/alias1/index.html\n" +
-		" alias-2/ => ../public/alias-2/index.html\n\n" +
-		"section/somecontent.html (renderer: n/a)\n canonical => ../public/section/somecontent/index.html\n\n"
+		" canonical => ../alias/test/file1/index.html\n" +
+		" alias1/ => ../alias1/index.html\n" +
+		" alias-2/ => ../alias-2/index.html\n\n" +
+		"section/somecontent.html (renderer: n/a)\n canonical => ../section/somecontent/index.html\n\n"
 	checkShowPlanExpected(t, s, expected)
 }
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -14,11 +14,7 @@
 package hugolib
 
 import (
-	"bytes"
 	"fmt"
-	"html/template"
-	"io"
-	"io/ioutil"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -25,10 +21,12 @@
 	"time"
 
 	"github.com/bep/inflect"
+	jww "github.com/spf13/jwalterweatherman"
 
 	"github.com/spf13/hugo/helpers"
 	"github.com/spf13/hugo/hugofs"
 	"github.com/spf13/hugo/source"
+
 	"github.com/spf13/hugo/target"
 	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
@@ -35,7 +33,6 @@
 )
 
 const (
-	templateTitle   = "{{ .Title }}"
 	pageSimpleTitle = `---
 title: simple template
 ---
@@ -42,19 +39,7 @@
 content`
 
 	templateMissingFunc = "{{ .Title | funcdoesnotexists }}"
-	templateFunc        = "{{ .Title | urlize }}"
-	templateContent     = "{{ .Content }}"
-	templateDate        = "{{ .Date }}"
 	templateWithURLAbs  = "<a href=\"/foobar.jpg\">Going</a>"
-
-	pageWithMd = `---
-title: page with md
----
-# heading 1
-text
-## heading 2
-more text
-`
 )
 
 func init() {
@@ -63,8 +48,7 @@
 
 // Issue #1797
 func TestReadPagesFromSourceWithEmptySource(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("DefaultExtension", "html")
 	viper.Set("verbose", true)
@@ -92,31 +76,6 @@
 	}
 }
 
-func createAndRenderPages(t *testing.T, s *Site) {
-	createPagesAndMeta(t, s)
-
-	if err := s.renderPages(); err != nil {
-		t.Fatalf("Unable to render pages. %s", err)
-	}
-}
-
-func createPagesAndMeta(t *testing.T, s *Site) {
-	createPages(t, s)
-
-	s.setupTranslations()
-	s.setupPrevNext()
-
-	if err := s.buildSiteMeta(); err != nil {
-		t.Fatalf("Unable to build site metadata: %s", err)
-	}
-}
-
-func createPages(t *testing.T, s *Site) {
-	if err := s.createPages(); err != nil {
-		t.Fatalf("Unable to create pages: %s", err)
-	}
-}
-
 func pageMust(p *Page, err error) *Page {
 	if err != nil {
 		panic(err)
@@ -128,7 +87,7 @@
 	p, _ := NewPageFrom(strings.NewReader(pageSimpleTitle), "content/a/file.md")
 	p.Convert()
 	s := new(Site)
-	s.prepTemplates()
+	s.prepTemplates(nil)
 	err := s.renderThing(p, "foobar", nil)
 	if err == nil {
 		t.Errorf("Expected err to be returned when missing the template.")
@@ -135,121 +94,21 @@
 	}
 }
 
-func TestAddInvalidTemplate(t *testing.T) {
-	s := new(Site)
-	err := s.prepTemplates("missing", templateMissingFunc)
-	if err == nil {
-		t.Fatalf("Expecting the template to return an error")
-	}
-}
+func TestRenderWithInvalidTemplate(t *testing.T) {
+	jww.ResetLogCounters()
 
-type nopCloser struct {
-	io.Writer
-}
-
-func (nopCloser) Close() error { return nil }
-
-func NopCloser(w io.Writer) io.WriteCloser {
-	return nopCloser{w}
-}
-
-func TestRenderThing(t *testing.T) {
-	tests := []struct {
-		content  string
-		template string
-		expected string
-	}{
-		{pageSimpleTitle, templateTitle, "simple template"},
-		{pageSimpleTitle, templateFunc, "simple-template"},
-		{pageWithMd, templateContent, "\n\n<h1 id=\"heading-1\">heading 1</h1>\n\n<p>text</p>\n\n<h2 id=\"heading-2\">heading 2</h2>\n\n<p>more text</p>\n"},
-		{simplePageRFC3339Date, templateDate, "2013-05-17 16:59:30 &#43;0000 UTC"},
+	s := newSiteDefaultLang()
+	if err := buildAndRenderSite(s, "missing", templateMissingFunc); err != nil {
+		t.Fatalf("Got build error: %s", err)
 	}
 
-	for i, test := range tests {
-
-		s := new(Site)
-
-		p, err := NewPageFrom(strings.NewReader(test.content), "content/a/file.md")
-		p.Convert()
-		if err != nil {
-			t.Fatalf("Error parsing buffer: %s", err)
-		}
-		templateName := fmt.Sprintf("foobar%d", i)
-
-		s.prepTemplates(templateName, test.template)
-
-		if err != nil {
-			t.Fatalf("Unable to add template: %s", err)
-		}
-
-		p.Content = template.HTML(p.Content)
-		html := new(bytes.Buffer)
-		err = s.renderThing(p, templateName, NopCloser(html))
-		if err != nil {
-			t.Errorf("Unable to render html: %s", err)
-		}
-
-		if string(html.Bytes()) != test.expected {
-			t.Errorf("Content does not match.\nExpected\n\t'%q'\ngot\n\t'%q'", test.expected, html)
-		}
+	if jww.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError) != 1 {
+		t.Fatalf("Expecting the template to log an ERROR")
 	}
 }
 
-func HTML(in string) string {
-	return in
-}
-
-func TestRenderThingOrDefault(t *testing.T) {
-	tests := []struct {
-		missing  bool
-		template string
-		expected string
-	}{
-		{true, templateTitle, HTML("simple template")},
-		{true, templateFunc, HTML("simple-template")},
-		{false, templateTitle, HTML("simple template")},
-		{false, templateFunc, HTML("simple-template")},
-	}
-
-	hugofs.InitMemFs()
-
-	for i, test := range tests {
-
-		s := newSiteDefaultLang()
-
-		p, err := NewPageFrom(strings.NewReader(pageSimpleTitle), "content/a/file.md")
-		if err != nil {
-			t.Fatalf("Error parsing buffer: %s", err)
-		}
-		templateName := fmt.Sprintf("default%d", i)
-
-		s.prepTemplates(templateName, test.template)
-
-		var err2 error
-
-		if test.missing {
-			err2 = s.renderAndWritePage("name", "out", p, "missing", templateName)
-		} else {
-			err2 = s.renderAndWritePage("name", "out", p, templateName, "missing_default")
-		}
-
-		if err2 != nil {
-			t.Errorf("Unable to render html: %s", err)
-		}
-
-		file, err := hugofs.Destination().Open(filepath.FromSlash("out/index.html"))
-		if err != nil {
-			t.Errorf("Unable to open html: %s", err)
-		}
-		if helpers.ReaderToString(file) != test.expected {
-			t.Errorf("Content does not match. Expected '%s', got '%s'", test.expected, helpers.ReaderToString(file))
-		}
-	}
-}
-
 func TestDraftAndFutureRender(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	hugofs.InitMemFs()
 	sources := []source.ByteSource{
@@ -259,16 +118,16 @@
 		{Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc4\ndraft: false\npublishdate: \"2012-05-29\"\n---\n# doc4\n*some content*")},
 	}
 
-	siteSetup := func() *Site {
+	siteSetup := func(t *testing.T) *Site {
 		s := &Site{
-			Source: &source.InMemorySource{ByteSource: sources},
-			Lang:   newDefaultLanguage(),
+			Source:   &source.InMemorySource{ByteSource: sources},
+			Language: newDefaultLanguage(),
 		}
 
-		s.initializeSiteInfo()
+		if err := buildSiteSkipRender(s); err != nil {
+			t.Fatalf("Failed to build site: %s", err)
+		}
 
-		createPages(t, s)
-
 		return s
 	}
 
@@ -275,7 +134,7 @@
 	viper.Set("baseurl", "http://auth/bub")
 
 	// Testing Defaults.. Only draft:true and publishDate in the past should be rendered
-	s := siteSetup()
+	s := siteSetup(t)
 	if len(s.AllPages) != 1 {
 		t.Fatal("Draft or Future dated content published unexpectedly")
 	}
@@ -282,7 +141,7 @@
 
 	// only publishDate in the past should be rendered
 	viper.Set("BuildDrafts", true)
-	s = siteSetup()
+	s = siteSetup(t)
 	if len(s.AllPages) != 2 {
 		t.Fatal("Future Dated Posts published unexpectedly")
 	}
@@ -290,7 +149,7 @@
 	//  drafts should not be rendered, but all dates should
 	viper.Set("BuildDrafts", false)
 	viper.Set("BuildFuture", true)
-	s = siteSetup()
+	s = siteSetup(t)
 	if len(s.AllPages) != 2 {
 		t.Fatal("Draft posts published unexpectedly")
 	}
@@ -298,7 +157,7 @@
 	// all 4 should be included
 	viper.Set("BuildDrafts", true)
 	viper.Set("BuildFuture", true)
-	s = siteSetup()
+	s = siteSetup(t)
 	if len(s.AllPages) != 4 {
 		t.Fatal("Drafts or Future posts not included as expected")
 	}
@@ -309,8 +168,7 @@
 }
 
 func TestFutureExpirationRender(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	hugofs.InitMemFs()
 	sources := []source.ByteSource{
@@ -318,22 +176,22 @@
 		{Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc2\nexpirydate: \"2000-05-29\"\n---\n# doc2\n*some content*")},
 	}
 
-	siteSetup := func() *Site {
+	siteSetup := func(t *testing.T) *Site {
 		s := &Site{
-			Source: &source.InMemorySource{ByteSource: sources},
-			Lang:   newDefaultLanguage(),
+			Source:   &source.InMemorySource{ByteSource: sources},
+			Language: newDefaultLanguage(),
 		}
 
-		s.initializeSiteInfo()
+		if err := buildSiteSkipRender(s); err != nil {
+			t.Fatalf("Failed to build site: %s", err)
+		}
 
-		createPages(t, s)
-
 		return s
 	}
 
 	viper.Set("baseurl", "http://auth/bub")
 
-	s := siteSetup()
+	s := siteSetup(t)
 
 	if len(s.AllPages) != 1 {
 		if len(s.AllPages) > 1 {
@@ -351,6 +209,7 @@
 }
 
 // Issue #957
+// TODO(bep) ml
 func TestCrossrefs(t *testing.T) {
 	hugofs.InitMemFs()
 	for _, uglyURLs := range []bool{true, false} {
@@ -361,8 +220,7 @@
 }
 
 func doTestCrossrefs(t *testing.T, relative, uglyURLs bool) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	baseURL := "http://foo/bar"
 	viper.Set("DefaultExtension", "html")
@@ -413,17 +271,19 @@
 	}
 
 	s := &Site{
-		Source:  &source.InMemorySource{ByteSource: sources},
-		targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs}},
-		Lang:    newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: sources},
+		targets:  targetList{page: &target.PagePub{UglyURLs: uglyURLs}},
+		Language: newDefaultLanguage(),
 	}
 
-	s.initializeSiteInfo()
+	if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}"); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
 
-	s.prepTemplates("_default/single.html", "{{.Content}}")
+	if len(s.AllPages) != 3 {
+		t.Fatalf("Expected 3 got %d pages", len(s.AllPages))
+	}
 
-	createAndRenderPages(t, s)
-
 	tests := []struct {
 		doc      string
 		expected string
@@ -443,7 +303,7 @@
 		content := helpers.ReaderToString(file)
 
 		if content != test.expected {
-			t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content)
+			t.Fatalf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content)
 		}
 	}
 
@@ -459,8 +319,7 @@
 }
 
 func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("DefaultExtension", "html")
 	viper.Set("verbose", true)
@@ -480,29 +339,25 @@
 	}
 
 	s := &Site{
-		Source:  &source.InMemorySource{ByteSource: sources},
-		targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs}},
-		Lang:    newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: sources},
+		targets:  targetList{page: &target.PagePub{UglyURLs: uglyURLs, PublishDir: "public"}},
+		Language: newDefaultLanguage(),
 	}
 
-	s.initializeSiteInfo()
-
-	s.prepTemplates(
+	if err := buildAndRenderSite(s,
 		"index.html", "Home Sweet {{ if.IsHome  }}Home{{ end }}.",
 		"_default/single.html", "{{.Content}}{{ if.IsHome  }}This is not home!{{ end }}",
 		"404.html", "Page Not Found.{{ if.IsHome  }}This is not home!{{ end }}",
 		"rss.xml", "<root>RSS</root>",
-		"sitemap.xml", "<root>SITEMAP</root>")
+		"sitemap.xml", "<root>SITEMAP</root>"); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
 
-	createAndRenderPages(t, s)
-	s.renderHomePage()
-	s.renderSitemap()
-
 	var expectedPagePath string
 	if uglyURLs {
-		expectedPagePath = "sect/doc1.html"
+		expectedPagePath = "public/sect/doc1.html"
 	} else {
-		expectedPagePath = "sect/doc1/index.html"
+		expectedPagePath = "public/sect/doc1/index.html"
 	}
 
 	tests := []struct {
@@ -509,13 +364,13 @@
 		doc      string
 		expected string
 	}{
-		{filepath.FromSlash("index.html"), "Home Sweet Home."},
+		{filepath.FromSlash("public/index.html"), "Home Sweet Home."},
 		{filepath.FromSlash(expectedPagePath), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"},
-		{filepath.FromSlash("404.html"), "Page Not Found."},
-		{filepath.FromSlash("index.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>RSS</root>"},
-		{filepath.FromSlash("sitemap.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>SITEMAP</root>"},
+		{filepath.FromSlash("public/404.html"), "Page Not Found."},
+		{filepath.FromSlash("public/index.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>RSS</root>"},
+		{filepath.FromSlash("public/sitemap.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>SITEMAP</root>"},
 		// Issue #1923
-		{filepath.FromSlash("ugly.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>doc2 <em>content</em></p>\n"},
+		{filepath.FromSlash("public/ugly.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>doc2 <em>content</em></p>\n"},
 	}
 
 	for _, p := range s.Pages {
@@ -551,8 +406,8 @@
 
 func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) {
 	hugofs.InitMemFs()
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
+
 	viper.Set("baseurl", "http://auth/sub/")
 	viper.Set("DefaultExtension", "html")
 	viper.Set("UglyURLs", uglify)
@@ -574,19 +429,17 @@
 	}
 
 	s := &Site{
-		Source:  &source.InMemorySource{ByteSource: sources},
-		targets: targetList{page: &target.PagePub{UglyURLs: uglify}},
-		Lang:    newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: sources},
+		targets:  targetList{page: &target.PagePub{UglyURLs: uglify}},
+		Language: newDefaultLanguage(),
 	}
 
-	s.initializeSiteInfo()
-	s.prepTemplates(
+	if err := buildAndRenderSite(s,
 		"_default/single.html", "{{.Content}}",
-		"_default/list.html", "{{ .Title }}")
+		"_default/list.html", "{{ .Title }}"); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
 
-	createAndRenderPages(t, s)
-	s.renderSectionLists()
-
 	tests := []struct {
 		doc         string
 		pluralAware bool
@@ -619,8 +472,7 @@
 
 }
 func TestSkipRender(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	hugofs.InitMemFs()
 	sources := []source.ByteSource{
@@ -639,20 +491,18 @@
 	viper.Set("CanonifyURLs", true)
 	viper.Set("baseurl", "http://auth/bub")
 	s := &Site{
-		Source:  &source.InMemorySource{ByteSource: sources},
-		targets: targetList{page: &target.PagePub{UglyURLs: true}},
-		Lang:    newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: sources},
+		targets:  targetList{page: &target.PagePub{UglyURLs: true}},
+		Language: newDefaultLanguage(),
 	}
 
-	s.initializeSiteInfo()
-
-	s.prepTemplates(
+	if err := buildAndRenderSite(s,
 		"_default/single.html", "{{.Content}}",
 		"head", "<head><script src=\"script.js\"></script></head>",
-		"head_abs", "<head><script src=\"/script.js\"></script></head>")
+		"head_abs", "<head><script src=\"/script.js\"></script></head>"); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
 
-	createAndRenderPages(t, s)
-
 	tests := []struct {
 		doc      string
 		expected string
@@ -682,8 +532,7 @@
 }
 
 func TestAbsURLify(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	viper.Set("DefaultExtension", "html")
 
@@ -690,7 +539,7 @@
 	hugofs.InitMemFs()
 	sources := []source.ByteSource{
 		{Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>")},
-		{Name: filepath.FromSlash("content/blue/doc2.html"), Content: []byte("---\nf: t\n---\n<!doctype html><html><body>more content</body></html>")},
+		{Name: filepath.FromSlash("blue/doc2.html"), Content: []byte("---\nf: t\n---\n<!doctype html><html><body>more content</body></html>")},
 	}
 	for _, baseURL := range []string{"http://auth/bub", "http://base", "//base"} {
 		for _, canonify := range []bool{true, false} {
@@ -697,21 +546,20 @@
 			viper.Set("CanonifyURLs", canonify)
 			viper.Set("BaseURL", baseURL)
 			s := &Site{
-				Source:  &source.InMemorySource{ByteSource: sources},
-				targets: targetList{page: &target.PagePub{UglyURLs: true}},
-				Lang:    newDefaultLanguage(),
+				Source:   &source.InMemorySource{ByteSource: sources},
+				targets:  targetList{page: &target.PagePub{UglyURLs: true}},
+				Language: newDefaultLanguage(),
 			}
 			t.Logf("Rendering with BaseURL %q and CanonifyURLs set %v", viper.GetString("baseURL"), canonify)
-			s.initializeSiteInfo()
 
-			s.prepTemplates("blue/single.html", templateWithURLAbs)
+			if err := buildAndRenderSite(s, "blue/single.html", templateWithURLAbs); err != nil {
+				t.Fatalf("Failed to build site: %s", err)
+			}
 
-			createAndRenderPages(t, s)
-
 			tests := []struct {
 				file, expected string
 			}{
-				{"content/blue/doc2.html", "<a href=\"%s/foobar.jpg\">Going</a>"},
+				{"blue/doc2.html", "<a href=\"%s/foobar.jpg\">Going</a>"},
 				{"sect/doc1.html", "<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>"},
 			}
 
@@ -787,19 +635,19 @@
 }
 
 func TestOrderedPages(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	hugofs.InitMemFs()
 
 	viper.Set("baseurl", "http://auth/bub")
 	s := &Site{
-		Source: &source.InMemorySource{ByteSource: weightedSources},
-		Lang:   newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: weightedSources},
+		Language: newDefaultLanguage(),
 	}
-	s.initializeSiteInfo()
 
-	createPagesAndMeta(t, s)
+	if err := buildSiteSkipRender(s); err != nil {
+		t.Fatalf("Failed to process site: %s", err)
+	}
 
 	if s.Sections["sect"][0].Weight != 2 || s.Sections["sect"][3].Weight != 6 {
 		t.Errorf("Pages in unexpected order. First should be '%d', got '%d'", 2, s.Sections["sect"][0].Weight)
@@ -850,8 +698,7 @@
 }
 
 func TestGroupedPages(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	defer func() {
 		if r := recover(); r != nil {
@@ -863,11 +710,13 @@
 
 	viper.Set("baseurl", "http://auth/bub")
 	s := &Site{
-		Source: &source.InMemorySource{ByteSource: groupedSources},
+		Source:   &source.InMemorySource{ByteSource: groupedSources},
+		Language: newDefaultLanguage(),
 	}
-	s.initializeSiteInfo()
 
-	createPagesAndMeta(t, s)
+	if err := buildSiteSkipRender(s); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
 
 	rbysection, err := s.Pages.GroupBy("Section", "desc")
 	if err != nil {
@@ -1030,8 +879,7 @@
 Front Matter with weighted tags and categories`)
 
 func TestWeightedTaxonomies(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	hugofs.InitMemFs()
 	sources := []source.ByteSource{
@@ -1047,12 +895,13 @@
 	viper.Set("baseurl", "http://auth/bub")
 	viper.Set("taxonomies", taxonomies)
 	s := &Site{
-		Source: &source.InMemorySource{ByteSource: sources},
-		Lang:   newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: sources},
+		Language: newDefaultLanguage(),
 	}
-	s.initializeSiteInfo()
 
-	createPagesAndMeta(t, s)
+	if err := buildSiteSkipRender(s); err != nil {
+		t.Fatalf("Failed to process site: %s", err)
+	}
 
 	if s.Taxonomies["tags"]["a"][0].Page.Title != "foo" {
 		t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies["tags"]["a"][0].Page.Title)
@@ -1115,20 +964,20 @@
 			"sourceRelativeLinksProjectFolder": "/docs"})
 
 	site := &Site{
-		Source: &source.InMemorySource{ByteSource: sources},
-		Lang:   newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: sources},
+		Language: newDefaultLanguage(),
 	}
 
-	site.initializeSiteInfo()
+	if err := buildSiteSkipRender(site); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
+	}
 
-	createPagesAndMeta(t, site)
-
 	return site
 }
 
 func TestRefLinking(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
+
 	site := setupLinkingMockSite(t)
 
 	currentPage := findPage(site, "level2/level3/index.md")
@@ -1151,8 +1000,8 @@
 }
 
 func TestSourceRelativeLinksing(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
+
 	site := setupLinkingMockSite(t)
 
 	type resultMap map[string]string
@@ -1287,8 +1136,8 @@
 }
 
 func TestSourceRelativeLinkFileing(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
+
 	site := setupLinkingMockSite(t)
 
 	type resultMap map[string]string
@@ -1330,166 +1179,4 @@
 			}
 		}
 	}
-}
-
-func TestMultilingualSwitch(t *testing.T) {
-	// General settings
-	viper.Set("DefaultExtension", "html")
-	viper.Set("baseurl", "http://example.com/blog")
-	viper.Set("DisableSitemap", false)
-	viper.Set("DisableRSS", false)
-	viper.Set("RSSUri", "index.xml")
-	viper.Set("Taxonomies", map[string]string{"tag": "tags"})
-	viper.Set("Permalinks", map[string]string{"other": "/somewhere/else/:filename"})
-
-	// Sources
-	sources := []source.ByteSource{
-		{filepath.FromSlash("sect/doc1.en.md"), []byte(`---
-title: doc1
-slug: doc1-slug
-tags:
- - tag1
-publishdate: "2000-01-01"
----
-# doc1
-*some content*
-NOTE: slug should be used as URL
-`)},
-		{filepath.FromSlash("sect/doc1.fr.md"), []byte(`---
-title: doc1
-tags:
- - tag1
- - tag2
-publishdate: "2000-01-04"
----
-# doc1
-*quelque contenu*
-NOTE: should be in the 'en' Page's 'Translations' field.
-NOTE: date is after "doc3"
-`)},
-		{filepath.FromSlash("sect/doc2.en.md"), []byte(`---
-title: doc2
-publishdate: "2000-01-02"
----
-# doc2
-*some content*
-NOTE: without slug, "doc2" should be used, without ".en" as URL
-`)},
-		{filepath.FromSlash("sect/doc3.en.md"), []byte(`---
-title: doc3
-publishdate: "2000-01-03"
-tags:
- - tag2
-url: /superbob
----
-# doc3
-*some content*
-NOTE: third 'en' doc, should trigger pagination on home page.
-`)},
-		{filepath.FromSlash("sect/doc4.md"), []byte(`---
-title: doc4
-tags:
- - tag1
-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'
-`)},
-		{filepath.FromSlash("other/doc5.fr.md"), []byte(`---
-title: doc5
-publishdate: "2000-01-06"
----
-# doc5
-*autre contenu francophone*
-NOTE: should use the "permalinks" configuration with :filename
-`)},
-	}
-
-	hugofs.InitMemFs()
-
-	// Multilingual settings
-	viper.Set("Multilingual", true)
-	en := NewLanguage("en")
-	viper.Set("DefaultContentLanguage", "fr")
-	viper.Set("paginate", "2")
-
-	languages := NewLanguages(en, NewLanguage("fr"))
-	s := &Site{
-		Source: &source.InMemorySource{ByteSource: sources},
-		Lang:   en,
-		Multilingual: &Multilingual{
-			Languages: languages,
-		},
-	}
-
-	s.prepTemplates()
-	s.initializeSiteInfo()
-
-	createPagesAndMeta(t, s)
-
-	assert.Len(t, s.Source.Files(), 6, "should have 6 source files")
-	assert.Len(t, s.Pages, 3, "should have 3 pages")
-	assert.Len(t, s.AllPages, 6, "should have 6 total pages (including translations)")
-
-	doc1en := s.Pages[0]
-	permalink, err := doc1en.Permalink()
-	assert.NoError(t, err, "permalink call failed")
-	assert.Equal(t, "http://example.com/blog/en/sect/doc1-slug", permalink, "invalid doc1.en permalink")
-	assert.Len(t, doc1en.Translations(), 1, "doc1-en should have one translation, excluding itself")
-
-	doc2 := s.Pages[1]
-	permalink, err = doc2.Permalink()
-	assert.NoError(t, err, "permalink call failed")
-	assert.Equal(t, "http://example.com/blog/en/sect/doc2", permalink, "invalid doc2 permalink")
-
-	doc3 := s.Pages[2]
-	permalink, err = doc3.Permalink()
-	assert.NoError(t, err, "permalink call failed")
-	assert.Equal(t, "http://example.com/blog/superbob", permalink, "invalid doc3 permalink")
-
-	// TODO(bep) multilingo. Check this case. This has url set in frontmatter, but we must split into lang folders
-	// The assertion below was missing the /en prefix.
-	assert.Equal(t, "/en/superbob", doc3.URL(), "invalid url, was specified on doc3 TODO(bep)")
-
-	assert.Equal(t, doc2.Next, doc3, "doc3 should follow doc2, in .Next")
-
-	doc1fr := doc1en.Translations()[0]
-	permalink, err = doc1fr.Permalink()
-	assert.NoError(t, err, "permalink call failed")
-	assert.Equal(t, "http://example.com/blog/fr/sect/doc1", permalink, "invalid doc1fr permalink")
-
-	assert.Equal(t, doc1en.Translations()[0], doc1fr, "doc1-en should have doc1-fr as translation")
-	assert.Equal(t, doc1fr.Translations()[0], doc1en, "doc1-fr should have doc1-en as translation")
-	assert.Equal(t, "fr", doc1fr.Language().Lang)
-
-	doc4 := s.AllPages[4]
-	permalink, err = doc4.Permalink()
-	assert.NoError(t, err, "permalink call failed")
-	assert.Equal(t, "http://example.com/blog/fr/sect/doc4", permalink, "invalid doc4 permalink")
-	assert.Len(t, doc4.Translations(), 0, "found translations for doc4")
-
-	doc5 := s.AllPages[5]
-	permalink, err = doc5.Permalink()
-	assert.NoError(t, err, "permalink call failed")
-	assert.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid doc5 permalink")
-
-	// Taxonomies and their URLs
-	assert.Len(t, s.Taxonomies, 1, "should have 1 taxonomy")
-	tags := s.Taxonomies["tags"]
-	assert.Len(t, tags, 2, "should have 2 different tags")
-	assert.Equal(t, tags["tag1"][0].Page, doc1en, "first tag1 page should be doc1")
-
-	// Expect the tags locations to be in certain places, with the /en/ prefixes, etc..
-}
-
-func assertFileContent(t *testing.T, path string, content string) {
-	fl, err := hugofs.Destination().Open(path)
-	assert.NoError(t, err, "file content not found when asserting on content of %s", path)
-
-	cnt, err := ioutil.ReadAll(fl)
-	assert.NoError(t, err, "cannot read file content when asserting on content of %s", path)
-
-	assert.Equal(t, content, string(cnt))
 }
--- a/hugolib/site_url_test.go
+++ b/hugolib/site_url_test.go
@@ -60,8 +60,7 @@
 
 // Issue #1105
 func TestShouldNotAddTrailingSlashToBaseURL(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
 	for i, this := range []struct {
 		in       string
@@ -84,45 +83,29 @@
 }
 
 func TestPageCount(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
-
+	testCommonResetState()
 	hugofs.InitMemFs()
 
 	viper.Set("uglyurls", false)
 	viper.Set("paginate", 10)
 	s := &Site{
-		Source: &source.InMemorySource{ByteSource: urlFakeSource},
-		Lang:   newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: urlFakeSource},
+		Language: newDefaultLanguage(),
 	}
-	s.initializeSiteInfo()
-	s.prepTemplates("indexes/blue.html", indexTemplate)
 
-	createPagesAndMeta(t, s)
-
-	if err := s.renderSectionLists(); err != nil {
-		t.Errorf("Unable to render section lists: %s", err)
+	if err := buildAndRenderSite(s, "indexes/blue.html", indexTemplate); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
 	}
-
-	if err := s.renderAliases(); err != nil {
-		t.Errorf("Unable to render site lists: %s", err)
-	}
-
-	_, err := hugofs.Destination().Open("blue")
+	_, err := hugofs.Destination().Open("public/blue")
 	if err != nil {
 		t.Errorf("No indexed rendered.")
 	}
 
-	//expected := ".."
-	//if string(blueIndex) != expected {
-	//t.Errorf("Index template does not match expected: %q, got: %q", expected, string(blueIndex))
-	//}
-
 	for _, s := range []string{
-		"sd1/foo/index.html",
-		"sd2/index.html",
-		"sd3/index.html",
-		"sd4.html",
+		"public/sd1/foo/index.html",
+		"public/sd2/index.html",
+		"public/sd3/index.html",
+		"public/sd4.html",
 	} {
 		if _, err := hugofs.Destination().Open(filepath.FromSlash(s)); err != nil {
 			t.Errorf("No alias rendered: %s", s)
--- a/hugolib/siteinfo_test.go
+++ /dev/null
@@ -1,64 +1,0 @@
-// Copyright 2015 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package hugolib
-
-import (
-	"bytes"
-	"testing"
-
-	"github.com/spf13/viper"
-)
-
-const siteInfoParamTemplate = `{{ .Site.Params.MyGlobalParam }}`
-
-func TestSiteInfoParams(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
-
-	viper.Set("Params", map[string]interface{}{"MyGlobalParam": "FOOBAR_PARAM"})
-	s := newSiteDefaultLang()
-
-	s.initialize()
-	if s.Info.Params["MyGlobalParam"] != "FOOBAR_PARAM" {
-		t.Errorf("Unable to set site.Info.Param")
-	}
-
-	s.prepTemplates("template", siteInfoParamTemplate)
-
-	buf := new(bytes.Buffer)
-
-	err := s.renderThing(s.newNode(), "template", buf)
-	if err != nil {
-		t.Errorf("Unable to render template: %s", err)
-	}
-
-	if buf.String() != "FOOBAR_PARAM" {
-		t.Errorf("Expected FOOBAR_PARAM: got %s", buf.String())
-	}
-}
-
-func TestSiteInfoPermalinks(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
-
-	viper.Set("Permalinks", map[string]interface{}{"section": "/:title"})
-	s := newSiteDefaultLang()
-
-	s.initialize()
-	permalink := s.Info.Permalinks["section"]
-
-	if permalink != "/:title" {
-		t.Errorf("Could not set permalink (%#v)", permalink)
-	}
-}
--- a/hugolib/sitemap_test.go
+++ b/hugolib/sitemap_test.go
@@ -37,37 +37,20 @@
 </urlset>`
 
 func TestSitemapOutput(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	testCommonResetState()
 
-	hugofs.InitMemFs()
-
 	viper.Set("baseurl", "http://auth/bub/")
 
 	s := &Site{
-		Source: &source.InMemorySource{ByteSource: weightedSources},
-		Lang:   newDefaultLanguage(),
+		Source:   &source.InMemorySource{ByteSource: weightedSources},
+		Language: newDefaultLanguage(),
 	}
 
-	s.initializeSiteInfo()
-
-	s.prepTemplates("sitemap.xml", SITEMAP_TEMPLATE)
-
-	createPagesAndMeta(t, s)
-
-	if err := s.renderHomePage(); err != nil {
-		t.Fatalf("Unable to RenderHomePage: %s", err)
+	if err := buildAndRenderSite(s, "sitemap.xml", SITEMAP_TEMPLATE); err != nil {
+		t.Fatalf("Failed to build site: %s", err)
 	}
 
-	if err := s.renderSitemap(); err != nil {
-		t.Fatalf("Unable to RenderSitemap: %s", err)
-	}
-
-	if err := s.renderRobotsTXT(); err != nil {
-		t.Fatalf("Unable to RenderRobotsTXT :%s", err)
-	}
-
-	sitemapFile, err := hugofs.Destination().Open("sitemap.xml")
+	sitemapFile, err := hugofs.Destination().Open("public/sitemap.xml")
 
 	if err != nil {
 		t.Fatalf("Unable to locate: sitemap.xml")
--- a/hugolib/taxonomy_test.go
+++ b/hugolib/taxonomy_test.go
@@ -21,8 +21,7 @@
 )
 
 func TestByCountOrderOfTaxonomies(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
+	defer testCommonResetState()
 
 	taxonomies := make(map[string]string)
 
--- a/hugolib/translations.go
+++ b/hugolib/translations.go
@@ -14,7 +14,7 @@
 package hugolib
 
 import (
-	"fmt"
+	jww "github.com/spf13/jwalterweatherman"
 )
 
 // Translations represent the other translations for a given page. The
@@ -41,7 +41,10 @@
 		language := ml.Language(pageLang)
 
 		if language == nil {
-			panic(fmt.Sprintf("Page language not found in multilang setup: %s", pageLang))
+			// TODO(bep) ml
+			// This may or may not be serious. It can be a file named stefano.chiodino.md.
+			jww.WARN.Printf("Page language (if it is that) not found in multilang setup: %s.", pageLang)
+			language = ml.DefaultLang
 		}
 
 		page.language = language
--- a/source/file.go
+++ b/source/file.go
@@ -108,10 +108,11 @@
 }
 
 // NewFileWithContents creates a new File pointer with the given relative path and
-// content.
+// content. The language defaults to "en".
 func NewFileWithContents(relpath string, content io.Reader) *File {
 	file := NewFile(relpath)
 	file.Contents = content
+	file.lang = "en"
 	return file
 }
 
@@ -124,15 +125,16 @@
 	f.dir, f.logicalName = filepath.Split(f.relpath)
 	f.ext = strings.TrimPrefix(filepath.Ext(f.LogicalName()), ".")
 	f.baseName = helpers.Filename(f.LogicalName())
-	if viper.GetBool("Multilingual") {
-		f.lang = strings.TrimPrefix(filepath.Ext(f.baseName), ".")
+
+	f.lang = strings.TrimPrefix(filepath.Ext(f.baseName), ".")
+	if f.lang == "" {
+		f.lang = viper.GetString("DefaultContentLanguage")
 		if f.lang == "" {
-			f.lang = viper.GetString("DefaultContentLanguage")
+			// TODO(bep) ml
+			f.lang = "en"
 		}
-		f.translationBaseName = helpers.Filename(f.baseName)
-	} else {
-		f.translationBaseName = f.baseName
 	}
+	f.translationBaseName = helpers.Filename(f.baseName)
 
 	f.section = helpers.GuessSection(f.Dir())
 	f.uniqueID = helpers.Md5String(f.LogicalName())
--- a/source/filesystem.go
+++ b/source/filesystem.go
@@ -105,6 +105,9 @@
 
 	if err != nil {
 		jww.ERROR.Println(err)
+		if err == helpers.WalkRootTooShortError {
+			panic("The root path is too short. If this is a test, make sure to init the content paths.")
+		}
 	}
 
 }
--- a/source/filesystem_test.go
+++ b/source/filesystem_test.go
@@ -22,7 +22,7 @@
 )
 
 func TestEmptySourceFilesystem(t *testing.T) {
-	src := new(Filesystem)
+	src := &Filesystem{Base: "Empty"}
 	if len(src.Files()) != 0 {
 		t.Errorf("new filesystem should contain 0 files.")
 	}
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -336,6 +336,8 @@
 			return err
 		}
 
+		jww.DEBUG.Printf("Add template file from path %s", path)
+
 		return t.AddTemplate(name, string(b))
 	}
 
@@ -366,11 +368,12 @@
 }
 
 func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
+	jww.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix)
 	walker := func(path string, fi os.FileInfo, err error) error {
 		if err != nil {
 			return nil
 		}
-
+		jww.DEBUG.Println("Template path", path)
 		if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
 			link, err := filepath.EvalSymlinks(absPath)
 			if err != nil {