shithub: hugo

Download patch

ref: 20c9b6ec81171d1c586ea31d5d08b40b0edaffc6
parent: f8a119b606d55aa4f31f16e5a3cadc929c99e4f8
author: Bjørn Erik Pedersen <[email protected]>
date: Mon Jan 15 15:40:39 EST 2018

resource: Add front matter metadata to Resource

This commit expands the Resource interface with 3 new methods:

* Name
* Title
* Params

All of these can be set in the Page front matter. `Name` will get its default value from the base filename, and is the value used in the ByPrefix and GetByPrefix lookup methods.

Fixes #4244

--- a/hugolib/hugo_sites_build_test.go
+++ b/hugolib/hugo_sites_build_test.go
@@ -225,7 +225,7 @@
 
 	gp1 := sites.GetContentPage(filepath.FromSlash("content/sect/doc1.en.md"))
 	require.NotNil(t, gp1)
-	require.Equal(t, "doc1", gp1.Title)
+	require.Equal(t, "doc1", gp1.title)
 	gp2 := sites.GetContentPage(filepath.FromSlash("content/dummysect/notfound.md"))
 	require.Nil(t, gp2)
 
@@ -317,9 +317,9 @@
 	require.Len(t, homeEn.Translations(), 3)
 	require.Equal(t, "fr", homeEn.Translations()[0].Lang())
 	require.Equal(t, "nn", homeEn.Translations()[1].Lang())
-	require.Equal(t, "På nynorsk", homeEn.Translations()[1].Title)
+	require.Equal(t, "På nynorsk", homeEn.Translations()[1].title)
 	require.Equal(t, "nb", homeEn.Translations()[2].Lang())
-	require.Equal(t, "På bokmål", homeEn.Translations()[2].Title, configSuffix)
+	require.Equal(t, "På bokmål", homeEn.Translations()[2].title, configSuffix)
 	require.Equal(t, "Bokmål", homeEn.Translations()[2].Language().LanguageName, configSuffix)
 
 	sectFr := frSite.getPage(KindSection, "sect")
@@ -328,7 +328,7 @@
 	require.Equal(t, "fr", sectFr.Lang())
 	require.Len(t, sectFr.Translations(), 1)
 	require.Equal(t, "en", sectFr.Translations()[0].Lang())
-	require.Equal(t, "Sects", sectFr.Translations()[0].Title)
+	require.Equal(t, "Sects", sectFr.Translations()[0].title)
 
 	nnSite := sites.Sites[2]
 	require.Equal(t, "nn", nnSite.Language.Lang)
@@ -495,9 +495,9 @@
 				require.Len(t, enSite.RegularPages, 6)
 				require.Len(t, enSite.AllPages, 34)
 				require.Len(t, frSite.RegularPages, 5)
-				require.Equal(t, "new_fr_1", frSite.RegularPages[3].Title)
-				require.Equal(t, "new_en_2", enSite.RegularPages[0].Title)
-				require.Equal(t, "new_en_1", enSite.RegularPages[1].Title)
+				require.Equal(t, "new_fr_1", frSite.RegularPages[3].title)
+				require.Equal(t, "new_en_2", enSite.RegularPages[0].title)
+				require.Equal(t, "new_en_1", enSite.RegularPages[1].title)
 
 				rendered := readDestination(t, fs, "public/en/new1/index.html")
 				require.True(t, strings.Contains(rendered, "new_en_1"), rendered)
@@ -531,7 +531,7 @@
 			},
 			func(t *testing.T) {
 				require.Len(t, enSite.RegularPages, 6, "Rename")
-				require.Equal(t, "new_en_1", enSite.RegularPages[1].Title)
+				require.Equal(t, "new_en_1", enSite.RegularPages[1].title)
 				rendered := readDestination(t, fs, "public/en/new1renamed/index.html")
 				require.True(t, strings.Contains(rendered, "new_en_1"), rendered)
 			}},
@@ -683,7 +683,7 @@
 	// Veriy Swedish site
 	require.Len(t, svSite.RegularPages, 1)
 	svPage := svSite.RegularPages[0]
-	require.Equal(t, "Swedish Contentfile", svPage.Title)
+	require.Equal(t, "Swedish Contentfile", svPage.title)
 	require.Equal(t, "sv", svPage.Lang())
 	require.Len(t, svPage.Translations(), 2)
 	require.Len(t, svPage.AllTranslations(), 3)
--- a/hugolib/node_as_page_test.go
+++ b/hugolib/node_as_page_test.go
@@ -104,7 +104,7 @@
 	require.True(t, home.Path() != "")
 
 	section2 := nodes[5]
-	require.Equal(t, "Section2", section2.Title)
+	require.Equal(t, "Section2", section2.title)
 
 	pages := sites.findAllPagesByKind(KindPage)
 	require.Len(t, pages, 4)
@@ -252,9 +252,9 @@
 	for _, p := range pages {
 		var want string
 		if ugly {
-			want = "/" + p.s.PathSpec.URLize(p.Title) + ".html"
+			want = "/" + p.s.PathSpec.URLize(p.title) + ".html"
 		} else {
-			want = "/" + p.s.PathSpec.URLize(p.Title) + "/"
+			want = "/" + p.s.PathSpec.URLize(p.title) + "/"
 		}
 		if p.URL() != want {
 			t.Errorf("Taxonomy term URL mismatch: want %q, got %q", want, p.URL())
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -111,6 +111,10 @@
 	// provided by the Resource object.
 	Resources resource.Resources
 
+	// This is the raw front matter metadata that is going to be assigned to
+	// the Resources above.
+	resourcesMetadata []map[string]interface{}
+
 	// translations will contain references to this page in other language
 	// if available.
 	translations Pages
@@ -120,7 +124,7 @@
 	translationKey string
 
 	// Params contains configuration defined in the params section of page frontmatter.
-	Params map[string]interface{}
+	params map[string]interface{}
 
 	// Content sections
 	Content         template.HTML
@@ -214,7 +218,7 @@
 
 	Site *SiteInfo `json:"-"`
 
-	Title       string
+	title       string
 	Description string
 	Keywords    []string
 	Data        map[string]interface{}
@@ -468,7 +472,7 @@
 
 func (p *Page) traverseDirect(key string) (interface{}, error) {
 	keyStr := strings.ToLower(key)
-	if val, ok := p.Params[keyStr]; ok {
+	if val, ok := p.params[keyStr]; ok {
 		return val, nil
 	}
 
@@ -476,7 +480,7 @@
 }
 
 func (p *Page) traverseNested(keySegments []string) (interface{}, error) {
-	result := traverse(keySegments, p.Params)
+	result := traverse(keySegments, p.params)
 	if result != nil {
 		return result, nil
 	}
@@ -519,7 +523,7 @@
 }
 
 func (p *Page) Authors() AuthorList {
-	authorKeys, ok := p.Params["authors"]
+	authorKeys, ok := p.params["authors"]
 	if !ok {
 		return AuthorList{}
 	}
@@ -757,7 +761,7 @@
 		contentType: "",
 		Source:      Source{File: fi},
 		Keywords:    []string{}, Sitemap: Sitemap{Priority: -1},
-		Params:       make(map[string]interface{}),
+		params:       make(map[string]interface{}),
 		translations: make(Pages, 0),
 		sections:     sectionsFromDir(fi.Dir()),
 		Site:         &s.Info,
@@ -927,7 +931,7 @@
 	if len(p.linkTitle) > 0 {
 		return p.linkTitle
 	}
-	return p.Title
+	return p.title
 }
 
 func (p *Page) shouldBuild() bool {
@@ -988,6 +992,22 @@
 	return p.relPermalink
 }
 
+// See resource.Resource
+func (p *Page) Name() string {
+	if p.File != nil {
+		return p.File.BaseFileName()
+	}
+	return p.title
+}
+
+func (p *Page) Title() string {
+	return p.title
+}
+
+func (p *Page) Params() map[string]interface{} {
+	return p.params
+}
+
 func (p *Page) subResourceTargetPathFactory(base string) string {
 	return path.Join(p.relTargetPathBase, base)
 }
@@ -1094,17 +1114,17 @@
 		loki := strings.ToLower(k)
 		switch loki {
 		case "title":
-			p.Title = cast.ToString(v)
-			p.Params[loki] = p.Title
+			p.title = cast.ToString(v)
+			p.params[loki] = p.title
 		case "linktitle":
 			p.linkTitle = cast.ToString(v)
-			p.Params[loki] = p.linkTitle
+			p.params[loki] = p.linkTitle
 		case "description":
 			p.Description = cast.ToString(v)
-			p.Params[loki] = p.Description
+			p.params[loki] = p.Description
 		case "slug":
 			p.Slug = cast.ToString(v)
-			p.Params[loki] = p.Slug
+			p.params[loki] = p.Slug
 		case "url":
 			if url := cast.ToString(v); strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
 				return fmt.Errorf("Only relative URLs are supported, %v provided", url)
@@ -1111,22 +1131,22 @@
 			}
 			p.URLPath.URL = cast.ToString(v)
 			p.URLPath.frontMatterURL = p.URLPath.URL
-			p.Params[loki] = p.URLPath.URL
+			p.params[loki] = p.URLPath.URL
 		case "type":
 			p.contentType = cast.ToString(v)
-			p.Params[loki] = p.contentType
+			p.params[loki] = p.contentType
 		case "extension", "ext":
 			p.extension = cast.ToString(v)
-			p.Params[loki] = p.extension
+			p.params[loki] = p.extension
 		case "keywords":
 			p.Keywords = cast.ToStringSlice(v)
-			p.Params[loki] = p.Keywords
+			p.params[loki] = p.Keywords
 		case "date":
 			p.Date, err = cast.ToTimeE(v)
 			if err != nil {
 				p.s.Log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path())
 			}
-			p.Params[loki] = p.Date
+			p.params[loki] = p.Date
 		case "lastmod":
 			p.Lastmod, err = cast.ToTimeE(v)
 			if err != nil {
@@ -1135,10 +1155,10 @@
 		case "modified":
 			vv, err := cast.ToTimeE(v)
 			if err == nil {
-				p.Params[loki] = vv
+				p.params[loki] = vv
 				modified = vv
 			} else {
-				p.Params[loki] = cast.ToString(v)
+				p.params[loki] = cast.ToString(v)
 			}
 		case "outputs":
 			o := cast.ToStringSlice(v)
@@ -1150,17 +1170,16 @@
 					p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err)
 				} else {
 					p.outputFormats = outFormats
-					p.Params[loki] = outFormats
+					p.params[loki] = outFormats
 				}
 
 			}
