shithub: hugo

Download patch

ref: cf978c06496d99e76b08418422dda5797d90fed6
parent: 44bf76d0f28cc88a1dc185d1c587e0977652edf2
author: Derek Perkins <[email protected]>
date: Thu Sep 15 16:28:13 EDT 2016

Add First Class Author Support

Closes #1850

--- a/docs/content/templates/rss.md
+++ b/docs/content/templates/rss.md
@@ -69,9 +69,7 @@
         <link>{{ .Permalink }}</link>
         <description>Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }}</description>
         <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
-        <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
-        <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
-        <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
+        <language>{{.}}</language>{{end}}{{ with .Site.Copyright }}
         <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
         <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
         <atom:link href="{{.URL}}" rel="self" type="application/rss+xml" />
@@ -80,7 +78,6 @@
           <title>{{ .Title }}</title>
           <link>{{ .Permalink }}</link>
           <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
-          {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
           <guid>{{ .Permalink }}</guid>
           <description>{{ .Content | html }}</description>
         </item>
--- a/docs/content/templates/variables.md
+++ b/docs/content/templates/variables.md
@@ -168,7 +168,7 @@
 **.Site.Files** All of the source files of the site.<br>
 **.Site.Menus** All of the menus in the site.<br>
 **.Site.Title** A string representing the title of the site.<br>
-**.Site.Author** A map of the authors as defined in the site configuration.<br>
+**.Site.Authors** An ordered list (ordered by defined weight) of the authors as defined in the site configuration.<br>
 **.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.<br>
 **.Site.DisqusShortname** A string representing the shortname of the Disqus shortcode as defined in the site configuration.<br>
 **.Site.GoogleAnalytics** A string representing your tracking code for Google Analytics as defined in the site configuration.<br>
--- 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 @@
 // - 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 }
--- a/hugolib/node.go
+++ b/hugolib/node.go
@@ -21,11 +21,9 @@
 	"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 {
@@ -321,4 +319,17 @@
 		return outfile
 	}
 	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
 }
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -190,33 +190,41 @@
 	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 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}
+		}
 	}
-	authors := authorKeys.([]string)
-	if len(authors) < 1 || len(p.Site.Authors) < 1 {
-		return AuthorList{}
+
+	authorIDs, ok := p.Params["authors"].([]string)
+	if !ok || len(authorIDs) == 0 || len(p.Site.Authors) == 0 {
+		return Authors{}
 	}
 
-	al := make(AuthorList)
-	for _, author := range authors {
-		a, ok := p.Site.Authors[author]
-		if ok {
-			al[author] = a
+	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 al
+
+	return authors
 }
 
 func (p *Page) UniqueID() string {
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -165,7 +165,7 @@
 
 	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 @@
 	Hugo                  *HugoInfo
 	Title                 string
 	RSSLink               string
-	Author                map[string]interface{}
 	LanguageCode          string
 	DisqusShortname       string
 	GoogleAnalytics       string
@@ -733,6 +732,11 @@
 	}
 
 	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 @@
 	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"),
--- a/tpl/template_embedded.go
+++ b/tpl/template_embedded.go
@@ -44,7 +44,7 @@
 	t.AddInternalShortcode("speakerdeck.html", "<script async class='speakerdeck-embed' data-id='{{ index .Params 0 }}' data-ratio='1.33333333333333' src='//speakerdeck.com/assets/embed.js'></script>")
 	t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
 <div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
-  <iframe src="//www.youtube.com/embed/{{ .Get "id" }}?{{ with .Get "autoplay" }}{{ if eq . "true" }}autoplay=1{{ end }}{{ end }}" 
+  <iframe src="//www.youtube.com/embed/{{ .Get "id" }}?{{ with .Get "autoplay" }}{{ if eq . "true" }}autoplay=1{{ end }}{{ end }}"
   {{ if not (.Get "class") }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}allowfullscreen frameborder="0"></iframe>
 </div>{{ else }}
 <div {{ if len .Params | eq 2 }}class="{{ .Get 1 }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
@@ -70,9 +70,7 @@
     <link>{{ .Permalink }}</link>
     <description>Recent content {{ with .Title }}in {{.}} {{ end }}on {{ .Site.Title }}</description>
     <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
-    <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
-    <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
-    <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
+    <language>{{.}}</language>{{end}}{{ with .Site.Copyright }}
     <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
     <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
     <atom:link href="{{.Permalink}}" rel="self" type="application/rss+xml" />
@@ -81,7 +79,6 @@
       <title>{{ .Title }}</title>
       <link>{{ .Permalink }}</link>
       <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
-      {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
       <guid>{{ .Permalink }}</guid>
       <description>{{ .Content | html }}</description>
     </item>