Add First Class Author Support

Closes #1850
This commit is contained in:
Derek Perkins 2016-09-15 20:28:13 -06:00 committed by Bjørn Erik Pedersen
parent 44bf76d0f2
commit cf978c0649
7 changed files with 198 additions and 49 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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 }

View file

@ -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
}

View file

@ -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 {

View file

@ -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"),

View file

@ -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>