-			//p.Params[loki] = p.Keywords
 		case "publishdate", "pubdate":
 			p.PublishDate, err = cast.ToTimeE(v)
 			if err != nil {
 				p.s.Log.ERROR.Printf("Failed to parse publishdate '%v' in page %s", v, p.File.Path())
 			}
-			p.Params[loki] = p.PublishDate
+			p.params[loki] = p.PublishDate
 		case "expirydate", "unpublishdate":
 			p.ExpiryDate, err = cast.ToTimeE(v)
 			if err != nil {
@@ -1178,20 +1197,20 @@
 				vv, err := cast.ToTimeE(v)
 				if err == nil {
 					p.PublishDate = vv
-					p.Params[loki] = p.PublishDate
+					p.params[loki] = p.PublishDate
 				} else {
-					p.Params[loki] = cast.ToString(v)
+					p.params[loki] = cast.ToString(v)
 				}
 			}
 		case "layout":
 			p.Layout = cast.ToString(v)
-			p.Params[loki] = p.Layout
+			p.params[loki] = p.Layout
 		case "markup":
 			p.Markup = cast.ToString(v)
-			p.Params[loki] = p.Markup
+			p.params[loki] = p.Markup
 		case "weight":
 			p.Weight = cast.ToInt(v)
-			p.Params[loki] = p.Weight
+			p.params[loki] = p.Weight
 		case "aliases":
 			p.Aliases = cast.ToStringSlice(v)
 			for _, alias := range p.Aliases {
@@ -1199,32 +1218,65 @@
 					return fmt.Errorf("Only relative aliases are supported, %v provided", alias)
 				}
 			}
-			p.Params[loki] = p.Aliases
+			p.params[loki] = p.Aliases
 		case "status":
 			p.Status = cast.ToString(v)
-			p.Params[loki] = p.Status
+			p.params[loki] = p.Status
 		case "sitemap":
 			p.Sitemap = parseSitemap(cast.ToStringMap(v))
-			p.Params[loki] = p.Sitemap
+			p.params[loki] = p.Sitemap
 		case "iscjklanguage":
 			isCJKLanguage = new(bool)
 			*isCJKLanguage = cast.ToBool(v)
 		case "translationkey":
 			p.translationKey = cast.ToString(v)
-			p.Params[loki] = p.translationKey
+			p.params[loki] = p.translationKey
+		case "resources":
+			var resources []map[string]interface{}
+			handled := true
+
+			switch vv := v.(type) {
+			case []map[interface{}]interface{}:
+				for _, vvv := range vv {
+					resources = append(resources, cast.ToStringMap(vvv))
+				}
+			case []map[string]interface{}:
+				for _, vvv := range vv {
+					resources = append(resources, vvv)
+				}
+			case []interface{}:
+				for _, vvv := range vv {
+					switch vvvv := vvv.(type) {
+					case map[interface{}]interface{}:
+						resources = append(resources, cast.ToStringMap(vvvv))
+					case map[string]interface{}:
+						resources = append(resources, vvvv)
+					}
+				}
+			default:
+				handled = false
+			}
+
+			if handled {
+				p.params[loki] = resources
+				p.resourcesMetadata = resources
+				break
+			}
+			fallthrough
+
 		default:
 			// If not one of the explicit values, store in Params
 			switch vv := v.(type) {
 			case bool:
-				p.Params[loki] = vv
+				p.params[loki] = vv
 			case string:
-				p.Params[loki] = vv
+				p.params[loki] = vv
 			case int64, int32, int16, int8, int:
-				p.Params[loki] = vv
+				p.params[loki] = vv
 			case float64, float32:
-				p.Params[loki] = vv
+				p.params[loki] = vv
 			case time.Time:
-				p.Params[loki] = vv
+				p.params[loki] = vv
 			default: // handle array of strings as well
 				switch vvv := vv.(type) {
 				case []interface{}:
@@ -1231,11 +1283,11 @@
 					if len(vvv) > 0 {
 						switch vvv[0].(type) {
 						case map[interface{}]interface{}: // Proper parsing structured array from YAML based FrontMatter
-							p.Params[loki] = vvv
+							p.params[loki] = vvv
 						case map[string]interface{}: // Proper parsing structured array from JSON based FrontMatter
-							p.Params[loki] = vvv
+							p.params[loki] = vvv
 						case []interface{}:
-							p.Params[loki] = vvv
+							p.params[loki] = vvv
 						default:
 							a := make([]string, len(vvv))
 							for i, u := range vvv {
@@ -1242,13 +1294,13 @@
 								a[i] = cast.ToString(u)
 							}
 
-							p.Params[loki] = a
+							p.params[loki] = a
 						}
 					} else {
-						p.Params[loki] = []string{}
+						p.params[loki] = []string{}
 					}
 				default:
-					p.Params[loki] = vv
+					p.params[loki] = vv
 				}
 			}
 		}
@@ -1263,7 +1315,7 @@
 	} else if published != nil {
 		p.Draft = !*published
 	}
-	p.Params["draft"] = p.Draft
+	p.params["draft"] = p.Draft
 
 	if p.Date.IsZero() {
 		p.Date = p.PublishDate
@@ -1277,7 +1329,7 @@
 		fi, err := p.s.Fs.Source.Stat(filepath.Join(p.s.PathSpec.AbsPathify(p.s.Cfg.GetString("contentDir")), p.File.Path()))
 		if err == nil {
 			p.Date = fi.ModTime()
-			p.Params["date"] = p.Date
+			p.params["date"] = p.Date
 		}
 	}
 
@@ -1289,9 +1341,9 @@
 		}
 
 	}
