shithub: hugo

Download patch

ref: e52e2a70e5e0a2d15fc9befbcd7290761c98589e
parent: ea165bf9e71c7ca9ddb9f14ddbdbcd506ce554bb
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Fri Mar 3 05:47:43 EST 2017

hugolib, target: Rework/move the target package

This relates to #3123.

The interfaces and types in `target` made sense at some point, but now this package is too restricted to a hardcoded set of media types.

The overall current logic:

* Create a file path based on some `Translator` with some hardcoded logic handling uglyURLs, hardcoded html suffix etc.
* In in some cases (alias), a template is applied to create the alias file.
* Then the content is written to destination.

One could argue that it is the last bullet that is the actual core responsibility.

This commit fixes that by moving the `hugolib`-related logic where it belong, and simplify the code, i.e. remove the abstractions.

This code will most certainly evolve once we start on #3123, but now it is at least possible to understand where to start.

Fixes #3123

--- /dev/null
+++ b/hugolib/alias.go
@@ -1,0 +1,69 @@
+// Copyright 2017 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"
+	"html/template"
+	"io"
+)
+
+const (
+	alias      = "<!DOCTYPE html><html><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>"
+	aliasXHtml = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>"
+)
+
+var defaultAliasTemplates *template.Template
+
+func init() {
+	defaultAliasTemplates = template.New("")
+	template.Must(defaultAliasTemplates.New("alias").Parse(alias))
+	template.Must(defaultAliasTemplates.New("alias-xhtml").Parse(aliasXHtml))
+}
+
+type aliasHandler struct {
+	Templates *template.Template
+}
+
+func newAliasHandler(t *template.Template) aliasHandler {
+	return aliasHandler{t}
+}
+
+func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (io.Reader, error) {
+	t := "alias"
+	if isXHTML {
+		t = "alias-xhtml"
+	}
+
+	template := defaultAliasTemplates
+	if a.Templates != nil {
+		template = a.Templates
+		t = "alias.html"
+	}
+
+	data := struct {
+		Permalink string
+		Page      *Page
+	}{
+		permalink,
+		page,
+	}
+
+	buffer := new(bytes.Buffer)
+	err := template.ExecuteTemplate(buffer, t, data)
+	if err != nil {
+		return nil, err
+	}
+	return buffer, nil
+}
--- a/hugolib/handler_file.go
+++ b/hugolib/handler_file.go
@@ -39,7 +39,7 @@
 
 func (h defaultHandler) Extensions() []string { return []string{"*"} }
 func (h defaultHandler) FileConvert(f *source.File, s *Site) HandledResult {
-	s.writeDestFile(f.Path(), f.Contents)
+	s.w.writeDestFile(f.Path(), f.Contents)
 	return HandledResult{file: f}
 }
 
@@ -48,6 +48,6 @@
 func (h cssHandler) Extensions() []string { return []string{"css"} }
 func (h cssHandler) FileConvert(f *source.File, s *Site) HandledResult {
 	x := cssmin.Minify(f.Bytes())
-	s.writeDestFile(f.Path(), bytes.NewReader(x))
+	s.w.writeDestFile(f.Path(), bytes.NewReader(x))
 	return HandledResult{file: f}
 }
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -194,6 +194,8 @@
 func (h *HugoSites) render(config *BuildCfg) error {
 	if !config.SkipRender {
 		for _, s := range h.Sites {
+			s.initSiteWriter()
+
 			if err := s.render(); err != nil {
 				return err
 			}
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -39,7 +39,6 @@
 	"github.com/spf13/hugo/helpers"
 	"github.com/spf13/hugo/parser"
 	"github.com/spf13/hugo/source"
-	"github.com/spf13/hugo/target"
 	"github.com/spf13/hugo/tpl"
 	"github.com/spf13/hugo/transform"
 	"github.com/spf13/nitro"
@@ -92,19 +91,21 @@
 	// is set.
 	taxonomiesOrigKey map[string]string
 
-	Source         source.Input
-	Sections       Taxonomy
-	Info           SiteInfo
-	Menus          Menus
-	timer          *nitro.B
-	targets        targetList
-	targetListInit sync.Once
-	draftCount     int
-	futureCount    int
-	expiredCount   int
-	Data           map[string]interface{}
-	Language       *helpers.Language
+	Source   source.Input
+	Sections Taxonomy
+	Info     SiteInfo
+	Menus    Menus
+	timer    *nitro.B
 
+	// This is not a pointer by design.
+	w siteWriter
+
+	draftCount   int
+	futureCount  int
+	expiredCount int
+	Data         map[string]interface{}
+	Language     *helpers.Language
+
 	disabledKinds map[string]bool
 
 	// Logger etc.
@@ -139,6 +140,7 @@
 	s := &Site{PageCollections: c, Language: cfg.Language, disabledKinds: disabledKinds}
 
 	s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language})
+
 	return s, nil
 
 }
