diff --git a/docs/content/templates/rss.md b/docs/content/templates/rss.md
index 70c3c7704..60e09ccf1 100644
--- a/docs/content/templates/rss.md
+++ b/docs/content/templates/rss.md
@@ -69,9 +69,7 @@ This is the default RSS template that ships with Hugo. It adheres to the [RSS 2.
{{ .Permalink }}
Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }}Hugo -- gohugo.io{{ with .Site.LanguageCode }}
- {{.}}{{end}}{{ with .Site.Author.email }}
- {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }}
- {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }}
+ {{.}}{{end}}{{ with .Site.Copyright }}
{{.}}{{end}}{{ if not .Date.IsZero }}
{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }}
@@ -80,7 +78,6 @@ This is the default RSS template that ships with Hugo. It adheres to the [RSS 2.
{{ .Title }}
{{ .Permalink }}
{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}
- {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}
{{ .Permalink }}{{ .Content | html }}
diff --git a/docs/content/templates/variables.md b/docs/content/templates/variables.md
index b85387b46..12f940f17 100644
--- a/docs/content/templates/variables.md
+++ b/docs/content/templates/variables.md
@@ -168,7 +168,7 @@ Also available is `.Site` which has the following:
**.Site.Files** All of the source files of the site.
**.Site.Menus** All of the menus in the site.
**.Site.Title** A string representing the title of the site.
-**.Site.Author** A map of the authors as defined in the site configuration.
+**.Site.Authors** An ordered list (ordered by defined weight) of the authors as defined in the site configuration.
**.Site.LanguageCode** A string representing the language as defined in the site configuration. This is mostly used to populate the RSS feeds with the right language code.
**.Site.DisqusShortname** A string representing the shortname of the Disqus shortcode as defined in the site configuration.
**.Site.GoogleAnalytics** A string representing your tracking code for Google Analytics as defined in the site configuration.
diff --git a/hugolib/author.go b/hugolib/author.go
index 0f4327097..2fbbfe793 100644
--- a/hugolib/author.go
+++ b/hugolib/author.go
@@ -13,23 +13,57 @@
package hugolib
-// AuthorList is a list of all authors and their metadata.
-type AuthorList map[string]Author
+import (
+ "fmt"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/spf13/cast"
+)
+
+var (
+ onlyNumbersRegExp = regexp.MustCompile("^[0-9]*$")
+)
+
+// Authors is a list of all authors and their metadata.
+type Authors []Author
+
+// Get returns an author from an ID
+func (a Authors) Get(id string) Author {
+ for _, author := range a {
+ if author.ID == id {
+ return author
+ }
+ }
+ return Author{}
+}
+
+// Sort sorts the authors by weight
+func (a Authors) Sort() Authors {
+ sort.Stable(a)
+ return a
+}
// Author contains details about the author of a page.
type Author struct {
- GivenName string
- FamilyName string
- DisplayName string
- Thumbnail string
- Image string
- ShortBio string
- LongBio string
- Email string
- Social AuthorSocial
+ ID string
+ GivenName string // givenName OR firstName
+ FirstName string // alias for GivenName
+ FamilyName string // familyName OR lastName
+ LastName string // alias for FamilyName
+ DisplayName string // displayName
+ Thumbnail string // thumbnail
+ Image string // image
+ ShortBio string // shortBio
+ Bio string // bio
+ Email string // email
+ Social AuthorSocial // social
+ Params map[string]string // params
+ Weight int
}
-// AuthorSocial is a place to put social details per author. These are the
+// AuthorSocial is a place to put social usernames per author. These are the
// standard keys that themes will expect to have available, but can be
// expanded to any others on a per site basis
// - website
@@ -43,3 +77,102 @@ type Author struct {
// - linkedin
// - skype
type AuthorSocial map[string]string
+
+// URL is a convenience function that provides the correct canonical URL
+// for a specific social network given a username. If an unsupported network
+// is requested, only the username is returned
+func (as AuthorSocial) URL(key string) string {
+ switch key {
+ case "github":
+ return fmt.Sprintf("https://github.com/%s", as[key])
+ case "facebook":
+ return fmt.Sprintf("https://www.facebook.com/%s", as[key])
+ case "twitter":
+ return fmt.Sprintf("https://twitter.com/%s", as[key])
+ case "googleplus":
+ isNumeric := onlyNumbersRegExp.Match([]byte(as[key]))
+ if isNumeric {
+ return fmt.Sprintf("https://plus.google.com/%s", as[key])
+ }
+ return fmt.Sprintf("https://plus.google.com/+%s", as[key])
+ case "pinterest":
+ return fmt.Sprintf("https://www.pinterest.com/%s/", as[key])
+ case "instagram":
+ return fmt.Sprintf("https://www.instagram.com/%s/", as[key])
+ case "youtube":
+ return fmt.Sprintf("https://www.youtube.com/user/%s", as[key])
+ case "linkedin":
+ return fmt.Sprintf("https://www.linkedin.com/in/%s", as[key])
+ default:
+ return as[key]
+ }
+}
+
+func mapToAuthors(m map[string]interface{}) Authors {
+ authors := make(Authors, len(m))
+ for authorID, data := range m {
+ authorMap, ok := data.(map[string]interface{})
+ if !ok {
+ continue
+ }
+ authors = append(authors, mapToAuthor(authorID, authorMap))
+ }
+ sort.Stable(authors)
+ return authors
+}
+
+func mapToAuthor(id string, m map[string]interface{}) Author {
+ author := Author{ID: id}
+ for k, data := range m {
+ switch k {
+ case "givenName", "firstName":
+ author.GivenName = cast.ToString(data)
+ author.FirstName = author.GivenName
+ case "familyName", "lastName":
+ author.FamilyName = cast.ToString(data)
+ author.LastName = author.FamilyName
+ case "displayName":
+ author.DisplayName = cast.ToString(data)
+ case "thumbnail":
+ author.Thumbnail = cast.ToString(data)
+ case "image":
+ author.Image = cast.ToString(data)
+ case "shortBio":
+ author.ShortBio = cast.ToString(data)
+ case "bio":
+ author.Bio = cast.ToString(data)
+ case "email":
+ author.Email = cast.ToString(data)
+ case "social":
+ author.Social = normalizeSocial(cast.ToStringMapString(data))
+ case "params":
+ author.Params = cast.ToStringMapString(data)
+ }
+ }
+
+ // set a reasonable default for DisplayName
+ if author.DisplayName == "" {
+ author.DisplayName = author.GivenName + " " + author.FamilyName
+ }
+
+ return author
+}
+
+// normalizeSocial makes a naive attempt to normalize social media usernames
+// and strips out extraneous characters or url info
+func normalizeSocial(m map[string]string) map[string]string {
+ for network, username := range m {
+ username = strings.TrimSpace(username)
+ username = strings.TrimSuffix(username, "/")
+ strs := strings.Split(username, "/")
+ username = strs[len(strs)-1]
+ username = strings.TrimPrefix(username, "@")
+ username = strings.TrimPrefix(username, "+")
+ m[network] = username
+ }
+ return m
+}
+
+func (a Authors) Len() int { return len(a) }
+func (a Authors) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a Authors) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
diff --git a/hugolib/node.go b/hugolib/node.go
index 566fd4799..820c483a6 100644
--- a/hugolib/node.go
+++ b/hugolib/node.go
@@ -21,11 +21,9 @@ import (
"sync"
"time"
- jww "github.com/spf13/jwalterweatherman"
-
- "github.com/spf13/hugo/helpers"
-
"github.com/spf13/cast"
+ "github.com/spf13/hugo/helpers"
+ jww "github.com/spf13/jwalterweatherman"
)
type Node struct {
@@ -322,3 +320,16 @@ func (n *Node) addLangFilepathPrefix(outfile string) string {
}
return helpers.FilePathSeparator + filepath.Join(n.Lang(), outfile)
}
+
+// Author returns the first defined author, sorted by Weight
+func (n *Node) Author() Author {
+ if len(n.Site.Authors) == 0 {
+ return Author{}
+ }
+ return n.Site.Authors[0]
+}
+
+// Authors returns all defined authors, sorted by Weight
+func (n *Node) Authors() Authors {
+ return n.Site.Authors
+}
diff --git a/hugolib/page.go b/hugolib/page.go
index fe4cd077f..6c6e984b4 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -190,33 +190,41 @@ func (p *Page) Param(key interface{}) (interface{}, error) {
return p.Site.Params[keyStr], nil
}
+// Author returns the first listed author for a page
func (p *Page) Author() Author {
authors := p.Authors()
-
- for _, author := range authors {
- return author
+ if len(authors) == 0 {
+ return Author{}
}
- return Author{}
+ return authors[0]
}
-func (p *Page) Authors() AuthorList {
- authorKeys, ok := p.Params["authors"]
- if !ok {
- return AuthorList{}
- }
- authors := authorKeys.([]string)
- if len(authors) < 1 || len(p.Site.Authors) < 1 {
- return AuthorList{}
- }
-
- al := make(AuthorList)
- for _, author := range authors {
- a, ok := p.Site.Authors[author]
- if ok {
- al[author] = a
+// Authors returns all listed authors for a page in the order they
+// are defined in the front matter. It first checks for a single author
+// since that it the most common use case, then checks for multiple authors.
+func (p *Page) Authors() Authors {
+ authorID, ok := p.Params["author"].(string)
+ if ok {
+ a := p.Site.Authors.Get(authorID)
+ if a.ID == authorID {
+ return Authors{a}
}
}
- return al
+
+ authorIDs, ok := p.Params["authors"].([]string)
+ if !ok || len(authorIDs) == 0 || len(p.Site.Authors) == 0 {
+ return Authors{}
+ }
+
+ authors := make([]Author, 0, len(authorIDs))
+ for _, authorID := range authorIDs {
+ a := p.Site.Authors.Get(authorID)
+ if a.ID == authorID {
+ authors = append(authors, a)
+ }
+ }
+
+ return authors
}
func (p *Page) UniqueID() string {
diff --git a/hugolib/site.go b/hugolib/site.go
index 87c440d38..f7872ba99 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -165,7 +165,7 @@ type SiteInfo struct {
BaseURL template.URL
Taxonomies TaxonomyList
- Authors AuthorList
+ Authors Authors
Social SiteSocial
Sections Taxonomy
Pages *Pages // Includes only pages in this language
@@ -176,7 +176,6 @@ type SiteInfo struct {
Hugo *HugoInfo
Title string
RSSLink string
- Author map[string]interface{}
LanguageCode string
DisqusShortname string
GoogleAnalytics string
@@ -733,6 +732,11 @@ func (s *Site) readDataFromSourceFS() error {
}
err = s.loadData(dataSources)
+
+ // extract author data from /data/_authors then delete it from .Data
+ s.Info.Authors = mapToAuthors(cast.ToStringMap(s.Data["_authors"]))
+ delete(s.Data, "_authors")
+
s.timerStep("load data")
return err
}
@@ -908,7 +912,6 @@ func (s *Site) initializeSiteInfo() {
s.Info = SiteInfo{
BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(viper.GetString("BaseURL"))),
Title: lang.GetString("Title"),
- Author: lang.GetStringMap("author"),
Social: lang.GetStringMapString("social"),
LanguageCode: lang.GetString("languagecode"),
Copyright: lang.GetString("copyright"),
diff --git a/tpl/template_embedded.go b/tpl/template_embedded.go
index c418511ac..185f7aecd 100644
--- a/tpl/template_embedded.go
+++ b/tpl/template_embedded.go
@@ -44,7 +44,7 @@ func (t *GoHTMLTemplate) EmbedShortcodes() {
t.AddInternalShortcode("speakerdeck.html", "")
t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
-
{{ else }}
@@ -70,9 +70,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
{{ .Permalink }}
Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }}Hugo -- gohugo.io{{ with .Site.LanguageCode }}
- {{.}}{{end}}{{ with .Site.Author.email }}
- {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }}
- {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }}
+ {{.}}{{end}}{{ with .Site.Copyright }}
{{.}}{{end}}{{ if not .Date.IsZero }}
{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }}
@@ -81,7 +79,6 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
{{ .Title }}
{{ .Permalink }}
{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}
- {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}
{{ .Permalink }}{{ .Content | html }}