-	p.Params["lastmod"] = p.Lastmod
-	p.Params["publishdate"] = p.PublishDate
-	p.Params["expirydate"] = p.ExpiryDate
+	p.params["lastmod"] = p.Lastmod
+	p.params["publishdate"] = p.PublishDate
+	p.params["expirydate"] = p.ExpiryDate
 
 	if isCJKLanguage != nil {
 		p.isCJKLanguage = *isCJKLanguage
@@ -1302,7 +1354,7 @@
 			p.isCJKLanguage = false
 		}
 	}
-	p.Params["iscjklanguage"] = p.isCJKLanguage
+	p.params["iscjklanguage"] = p.isCJKLanguage
 
 	return nil
 
@@ -1317,7 +1369,7 @@
 }
 
 func (p *Page) getParam(key string, stringToLower bool) interface{} {
-	v := p.Params[strings.ToLower(key)]
+	v := p.params[strings.ToLower(key)]
 
 	if v == nil {
 		return nil
@@ -1390,7 +1442,7 @@
 
 	// The following logic is kept from back when Hugo had both Page and Node types.
 	// TODO(bep) consolidate / clean
-	nme := MenuEntry{Page: p, Name: p.Title, URL: p.URL()}
+	nme := MenuEntry{Page: p, Name: p.title, URL: p.URL()}
 
 	for _, child := range me.Children {
 		if nme.IsSameResource(child) {
@@ -1421,7 +1473,7 @@
 
 	// The following logic is kept from back when Hugo had both Page and Node types.
 	// TODO(bep) consolidate / clean
-	me := MenuEntry{Page: p, Name: p.Title, URL: p.URL()}
+	me := MenuEntry{Page: p, Name: p.title, URL: p.URL()}
 
 	if !me.IsSameResource(inme) {
 		return false
@@ -1465,7 +1517,7 @@
 	p.pageMenusInit.Do(func() {
 		p.pageMenus = PageMenus{}
 
-		if ms, ok := p.Params["menu"]; ok {
+		if ms, ok := p.params["menu"]; ok {
 			link := p.RelPermalink()
 
 			me := MenuEntry{Page: p, Name: p.LinkTitle(), Weight: p.Weight, URL: link}
@@ -1494,16 +1546,16 @@
 			menus, err := cast.ToStringMapE(ms)
 
 			if err != nil {
-				p.s.Log.ERROR.Printf("unable to process menus for %q\n", p.Title)
+				p.s.Log.ERROR.Printf("unable to process menus for %q\n", p.title)
 			}
 
 			for name, menu := range menus {
 				menuEntry := MenuEntry{Page: p, Name: p.LinkTitle(), URL: link, Weight: p.Weight, Menu: name}
 				if menu != nil {
-					p.s.Log.DEBUG.Printf("found menu: %q, in %q\n", name, p.Title)
+					p.s.Log.DEBUG.Printf("found menu: %q, in %q\n", name, p.title)
 					ime, err := cast.ToStringMapE(menu)
 					if err != nil {
-						p.s.Log.ERROR.Printf("unable to process menus for %q: %s", p.Title, err)
+						p.s.Log.ERROR.Printf("unable to process menus for %q: %s", p.title, err)
 					}
 
 					menuEntry.marshallMap(ime)
@@ -1805,7 +1857,7 @@
 }
 
 func (p *Page) String() string {
-	return fmt.Sprintf("Page(%q)", p.Title)
+	return fmt.Sprintf("Page(%q)", p.title)
 }
 
 type URLPath struct {
@@ -2003,5 +2055,5 @@
 	if p.Path() != "" {
 		return p.Path()
 	}
-	return p.Title
+	return p.title
 }
--- a/hugolib/pageGroup_test.go
+++ b/hugolib/pageGroup_test.go
@@ -49,8 +49,8 @@
 		p.Date = cast.ToTime(src.date)
 		p.PublishDate = cast.ToTime(src.date)
 		p.ExpiryDate = cast.ToTime(src.date)
-		p.Params["custom_param"] = src.param
-		p.Params["custom_date"] = cast.ToTime(src.date)
+		p.params["custom_param"] = src.param
+		p.params["custom_date"] = cast.ToTime(src.date)
 		pages = append(pages, p)
 	}
 	return pages
@@ -253,7 +253,7 @@
 	if err != nil {
 		t.Fatalf("failed to prepare test page %s", f)
 	}
-	p.Params["custom_param"] = testStr
+	p.params["custom_param"] = testStr
 	pages := Pages{p}
 
 	groups, err := pages.GroupByParam("custom_param")
@@ -268,9 +268,9 @@
 func TestGroupByParamCalledWithSomeUnavailableParams(t *testing.T) {
 	t.Parallel()
 	pages := preparePageGroupTestPages(t)
-	delete(pages[1].Params, "custom_param")
-	delete(pages[3].Params, "custom_param")
-	delete(pages[4].Params, "custom_param")
+	delete(pages[1].params, "custom_param")
+	delete(pages[3].params, "custom_param")
+	delete(pages[4].params, "custom_param")
 
 	expect := PagesGroup{
 		{Key: "foo", Pages: Pages{pages[0], pages[2]}},
--- a/hugolib/pageSort.go
+++ b/hugolib/pageSort.go
@@ -129,7 +129,7 @@
 	key := "pageSort.ByTitle"
 
 	title := func(p1, p2 *Page) bool {
-		return p1.Title < p2.Title
+		return p1.title < p2.title
 	}
 
 	pages, _ := spc.get(key, p, pageBy(title).Sort)
--- a/hugolib/pageSort_test.go
+++ b/hugolib/pageSort_test.go
@@ -74,7 +74,7 @@
 		assertFunc func(p Pages) bool
 	}{
 		{(Pages).ByWeight, func(p Pages) bool { return p[0].Weight == 1 }},
-		{(Pages).ByTitle, func(p Pages) bool { return p[0].Title == "ab" }},
+		{(Pages).ByTitle, func(p Pages) bool { return p[0].title == "ab" }},
 		{(Pages).ByLinkTitle, func(p Pages) bool { return p[0].LinkTitle() == "abl" }},
 		{(Pages).ByDate, func(p Pages) bool { return p[0].Date == d4 }},
 		{(Pages).ByPublishDate, func(p Pages) bool { return p[0].PublishDate == d4 }},
@@ -124,7 +124,7 @@
 	s := newTestSite(t)
 
 	unsorted := createSortTestPages(s, 10)
-	delete(unsorted[9].Params, "arbitrarily")
+	delete(unsorted[9].params, "arbitrarily")
 
 	firstSetValue, _ := unsorted[0].Param(k)
 	secondSetValue, _ := unsorted[1].Param(k)
@@ -163,9 +163,9 @@
 		pages[i].Date = dates[i]
 		pages[i].Lastmod = dates[i]
 		pages[i].Weight = weights[i]
-		pages[i].Title = titles[i]
+		pages[i].title = titles[i]
 		// make sure we compare apples and ... apples ...
-		pages[len(dates)-1-i].linkTitle = pages[i].Title + "l"
+		pages[len(dates)-1-i].linkTitle = pages[i].title + "l"
 		pages[len(dates)-1-i].PublishDate = dates[i]
 		pages[len(dates)-1-i].ExpiryDate = dates[i]
 		pages[len(dates)-1-i].Content = template.HTML(titles[i] + "_content")
@@ -180,7 +180,7 @@
 
 	for i := 0; i < num; i++ {
 		p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))
-		p.Params = map[string]interface{}{
+		p.params = map[string]interface{}{
 			"arbitrarily": map[string]interface{}{
 				"nested": ("xyz" + fmt.Sprintf("%v", 100-i)),
 			},
--- a/hugolib/page_bundler_handlers.go
+++ b/hugolib/page_bundler_handlers.go
@@ -254,6 +254,12 @@
 
 				return p.Resources[i].RelPermalink() < p.Resources[j].RelPermalink()
 			})
+
+			// Assign metadata from front matter if set
+			if len(p.resourcesMetadata) > 0 {
+				resource.AssignMetadata(p.resourcesMetadata, p.Resources...)
+			}
+
 		}
 
 		return h(ctx)
--- a/hugolib/page_bundler_test.go
+++ b/hugolib/page_bundler_test.go
@@ -136,8 +136,14 @@
 						"TheContent",
 						"Sunset RelPermalink: /2017/pageslug/sunset1.jpg",
 						"Thumb Width: 123",
+						"Thumb Name: my-sunset-1",
 						"Short Sunset RelPermalink: /2017/pageslug/sunset2.jpg",
 						"Short Thumb Width: 56",
+						"1: Image Title: Sunset Galore 1",
+						"1: Image Params: map[myparam:My Sunny Param]",
+						"2: Image Title: Sunset Galore 2",
+						"2: Image Params: map[myparam:My Sunny Param]",
+						"1: Image myParam: Lower: My Sunny Param Caps: My Sunny Param",
 					)
 					th.assertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug.html"), "TheContent")
 
@@ -205,10 +211,16 @@
 TheContent.
 `
 
-	pageWithImageShortcodeContent := `---
+	pageWithImageShortcodeAndResourceMetadataContent := `---
 title: "Bundle Galore"
 slug: pageslug
 date: 2017-10-09
+resources:
+- src: "*.jpg"
+  name: "my-sunset-:counter"
+  title: "Sunset Galore :counter"
+  params:
+    myParam: "My Sunny Param"
 ---
 
 TheContent.
@@ -227,17 +239,25 @@
 	singleLayout := `
 Title: {{ .Title }}
 Content: {{ .Content }}
-{{ $sunset := .Resources.GetByPrefix "sunset1" }}
+{{ $sunset := .Resources.GetByPrefix "my-sunset-1" }}
 {{ with $sunset }}
 Sunset RelPermalink: {{ .RelPermalink }}
 {{ $thumb := .Fill "123x123" }}
 Thumb Width: {{ $thumb.Width }}
+Thumb Name: {{ $thumb.Name }}
+Thumb Title: {{ $thumb.Title }}
+Thumb RelPermalink: {{ $thumb.RelPermalink }}
 {{ end }}
-
+{{ range $i, $e := .Resources.ByType "image" }}
+{{ $i }}: Image Title: {{ .Title }}
+{{ $i }}: Image Name: {{ .Name }}
+{{ $i }}: Image Params: {{ printf "%v" .Params }}
+{{ $i }}: Image myParam: Lower: {{ .Params.myparam }} Caps: {{ .Params.MYPARAM }}
+{{ end }}
 `
 
 	myShort := `
-{{ $sunset := .Page.Resources.GetByPrefix "sunset2" }}
+{{ $sunset := .Page.Resources.GetByPrefix "my-sunset-2" }}
 {{ with $sunset }}
 Short Sunset RelPermalink: {{ .RelPermalink }}
 {{ $thumb := .Fill "56x56" }}
@@ -268,7 +288,7 @@
 	writeSource(t, fs, filepath.Join(workDir, "base", "assets", "pages", "mypage.md"), pageContent)
 
 	// Bundle
-	writeSource(t, fs, filepath.Join(workDir, "base", "b", "index.md"), pageWithImageShortcodeContent)
+	writeSource(t, fs, filepath.Join(workDir, "base", "b", "index.md"), pageWithImageShortcodeAndResourceMetadataContent)
 	writeSource(t, fs, filepath.Join(workDir, "base", "b", "1.md"), pageContent)
 	writeSource(t, fs, filepath.Join(workDir, "base", "b", "2.md"), pageContent)
 	writeSource(t, fs, filepath.Join(workDir, "base", "b", "custom-mime.bep"), "bepsays")
--- a/hugolib/page_collections_test.go
+++ b/hugolib/page_collections_test.go
@@ -134,7 +134,7 @@
 		page := s.getPage(test.kind, test.path...)
 		assert.NotNil(page, errorMsg)
 		assert.Equal(test.kind, page.Kind, errorMsg)
-		assert.Equal(test.expectedTitle, page.Title)
+		assert.Equal(test.expectedTitle, page.title)
 	}
 
 }
--- a/hugolib/page_paths.go
+++ b/hugolib/page_paths.go
@@ -74,7 +74,7 @@
 // and URLs for this Page.
 func (p *Page) createTargetPathDescriptor(t output.Format) (targetPathDescriptor, error) {
 	if p.targetPathDescriptorPrototype == nil {
-		panic(fmt.Sprintf("Must run initTargetPathDescriptor() for page %q, kind %q", p.Title, p.Kind))
+		panic(fmt.Sprintf("Must run initTargetPathDescriptor() for page %q, kind %q", p.title, p.Kind))
 	}
 	d := *p.targetPathDescriptorPrototype
 	d.Type = t
@@ -271,9 +271,9 @@
 
 	if len(p.outputFormats) == 0 {
 		if p.Kind == kindUnknown {
-			panic(fmt.Sprintf("Page %q has unknown kind", p.Title))
+			panic(fmt.Sprintf("Page %q has unknown kind", p.title))
 		}
-		panic(fmt.Sprintf("Page %q missing output format(s)", p.Title))
+		panic(fmt.Sprintf("Page %q missing output format(s)", p.title))
 	}
 
 	// Choose the main output format. In most cases, this will be HTML.
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -468,8 +468,8 @@
 }
 
 func checkPageTitle(t *testing.T, page *Page, title string) {
-	if page.Title != title {
-		t.Fatalf("Page title is: %s.  Expected %s", page.Title, title)
+	if page.title != title {
+		t.Fatalf("Page title is: %s.  Expected %s", page.title, title)
 	}
 }
 
@@ -1066,8 +1066,8 @@
 	pageTOML, _ := s.NewPage("test/fileTOML.md")
 	_, _ = pageTOML.ReadFrom(strings.NewReader(pageWithCalendarTOMLFrontmatter))
 
-	assert.True(t, compareObjects(pageJSON.Params, pageYAML.Params))
-	assert.True(t, compareObjects(pageJSON.Params, pageTOML.Params))
+	assert.True(t, compareObjects(pageJSON.params, pageYAML.params))
+	assert.True(t, compareObjects(pageJSON.params, pageTOML.params))
 
 }
 
@@ -1095,10 +1095,10 @@
 	}
 	param := page.getParamToLower("a_table")
 	if param == nil {
-		t.Errorf("frontmatter not handling tables correctly should be type of %v, got: type of %v", reflect.TypeOf(page.Params["a_table"]), reflect.TypeOf(param))
+		t.Errorf("frontmatter not handling tables correctly should be type of %v, got: type of %v", reflect.TypeOf(page.params["a_table"]), reflect.TypeOf(param))
 	}
 	if cast.ToStringMap(param)["a_key"] != "a_value" {
-		t.Errorf("frontmatter not handling values inside a table correctly should be %s, got: %s", "a_value", cast.ToStringMap(page.Params["a_table"])["a_key"])
+		t.Errorf("frontmatter not handling values inside a table correctly should be %s, got: %s", "a_value", cast.ToStringMap(page.params["a_table"])["a_key"])
 	}
 }
 
@@ -1370,7 +1370,7 @@
 		p, err := s.NewPageFrom(strings.NewReader(c), "content/post/params.md")
 		require.NoError(t, err, "err during parse", "#%d", i)
 		for key := range wantedMap {
-			assert.Equal(t, wantedMap[key], p.Params[key], "#%d", key)
+			assert.Equal(t, wantedMap[key], p.params[key], "#%d", key)
 		}
 	}
 }
--- a/hugolib/pages_related_test.go
+++ b/hugolib/pages_related_test.go
@@ -54,22 +54,22 @@
 
 	assert.NoError(err)
 	assert.Len(result, 2)
-	assert.Equal("Page 2", result[0].Title)
-	assert.Equal("Page 1", result[1].Title)
+	assert.Equal("Page 2", result[0].title)
+	assert.Equal("Page 1", result[1].title)
 
 	result, err = s.RegularPages.Related(s.RegularPages[0])
 	assert.Len(result, 2)
-	assert.Equal("Page 2", result[0].Title)
-	assert.Equal("Page 3", result[1].Title)
+	assert.Equal("Page 2", result[0].title)
+	assert.Equal("Page 3", result[1].title)
 
 	result, err = s.RegularPages.RelatedIndices(s.RegularPages[0], "keywords")
 	assert.Len(result, 2)
-	assert.Equal("Page 2", result[0].Title)
-	assert.Equal("Page 3", result[1].Title)
+	assert.Equal("Page 2", result[0].title)
+	assert.Equal("Page 3", result[1].title)
 
 	result, err = s.RegularPages.RelatedTo(types.NewKeyValuesStrings("keywords", "bep", "rocks"))
 	assert.NoError(err)
 	assert.Len(result, 2)
-	assert.Equal("Page 2", result[0].Title)
-	assert.Equal("Page 3", result[1].Title)
+	assert.Equal("Page 2", result[0].title)
+	assert.Equal("Page 3", result[1].title)
 }
--- a/hugolib/pagination.go
+++ b/hugolib/pagination.go
@@ -270,7 +270,7 @@
 // If it's not, one will be created with all pages in Data["Pages"].
 func (p *PageOutput) Paginator(options ...interface{}) (*Pager, error) {
 	if !p.IsNode() {
-		return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.Title)
+		return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.title)
 	}
 	pagerSize, err := resolvePagerSize(p.s.Cfg, options...)
 