@@ -210,14 +212,6 @@
 	return s, nil
 }
 
-type targetList struct {
-	page          target.Output
-	pageUgly      target.Output
-	file          target.Output
-	alias         target.AliasPublisher
-	languageAlias target.AliasPublisher
-}
-
 type SiteInfo struct {
 	// atomic requires 64-bit alignment for struct field access
 	// According to the docs, " The first word in a global variable or in an
@@ -1180,6 +1174,8 @@
 	numWorkers := getGoMaxProcs() * 4
 	wg := &sync.WaitGroup{}
 
+	s.initSiteWriter()
+
 	for i := 0; i < numWorkers; i++ {
 		wg.Add(2)
 		go fileConverter(s, fileConvChan, results, wg)
@@ -1757,7 +1753,7 @@
 	transformer := transform.NewChain(transform.AbsURLInXML)
 	transformer.Apply(outBuffer, renderBuffer, path)
 
-	return s.writeDestFile(dest, outBuffer)
+	return s.w.writeDestFile(dest, outBuffer)
 
 }
 
@@ -1774,14 +1770,13 @@
 	outBuffer := bp.GetBuffer()
 	defer bp.PutBuffer(outBuffer)
 
-	var pageTarget target.Output
+	// Note: this is not a pointer, as we may mutate the state below.
+	w := s.w
 
 	if p, ok := d.(*Page); ok && p.IsPage() && path.Ext(p.URLPath.URL) != "" {
 		// user has explicitly set a URL with extension for this page
 		// make sure it sticks even if "ugly URLs" are turned off.
-		pageTarget = s.pageUglyTarget()
-	} else {
-		pageTarget = s.pageTarget()
+		w.uglyURLs = true
 	}
 
 	transformLinks := transform.NewEmptyTransforms()
@@ -1804,7 +1799,7 @@
 	var path []byte
 
 	if s.Info.relativeURLs {
-		translated, err := pageTarget.(target.OptionalTranslator).TranslateRelative(dest)
+		translated, err := w.baseTargetPathPage(dest)
 		if err != nil {
 			return err
 		}
@@ -1844,7 +1839,7 @@
 
 	}
 
-	if err = s.writeDestPage(dest, pageTarget, outBuffer); err != nil {
+	if err = w.writeDestPage(dest, outBuffer); err != nil {
 		return err
 	}
 
@@ -1893,95 +1888,37 @@
 
 }
 
-func (s *Site) pageTarget() target.Output {
-	s.initTargetList()
-	return s.targets.page
+func (s *Site) langDir() string {
+	if s.Language.Lang != s.Info.multilingual.DefaultLang.Lang || s.Info.defaultContentLanguageInSubdir {
+		return s.Language.Lang
+	}
+	return ""
 }
 
-func (s *Site) pageUglyTarget() target.Output {
-	s.initTargetList()
-	return s.targets.pageUgly
-}
-
-func (s *Site) fileTarget() target.Output {
-	s.initTargetList()
-	return s.targets.file
-}
-
-func (s *Site) aliasTarget() target.AliasPublisher {
-	s.initTargetList()
-	return s.targets.alias
-}
-
-func (s *Site) languageAliasTarget() target.AliasPublisher {
-	s.initTargetList()
-	return s.targets.languageAlias
-}
-
-func (s *Site) initTargetList() {
+func (s *Site) initSiteWriter() {
 	if s.Fs == nil {
 		panic("Must have Fs")
 	}
-	s.targetListInit.Do(func() {
-		langDir := ""
-		if s.Language.Lang != s.Info.multilingual.DefaultLang.Lang || s.Info.defaultContentLanguageInSubdir {
-			langDir = s.Language.Lang
-		}
-		if s.targets.page == nil {
-			s.targets.page = &target.PagePub{
-				Fs:         s.Fs,
-				PublishDir: s.absPublishDir(),
-				UglyURLs:   s.Cfg.GetBool("uglyURLs"),
-				LangDir:    langDir,
-			}
-		}
-		if s.targets.pageUgly == nil {
-			s.targets.pageUgly = &target.PagePub{
-				Fs:         s.Fs,
-				PublishDir: s.absPublishDir(),
-				UglyURLs:   true,
-				LangDir:    langDir,
-			}
-		}
-		if s.targets.file == nil {
-			s.targets.file = &target.Filesystem{
-				Fs:         s.Fs,
-				PublishDir: s.absPublishDir(),
-			}
-		}
-		if s.targets.alias == nil {
-			s.targets.alias = &target.HTMLRedirectAlias{
-				Fs:         s.Fs,
-				PublishDir: s.absPublishDir(),
-				Templates:  s.Tmpl.Lookup("alias.html"),
-			}
-		}
-		if s.targets.languageAlias == nil {
-			s.targets.languageAlias = &target.HTMLRedirectAlias{
-				Fs:         s.Fs,
-				PublishDir: s.absPublishDir(),
-				AllowRoot:  true,
-			}
-		}
-	})
+	s.w = siteWriter{
+		langDir:      s.langDir(),
+		publishDir:   s.absPublishDir(),
+		uglyURLs:     s.Cfg.GetBool("uglyURLs"),
+		relativeURLs: s.Info.relativeURLs,
+		fs:           s.Fs,
+		log:          s.Log,
+	}
 }
 
-func (s *Site) writeDestFile(path string, reader io.Reader) (err error) {
-	s.Log.DEBUG.Println("creating file:", path)
-	return s.fileTarget().Publish(path, reader)
+func (s *Site) writeDestAlias(path, permalink string, p *Page) (err error) {
+	return s.publishDestAlias(false, path, permalink, p)
 }
 
-func (s *Site) writeDestPage(path string, publisher target.Publisher, reader io.Reader) (err error) {
-	s.Log.DEBUG.Println("creating page:", path)
-	return publisher.Publish(path, reader)
-}
+func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, p *Page) (err error) {
+	w := s.w
+	w.allowRoot = allowRoot
 
-// AliasPublisher
-func (s *Site) writeDestAlias(path, permalink string, p *Page) (err error) {
-	return s.publishDestAlias(s.aliasTarget(), path, permalink, p)
-}
+	isXHTML := strings.HasSuffix(path, ".xhtml")
 
-func (s *Site) publishDestAlias(aliasPublisher target.AliasPublisher, path, permalink string, p *Page) (err error) {
 	if s.Info.relativeURLs {
 		// convert `permalink` into URI relative to location of `path`
 		baseURL := helpers.SanitizeURLKeepTrailingSlash(s.Cfg.GetString("baseURL"))
@@ -1995,7 +1932,20 @@
 		permalink = filepath.ToSlash(permalink)
 	}
 	s.Log.DEBUG.Println("creating alias:", path, "redirecting to", permalink)
-	return aliasPublisher.Publish(path, permalink, p)
+
+	targetPath, err := w.targetPathAlias(path)
+	if err != nil {
+		return err
+	}
+
+	handler := newAliasHandler(s.Tmpl.Lookup("alias.html"))
+	aliasContent, err := handler.renderAlias(isXHTML, permalink, p)
+	if err != nil {
+		return err
+	}
+
+	return w.publish(targetPath, aliasContent)
+
 }
 
 func (s *Site) draftStats() string {
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -257,7 +257,7 @@
 	err := s.renderForLayouts("robots", n, outBuffer, s.appendThemeTemplates(rLayouts)...)
 
 	if err == nil {
-		err = s.writeDestFile("robots.txt", outBuffer)
+		err = s.w.writeDestFile("robots.txt", outBuffer)
 	}
 
 	return err
@@ -284,13 +284,13 @@
 		if s.Info.defaultContentLanguageInSubdir {
 			mainLangURL := s.PathSpec.AbsURL(mainLang.Lang, false)
 			s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
-			if err := s.publishDestAlias(s.languageAliasTarget(), "/", mainLangURL, nil); err != nil {
+			if err := s.publishDestAlias(true, "/", mainLangURL, nil); err != nil {
 				return err
 			}
 		} else {
 			mainLangURL := s.PathSpec.AbsURL("", false)
 			s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)
-			if err := s.publishDestAlias(s.languageAliasTarget(), mainLang.Lang, mainLangURL, nil); err != nil {
+			if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, nil); err != nil {
 				return err
 			}
 		}
--- /dev/null
+++ b/hugolib/site_writer.go
@@ -1,0 +1,193 @@
+// Copyright 2017 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 (
+	"fmt"
+	"io"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/spf13/hugo/helpers"
+	"github.com/spf13/hugo/hugofs"
+	jww "github.com/spf13/jwalterweatherman"
+)
+
+// We may find some abstractions/interface(s) here once we star with
+// "Multiple Output Types".
+type siteWriter struct {
+	langDir      string
+	publishDir   string
+	relativeURLs bool
+	uglyURLs     bool
+	allowRoot    bool // For aliases
+
+	fs *hugofs.Fs
+
+	log *jww.Notepad
+}
+
+func (w siteWriter) targetPathPage(src string) (string, error) {
+	dir, err := w.baseTargetPathPage(src)
+	if err != nil {
+		return "", err
+	}
+	if w.publishDir != "" {
+		dir = filepath.Join(w.publishDir, dir)
+	}
+	return dir, nil
+}
+
+func (w siteWriter) baseTargetPathPage(src string) (string, error) {
+	if src == helpers.FilePathSeparator {
+		return "index.html", nil
+	}
+
+	dir, file := filepath.Split(src)
+	isRoot := dir == ""
+	ext := extension(filepath.Ext(file))
+	name := filename(file)
+
+	if w.langDir != "" && dir == helpers.FilePathSeparator && name == w.langDir {
+		return filepath.Join(dir, name, "index"+ext), nil
+	}
+
+	if w.uglyURLs || file == "index.html" || (isRoot && file == "404.html") {
+		return filepath.Join(dir, name+ext), nil
+	}
+
+	dir = filepath.Join(dir, name, "index"+ext)
+
+	return dir, nil
+
+}
+
+func (w siteWriter) targetPathFile(src string) (string, error) {
+	return filepath.Join(w.publishDir, filepath.FromSlash(src)), nil
+}
+
+func (w siteWriter) targetPathAlias(src string) (string, error) {
+	originalAlias := src
+	if len(src) <= 0 {
+		return "", fmt.Errorf("Alias \"\" is an empty string")
+	}
+
+	alias := filepath.Clean(src)
+	components := strings.Split(alias, helpers.FilePathSeparator)
+
+	if !w.allowRoot && alias == helpers.FilePathSeparator {
+		return "", fmt.Errorf("Alias \"%s\" resolves to website root directory", originalAlias)
+	}
+
+	// Validate against directory traversal
+	if components[0] == ".." {
+		return "", fmt.Errorf("Alias \"%s\" traverses outside the website root directory", originalAlias)
+	}
+
+	// Handle Windows file and directory naming restrictions
+	// See "Naming Files, Paths, and Namespaces" on MSDN
+	// https://msdn.microsoft.com/en-us/library/aa365247%28v=VS.85%29.aspx?f=255&MSPPError=-2147217396
+	msgs := []string{}
+	reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}
+
+	if strings.ContainsAny(alias, ":*?\"<>|") {
+		msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains invalid characters on Windows: : * ? \" < > |", originalAlias))
+	}
+	for _, ch := range alias {
+		if ch < ' ' {
+			msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains ASCII control code (0x00 to 0x1F), invalid on Windows: : * ? \" < > |", originalAlias))
+			continue
+		}
+	}
+	for _, comp := range components {
+		if strings.HasSuffix(comp, " ") || strings.HasSuffix(comp, ".") {
+			msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with a trailing space or period, problematic on Windows", originalAlias))
+		}
+		for _, r := range reservedNames {
+			if comp == r {
+				msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with reserved name \"%s\" on Windows", originalAlias, r))
+			}
+		}
+	}
+	if len(msgs) > 0 {
+		if runtime.GOOS == "windows" {
+			for _, m := range msgs {
+				w.log.ERROR.Println(m)
+			}
+			return "", fmt.Errorf("Cannot create \"%s\": Windows filename restriction", originalAlias)
+		}
+		for _, m := range msgs {
+			w.log.WARN.Println(m)
+		}
+	}
+
+	// Add the final touch
+	alias = strings.TrimPrefix(alias, helpers.FilePathSeparator)
+	if strings.HasSuffix(alias, helpers.FilePathSeparator) {
+		alias = alias + "index.html"
+	} else if !strings.HasSuffix(alias, ".html") {
+		alias = alias + helpers.FilePathSeparator + "index.html"
+	}
+	if originalAlias != alias {
+		w.log.INFO.Printf("Alias \"%s\" translated to \"%s\"\n", originalAlias, alias)
+	}
+
+	return filepath.Join(w.publishDir, alias), nil
+}
+
+func extension(ext string) string {
+	switch ext {
+	case ".md", ".rst":
+		return ".html"
+	}
+
+	if ext != "" {
+		return ext
+	}
+
+	return ".html"
+}
+
+func filename(f string) string {
+	ext := filepath.Ext(f)
+	if ext == "" {
+		return f
+	}
+
+	return f[:len(f)-len(ext)]
+}
+
+func (w siteWriter) writeDestPage(path string, reader io.Reader) (err error) {
+	w.log.DEBUG.Println("creating page:", path)
+	targetPath, err := w.targetPathPage(path)
+	if err != nil {
+		return err
+	}
+
+	return w.publish(targetPath, reader)
+}
+
+func (w siteWriter) writeDestFile(path string, r io.Reader) (err error) {
+	w.log.DEBUG.Println("creating file:", path)
+	targetPath, err := w.targetPathFile(path)
+	if err != nil {
+		return err
+	}
+	return w.publish(targetPath, r)
+}
+
+func (w siteWriter) publish(path string, r io.Reader) (err error) {
+	return helpers.WriteToDisk(path, r, w.fs.Destination)
+}
--- /dev/null
+++ b/hugolib/site_writer_test.go
@@ -1,0 +1,146 @@
+// Copyright 2017 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 (
+	"path/filepath"
+	"runtime"
+	"testing"
+)
+
+func TestTargetPathHTMLRedirectAlias(t *testing.T) {
+	w := siteWriter{log: newErrorLogger()}
+
+	errIsNilForThisOS := runtime.GOOS != "windows"
+
+	tests := []struct {
+		value    string
+		expected string
+		errIsNil bool
+	}{
+		{"", "", false},
+		{"s", filepath.FromSlash("s/index.html"), true},
+		{"/", "", false},
+		{"alias 1", filepath.FromSlash("alias 1/index.html"), true},
+		{"alias 2/", filepath.FromSlash("alias 2/index.html"), true},
+		{"alias 3.html", "alias 3.html", true},
+		{"alias4.html", "alias4.html", true},
+		{"/alias 5.html", "alias 5.html", true},
+		{"/трям.html", "трям.html", true},
+		{"../../../../tmp/passwd", "", false},
+		{"/foo/../../../../tmp/passwd", filepath.FromSlash("tmp/passwd/index.html"), true},
+		{"foo/../../../../tmp/passwd", "", false},
+		{"C:\\Windows", filepath.FromSlash("C:\\Windows/index.html"), errIsNilForThisOS},
+		{"/trailing-space /", filepath.FromSlash("trailing-space /index.html"), errIsNilForThisOS},
+		{"/trailing-period./", filepath.FromSlash("trailing-period./index.html"), errIsNilForThisOS},
+		{"/tab\tseparated/", filepath.FromSlash("tab\tseparated/index.html"), errIsNilForThisOS},
+		{"/chrome/?p=help&ctx=keyboard#topic=3227046", filepath.FromSlash("chrome/?p=help&ctx=keyboard#topic=3227046/index.html"), errIsNilForThisOS},
+		{"/LPT1/Printer/", filepath.FromSlash("LPT1/Printer/index.html"), errIsNilForThisOS},
+	}
+
+	for _, test := range tests {
+		path, err := w.targetPathAlias(test.value)
+		if (err == nil) != test.errIsNil {
+			t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err)
+			continue
+		}
+		if err == nil && path != test.expected {
+			t.Errorf("Expected: \"%s\", got: \"%s\"", test.expected, path)
+		}
+	}
+}
+
+func TestTargetPathPage(t *testing.T) {
+	w := siteWriter{log: newErrorLogger()}
+
+	tests := []struct {
+		content  string
+		expected string
+	}{
+		{"/", "index.html"},
+		{"index.html", "index.html"},
+		{"bar/index.html", "bar/index.html"},
+		{"foo", "foo/index.html"},
+		{"foo.html", "foo/index.html"},
+		{"foo.xhtml", "foo/index.xhtml"},
+		{"section", "section/index.html"},
+		{"section/", "section/index.html"},
+		{"section/foo", "section/foo/index.html"},
+		{"section/foo.html", "section/foo/index.html"},
+		{"section/foo.rss", "section/foo/index.rss"},
+	}
+
+	for _, test := range tests {
+		dest, err := w.targetPathPage(filepath.FromSlash(test.content))
+		expected := filepath.FromSlash(test.expected)
+		if err != nil {
+			t.Fatalf("Translate returned and unexpected err: %s", err)
+		}
+
+		if dest != expected {
+			t.Errorf("Translate expected return: %s, got: %s", expected, dest)
+		}
+	}
+}
+
+func TestTargetPathPageBase(t *testing.T) {
+	w := siteWriter{log: newErrorLogger()}
+
+	tests := []struct {
+		content  string
+		expected string
+	}{
+		{"/", "a/base/index.html"},
+	}
+
+	for _, test := range tests {
+
+		for _, pd := range []string{"a/base", "a/base/"} {
+			w.publishDir = pd
+			dest, err := w.targetPathPage(test.content)
+			if err != nil {
+				t.Fatalf("Translated returned and err: %s", err)
+			}
+
+			if dest != filepath.FromSlash(test.expected) {
+				t.Errorf("Translate expected: %s, got: %s", test.expected, dest)
+			}
+		}
+	}
+}
+
+func TestTargetPathUglyURLs(t *testing.T) {
+	w := siteWriter{log: newErrorLogger(), uglyURLs: true}
+
+	tests := []struct {
+		content  string
+		expected string
+	}{
+		{"foo.html", "foo.html"},
+		{"/", "index.html"},
+		{"section", "section.html"},
+		{"index.html", "index.html"},
+	}
+
+	for _, test := range tests {
+		dest, err := w.targetPathPage(filepath.FromSlash(test.content))
+		if err != nil {
+			t.Fatalf("Translate returned an unexpected err: %s", err)
+		}
+
+		if dest != test.expected {
+			t.Errorf("Translate expected return: %s, got: %s", test.expected, dest)
+		}
+	}
+}
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -151,6 +151,9 @@
 	return jww.NewNotepad(jww.LevelDebug, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
 }
 
