shithub: hugo

Download patch

ref: a3af4fe46e1e1d184538a83bc8375154a9669316
parent: 2a6b26a7a520bf88af1413f3f00c7e2782abe8cd
author: Bjørn Erik Pedersen <[email protected]>
date: Mon Feb 20 04:33:35 EST 2017

hugolib: Finish menu vs section content pages

This commit also fixes the default menu sort when the weight is 0.

Closes #2974

--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -167,6 +167,7 @@
 	}
 
 	for _, s := range h.Sites {
+		s.assembleMenus()
 		s.refreshPageCaches()
 		s.setupSitePages()
 	}
--- a/hugolib/hugo_sites_build_test.go
+++ b/hugolib/hugo_sites_build_test.go
@@ -1235,11 +1235,11 @@
 	return sites
 }
 
-func writeSource(t *testing.T, fs *hugofs.Fs, filename, content string) {
+func writeSource(t testing.TB, fs *hugofs.Fs, filename, content string) {
 	writeToFs(t, fs.Source, filename, content)
 }
 
-func writeToFs(t *testing.T, fs afero.Fs, filename, content string) {
+func writeToFs(t testing.TB, fs afero.Fs, filename, content string) {
 	if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0755); err != nil {
 		t.Fatalf("Failed to write file: %s", err)
 	}
--- a/hugolib/menu.go
+++ b/hugolib/menu.go
@@ -157,6 +157,15 @@
 		}
 		return m1.Name < m2.Name
 	}
+
+	if m2.Weight == 0 {
+		return true
+	}
+
+	if m1.Weight == 0 {
+		return false
+	}
+
 	return m1.Weight < m2.Weight
 }
 