@@ -321,7 +321,7 @@
 // Note that repeated calls will return the same result, even if the sequence is different.
 func (p *PageOutput) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
 	if !p.IsNode() {
-		return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.Title)
+		return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.title)
 	}
 
 	pagerSize, err := resolvePagerSize(p.s.Cfg, options...)
--- a/hugolib/permalinks.go
+++ b/hugolib/permalinks.go
@@ -154,7 +154,7 @@
 func pageToPermalinkTitle(p *Page, _ string) (string, error) {
 	// Page contains Node which has Title
 	// (also contains URLPath which has Slug, sometimes)
-	return p.s.PathSpec.URLize(p.Title), nil
+	return p.s.PathSpec.URLize(p.title), nil
 }
 
 // pageToPermalinkFilename returns the URL-safe form of the filename
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -1877,7 +1877,7 @@
 
 func (s *Site) newHomePage() *Page {
 	p := s.newNodePage(KindHome)
-	p.Title = s.Info.Title
+	p.title = s.Info.Title
 	pages := Pages{}
 	p.Data["Pages"] = pages
 	p.Pages = pages
@@ -1892,10 +1892,10 @@
 		// Keep (mostly) as is in the title
 		// We make the first character upper case, mostly because
 		// it is easier to reason about in the tests.
-		p.Title = helpers.FirstUpper(key)
+		p.title = helpers.FirstUpper(key)
 		key = s.PathSpec.MakePathSanitized(key)
 	} else {
-		p.Title = strings.Replace(s.titleFunc(key), "-", " ", -1)
+		p.title = strings.Replace(s.titleFunc(key), "-", " ", -1)
 	}
 
 	return p