+func newErrorLogger() *jww.Notepad {
+	return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
+}
 func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.Template) error {
 
 	return func(templ tpl.Template) error {
--- a/target/alias_test.go
+++ /dev/null
@@ -1,63 +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 target
-
-import (
-	"path/filepath"
-	"runtime"
-	"testing"
-)
-
-func TestHTMLRedirectAlias(t *testing.T) {
-	var o Translator
-	o = new(HTMLRedirectAlias)
-
-	errIsNilForThisOS := runtime.GOOS != "windows"
-
-	tests := []struct {
-		value    string
-		expected string
-		errIsNil bool
-	}{
-		{"", "", false},
-		{"s", filepath.FromSlash("s/index.html"), true},
-		{"/", "", false},
-		{"alias 1", filepath.FromSlash("alias 1/index.html"), true},
-		{"alias 2/", filepath.FromSlash("alias 2/index.html"), true},
-		{"alias 3.html", "alias 3.html", true},
-		{"alias4.html", "alias4.html", true},
-		{"/alias 5.html", "alias 5.html", true},
-		{"/трям.html", "трям.html", true},
-		{"../../../../tmp/passwd", "", false},
-		{"/foo/../../../../tmp/passwd", filepath.FromSlash("tmp/passwd/index.html"), true},
-		{"foo/../../../../tmp/passwd", "", false},
-		{"C:\\Windows", filepath.FromSlash("C:\\Windows/index.html"), errIsNilForThisOS},
-		{"/trailing-space /", filepath.FromSlash("trailing-space /index.html"), errIsNilForThisOS},
-		{"/trailing-period./", filepath.FromSlash("trailing-period./index.html"), errIsNilForThisOS},
-		{"/tab\tseparated/", filepath.FromSlash("tab\tseparated/index.html"), errIsNilForThisOS},
-		{"/chrome/?p=help&ctx=keyboard#topic=3227046", filepath.FromSlash("chrome/?p=help&ctx=keyboard#topic=3227046/index.html"), errIsNilForThisOS},
-		{"/LPT1/Printer/", filepath.FromSlash("LPT1/Printer/index.html"), errIsNilForThisOS},
-	}
-
-	for _, test := range tests {
-		path, err := o.Translate(test.value)
-		if (err == nil) != test.errIsNil {
-			t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err)
-			continue
-		}
-		if err == nil && path != test.expected {
-			t.Errorf("Expected: \"%s\", got: \"%s\"", test.expected, path)
-		}
-	}
-}
--- a/target/file.go
+++ /dev/null
@@ -1,68 +1,0 @@
-// Copyright 2016 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 target
-
-import (
-	"io"
-	"path/filepath"
-
-	"github.com/spf13/hugo/helpers"
-	"github.com/spf13/hugo/hugofs"
-)
-
-type Publisher interface {
-	Publish(string, io.Reader) error
-}
-
-type Translator interface {
-	Translate(string) (string, error)
-}
-
-// TODO(bep) consider other ways to solve this.
-type OptionalTranslator interface {
-	TranslateRelative(string) (string, error)
-}
-
-type Output interface {
-	Publisher
-	Translator
-}
-
-type Filesystem struct {
-	PublishDir string
-
-	Fs *hugofs.Fs
-}
-
-func (fs *Filesystem) Publish(path string, r io.Reader) (err error) {
-	translated, err := fs.Translate(path)
-	if err != nil {
-		return
-	}
-
-	return helpers.WriteToDisk(translated, r, fs.Fs.Destination)
-}
-
-func (fs *Filesystem) Translate(src string) (dest string, err error) {
-	return filepath.Join(fs.PublishDir, filepath.FromSlash(src)), nil
-}
-
-func filename(f string) string {
-	ext := filepath.Ext(f)
-	if ext == "" {
-		return f
-	}
-
-	return f[:len(f)-len(ext)]
-}
--- a/target/htmlredirect.go
+++ /dev/null
@@ -1,151 +1,0 @@
-// Copyright 2016 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 target
-
-import (
-	"bytes"
-	"fmt"
-	"html/template"
-	"path/filepath"
-	"runtime"
-	"strings"
-
-	"github.com/spf13/hugo/helpers"
-	"github.com/spf13/hugo/hugofs"
-	jww "github.com/spf13/jwalterweatherman"
-)
-
-const alias = "<!DOCTYPE html><html><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>"
-const aliasXHtml = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>"
-
-var defaultAliasTemplates *template.Template
-
-func init() {
-	defaultAliasTemplates = template.New("")
-	template.Must(defaultAliasTemplates.New("alias").Parse(alias))
-	template.Must(defaultAliasTemplates.New("alias-xhtml").Parse(aliasXHtml))
-}
-
-type AliasPublisher interface {
-	Translator
-	Publish(path string, permalink string, page interface{}) error
-}
-
-type HTMLRedirectAlias struct {
-	PublishDir string
-	Templates  *template.Template
-	AllowRoot  bool // for the language redirects
-
-	Fs *hugofs.Fs
-}
-
-func (h *HTMLRedirectAlias) Translate(alias string) (aliasPath string, err error) {
-	originalAlias := alias
-	if len(alias) <= 0 {
-		return "", fmt.Errorf("Alias \"\" is an empty string")
-	}
-
-	alias = filepath.Clean(alias)
-	components := strings.Split(alias, helpers.FilePathSeparator)
-
-	if !h.AllowRoot && alias == helpers.FilePathSeparator {
-		return "", fmt.Errorf("Alias \"%s\" resolves to website root directory", originalAlias)
-	}
-
-	// Validate against directory traversal
-	if components[0] == ".." {
-		return "", fmt.Errorf("Alias \"%s\" traverses outside the website root directory", originalAlias)
-	}
-
-	// Handle Windows file and directory naming restrictions
-	// See "Naming Files, Paths, and Namespaces" on MSDN
-	// https://msdn.microsoft.com/en-us/library/aa365247%28v=VS.85%29.aspx?f=255&MSPPError=-2147217396
-	msgs := []string{}
-	reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}
-
-	if strings.ContainsAny(alias, ":*?\"<>|") {
-		msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains invalid characters on Windows: : * ? \" < > |", originalAlias))
-	}
-	for _, ch := range alias {
-		if ch < ' ' {
-			msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains ASCII control code (0x00 to 0x1F), invalid on Windows: : * ? \" < > |", originalAlias))
-			continue
-		}
-	}
-	for _, comp := range components {
-		if strings.HasSuffix(comp, " ") || strings.HasSuffix(comp, ".") {
-			msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with a trailing space or period, problematic on Windows", originalAlias))
-		}
-		for _, r := range reservedNames {
-			if comp == r {
-				msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with reserved name \"%s\" on Windows", originalAlias, r))
-			}
-		}
-	}
-	if len(msgs) > 0 {
-		if runtime.GOOS == "windows" {
-			for _, m := range msgs {
-				jww.ERROR.Println(m)
-			}
-			return "", fmt.Errorf("Cannot create \"%s\": Windows filename restriction", originalAlias)
-		}
-		for _, m := range msgs {
-			jww.WARN.Println(m)
-		}
-	}
-
-	// Add the final touch
-	alias = strings.TrimPrefix(alias, helpers.FilePathSeparator)
-	if strings.HasSuffix(alias, helpers.FilePathSeparator) {
-		alias = alias + "index.html"
-	} else if !strings.HasSuffix(alias, ".html") {
-		alias = alias + helpers.FilePathSeparator + "index.html"
-	}
-	if originalAlias != alias {
-		jww.INFO.Printf("Alias \"%s\" translated to \"%s\"\n", originalAlias, alias)
-	}
-
-	return filepath.Join(h.PublishDir, alias), nil
-}
-
-type AliasNode struct {
-	Permalink string
-	Page      interface{}
-}
-
-func (h *HTMLRedirectAlias) Publish(path string, permalink string, page interface{}) (err error) {
-	if path, err = h.Translate(path); err != nil {
-		jww.ERROR.Printf("%s, skipping.", err)
-		return nil
-	}
-
-	t := "alias"
-	if strings.HasSuffix(path, ".xhtml") {
-		t = "alias-xhtml"
-	}
-
-	template := defaultAliasTemplates
-	if h.Templates != nil {
-		template = h.Templates
-		t = "alias.html"
-	}
-
-	buffer := new(bytes.Buffer)
-	err = template.ExecuteTemplate(buffer, t, &AliasNode{permalink, page})
-	if err != nil {
-		return
-	}
-
-	return helpers.WriteToDisk(path, buffer, h.Fs.Destination)
-}
--- a/target/memory.go
+++ /dev/null
@@ -1,37 +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 target
-
-import (
-	"bytes"
-	"io"
-)
-
-type InMemoryTarget struct {
-	Files map[string][]byte
-}
-
-func (t *InMemoryTarget) Publish(label string, reader io.Reader) (err error) {
-	if t.Files == nil {
-		t.Files = make(map[string][]byte)
-	}
-	bytes := new(bytes.Buffer)
-	bytes.ReadFrom(reader)
-	t.Files[label] = bytes.Bytes()
-	return
-}
-
-func (t *InMemoryTarget) Translate(label string) (dest string, err error) {
-	return label, nil
-}
--- a/target/page.go
+++ /dev/null
@@ -1,103 +1,0 @@
-// Copyright 2016 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 target
-
-import (
-	"html/template"
-	"io"
-	"path/filepath"
-
-	"github.com/spf13/hugo/helpers"
-	"github.com/spf13/hugo/hugofs"
-)
-
-type PagePublisher interface {
-	Translator
-	Publish(string, template.HTML) error
-}
-
-type PagePub struct {
-	UglyURLs         bool
-	DefaultExtension string
-	PublishDir       string
-
-	// LangDir will contain the subdir for the language, i.e. "en", "de" etc.
-	// It will be empty if the site is rendered in root.
-	LangDir string
-
-	Fs *hugofs.Fs
-}
-
-func (pp *PagePub) Publish(path string, r io.Reader) (err error) {
-
-	translated, err := pp.Translate(path)
-	if err != nil {
-		return
-	}
-
-	return helpers.WriteToDisk(translated, r, pp.Fs.Destination)
-}
-
-func (pp *PagePub) Translate(src string) (dest string, err error) {
-	dir, err := pp.TranslateRelative(src)
-	if err != nil {
-		return dir, err
-	}
-	if pp.PublishDir != "" {
-		dir = filepath.Join(pp.PublishDir, dir)
-	}
-	return dir, nil
-}
-
-func (pp *PagePub) TranslateRelative(src string) (dest string, err error) {
-	if src == helpers.FilePathSeparator {
-		return "index.html", nil
-	}
-
-	dir, file := filepath.Split(src)
-	isRoot := dir == ""
-	ext := pp.extension(filepath.Ext(file))
-	name := filename(file)
-
-	// TODO(bep) Having all of this path logic here seems wrong, but I guess
-	// we'll clean this up when we redo the output files.
-	// This catches the home page in a language sub path. They should never
-	// have any ugly URLs.
-	if pp.LangDir != "" && dir == helpers.FilePathSeparator && name == pp.LangDir {
-		return filepath.Join(dir, name, "index"+ext), nil
-	}
-
-	if pp.UglyURLs || file == "index.html" || (isRoot && file == "404.html") {
-		return filepath.Join(dir, name+ext), nil
-	}
-
-	return filepath.Join(dir, name, "index"+ext), nil
-}
-
-func (pp *PagePub) extension(ext string) string {
-	switch ext {
-	case ".md", ".rst": // TODO make this list configurable.  page.go has the list of markup types.
-		return ".html"
-	}
-
-	if ext != "" {
-		return ext
-	}
-
-	if pp.DefaultExtension != "" {
-		return pp.DefaultExtension
-	}
-
-	return ".html"
-}
--- a/target/page_test.go
+++ /dev/null
@@ -1,113 +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 target
-
-import (
-	"path/filepath"
-	"testing"
-
-	"github.com/spf13/hugo/hugofs"
-	"github.com/spf13/viper"
-)
-
-func TestPageTranslator(t *testing.T) {
-	fs := hugofs.NewMem(viper.New())
-
-	tests := []struct {
-		content  string
-		expected string
-	}{
-		{"/", "index.html"},
-		{"index.html", "index.html"},
-		{"bar/index.html", "bar/index.html"},
-		{"foo", "foo/index.html"},
-		{"foo.html", "foo/index.html"},
-		{"foo.xhtml", "foo/index.xhtml"},
-		{"section", "section/index.html"},
-		{"section/", "section/index.html"},
-		{"section/foo", "section/foo/index.html"},
-		{"section/foo.html", "section/foo/index.html"},
-		{"section/foo.rss", "section/foo/index.rss"},
-	}
-
-	for _, test := range tests {
-		f := &PagePub{Fs: fs}
-		dest, err := f.Translate(filepath.FromSlash(test.content))
-		expected := filepath.FromSlash(test.expected)
-		if err != nil {
-			t.Fatalf("Translate returned and unexpected err: %s", err)
-		}
-
-		if dest != expected {
-			t.Errorf("Translate expected return: %s, got: %s", expected, dest)
-		}
-	}
-}
-
-func TestPageTranslatorBase(t *testing.T) {
-	tests := []struct {
-		content  string
-		expected string
-	}{
-		{"/", "a/base/index.html"},
-	}
-
-	for _, test := range tests {
-		f := &PagePub{PublishDir: "a/base"}
-		fts := &PagePub{PublishDir: "a/base/"}
-
-		for _, fs := range []*PagePub{f, fts} {
-			dest, err := fs.Translate(test.content)
-			if err != nil {
-				t.Fatalf("Translated returned and err: %s", err)
-			}
-
-			if dest != filepath.FromSlash(test.expected) {
-				t.Errorf("Translate expected: %s, got: %s", test.expected, dest)
-			}
-		}
-	}
-}
-
-func TestTranslateUglyURLs(t *testing.T) {
-	tests := []struct {
-		content  string
-		expected string
-	}{
-		{"foo.html", "foo.html"},
-		{"/", "index.html"},
-		{"section", "section.html"},
-		{"index.html", "index.html"},
-	}
-
-	for _, test := range tests {
-		f := &PagePub{UglyURLs: true}
-		dest, err := f.Translate(filepath.FromSlash(test.content))
-		if err != nil {
-			t.Fatalf("Translate returned an unexpected err: %s", err)
-		}
-
-		if dest != test.expected {
-			t.Errorf("Translate expected return: %s, got: %s", test.expected, dest)
-		}
-	}
-}
-
-func TestTranslateDefaultExtension(t *testing.T) {
-	f := &PagePub{DefaultExtension: ".foobar"}
-	dest, _ := f.Translate("baz")
-	if dest != filepath.FromSlash("baz/index.foobar") {
-		t.Errorf("Translate expected return: %s, got %s", "baz/index.foobar", dest)
-	}
-}
--