--- /dev/null
+++ b/hugolib/menu_old_test.go
@@ -1,0 +1,693 @@
+// 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 hugolib
+
+// TODO(bep) remove this file when the reworked tests in menu_test.go is done.
+// NOTE: Do not add more tests to this file!
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/spf13/hugo/deps"
+
+	"path/filepath"
+
+	toml "github.com/pelletier/go-toml"
+	"github.com/spf13/hugo/source"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	confMenu1 = `
+[[menu.main]]
+    name = "Go Home"
+    url = "/"
+	weight = 1
+	pre = "<div>"
+	post = "</div>"
+[[menu.main]]
+    name = "Blog"
+    url = "/posts"
+[[menu.main]]
+    name = "ext"
+    url = "http://gohugo.io"
+	identifier = "ext"
+[[menu.main]]
+    name = "ext2"
+    url = "http://foo.local/Zoo/foo"
+	identifier = "ext2"
+[[menu.grandparent]]
+	name = "grandparent"
+	url = "/grandparent"
+	identifier = "grandparentId"
+[[menu.grandparent]]
+	name = "parent"
+	url = "/parent"
+	identifier = "parentId"
+	parent = "grandparentId"
+[[menu.grandparent]]
+	name = "Go Home3"
+    url = "/"
+	identifier = "grandchildId"
+	parent = "parentId"
+[[menu.tax]]
+	name = "Tax1"
+    url = "/two/key/"
+	identifier="1"
+[[menu.tax]]
+	name = "Tax2"
+    url = "/two/key/"
+	identifier="2"
+[[menu.tax]]
+	name = "Tax RSS"
+    url = "/two/key.xml"
+	identifier="xml"
+[[menu.hash]]
+   name = "Tax With #"
+   url = "/resource#anchor"
+   identifier="hash"
+[[menu.unicode]]
+   name = "Unicode Russian"
+   identifier = "unicode-russian"
+   url = "/новости-проекта"` // Russian => "news-project"
+)
+
+var menuPage1 = []byte(`+++
+title = "One"
+weight = 1
+[menu]
+	[menu.p_one]
++++
+Front Matter with Menu Pages`)
+
+var menuPage2 = []byte(`+++
+title = "Two"
+weight = 2
+[menu]
+	[menu.p_one]
+	[menu.p_two]
+		identifier = "Two"
+
++++
+Front Matter with Menu Pages`)
+
+var menuPage3 = []byte(`+++
+title = "Three"
+weight = 3
+[menu]
+	[menu.p_two]
+		Name = "Three"
+		Parent = "Two"
++++
+Front Matter with Menu Pages`)
+
+var menuPage4 = []byte(`+++
+title = "Four"
+weight = 4
+[menu]
+	[menu.p_two]
+		Name = "Four"
+		Parent = "Three"
++++
+Front Matter with Menu Pages`)
+
+var menuPageSources = []source.ByteSource{
+	{Name: filepath.FromSlash("sect/doc1.md"), Content: menuPage1},
+	{Name: filepath.FromSlash("sect/doc2.md"), Content: menuPage2},
+	{Name: filepath.FromSlash("sect/doc3.md"), Content: menuPage3},
+}
+
+var menuPageSectionsSources = []source.ByteSource{
+	{Name: filepath.FromSlash("first/doc1.md"), Content: menuPage1},
+	{Name: filepath.FromSlash("first/doc2.md"), Content: menuPage2},
+	{Name: filepath.FromSlash("second-section/doc3.md"), Content: menuPage3},
+	{Name: filepath.FromSlash("Fish and Chips/doc4.md"), Content: menuPage4},
+}
+
+func tstCreateMenuPageWithNameTOML(title, menu, name string) []byte {
+	return []byte(fmt.Sprintf(`+++
+title = "%s"
+weight = 1
+[menu]
+	[menu.%s]
+		name = "%s"
++++
+Front Matter with Menu with Name`, title, menu, name))
+}
+
+func tstCreateMenuPageWithIdentifierTOML(title, menu, identifier string) []byte {
+	return []byte(fmt.Sprintf(`+++
+title = "%s"
+weight = 1
+[menu]
+	[menu.%s]
+		identifier = "%s"
+		name = "somename"
++++
+Front Matter with Menu with Identifier`, title, menu, identifier))
+}
+
+func tstCreateMenuPageWithNameYAML(title, menu, name string) []byte {
+	return []byte(fmt.Sprintf(`---
+title: "%s"
+weight: 1
+menu:
+    %s:
+      name: "%s"
+---
+Front Matter with Menu with Name`, title, menu, name))
+}
+
+func tstCreateMenuPageWithIdentifierYAML(title, menu, identifier string) []byte {
+	return []byte(fmt.Sprintf(`---
+title: "%s"
+weight: 1
+menu:
+    %s:
+      identifier: "%s"
+      name: "somename"
+---
+Front Matter with Menu with Identifier`, title, menu, identifier))
+}
+
+// Issue 817 - identifier should trump everything
+func TestPageMenuWithIdentifier(t *testing.T) {
+	t.Parallel()
+	toml := []source.ByteSource{
+		{Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i1")},
+		{Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")},
+		{Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")}, // duplicate
+	}
+
+	yaml := []source.ByteSource{
+		{Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i1")},
+		{Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")},
+		{Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")}, // duplicate
+	}
+
+	doTestPageMenuWithIdentifier(t, toml)
+	doTestPageMenuWithIdentifier(t, yaml)
+
+}
+
+func doTestPageMenuWithIdentifier(t *testing.T, menuPageSources []source.ByteSource) {
+
+	s := setupMenuTests(t, menuPageSources)
+
+	assert.Equal(t, 3, len(s.RegularPages), "Not enough pages")
+
+	me1 := findTestMenuEntryByID(s, "m1", "i1")
+	me2 := findTestMenuEntryByID(s, "m1", "i2")
+
+	require.NotNil(t, me1)
+	require.NotNil(t, me2)
+
+	assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL)
+	assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL)
+
+}
+
+// Issue 817 contd - name should be second identifier in
+func TestPageMenuWithDuplicateName(t *testing.T) {
+	t.Parallel()
+	toml := []source.ByteSource{
+		{Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n1")},
+		{Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")},
+		{Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")}, // duplicate
+	}
+
+	yaml := []source.ByteSource{
+		{Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n1")},
+		{Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")},
+		{Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")}, // duplicate
+	}
+
+	doTestPageMenuWithDuplicateName(t, toml)
+	doTestPageMenuWithDuplicateName(t, yaml)
+
+}
+
+func doTestPageMenuWithDuplicateName(t *testing.T, menuPageSources []source.ByteSource) {
+
+	s := setupMenuTests(t, menuPageSources)
+
+	assert.Equal(t, 3, len(s.RegularPages), "Not enough pages")
+
+	me1 := findTestMenuEntryByName(s, "m1", "n1")
+	me2 := findTestMenuEntryByName(s, "m1", "n2")
+
+	require.NotNil(t, me1)
+	require.NotNil(t, me2)
+
+	assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL)
+	assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL)
+
+}
+
+func TestPageMenu(t *testing.T) {
+	t.Parallel()
+	s := setupMenuTests(t, menuPageSources)
+
+	if len(s.RegularPages) != 3 {
+		t.Fatalf("Posts not created, expected 3 got %d", len(s.RegularPages))
+	}
+
+	first := s.RegularPages[0]
+	second := s.RegularPages[1]
+	third := s.RegularPages[2]
+
+	pOne := findTestMenuEntryByName(s, "p_one", "One")
+	pTwo := findTestMenuEntryByID(s, "p_two", "Two")
+
+	for i, this := range []struct {
+		menu           string
+		page           *Page
+		menuItem       *MenuEntry
+		isMenuCurrent  bool
+		hasMenuCurrent bool
+	}{
+		{"p_one", first, pOne, true, false},
+		{"p_one", first, pTwo, false, false},
+		{"p_one", second, pTwo, false, false},
+		{"p_two", second, pTwo, true, false},
+		{"p_two", third, pTwo, false, true},
+		{"p_one", third, pTwo, false, false},
+	} {
+
+		if i != 4 {
+			continue
+		}
+
+		isMenuCurrent := this.page.IsMenuCurrent(this.menu, this.menuItem)
+		hasMenuCurrent := this.page.HasMenuCurrent(this.menu, this.menuItem)
+
+		if isMenuCurrent != this.isMenuCurrent {
+			t.Errorf("[%d] Wrong result from IsMenuCurrent: %v", i, isMenuCurrent)
+		}
+
+		if hasMenuCurrent != this.hasMenuCurrent {
+			t.Errorf("[%d] Wrong result for menuItem %v for HasMenuCurrent: %v", i, this.menuItem, hasMenuCurrent)
+		}
+
+	}
+
+}
+
+func TestMenuURL(t *testing.T) {
+	t.Parallel()
+	s := setupMenuTests(t, menuPageSources)
+
+	for i, this := range []struct {
+		me          *MenuEntry
+		expectedURL string
+	}{
+		// issue #888
+		{findTestMenuEntryByID(s, "hash", "hash"), "/Zoo/resource#anchor"},
+		// issue #1774
+		{findTestMenuEntryByID(s, "main", "ext"), "http://gohugo.io"},
+		{findTestMenuEntryByID(s, "main", "ext2"), "http://foo.local/Zoo/foo"},
+	} {
+
+		if this.me == nil {
+			t.Errorf("[%d] MenuEntry not found", i)
+			continue
+		}
+
+		if this.me.URL != this.expectedURL {
+			t.Errorf("[%d] Got URL %s expected %s", i, this.me.URL, this.expectedURL)
+		}
+
+	}
+
+}
+
+// Issue #1934
+func TestYAMLMenuWithMultipleEntries(t *testing.T) {
+	t.Parallel()
+	ps1 := []byte(`---
+title: "Yaml 1"
+weight: 5
+menu: ["p_one", "p_two"]
+---
+Yaml Front Matter with Menu Pages`)
+
+	ps2 := []byte(`---
+title: "Yaml 2"
+weight: 5
+menu:
+    p_three:
+    p_four:
+---
+Yaml Front Matter with Menu Pages`)
+
+	s := setupMenuTests(t, []source.ByteSource{
+		{Name: filepath.FromSlash("sect/yaml1.md"), Content: ps1},
+		{Name: filepath.FromSlash("sect/yaml2.md"), Content: ps2}})
+
+	p1 := s.RegularPages[0]
+	assert.Len(t, p1.Menus(), 2, "List YAML")
+	p2 := s.RegularPages[1]
+	assert.Len(t, p2.Menus(), 2, "Map YAML")
+
+}
+
+// issue #719
+func TestMenuWithUnicodeURLs(t *testing.T) {
+	t.Parallel()
+	for _, canonifyURLs := range []bool{true, false} {
+		doTestMenuWithUnicodeURLs(t, canonifyURLs)
+	}
+}
+
+func doTestMenuWithUnicodeURLs(t *testing.T, canonifyURLs bool) {
+
+	s := setupMenuTests(t, menuPageSources, "canonifyURLs", canonifyURLs)
+
+	unicodeRussian := findTestMenuEntryByID(s, "unicode", "unicode-russian")
+
+	expected := "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0"
+
+	if !canonifyURLs {
+		expected = "/Zoo" + expected
+	}
+
+	assert.Equal(t, expected, unicodeRussian.URL)
+}
+
+// Issue #1114
+func TestSectionPagesMenu2(t *testing.T) {
+	t.Parallel()
+	doTestSectionPagesMenu(true, t)
+	doTestSectionPagesMenu(false, t)
+}
+
+func doTestSectionPagesMenu(canonifyURLs bool, t *testing.T) {
+
+	s := setupMenuTests(t, menuPageSectionsSources,
+		"sectionPagesMenu", "spm",
+		"canonifyURLs", canonifyURLs,
+	)
+
+	require.Equal(t, 3, len(s.Sections))
+
+	firstSectionPages := s.Sections["first"]
+	require.Equal(t, 2, len(firstSectionPages))
+	secondSectionPages := s.Sections["second-section"]
+	require.Equal(t, 1, len(secondSectionPages))
+	fishySectionPages := s.Sections["fish-and-chips"]
+	require.Equal(t, 1, len(fishySectionPages))
+
+	nodeFirst := s.getPage(KindSection, "first")
+	require.NotNil(t, nodeFirst)
+	nodeSecond := s.getPage(KindSection, "second-section")
+	require.NotNil(t, nodeSecond)
+	nodeFishy := s.getPage(KindSection, "fish-and-chips")
+	require.Equal(t, "fish-and-chips", nodeFishy.sections[0])
+
+	firstSectionMenuEntry := findTestMenuEntryByID(s, "spm", "first")
+	secondSectionMenuEntry := findTestMenuEntryByID(s, "spm", "second-section")
+	fishySectionMenuEntry := findTestMenuEntryByID(s, "spm", "fish-and-chips")
+
+	require.NotNil(t, firstSectionMenuEntry)
+	require.NotNil(t, secondSectionMenuEntry)
+	require.NotNil(t, nodeFirst)
+	require.NotNil(t, nodeSecond)
+	require.NotNil(t, fishySectionMenuEntry)
+	require.NotNil(t, nodeFishy)
+
+	require.True(t, nodeFirst.IsMenuCurrent("spm", firstSectionMenuEntry))
+	require.False(t, nodeFirst.IsMenuCurrent("spm", secondSectionMenuEntry))
+	require.False(t, nodeFirst.IsMenuCurrent("spm", fishySectionMenuEntry))
+	require.True(t, nodeFishy.IsMenuCurrent("spm", fishySectionMenuEntry))
+	require.Equal(t, "Fish and Chips", fishySectionMenuEntry.Name)
+
+	for _, p := range firstSectionPages {
+		require.True(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
+		require.False(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
+	}
+
+	for _, p := range secondSectionPages {
+		require.False(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
+		require.True(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
+	}
+
+	for _, p := range fishySectionPages {
+		require.False(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
+		require.False(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
+		require.True(t, p.Page.HasMenuCurrent("spm", fishySectionMenuEntry))
+	}
+}
+
+func TestTaxonomyNodeMenu(t *testing.T) {
+	t.Parallel()
+
+	type taxRenderInfo struct {
+		key      string
+		singular string
+		plural   string
+	}
+
+	s := setupMenuTests(t, menuPageSources, "canonifyURLs", true)
+
+	for i, this := range []struct {
+		menu           string
+		taxInfo        taxRenderInfo
+		menuItem       *MenuEntry
+		isMenuCurrent  bool
+		hasMenuCurrent bool
+	}{
+		{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
+			findTestMenuEntryByID(s, "tax", "1"), true, false},
+		{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
+			findTestMenuEntryByID(s, "tax", "2"), true, false},
+		{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
+			&MenuEntry{Name: "Somewhere else", URL: "/somewhereelse"}, false, false},
+	} {
+
+		p := s.newTaxonomyPage(this.taxInfo.plural, this.taxInfo.key)
+
+		isMenuCurrent := p.IsMenuCurrent(this.menu, this.menuItem)
+		hasMenuCurrent := p.HasMenuCurrent(this.menu, this.menuItem)
+
+		if isMenuCurrent != this.isMenuCurrent {
+			t.Errorf("[%d] Wrong result from IsMenuCurrent: %v", i, isMenuCurrent)
+		}
+
+		if hasMenuCurrent != this.hasMenuCurrent {
+			t.Errorf("[%d] Wrong result for menuItem %v for HasMenuCurrent: %v", i, this.menuItem, hasMenuCurrent)
+		}
+
+	}
+
+	menuEntryXML := findTestMenuEntryByID(s, "tax", "xml")
+
+	if strings.HasSuffix(menuEntryXML.URL, "/") {
+		t.Error("RSS menu item should not be padded with trailing slash")
+	}
+}
+
+func TestMenuLimit(t *testing.T) {
+	t.Parallel()
+	s := setupMenuTests(t, menuPageSources)
+	m := *s.Menus["main"]
+
+	// main menu has 4 entries
+	firstTwo := m.Limit(2)
+	assert.Equal(t, 2, len(firstTwo))
+	for i := 0; i < 2; i++ {
+		assert.Equal(t, m[i], firstTwo[i])
+	}
+	assert.Equal(t, m, m.Limit(4))
+	assert.Equal(t, m, m.Limit(5))
+}
+
+func TestMenuSortByN(t *testing.T) {
+	t.Parallel()
+	for i, this := range []struct {
+		sortFunc   func(p Menu) Menu
+		assertFunc func(p Menu) bool
+	}{
+		{(Menu).Sort, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }},
+		{(Menu).ByWeight, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }},
+		{(Menu).ByName, func(p Menu) bool { return p[0].Name == "na" }},
+		{(Menu).Reverse, func(p Menu) bool { return p[0].Identifier == "ib" && p[len(p)-1].Identifier == "ia" }},
+	} {
+		menu := Menu{&MenuEntry{Weight: 3, Name: "nb", Identifier: "ia"},
+			&MenuEntry{Weight: 1, Name: "na", Identifier: "ic"},
+			&MenuEntry{Weight: 1, Name: "nx", Identifier: "ic"},
+			&MenuEntry{Weight: 2, Name: "nb", Identifier: "ix"},
+			&MenuEntry{Weight: 2, Name: "nb", Identifier: "ib"}}
+
+		sorted := this.sortFunc(menu)
+
+		if !this.assertFunc(sorted) {
+			t.Errorf("[%d] sort error", i)
+		}
+	}
+
+}
+
+func TestHomeNodeMenu(t *testing.T) {
+	t.Parallel()
+	s := setupMenuTests(t, menuPageSources,
+		"canonifyURLs", true,
+		"uglyURLs", false,
+	)
+
+	home := s.getPage(KindHome)
+	homeMenuEntry := &MenuEntry{Name: home.Title, URL: home.URL()}
+
+	for i, this := range []struct {
+		menu           string
+		menuItem       *MenuEntry
+		isMenuCurrent  bool
+		hasMenuCurrent bool
+	}{
+		{"main", homeMenuEntry, true, false},
+		{"doesnotexist", homeMenuEntry, false, false},
+		{"main", &MenuEntry{Name: "Somewhere else", URL: "/somewhereelse"}, false, false},
+		{"grandparent", findTestMenuEntryByID(s, "grandparent", "grandparentId"), false, true},
+		{"grandparent", findTestMenuEntryByID(s, "grandparent", "parentId"), false, true},
+		{"grandparent", findTestMenuEntryByID(s, "grandparent", "grandchildId"), true, false},
+	} {
+
+		isMenuCurrent := home.IsMenuCurrent(this.menu, this.menuItem)
+		hasMenuCurrent := home.HasMenuCurrent(this.menu, this.menuItem)
+
+		if isMenuCurrent != this.isMenuCurrent {
+			fmt.Println("isMenuCurrent", isMenuCurrent)
+			fmt.Printf("this: %#v\n", this)
+			t.Errorf("[%d] Wrong result from IsMenuCurrent: %v for %q", i, isMenuCurrent, this.menuItem)
+		}
+
+		if hasMenuCurrent != this.hasMenuCurrent {
+			fmt.Println("hasMenuCurrent", hasMenuCurrent)
+			fmt.Printf("this: %#v\n", this)
+			t.Errorf("[%d] Wrong result for menu %q menuItem %v for HasMenuCurrent: %v", i, this.menu, this.menuItem, hasMenuCurrent)
+		}
+	}
+}
+
+func TestHopefullyUniqueID(t *testing.T) {
+	t.Parallel()
+	assert.Equal(t, "i", (&MenuEntry{Identifier: "i", URL: "u", Name: "n"}).hopefullyUniqueID())
+	assert.Equal(t, "u", (&MenuEntry{Identifier: "", URL: "u", Name: "n"}).hopefullyUniqueID())
+	assert.Equal(t, "n", (&MenuEntry{Identifier: "", URL: "", Name: "n"}).hopefullyUniqueID())
+}
+
+func TestAddMenuEntryChild(t *testing.T) {
+	t.Parallel()
+	root := &MenuEntry{Weight: 1}
+	root.addChild(&MenuEntry{Weight: 2})
+	root.addChild(&MenuEntry{Weight: 1})
+	assert.Equal(t, 2, len(root.Children))
+	assert.Equal(t, 1, root.Children[0].Weight)
+}
+
+var testMenuIdentityMatcher = func(me *MenuEntry, id string) bool { return me.Identifier == id }
+var testMenuNameMatcher = func(me *MenuEntry, id string) bool { return me.Name == id }
+
+func findTestMenuEntryByID(s *Site, mn string, id string) *MenuEntry {
+	return findTestMenuEntry(s, mn, id, testMenuIdentityMatcher)
+}
+func findTestMenuEntryByName(s *Site, mn string, id string) *MenuEntry {
+	return findTestMenuEntry(s, mn, id, testMenuNameMatcher)
+}
+
+func findTestMenuEntry(s *Site, mn string, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry {
+	var found *MenuEntry
+	if menu, ok := s.Menus[mn]; ok {
+		for _, me := range *menu {
+
+			if matcher(me, id) {
+				if found != nil {
+					panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id))
+				}
+				found = me
+			}
+
+			descendant := findDescendantTestMenuEntry(me, id, matcher)
+			if descendant != nil {
+				if found != nil {
+					panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id))
+				}
+				found = descendant
+			}
+		}
+	}
+	return found
+}
+
+func findDescendantTestMenuEntry(parent *MenuEntry, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry {
+	var found *MenuEntry
+	if parent.HasChildren() {
+		for _, child := range parent.Children {
+
+			if matcher(child, id) {
+				if found != nil {
+					panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id))
+				}
+				found = child
+			}
+
+			descendant := findDescendantTestMenuEntry(child, id, matcher)
+			if descendant != nil {
+				if found != nil {
+					panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id))
+				}
+				found = descendant
+			}
+		}
+	}
+	return found
+}
+
+func setupMenuTests(t *testing.T, pageSources []source.ByteSource, configKeyValues ...interface{}) *Site {
+
+	var (
+		cfg, fs = newTestCfg()
+	)
+
+	menus, err := tomlToMap(confMenu1)
+	require.NoError(t, err)
+
+	cfg.Set("menu", menus["menu"])
+	cfg.Set("baseURL", "http://foo.local/Zoo/")
+
+	for i := 0; i < len(configKeyValues); i += 2 {
+		cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
+	}
+
+	for _, src := range pageSources {
+		writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content))
+
+	}
+
+	return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
+
+}
+
+func tomlToMap(s string) (map[string]interface{}, error) {
+	tree, err := toml.Load(s)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return tree.ToMap(), nil
+
+}
--- a/hugolib/menu_test.go
+++ b/hugolib/menu_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Hugo Authors. All rights reserved.
+// 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.
@@ -14,677 +14,81 @@
 package hugolib
 
 import (
-	"fmt"
-	"strings"
 	"testing"
 
-	"github.com/spf13/hugo/deps"
+	"fmt"
 
-	"path/filepath"
-
-	toml "github.com/pelletier/go-toml"
-	"github.com/spf13/hugo/source"
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
 const (
-	confMenu1 = `
-[[menu.main]]
-    name = "Go Home"
-    url = "/"
-	weight = 1
-	pre = "<div>"
-	post = "</div>"
-[[menu.main]]
-    name = "Blog"
-    url = "/posts"
-[[menu.main]]
-    name = "ext"
-    url = "http://gohugo.io"
-	identifier = "ext"
-[[menu.main]]
-    name = "ext2"
-    url = "http://foo.local/Zoo/foo"
-	identifier = "ext2"
-[[menu.grandparent]]
-	name = "grandparent"
-	url = "/grandparent"
-	identifier = "grandparentId"
-[[menu.grandparent]]
-	name = "parent"
-	url = "/parent"
-	identifier = "parentId"
-	parent = "grandparentId"
-[[menu.grandparent]]
-	name = "Go Home3"
-    url = "/"
-	identifier = "grandchildId"
-	parent = "parentId"
-[[menu.tax]]
-	name = "Tax1"
-    url = "/two/key/"
-	identifier="1"
-[[menu.tax]]
-	name = "Tax2"
-    url = "/two/key/"
-	identifier="2"
-[[menu.tax]]
-	name = "Tax RSS"
-    url = "/two/key.xml"
-	identifier="xml"
-[[menu.hash]]
-   name = "Tax With #"
-   url = "/resource#anchor"
-   identifier="hash"
-[[menu.unicode]]
-   name = "Unicode Russian"
-   identifier = "unicode-russian"
-   url = "/новости-проекта"` // Russian => "news-project"
-)
-
-var menuPage1 = []byte(`+++
-title = "One"
-weight = 1
-[menu]
-	[menu.p_one]
-+++
-Front Matter with Menu Pages`)
-
-var menuPage2 = []byte(`+++
-title = "Two"
-weight = 2
-[menu]
-	[menu.p_one]
-	[menu.p_two]
-		identifier = "Two"
-
-+++
-Front Matter with Menu Pages`)
-
-var menuPage3 = []byte(`+++
-title = "Three"
-weight = 3
-[menu]
-	[menu.p_two]
-		Name = "Three"
-		Parent = "Two"
-+++
-Front Matter with Menu Pages`)
-
-var menuPage4 = []byte(`+++
-title = "Four"
-weight = 4
-[menu]
-	[menu.p_two]
-		Name = "Four"
-		Parent = "Three"
-+++
-Front Matter with Menu Pages`)
-
-var menuPageSources = []source.ByteSource{
-	{Name: filepath.FromSlash("sect/doc1.md"), Content: menuPage1},
-	{Name: filepath.FromSlash("sect/doc2.md"), Content: menuPage2},
-	{Name: filepath.FromSlash("sect/doc3.md"), Content: menuPage3},
-}
-
-var menuPageSectionsSources = []source.ByteSource{
-	{Name: filepath.FromSlash("first/doc1.md"), Content: menuPage1},
-	{Name: filepath.FromSlash("first/doc2.md"), Content: menuPage2},
-	{Name: filepath.FromSlash("second-section/doc3.md"), Content: menuPage3},
-	{Name: filepath.FromSlash("Fish and Chips/doc4.md"), Content: menuPage4},
-}
-
-func tstCreateMenuPageWithNameTOML(title, menu, name string) []byte {
-	return []byte(fmt.Sprintf(`+++
-title = "%s"
-weight = 1
-[menu]
-	[menu.%s]
-		name = "%s"
-+++
-Front Matter with Menu with Name`, title, menu, name))
-}
-
-func tstCreateMenuPageWithIdentifierTOML(title, menu, identifier string) []byte {
-	return []byte(fmt.Sprintf(`+++
-title = "%s"
-weight = 1
-[menu]
-	[menu.%s]
-		identifier = "%s"
-		name = "somename"
-+++
-Front Matter with Menu with Identifier`, title, menu, identifier))
-}
-
-func tstCreateMenuPageWithNameYAML(title, menu, name string) []byte {
-	return []byte(fmt.Sprintf(`---
-title: "%s"
-weight: 1
+	menuPageTemplate = `---
+title: %q
+weight: %d
 menu:
-    %s:
-      name: "%s"
+  %s:
+    weight: %d
 ---
-Front Matter with Menu with Name`, title, menu, name))
-}
+# Doc Menu
+`
+)
 
-func tstCreateMenuPageWithIdentifierYAML(title, menu, identifier string) []byte {
-	return []byte(fmt.Sprintf(`---
-title: "%s"
-weight: 1
-menu:
-    %s:
-      identifier: "%s"
-      name: "somename"
----
-Front Matter with Menu with Identifier`, title, menu, identifier))
-}
-
-// Issue 817 - identifier should trump everything
-func TestPageMenuWithIdentifier(t *testing.T) {
-	t.Parallel()
-	toml := []source.ByteSource{
-		{Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i1")},
-		{Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")},
-		{Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")}, // duplicate
-	}
-
-	yaml := []source.ByteSource{
-		{Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i1")},
-		{Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")},
-		{Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")}, // duplicate
-	}
-
-	doTestPageMenuWithIdentifier(t, toml)
-	doTestPageMenuWithIdentifier(t, yaml)
-
-}
-
-func doTestPageMenuWithIdentifier(t *testing.T, menuPageSources []source.ByteSource) {
-
-	s := setupMenuTests(t, menuPageSources)
-
-	assert.Equal(t, 3, len(s.RegularPages), "Not enough pages")
-
-	me1 := findTestMenuEntryByID(s, "m1", "i1")
-	me2 := findTestMenuEntryByID(s, "m1", "i2")
-
-	require.NotNil(t, me1)
-	require.NotNil(t, me2)
-
-	assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL)
-	assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL)
-
-}
-
-// Issue 817 contd - name should be second identifier in
-func TestPageMenuWithDuplicateName(t *testing.T) {
-	t.Parallel()
-	toml := []source.ByteSource{
-		{Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n1")},
-		{Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")},
-		{Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")}, // duplicate
-	}
-
-	yaml := []source.ByteSource{
-		{Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n1")},
-		{Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")},
-		{Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")}, // duplicate
-	}
-
-	doTestPageMenuWithDuplicateName(t, toml)
-	doTestPageMenuWithDuplicateName(t, yaml)
-
-}
-
-func doTestPageMenuWithDuplicateName(t *testing.T, menuPageSources []source.ByteSource) {
-
-	s := setupMenuTests(t, menuPageSources)
-
-	assert.Equal(t, 3, len(s.RegularPages), "Not enough pages")
-
-	me1 := findTestMenuEntryByName(s, "m1", "n1")
-	me2 := findTestMenuEntryByName(s, "m1", "n2")
-
-	require.NotNil(t, me1)
-	require.NotNil(t, me2)
-
-	assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL)
-	assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL)
-
-}
-
-func TestPageMenu(t *testing.T) {
-	t.Parallel()
-	s := setupMenuTests(t, menuPageSources)
-
-	if len(s.RegularPages) != 3 {
-		t.Fatalf("Posts not created, expected 3 got %d", len(s.RegularPages))
-	}
-
-	first := s.RegularPages[0]
-	second := s.RegularPages[1]
-	third := s.RegularPages[2]
-
-	pOne := findTestMenuEntryByName(s, "p_one", "One")
-	pTwo := findTestMenuEntryByID(s, "p_two", "Two")
-
-	for i, this := range []struct {
-		menu           string
-		page           *Page
-		menuItem       *MenuEntry
-		isMenuCurrent  bool
-		hasMenuCurrent bool
-	}{
-		{"p_one", first, pOne, true, false},
-		{"p_one", first, pTwo, false, false},
-		{"p_one", second, pTwo, false, false},
-		{"p_two", second, pTwo, true, false},
-		{"p_two", third, pTwo, false, true},
-		{"p_one", third, pTwo, false, false},
-	} {
-
-		if i != 4 {
-			continue
-		}
-
-		isMenuCurrent := this.page.IsMenuCurrent(this.menu, this.menuItem)
-		hasMenuCurrent := this.page.HasMenuCurrent(this.menu, this.menuItem)
-
-		if isMenuCurrent != this.isMenuCurrent {
-			t.Errorf("[%d] Wrong result from IsMenuCurrent: %v", i, isMenuCurrent)
-		}
-
-		if hasMenuCurrent != this.hasMenuCurrent {
-			t.Errorf("[%d] Wrong result for menuItem %v for HasMenuCurrent: %v", i, this.menuItem, hasMenuCurrent)
-		}
-
-	}
-
-}
-
-func TestMenuURL(t *testing.T) {
-	t.Parallel()
-	s := setupMenuTests(t, menuPageSources)
-
-	for i, this := range []struct {
-		me          *MenuEntry
-		expectedURL string
-	}{
-		// issue #888
-		{findTestMenuEntryByID(s, "hash", "hash"), "/Zoo/resource#anchor"},
-		// issue #1774
-		{findTestMenuEntryByID(s, "main", "ext"), "http://gohugo.io"},
-		{findTestMenuEntryByID(s, "main", "ext2"), "http://foo.local/Zoo/foo"},
-	} {
-
-		if this.me == nil {
-			t.Errorf("[%d] MenuEntry not found", i)
-			continue
-		}
-
-		if this.me.URL != this.expectedURL {
-			t.Errorf("[%d] Got URL %s expected %s", i, this.me.URL, this.expectedURL)
-		}
-
-	}
-
-}
-
-// Issue #1934
-func TestYAMLMenuWithMultipleEntries(t *testing.T) {
-	t.Parallel()
-	ps1 := []byte(`---
-title: "Yaml 1"
-weight: 5
-menu: ["p_one", "p_two"]
----
-Yaml Front Matter with Menu Pages`)
-
-	ps2 := []byte(`---
-title: "Yaml 2"
-weight: 5
-menu:
-    p_three:
-    p_four:
----
-Yaml Front Matter with Menu Pages`)
-
-	s := setupMenuTests(t, []source.ByteSource{
-		{Name: filepath.FromSlash("sect/yaml1.md"), Content: ps1},
-		{Name: filepath.FromSlash("sect/yaml2.md"), Content: ps2}})
-
-	p1 := s.RegularPages[0]
-	assert.Len(t, p1.Menus(), 2, "List YAML")
-	p2 := s.RegularPages[1]
-	assert.Len(t, p2.Menus(), 2, "Map YAML")
-
-}
-
-// issue #719
-func TestMenuWithUnicodeURLs(t *testing.T) {
-	t.Parallel()
-	for _, canonifyURLs := range []bool{true, false} {
-		doTestMenuWithUnicodeURLs(t, canonifyURLs)
-	}
-}
-
-func doTestMenuWithUnicodeURLs(t *testing.T, canonifyURLs bool) {
-
-	s := setupMenuTests(t, menuPageSources, "canonifyURLs", canonifyURLs)
-
-	unicodeRussian := findTestMenuEntryByID(s, "unicode", "unicode-russian")
-
-	expected := "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0"
-
-	if !canonifyURLs {
-		expected = "/Zoo" + expected
-	}
-
-	assert.Equal(t, expected, unicodeRussian.URL)
-}
-
-// Issue #1114
 func TestSectionPagesMenu(t *testing.T) {
 	t.Parallel()
-	doTestSectionPagesMenu(true, t)
-	doTestSectionPagesMenu(false, t)
-}
 
-func doTestSectionPagesMenu(canonifyURLs bool, t *testing.T) {
+	siteConfig := `
+baseurl = "http://example.com/"
+title = "Section Menu"
+sectionPagesMenu = "sect"
+`
 
-	s := setupMenuTests(t, menuPageSectionsSources,
-		"sectionPagesMenu", "spm",
-		"canonifyURLs", canonifyURLs,
+	th, h := newTestSitesFromConfig(t, siteConfig,
+		"layouts/partials/menu.html", `{{- $p := .page -}}
+{{- $m := .menu -}}
+{{ range (index $p.Site.Menus $m) -}}
+{{- .URL }}|{{ .Name }}|{{ .Weight -}}|
+{{- if $p.IsMenuCurrent $m . }}IsMenuCurrent{{ else }}-{{ end -}}|
+{{- if $p.HasMenuCurrent $m . }}HasMenuCurrent{{ else }}-{{ end -}}|
+{{- end -}}
+`,
+		"layouts/_default/single.html",
+		`Single|{{ .Title }}
+Menu Sect:  {{ partial "menu.html" (dict "page" . "menu" "sect") }}
+Menu Main:  {{ partial "menu.html" (dict "page" . "menu" "main") }}`,
+		"layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}",
 	)
+	require.Len(t, h.Sites, 1)
 
-	require.Equal(t, 3, len(s.Sections))
+	fs := th.Fs
 
-	firstSectionPages := s.Sections["first"]
-	require.Equal(t, 2, len(firstSectionPages))
-	secondSectionPages := s.Sections["second-section"]
-	require.Equal(t, 1, len(secondSectionPages))
-	fishySectionPages := s.Sections["fish-and-chips"]
-	require.Equal(t, 1, len(fishySectionPages))
+	writeSource(t, fs, "content/sect1/p1.md", fmt.Sprintf(menuPageTemplate, "p1", 1, "main", 40))
+	writeSource(t, fs, "content/sect1/p2.md", fmt.Sprintf(menuPageTemplate, "p2", 2, "main", 30))
+	writeSource(t, fs, "content/sect2/p3.md", fmt.Sprintf(menuPageTemplate, "p3", 3, "main", 20))
+	writeSource(t, fs, "content/sect2/p4.md", fmt.Sprintf(menuPageTemplate, "p4", 4, "main", 10))
+	writeSource(t, fs, "content/sect3/p5.md", fmt.Sprintf(menuPageTemplate, "p5", 5, "main", 5))
 
-	nodeFirst := s.getPage(KindSection, "first")
-	require.NotNil(t, nodeFirst)
-	nodeSecond := s.getPage(KindSection, "second-section")
-	require.NotNil(t, nodeSecond)
-	nodeFishy := s.getPage(KindSection, "fish-and-chips")
-	require.Equal(t, "fish-and-chips", nodeFishy.sections[0])
+	writeNewContentFile(t, fs, "Section One", "2017-01-01", "content/sect1/_index.md", 100)
+	writeNewContentFile(t, fs, "Section Five", "2017-01-01", "content/sect5/_index.md", 10)
 
-	firstSectionMenuEntry := findTestMenuEntryByID(s, "spm", "first")
-	secondSectionMenuEntry := findTestMenuEntryByID(s, "spm", "second-section")
-	fishySectionMenuEntry := findTestMenuEntryByID(s, "spm", "Fish and Chips")
+	err := h.Build(BuildCfg{})
 
-	require.NotNil(t, firstSectionMenuEntry)
-	require.NotNil(t, secondSectionMenuEntry)
-	require.NotNil(t, nodeFirst)
-	require.NotNil(t, nodeSecond)
-	require.NotNil(t, fishySectionMenuEntry)
-	require.NotNil(t, nodeFishy)
+	require.NoError(t, err)
 
-	require.True(t, nodeFirst.IsMenuCurrent("spm", firstSectionMenuEntry))
-	require.False(t, nodeFirst.IsMenuCurrent("spm", secondSectionMenuEntry))
-	require.False(t, nodeFirst.IsMenuCurrent("spm", fishySectionMenuEntry))
-	require.True(t, nodeFishy.IsMenuCurrent("spm", fishySectionMenuEntry))
-	require.Equal(t, "Fish and Chips", fishySectionMenuEntry.Name)
+	s := h.Sites[0]
 
-	for _, p := range firstSectionPages {
-		require.True(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
-		require.False(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
-	}
+	require.Len(t, s.Menus, 2)
 
-	for _, p := range secondSectionPages {
-		require.False(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
-		require.True(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
-	}
+	p1 := s.RegularPages[0].Menus()
 
-	for _, p := range fishySectionPages {
-		require.False(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
-		require.False(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
-		require.True(t, p.Page.HasMenuCurrent("spm", fishySectionMenuEntry))
-	}
-}
+	// There is only one menu in the page, but it is "member of" 2
+	require.Len(t, p1, 1)
 
-func TestTaxonomyNodeMenu(t *testing.T) {
-	t.Parallel()
-
-	type taxRenderInfo struct {
-		key      string
-		singular string
-		plural   string
-	}
-
-	s := setupMenuTests(t, menuPageSources, "canonifyURLs", true)
-
-	for i, this := range []struct {
-		menu           string
-		taxInfo        taxRenderInfo
-		menuItem       *MenuEntry
-		isMenuCurrent  bool
-		hasMenuCurrent bool
-	}{
-		{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
-			findTestMenuEntryByID(s, "tax", "1"), true, false},
-		{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
-			findTestMenuEntryByID(s, "tax", "2"), true, false},
-		{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
-			&MenuEntry{Name: "Somewhere else", URL: "/somewhereelse"}, false, false},
-	} {
-
-		p := s.newTaxonomyPage(this.taxInfo.plural, this.taxInfo.key)
-
-		isMenuCurrent := p.IsMenuCurrent(this.menu, this.menuItem)
-		hasMenuCurrent := p.HasMenuCurrent(this.menu, this.menuItem)
-
-		if isMenuCurrent != this.isMenuCurrent {
-			t.Errorf("[%d] Wrong result from IsMenuCurrent: %v", i, isMenuCurrent)
-		}
-
-		if hasMenuCurrent != this.hasMenuCurrent {
-			t.Errorf("[%d] Wrong result for menuItem %v for HasMenuCurrent: %v", i, this.menuItem, hasMenuCurrent)
-		}
-
-	}
-
-	menuEntryXML := findTestMenuEntryByID(s, "tax", "xml")
-
-	if strings.HasSuffix(menuEntryXML.URL, "/") {
-		t.Error("RSS menu item should not be padded with trailing slash")
-	}
-}
-
-func TestMenuLimit(t *testing.T) {
-	t.Parallel()
-	s := setupMenuTests(t, menuPageSources)
-	m := *s.Menus["main"]
-
-	// main menu has 4 entries
-	firstTwo := m.Limit(2)
-	assert.Equal(t, 2, len(firstTwo))
-	for i := 0; i < 2; i++ {
-		assert.Equal(t, m[i], firstTwo[i])
-	}
-	assert.Equal(t, m, m.Limit(4))
-	assert.Equal(t, m, m.Limit(5))
-}
-
-func TestMenuSortByN(t *testing.T) {
-	t.Parallel()
-	for i, this := range []struct {
-		sortFunc   func(p Menu) Menu
-		assertFunc func(p Menu) bool
-	}{
-		{(Menu).Sort, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }},
-		{(Menu).ByWeight, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }},
-		{(Menu).ByName, func(p Menu) bool { return p[0].Name == "na" }},
-		{(Menu).Reverse, func(p Menu) bool { return p[0].Identifier == "ib" && p[len(p)-1].Identifier == "ia" }},
-	} {
-		menu := Menu{&MenuEntry{Weight: 3, Name: "nb", Identifier: "ia"},
-			&MenuEntry{Weight: 1, Name: "na", Identifier: "ic"},
-			&MenuEntry{Weight: 1, Name: "nx", Identifier: "ic"},
-			&MenuEntry{Weight: 2, Name: "nb", Identifier: "ix"},
-			&MenuEntry{Weight: 2, Name: "nb", Identifier: "ib"}}
-
-		sorted := this.sortFunc(menu)
-
-		if !this.assertFunc(sorted) {
-			t.Errorf("[%d] sort error", i)
-		}
-	}
-
-}
-
-func TestHomeNodeMenu(t *testing.T) {
-	t.Parallel()
-	s := setupMenuTests(t, menuPageSources,
-		"canonifyURLs", true,
-		"uglyURLs", false,
+	th.assertFileContent("public/sect1/p1/index.html", "Single",
+		"Menu Sect:  /sect5/|Section Five|10|-|-|/sect1/|Section One|100|-|HasMenuCurrent|/sect2/|Sect2s|0|-|-|/sect3/|Sect3s|0|-|-|",
+		"Menu Main:  /sect3/p5/|p5|5|-|-|/sect2/p4/|p4|10|-|-|/sect2/p3/|p3|20|-|-|/sect1/p2/|p2|30|-|-|/sect1/p1/|p1|40|IsMenuCurrent|-|",
 	)
 
-	home := s.getPage(KindHome)
-	homeMenuEntry := &MenuEntry{Name: home.Title, URL: home.URL()}
-
-	for i, this := range []struct {
-		menu           string
-		menuItem       *MenuEntry
-		isMenuCurrent  bool
-		hasMenuCurrent bool
-	}{
-		{"main", homeMenuEntry, true, false},
-		{"doesnotexist", homeMenuEntry, false, false},
-		{"main", &MenuEntry{Name: "Somewhere else", URL: "/somewhereelse"}, false, false},
-		{"grandparent", findTestMenuEntryByID(s, "grandparent", "grandparentId"), false, true},
-		{"grandparent", findTestMenuEntryByID(s, "grandparent", "parentId"), false, true},
-		{"grandparent", findTestMenuEntryByID(s, "grandparent", "grandchildId"), true, false},
-	} {
-
-		isMenuCurrent := home.IsMenuCurrent(this.menu, this.menuItem)
-		hasMenuCurrent := home.HasMenuCurrent(this.menu, this.menuItem)
-
-		if isMenuCurrent != this.isMenuCurrent {
-			fmt.Println("isMenuCurrent", isMenuCurrent)
-			fmt.Printf("this: %#v\n", this)
-			t.Errorf("[%d] Wrong result from IsMenuCurrent: %v for %q", i, isMenuCurrent, this.menuItem)
-		}
-
-		if hasMenuCurrent != this.hasMenuCurrent {
-			fmt.Println("hasMenuCurrent", hasMenuCurrent)
-			fmt.Printf("this: %#v\n", this)
-			t.Errorf("[%d] Wrong result for menu %q menuItem %v for HasMenuCurrent: %v", i, this.menu, this.menuItem, hasMenuCurrent)
-		}
-	}
-}
-
-func TestHopefullyUniqueID(t *testing.T) {
-	t.Parallel()
-	assert.Equal(t, "i", (&MenuEntry{Identifier: "i", URL: "u", Name: "n"}).hopefullyUniqueID())
-	assert.Equal(t, "u", (&MenuEntry{Identifier: "", URL: "u", Name: "n"}).hopefullyUniqueID())
-	assert.Equal(t, "n", (&MenuEntry{Identifier: "", URL: "", Name: "n"}).hopefullyUniqueID())
-}
-
-func TestAddMenuEntryChild(t *testing.T) {
-	t.Parallel()
-	root := &MenuEntry{Weight: 1}
-	root.addChild(&MenuEntry{Weight: 2})
-	root.addChild(&MenuEntry{Weight: 1})
-	assert.Equal(t, 2, len(root.Children))
-	assert.Equal(t, 1, root.Children[0].Weight)
-}
-
-var testMenuIdentityMatcher = func(me *MenuEntry, id string) bool { return me.Identifier == id }
-var testMenuNameMatcher = func(me *MenuEntry, id string) bool { return me.Name == id }
-
-func findTestMenuEntryByID(s *Site, mn string, id string) *MenuEntry {
-	return findTestMenuEntry(s, mn, id, testMenuIdentityMatcher)
-}
-func findTestMenuEntryByName(s *Site, mn string, id string) *MenuEntry {
-	return findTestMenuEntry(s, mn, id, testMenuNameMatcher)
-}
-
-func findTestMenuEntry(s *Site, mn string, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry {
-	var found *MenuEntry
-	if menu, ok := s.Menus[mn]; ok {
-		for _, me := range *menu {
-
-			if matcher(me, id) {
-				if found != nil {
-					panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id))
-				}
-				found = me
-			}
-
-			descendant := findDescendantTestMenuEntry(me, id, matcher)
-			if descendant != nil {
-				if found != nil {
-					panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id))
-				}
-				found = descendant
-			}
-		}
-	}
-	return found
-}
-
-func findDescendantTestMenuEntry(parent *MenuEntry, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry {
-	var found *MenuEntry
-	if parent.HasChildren() {
-		for _, child := range parent.Children {
-
-			if matcher(child, id) {
-				if found != nil {
-					panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id))
-				}
-				found = child
-			}
-
-			descendant := findDescendantTestMenuEntry(child, id, matcher)
-			if descendant != nil {
-				if found != nil {
-					panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id))
-				}
-				found = descendant
-			}
-		}
-	}
-	return found
-}
-
-func setupMenuTests(t *testing.T, pageSources []source.ByteSource, configKeyValues ...interface{}) *Site {
-
-	var (
-		cfg, fs = newTestCfg()
-	)
-
-	menus, err := tomlToMap(confMenu1)
-	require.NoError(t, err)
-
-	cfg.Set("menu", menus["menu"])
-	cfg.Set("baseURL", "http://foo.local/Zoo/")
-
-	for i := 0; i < len(configKeyValues); i += 2 {
-		cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
-	}
-
-	for _, src := range pageSources {
-		writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content))
-
-	}
-
-	return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
-
-}
-
-func tomlToMap(s string) (map[string]interface{}, error) {
-	tree, err := toml.Load(s)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return tree.ToMap(), nil
+	th.assertFileContent("public/sect2/p3/index.html", "Single",
+		"Menu Sect:  /sect5/|Section Five|10|-|-|/sect1/|Section One|100|-|-|/sect2/|Sect2s|0|-|HasMenuCurrent|/sect3/|Sect3s|0|-|-|")
 
 }
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -621,6 +621,9 @@
 }
 
 func (p *Page) Section() string {
+	if p.Kind == KindSection {
+		return p.sections[0]
+	}
 	return p.Source.Section()
 }
 
@@ -1167,8 +1170,16 @@
 	sectionPagesMenu := p.Site.sectionPagesMenu
 
 	// page is labeled as "shadow-member" of the menu with the same identifier as the section
-	if sectionPagesMenu != "" && p.Section() != "" && sectionPagesMenu == menuID && p.Section() == me.Identifier {
-		return true
+	if sectionPagesMenu != "" {
+		section := p.Section()
+
+		if !p.s.Info.preserveTaxonomyNames {
+			section = p.s.PathSpec.MakePathSanitized(section)
+		}
+
+		if section != "" && sectionPagesMenu == menuID && section == me.Identifier {
+			return true
+		}
 	}
 
 	if !me.HasChildren() {
@@ -1537,7 +1548,7 @@
 	case KindHome:
 		pages = s.RegularPages
 	case KindSection:
-		sectionData, ok := s.Sections[p.sections[0]]
+		sectionData, ok := s.Sections[p.Section()]
 		if !ok {
 			return fmt.Errorf("Data for section %s not found", p.Section())
 		}
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -1366,8 +1366,6 @@
 
 	s.assembleSections()
 
-	s.assembleMenus()
-
 	return
 }
 
@@ -1442,42 +1440,20 @@
 	pages := s.Pages
 
 	if sectionPagesMenu != "" {
-		// Create menu entries for section pages with content
 		for _, p := range pages {
 			if p.Kind == KindSection {
-				// menu with same id defined in config, let that one win
-				if _, ok := flat[twoD{sectionPagesMenu, p.Section()}]; ok {
+				id := p.Section()
+				if _, ok := flat[twoD{sectionPagesMenu, id}]; ok {
 					continue
 				}
 
-				me := MenuEntry{Identifier: p.Section(),
+				me := MenuEntry{Identifier: id,
 					Name:   p.LinkTitle(),
 					Weight: p.Weight,
 					URL:    p.RelPermalink()}
-
 				flat[twoD{sectionPagesMenu, me.KeyName()}] = &me
 			}
 		}
-
-		// Create entries for remaining content-less section pages
-		sectionPagesMenus := make(map[string]interface{})
-		for _, p := range pages {
-			if _, ok := sectionPagesMenus[p.Section()]; !ok {
-				if p.Section() != "" {
-					// menu with same id defined in config, let that one win
-					if _, ok := flat[twoD{sectionPagesMenu, p.Section()}]; ok {
-						continue
-					}
-
-					me := MenuEntry{Identifier: p.Section(),
-						Name: helpers.MakeTitle(helpers.FirstUpper(p.Section())),
-						URL:  s.Info.createNodeMenuEntryURL(p.addLangPathPrefix("/"+p.Section()) + "/")}
-
-					flat[twoD{sectionPagesMenu, me.KeyName()}] = &me
-					sectionPagesMenus[p.Section()] = true
-				}
-			}
-		}
 	}
 
 	// Add menu entries provided by pages
@@ -1610,7 +1586,8 @@
 	sectionPages := s.findPagesByKind(KindSection)
 
 	for i, p := range regularPages {
-		s.Sections.add(s.getTaxonomyKey(p.Section()), WeightedPage{regularPages[i].Weight, regularPages[i]})
+		section := s.getTaxonomyKey(p.Section())
+		s.Sections.add(section, WeightedPage{regularPages[i].Weight, regularPages[i]})
 	}
 
 	// Add sections without regular pages, but with a content page
@@ -2135,7 +2112,6 @@
 }
 
 func (s *Site) newSectionPage(name string, section WeightedPages) *Page {
-
 	p := s.newNodePage(KindSection)
 	p.sections = []string{name}
 
--- a/hugolib/taxonomy_test.go
+++ b/hugolib/taxonomy_test.go
@@ -19,11 +19,9 @@
 	"reflect"
 	"testing"
 
-	"github.com/spf13/afero"
 	"github.com/stretchr/testify/require"
 
 	"github.com/spf13/hugo/deps"
-	"github.com/spf13/hugo/hugofs"
 )
 
 func TestByCountOrderOfTaxonomies(t *testing.T) {
@@ -79,20 +77,11 @@
 # Doc
 `
 
-	mf := afero.NewMemMapFs()
+	th, h := newTestSitesFromConfigWithDefaultTemplates(t, siteConfig)
+	require.Len(t, h.Sites, 1)
 
-	writeToFs(t, mf, "config.toml", siteConfig)
+	fs := th.Fs
 
-	cfg, err := LoadConfig(mf, "", "config.toml")
-	require.NoError(t, err)
-
-	fs := hugofs.NewFrom(mf, cfg)
-	th := testHelper{cfg, fs, t}
-
-	writeSource(t, fs, "layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}")
-	writeSource(t, fs, "layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}")
-	writeSource(t, fs, "layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}")
-
 	writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- tag1", "- cat1", "- o1"))
 	writeSource(t, fs, "content/p2.md", fmt.Sprintf(pageTemplate, "t2/c1", "- tag2", "- cat1", "- o1"))
 
@@ -99,12 +88,7 @@
 	writeNewContentFile(t, fs, "Category Terms", "2017-01-01", "content/categories/_index.md", 10)
 	writeNewContentFile(t, fs, "Tag1 List", "2017-01-01", "content/tags/tag1/_index.md", 10)
 
-	h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
-
-	require.NoError(t, err)
-	require.Len(t, h.Sites, 1)
-
-	err = h.Build(BuildCfg{})
+	err := h.Build(BuildCfg{})
 
 	require.NoError(t, err)
 
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -6,12 +6,13 @@
 
 	"regexp"
 
-	"github.com/spf13/hugo/config"
-	"github.com/spf13/hugo/deps"
-
 	"fmt"
 	"strings"
 
+	"github.com/spf13/afero"
+	"github.com/spf13/hugo/config"
+	"github.com/spf13/hugo/deps"
+
 	"github.com/spf13/hugo/helpers"
 	"github.com/spf13/hugo/source"
 	"github.com/spf13/hugo/tpl"
@@ -111,6 +112,39 @@
 		t.Fatalf("Failed to create Site: %s", err)
 	}
 	return s
+}
+
+func newTestSitesFromConfig(t testing.TB, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) {
+	if len(layoutPathContentPairs)%2 != 0 {
+		t.Fatalf("Layouts must be provided in pairs")
+	}
+	mf := afero.NewMemMapFs()
+
+	writeToFs(t, mf, "config.toml", tomlConfig)
+
+	cfg, err := LoadConfig(mf, "", "config.toml")
+	require.NoError(t, err)
+
+	fs := hugofs.NewFrom(mf, cfg)
+	th := testHelper{cfg, fs, t}
+
+	for i := 0; i < len(layoutPathContentPairs); i += 2 {
+		writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1])
+	}
+
+	h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
+
+	require.NoError(t, err)
+
+	return th, h
+}
+
+func newTestSitesFromConfigWithDefaultTemplates(t testing.TB, tomlConfig string) (testHelper, *HugoSites) {
+	return newTestSitesFromConfig(t, tomlConfig,
+		"layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}",
+		"layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}",
+		"layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}",
+	)
 }
 
 func newDebugLogger() *jww.Notepad {