@@ -1906,9 +1906,9 @@
 
 	sectionName := helpers.FirstUpper(name)
 	if s.Cfg.GetBool("pluralizeListTitles") {
-		p.Title = inflect.Pluralize(sectionName)
+		p.title = inflect.Pluralize(sectionName)
 	} else {
-		p.Title = sectionName
+		p.title = sectionName
 	}
 	return p
 }
@@ -1915,6 +1915,6 @@
 
 func (s *Site) newTaxonomyTermsPage(plural string) *Page {
 	p := s.newNodePage(KindTaxonomyTerm, plural)
-	p.Title = s.titleFunc(plural)
+	p.title = s.titleFunc(plural)
 	return p
 }
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -212,7 +212,7 @@
 
 			if err := s.renderAndWritePage(
 				&s.PathSpec.ProcessingStats.PaginatorPages,
-				pagerNode.Title,
+				pagerNode.title,
 				targetPath, pagerNode, layouts...); err != nil {
 				return err
 			}
@@ -252,7 +252,7 @@
 		return err
 	}
 
-	return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Pages, p.Title,
+	return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Pages, p.title,
 		targetPath, p, layouts...)
 }
 
@@ -267,7 +267,7 @@
 
 	p := s.newNodePage(kind404)
 
-	p.Title = "404 Page not found"
+	p.title = "404 Page not found"
 	p.Data["Pages"] = s.Pages
 	p.Pages = s.Pages
 	p.URLPath.URL = "404.html"
--- a/hugolib/site_sections_test.go
+++ b/hugolib/site_sections_test.go
@@ -143,13 +143,13 @@
 			// > b,c,d where b and d have content files.
 			b := p.s.getPage(KindSection, "empty2", "b")
 			assert.NotNil(b)
-			assert.Equal("T40_-1", b.Title)
+			assert.Equal("T40_-1", b.title)
 			c := p.s.getPage(KindSection, "empty2", "b", "c")
 			assert.NotNil(c)
-			assert.Equal("Cs", c.Title)
+			assert.Equal("Cs", c.title)
 			d := p.s.getPage(KindSection, "empty2", "b", "c", "d")
 			assert.NotNil(d)
-			assert.Equal("T41_-1", d.Title)
+			assert.Equal("T41_-1", d.title)
 
 			assert.False(c.Eq(d))
 			assert.True(c.Eq(c))
@@ -165,7 +165,7 @@
 
 		}},
 		{"top", func(p *Page) {
-			assert.Equal("Tops", p.Title)
+			assert.Equal("Tops", p.title)
 			assert.Len(p.Pages, 2)
 			assert.Equal("mypage2.md", p.Pages[0].LogicalName())
 			assert.Equal("mypage3.md", p.Pages[1].LogicalName())
@@ -178,16 +178,16 @@
 			assert.True(active)
 		}},
 		{"l1", func(p *Page) {
-			assert.Equal("L1s", p.Title)
+			assert.Equal("L1s", p.title)
 			assert.Len(p.Pages, 2)
 			assert.True(p.Parent().IsHome())
 			assert.Len(p.Sections(), 2)
 		}},
 		{"l1,l2", func(p *Page) {
-			assert.Equal("T2_-1", p.Title)
+			assert.Equal("T2_-1", p.title)
 			assert.Len(p.Pages, 3)
 			assert.Equal(p, p.Pages[0].Parent())
-			assert.Equal("L1s", p.Parent().Title)
+			assert.Equal("L1s", p.Parent().title)
 			assert.Equal("/l1/l2/", p.URLPath.URL)
 			assert.Equal("/l1/l2/", p.RelPermalink())
 			assert.Len(p.Sections(), 1)
@@ -223,16 +223,16 @@
 
 		}},
 		{"l1,l2_2", func(p *Page) {
-			assert.Equal("T22_-1", p.Title)
+			assert.Equal("T22_-1", p.title)
 			assert.Len(p.Pages, 2)
 			assert.Equal(filepath.FromSlash("l1/l2_2/page_2_2_1.md"), p.Pages[0].Path())
-			assert.Equal("L1s", p.Parent().Title)
+			assert.Equal("L1s", p.Parent().title)
 			assert.Len(p.Sections(), 0)
 		}},
 		{"l1,l2,l3", func(p *Page) {
-			assert.Equal("T3_-1", p.Title)
+			assert.Equal("T3_-1", p.title)
 			assert.Len(p.Pages, 2)
-			assert.Equal("T2_-1", p.Parent().Title)
+			assert.Equal("T2_-1", p.Parent().title)
 			assert.Len(p.Sections(), 0)
 
 			l1 := p.s.getPage(KindSection, "l1")
@@ -252,7 +252,7 @@
 
 		}},
 		{"perm a,link", func(p *Page) {
-			assert.Equal("T9_-1", p.Title)
+			assert.Equal("T9_-1", p.title)
 			assert.Equal("/perm-a/link/", p.RelPermalink())
 			assert.Len(p.Pages, 4)
 			first := p.Pages[0]
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -160,7 +160,7 @@
 		}
 	}
 
-	if s.AllPages[0].Title == "doc2" {
+	if s.AllPages[0].title == "doc2" {
 		t.Fatal("Expired content published unexpectedly")
 	}
 }
@@ -642,40 +642,40 @@
 
 	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
 
-	if s.getPage(KindSection, "sect").Pages[1].Title != "Three" || s.getPage(KindSection, "sect").Pages[2].Title != "Four" {
+	if s.getPage(KindSection, "sect").Pages[1].title != "Three" || s.getPage(KindSection, "sect").Pages[2].title != "Four" {
 		t.Error("Pages in unexpected order.")
 	}
 
 	bydate := s.RegularPages.ByDate()
 
-	if bydate[0].Title != "One" {
-		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bydate[0].Title)
+	if bydate[0].title != "One" {
+		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bydate[0].title)
 	}
 
 	rev := bydate.Reverse()
