summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDerek Perkins <derek@derekperkins.com>2016-09-15 20:28:13 -0600
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2016-09-17 00:49:24 +0200
commitcf978c06496d99e76b08418422dda5797d90fed6 (patch)
tree301a0eb848539cef0ae7226d0816f37767d69acc
parent44bf76d0f28cc88a1dc185d1c587e0977652edf2 (diff)
Add First Class Author Support
Closes #1850
-rw-r--r--docs/content/templates/rss.md5
-rw-r--r--docs/content/templates/variables.md2
-rw-r--r--hugolib/author.go157
-rw-r--r--hugolib/node.go19
-rw-r--r--hugolib/page.go42
-rw-r--r--hugolib/site.go9
-rw-r--r--tpl/template_embedded.go7
7 files changed, 195 insertions, 46 deletions
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.
<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 @@ This is the default RSS template that ships with Hugo. It adheres to the [RSS 2.
<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>
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.<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>
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 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 {
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", "<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 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
<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 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
<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>