-	if rev[0].Title != "Three" {
-		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rev[0].Title)
+	if rev[0].title != "Three" {
+		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rev[0].title)
 	}
 
 	bypubdate := s.RegularPages.ByPublishDate()
 
-	if bypubdate[0].Title != "One" {
-		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bypubdate[0].Title)
+	if bypubdate[0].title != "One" {
+		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bypubdate[0].title)
 	}
 
 	rbypubdate := bypubdate.Reverse()
-	if rbypubdate[0].Title != "Three" {
-		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rbypubdate[0].Title)
+	if rbypubdate[0].title != "Three" {
+		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rbypubdate[0].title)
 	}
 
 	bylength := s.RegularPages.ByLength()
-	if bylength[0].Title != "One" {
-		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bylength[0].Title)
+	if bylength[0].title != "One" {
+		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bylength[0].title)
 	}
 
 	rbylength := bylength.Reverse()
-	if rbylength[0].Title != "Four" {
-		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Four", rbylength[0].Title)
+	if rbylength[0].title != "Four" {
+		t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Four", rbylength[0].title)
 	}
 }
 
@@ -714,8 +714,8 @@
 	if rbysection[2].Key != "sect1" {
 		t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "sect1", rbysection[2].Key)
 	}
-	if rbysection[0].Pages[0].Title != "Four" {
-		t.Errorf("PageGroup has an unexpected page. First group's pages should have '%s', got '%s'", "Four", rbysection[0].Pages[0].Title)
+	if rbysection[0].Pages[0].title != "Four" {
+		t.Errorf("PageGroup has an unexpected page. First group's pages should have '%s', got '%s'", "Four", rbysection[0].Pages[0].title)
 	}
 	if len(rbysection[2].Pages) != 2 {
 		t.Errorf("PageGroup has unexpected number of pages. Third group should have '%d' pages, got '%d' pages", 2, len(rbysection[2].Pages))
@@ -734,8 +734,8 @@
 	if bytype[2].Key != "sect3" {
 		t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "sect3", bytype[2].Key)
 	}
-	if bytype[2].Pages[0].Title != "Four" {
-		t.Errorf("PageGroup has an unexpected page. Third group's data should have '%s', got '%s'", "Four", bytype[0].Pages[0].Title)
+	if bytype[2].Pages[0].title != "Four" {
+		t.Errorf("PageGroup has an unexpected page. Third group's data should have '%s', got '%s'", "Four", bytype[0].Pages[0].title)
 	}
 	if len(bytype[0].Pages) != 2 {
 		t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(bytype[2].Pages))
@@ -762,8 +762,8 @@
 	if bypubdate[1].Key != "0001" {
 		t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "0001", bypubdate[1].Key)
 	}
-	if bypubdate[0].Pages[0].Title != "Three" {
-		t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", bypubdate[0].Pages[0].Title)
+	if bypubdate[0].Pages[0].title != "Three" {
+		t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", bypubdate[0].Pages[0].title)
 	}
 	if len(bypubdate[0].Pages) != 3 {
 		t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 3, len(bypubdate[0].Pages))
@@ -782,8 +782,8 @@
 	if byparam[2].Key != "bar" {
 		t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "bar", byparam[2].Key)
 	}
-	if byparam[2].Pages[0].Title != "Three" {
-		t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", byparam[2].Pages[0].Title)
+	if byparam[2].Pages[0].title != "Three" {
+		t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", byparam[2].Pages[0].title)
 	}
 	if len(byparam[0].Pages) != 2 {
 		t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(byparam[0].Pages))
@@ -815,8 +815,8 @@
 	if byParamDate[1].Key != "1979-05" {
 		t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "1979-05", byParamDate[1].Key)
 	}
-	if byParamDate[1].Pages[0].Title != "One" {
-		t.Errorf("PageGroup has an unexpected page. Second group's pages should have '%s', got '%s'", "One", byParamDate[1].Pages[0].Title)
+	if byParamDate[1].Pages[0].title != "One" {
+		t.Errorf("PageGroup has an unexpected page. Second group's pages should have '%s', got '%s'", "One", byParamDate[1].Pages[0].title)
 	}
 	if len(byParamDate[0].Pages) != 2 {
 		t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(byParamDate[2].Pages))
@@ -872,16 +872,16 @@
 	writeSourcesToSource(t, "content", fs, sources...)
 	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
 
-	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)
+	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)
 	}
 
-	if s.Taxonomies["categories"]["d"][0].Page.Title != "bar" {
-		t.Errorf("Pages in unexpected order, 'bar' expected first, got '%v'", s.Taxonomies["categories"]["d"][0].Page.Title)
+	if s.Taxonomies["categories"]["d"][0].Page.title != "bar" {
+		t.Errorf("Pages in unexpected order, 'bar' expected first, got '%v'", s.Taxonomies["categories"]["d"][0].Page.title)
 	}
 
-	if s.Taxonomies["categories"]["e"][0].Page.Title != "bza" {
-		t.Errorf("Pages in unexpected order, 'bza' expected first, got '%v'", s.Taxonomies["categories"]["e"][0].Page.Title)
+	if s.Taxonomies["categories"]["e"][0].Page.title != "bza" {
+		t.Errorf("Pages in unexpected order, 'bza' expected first, got '%v'", s.Taxonomies["categories"]["e"][0].Page.title)
 	}
 }
 
--- a/hugolib/taxonomy.go
+++ b/hugolib/taxonomy.go
@@ -43,7 +43,7 @@
 }
 
 func (w WeightedPage) String() string {
-	return fmt.Sprintf("WeightedPage(%d,%q)", w.Weight, w.Page.Title)
+	return fmt.Sprintf("WeightedPage(%d,%q)", w.Weight, w.Page.title)
 }
 
 // OrderedTaxonomy is another representation of an Taxonomy using an array rather than a map.
@@ -214,7 +214,7 @@
 func (wp WeightedPages) Less(i, j int) bool {
 	if wp[i].Weight == wp[j].Weight {
 		if wp[i].Page.Date.Equal(wp[j].Page.Date) {
-			return wp[i].Page.Title < wp[j].Page.Title
+			return wp[i].Page.title < wp[j].Page.title
 		}
 		return wp[i].Page.Date.After(wp[i].Page.Date)
 	}
--- a/hugolib/taxonomy_test.go
+++ b/hugolib/taxonomy_test.go
@@ -200,11 +200,11 @@
 	if preserveTaxonomyNames {
 		helloWorld := s.getPage(KindTaxonomy, "others", "Hello Hugo world")
 		require.NotNil(t, helloWorld)
-		require.Equal(t, "Hello Hugo world", helloWorld.Title)
+		require.Equal(t, "Hello Hugo world", helloWorld.title)
 	} else {
 		helloWorld := s.getPage(KindTaxonomy, "others", "hello-hugo-world")
 		require.NotNil(t, helloWorld)
-		require.Equal(t, "Hello Hugo World", helloWorld.Title)
+		require.Equal(t, "Hello Hugo World", helloWorld.title)
 	}
 
 	// Issue #2977
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -217,7 +217,7 @@
 	for i, p := range pages {
 		fmt.Printf("%d: Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s Len Sections(): %d\n",
 			i+1,
-			p.Kind, p.Title, p.RelPermalink(), p.Path(), p.sections, len(p.Sections()))
+			p.Kind, p.title, p.RelPermalink(), p.Path(), p.sections, len(p.Sections()))
 	}
 }
 
--- a/magefile.go
+++ b/magefile.go
@@ -231,6 +231,9 @@
 		}
 		b, err := ioutil.ReadFile(cover)
 		if err != nil {
+			if os.IsNotExist(err) {
+				continue
+			}
 			return err
 		}
 		idx := bytes.Index(b, []byte{'\n'})
--- a/resource/image.go
+++ b/resource/image.go
@@ -208,7 +208,7 @@
 
 	key := i.relTargetPathForRel(i.filenameFromConfig(conf), false)
 
-	return i.spec.imageCache.getOrCreate(i.spec, key, func(resourceCacheFilename string) (*Image, error) {
+	return i.spec.imageCache.getOrCreate(i, key, func(resourceCacheFilename string) (*Image, error) {
 		ci := i.clone()
 
 		ci.setBasePath(conf)
--- a/resource/image_cache.go
+++ b/resource/image_cache.go
@@ -15,7 +15,6 @@
 
 import (
 	"fmt"
-	"os"
 	"path/filepath"
 	"strings"
 	"sync"
@@ -50,7 +49,7 @@
 }
 
 func (c *imageCache) getOrCreate(
-	spec *Spec, key string, create func(resourceCacheFilename string) (*Image, error)) (*Image, error) {
+	parent *Image, key string, create func(resourceCacheFilename string) (*Image, error)) (*Image, error) {
 
 	relTargetFilename := key
 
@@ -77,19 +76,20 @@
 	//  but the count of processed image variations for this site.
 	c.pathSpec.ProcessingStats.Incr(&c.pathSpec.ProcessingStats.ProcessedImages)
 
-	r, err := spec.NewResourceFromFilename(nil, c.absPublishDir, cacheFilename, relTargetFilename)
-	notFound := err != nil && os.IsNotExist(err)
-	if err != nil && !os.IsNotExist(err) {
+	exists, err := helpers.Exists(cacheFilename, c.pathSpec.Fs.Source)
+	if err != nil {
 		return nil, err
 	}
 
-	if notFound {
+	if exists {
+		img = parent.clone()
+		img.relTargetPath = relTargetFilename
+		img.absSourceFilename = cacheFilename
+	} else {
 		img, err = create(cacheFilename)
 		if err != nil {
 			return nil, err
 		}
-	} else {
-		img = r.(*Image)
 	}
 
 	c.mu.Lock()
@@ -102,7 +102,7 @@
 
 	c.mu.Unlock()
 
-	if notFound {
+	if !exists {
 		// File already written to destination
 		return img, nil
 	}
--- a/resource/image_test.go
+++ b/resource/image_test.go
@@ -147,3 +147,25 @@
 	assert.Equal(42, imaging.Quality)
 	assert.Equal("nearestneighbor", imaging.ResampleFilter)
 }
+
+func TestImageWithMetadata(t *testing.T) {
+	assert := require.New(t)
+
+	image := fetchSunset(assert)
+
+	var meta = []map[string]interface{}{
+		map[string]interface{}{
+			"title": "My Sunset",
+			"name":  "Sunset #:counter",
+			"src":   "*.jpg",
+		},
+	}
+
+	assert.NoError(AssignMetadata(meta, image))
+	assert.Equal("Sunset #1", image.Name())
+
+	resized, err := image.Resize("200x")
+	assert.NoError(err)
+	assert.Equal("Sunset #1", resized.Name())
+
+}
--- a/resource/resource.go
+++ b/resource/resource.go
@@ -19,8 +19,11 @@
 	"os"
 	"path"
 	"path/filepath"
+	"strconv"
 	"strings"
 
+	"github.com/spf13/cast"
+
 	"github.com/gohugoio/hugo/media"
 	"github.com/gohugoio/hugo/source"
 
@@ -28,9 +31,10 @@
 )
 
 var (
-	_ Resource = (*genericResource)(nil)
-	_ Source   = (*genericResource)(nil)
-	_ Cloner   = (*genericResource)(nil)
+	_ Resource     = (*genericResource)(nil)
+	_ metaAssigner = (*genericResource)(nil)
+	_ Source       = (*genericResource)(nil)
+	_ Cloner       = (*genericResource)(nil)
 )
 
 const DefaultResourceType = "unknown"
@@ -48,11 +52,38 @@
 	WithNewBase(base string) Resource
 }
 
+type metaAssigner interface {
+	setTitle(title string)
+	setName(name string)
+	setParams(params map[string]interface{})
+}
+
 // Resource represents a linkable resource, i.e. a content page, image etc.
 type Resource interface {
+	// Permalink represents the absolute link to this resource.
 	Permalink() string
+
+	// RelPermalink represents the host relative link to this resource.
 	RelPermalink() string
+
+	// ResourceType is the resource type. For most file types, this is the main
+	// part of the MIME type, e.g. "image", "application", "text" etc.
+	// For content pages, this value is "page".
 	ResourceType() string
+
+	// Name is the logical name of this resource. This can be set in the front matter
+	// metadata for this resource. If not set, Hugo will assign a value.
+	// This will in most cases be the base filename.
+	// So, for the image "/some/path/sunset.jpg" this will be "sunset.jpg".
+	// The value returned by this method will be used in the GetByPrefix and ByPrefix methods
+	// on Resources.
+	Name() string
+
+	// Title returns the title if set in front matter. For content pages, this will be the expected value.
+	Title() string
+
+	// Params set in front matter for this resource.
+	Params() map[string]interface{}
 }
 
 // Resources represents a slice of resources, which can be a mix of different types.
@@ -97,16 +128,7 @@
 }
 
 func matchesPrefix(r Resource, prefix string) bool {
-	var name string
-	f, ok := r.(source.File)
-	if ok {
-		name = f.BaseFileName()
-	} else {
-		_, name = filepath.Split(r.RelPermalink())
-	}
-	name = strings.ToLower(name)
-
-	return strings.HasPrefix(name, prefix)
+	return strings.HasPrefix(strings.ToLower(r.Name()), prefix)
 }
 
 type Spec struct {
@@ -238,6 +260,10 @@
 	// Base is set when the output format's path has a offset, e.g. for AMP.
 	base string
 
+	title  string
+	name   string
+	params map[string]interface{}
+
 	// Absolute filename to the source, including any content folder path.
 	absSourceFilename string
 	absPublishDir     string
@@ -256,6 +282,30 @@
 	return l.relPermalinkForRel(l.relTargetPath, true)
 }
 
+func (l *genericResource) Name() string {
+	return l.name
+}
+
+func (l *genericResource) Title() string {
+	return l.title
+}
+
+func (l *genericResource) Params() map[string]interface{} {
+	return l.params
+}
+
+func (l *genericResource) setTitle(title string) {
+	l.title = title
+}
+
+func (l *genericResource) setName(name string) {
+	l.name = name
+}
+
+func (l *genericResource) setParams(params map[string]interface{}) {
+	l.params = params
+}
+
 // Implement the Cloner interface.
 func (l genericResource) WithNewBase(base string) Resource {
 	l.base = base
@@ -306,6 +356,98 @@
 	return helpers.WriteToDisk(target, f, l.spec.Fs.Destination)
 }
 
+// AssignMetadata assigns the given metadata to those resources that supports updates
+// and matching by wildcard given in `src` using `filepath.Match` with lower cased values.
+// This assignment is additive, but the most specific match needs to be first.
+// The `name` and `title` metadata field support shell-matched collection it got a match in.
+// See https://golang.org/pkg/path/filepath/#Match
+func AssignMetadata(metadata []map[string]interface{}, resources ...Resource) error {
+
+	counters := make(map[string]int)
+
+	for _, r := range resources {
+		if _, ok := r.(metaAssigner); !ok {
+			continue
+		}
+
+		var (
+			nameSet, titleSet, paramsSet bool
+			currentCounter               = 0
+			resourceSrcKey               = strings.ToLower(r.Name())
+		)
+
+		ma := r.(metaAssigner)
+		for _, meta := range metadata {
+			if nameSet && titleSet && paramsSet {
+				// No need to look further
+				break
+			}
+
+			src, found := meta["src"]
+			if !found {
+				return fmt.Errorf("missing 'src' in metadata for resource")
+			}
+
+			srcKey := strings.ToLower(cast.ToString(src))
+
+			match, err := filepath.Match(srcKey, resourceSrcKey)
+			if err != nil {
+				return fmt.Errorf("failed to match resource with metadata: %s", err)
+			}
+
+			if match {
+				if !nameSet {
+					name, found := meta["name"]
+					if found {
+						if currentCounter == 0 {
+							currentCounter = counters[srcKey] + 1
+							counters[srcKey] = currentCounter
+						}
+
+						ma.setName(replaceResourcePlaceholders(cast.ToString(name), currentCounter))
+						nameSet = true
+					}
+				}
+
+				if !titleSet {
+					title, found := meta["title"]
+					if found {
+						if currentCounter == 0 {
+							currentCounter = counters[srcKey] + 1
+							counters[srcKey] = currentCounter
+						}
+						ma.setTitle((replaceResourcePlaceholders(cast.ToString(title), currentCounter)))
+						titleSet = true
+					}
+				}
+
+				if !paramsSet {
+					params, found := meta["params"]
+					if found {
+						m := cast.ToStringMap(params)
+						// Needed for case insensitive fetching of params values
+						helpers.ToLowerMap(m)
+						ma.setParams(m)
+
+						if currentCounter == 0 {
+							currentCounter = counters[srcKey] + 1
+							counters[srcKey] = currentCounter
+						}
+
+						paramsSet = true
+					}
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func replaceResourcePlaceholders(in string, counter int) string {
+	return strings.Replace(in, ":counter", strconv.Itoa(counter), -1)
+}
+
 func (l *genericResource) target() string {
 	target := l.relTargetPathForRel(l.relTargetPath, false)
 	if l.spec.PathSpec.Languages.IsMultihost() {
@@ -330,5 +472,8 @@
 		relTargetPath:     baseFilename,
 		resourceType:      resourceType,
 		spec:              r,
+		params:            make(map[string]interface{}),
+		name:              baseFilename,
+		title:             baseFilename,
 	}
 }
--- a/resource/resource_test.go
+++ b/resource/resource_test.go
@@ -14,6 +14,7 @@
 package resource
 
 import (
+	"fmt"
 	"path"
 	"path/filepath"
 	"testing"
@@ -128,5 +129,196 @@
 
 	assert.Equal(2, len(resources.ByPrefix("logo")))
 	assert.Equal(1, len(resources.ByPrefix("logo2")))
+
+	logo := resources.GetByPrefix("logo")
+	assert.NotNil(logo.Params())
+	assert.Equal("logo1.png", logo.Name())
+	assert.Equal("logo1.png", logo.Title())
+
+}
+
+func TestAssignMetadata(t *testing.T) {
+	assert := require.New(t)
+	spec := newTestResourceSpec(assert)
+
+	var foo1, foo2, foo3, logo1, logo2, logo3 Resource
+	var resources Resources
+
+	for _, this := range []struct {
+		metaData   []map[string]interface{}
+		assertFunc func(err error)
+	}{
+		{[]map[string]interface{}{
+			map[string]interface{}{
+				"title": "My Resource",
+				"name":  "My Name",
+				"src":   "*",
+			},
+		}, func(err error) {
+			assert.Equal("My Resource", logo1.Title())
+			assert.Equal("My Name", logo1.Name())
+			assert.Equal("My Name", foo2.Name())
+
+		}},
+		{[]map[string]interface{}{
+			map[string]interface{}{
+				"title": "My Logo",
+				"src":   "*loGo*",
+			},
+			map[string]interface{}{
+				"title": "My Resource",
+				"name":  "My Name",
+				"src":   "*",
+			},
+		}, func(err error) {
+			assert.Equal("My Logo", logo1.Title())
+			assert.Equal("My Logo", logo2.Title())
+			assert.Equal("My Name", logo1.Name())
+			assert.Equal("My Name", foo2.Name())
+			assert.Equal("My Name", foo3.Name())
+			assert.Equal("My Resource", foo3.Title())
+
+		}},
+		{[]map[string]interface{}{
+			map[string]interface{}{
+				"title": "My Logo",
+				"src":   "*loGo*",
+				"params": map[string]interface{}{
+					"Param1": true,
+				},
+			},
+			map[string]interface{}{
+				"title": "My Resource",
+				"src":   "*",
+				"params": map[string]interface{}{
+					"Param2": true,
+				},
+			},
+		}, func(err error) {
+			assert.NoError(err)
+			assert.Equal("My Logo", logo1.Title())
+			assert.Equal("My Resource", foo3.Title())
+			_, p1 := logo2.Params()["param1"]
+			_, p2 := foo2.Params()["param2"]
+			assert.True(p1)
+			assert.True(p2)
+
+		}},
+		{[]map[string]interface{}{
+			map[string]interface{}{
+				"name": "Logo Name #:counter",
+				"src":  "*logo*",
+			},
+			map[string]interface{}{
+				"title": "Resource #:counter",
+				"name":  "Name #:counter",
+				"src":   "*",
+			},
+		}, func(err error) {
+			assert.NoError(err)
+			assert.Equal("Resource #1", logo2.Title())
+			assert.Equal("Logo Name #1", logo2.Name())
+			assert.Equal("Resource #2", logo1.Title())
+			assert.Equal("Logo Name #2", logo1.Name())
+			assert.Equal("Resource #1", foo2.Title())
+			assert.Equal("Resource #2", foo1.Title())
+			assert.Equal("Name #2", foo1.Name())
+			assert.Equal("Resource #3", foo3.Title())
+
+			assert.Equal(logo2, resources.GetByPrefix("logo name #1"))
+
+		}},
+		{[]map[string]interface{}{
+			map[string]interface{}{
+				"title": "Third Logo #:counter",
+				"src":   "logo3.png",
+			},
+			map[string]interface{}{
+				"title": "Other Logo #:counter",
+				"name":  "Name #:counter",
+				"src":   "logo*",
+			},
+		}, func(err error) {
+			assert.NoError(err)
+			assert.Equal("Third Logo #1", logo3.Title())
+			assert.Equal("Name #1", logo3.Name())
+			assert.Equal("Other Logo #1", logo2.Title())
+			assert.Equal("Name #1", logo2.Name())
+			assert.Equal("Other Logo #2", logo1.Title())
+			assert.Equal("Name #2", logo1.Name())
+
+		}},
+		{[]map[string]interface{}{
+			map[string]interface{}{
+				"title": "Third Logo #:counter",
+			},
+		}, func(err error) {
+			// Missing src
+			assert.Error(err)
+
+		}},
+		{[]map[string]interface{}{
+			map[string]interface{}{
+				"title": "Title",
+				"src":   "[]",
+			},
+		}, func(err error) {
+			// Invalid pattern
+			assert.Error(err)
+
+		}},
+	} {
+
+		foo2 = spec.newGenericResource(nil, nil, "/public", "/b/foo2.css", "foo2.css", "css")
+		logo2 = spec.newGenericResource(nil, nil, "/public", "/b/Logo2.png", "Logo2.png", "image")
+		foo1 = spec.newGenericResource(nil, nil, "/public", "/a/foo1.css", "foo1.css", "css")
+		logo1 = spec.newGenericResource(nil, nil, "/public", "/a/logo1.png", "logo1.png", "image")
+		foo3 = spec.newGenericResource(nil, nil, "/public", "/b/foo3.css", "foo3.css", "css")
+		logo3 = spec.newGenericResource(nil, nil, "/public", "/b/logo3.png", "logo3.png", "image")
+
+		resources = Resources{
+			foo2,
+			logo2,
+			foo1,
+			logo1,
+			foo3,
+			logo3,
+		}
+
+		this.assertFunc(AssignMetadata(this.metaData, resources...))
+	}
+
+}
+
+func BenchmarkAssignMetadata(b *testing.B) {
+	assert := require.New(b)
+	spec := newTestResourceSpec(assert)
+
+	for i := 0; i < b.N; i++ {
+		b.StopTimer()
+		var resources Resources
+		var meta = []map[string]interface{}{
+			map[string]interface{}{
+				"title": "Foo #:counter",
+				"name":  "Foo Name #:counter",
+				"src":   "foo1*",
+			},
+			map[string]interface{}{
+				"title": "Rest #:counter",
+				"name":  "Rest Name #:counter",
+				"src":   "*",
+			},
+		}
+		for i := 0; i < 20; i++ {
+			name := fmt.Sprintf("foo%d_%d.css", i%5, i)
+			resources = append(resources, spec.newGenericResource(nil, nil, "/public", "/a/"+name, name, "css"))
+		}
+		b.StartTimer()
+
+		if err := AssignMetadata(meta, resources...); err != nil {
+			b.Fatal(err)
+		}
+
+	}
 
 }