1
0
mirror of https://github.com/danog/gojekyll.git synced 2024-11-30 07:08:59 +01:00

First pass at SEO tag

This commit is contained in:
Oliver Steele 2017-07-11 12:03:52 -04:00
parent e0b451cf2f
commit e200d0c98c
5 changed files with 262 additions and 57 deletions

View File

@ -4,7 +4,6 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
@ -121,7 +120,7 @@ func varsCommand(site *site.Site) (err error) {
var data interface{}
switch {
case strings.HasPrefix(*variablePath, "site"):
data, err = followDots(site, strings.Split(*variablePath, ".")[1:])
data, err = utils.FollowDots(site, strings.Split(*variablePath, ".")[1:])
if err != nil {
return
}
@ -133,7 +132,7 @@ func varsCommand(site *site.Site) (err error) {
default:
data = site
}
b, err := yaml.Marshal(toLiquid(data))
b, err := yaml.Marshal(liquid.FromDrop(data))
if err != nil {
return err
}
@ -141,29 +140,3 @@ func varsCommand(site *site.Site) (err error) {
fmt.Println(string(b))
return nil
}
func followDots(data interface{}, props []string) (interface{}, error) {
for _, name := range props {
if drop, ok := data.(liquid.Drop); ok {
data = drop.ToLiquid()
}
if reflect.TypeOf(data).Kind() == reflect.Map {
item := reflect.ValueOf(data).MapIndex(reflect.ValueOf(name))
if item.CanInterface() && !item.IsNil() {
data = item.Interface()
continue
}
}
return nil, fmt.Errorf("no such property: %q", name)
}
return data, nil
}
func toLiquid(value interface{}) interface{} {
switch value := value.(type) {
case liquid.Drop:
return value.ToLiquid()
default:
return value
}
}

View File

@ -26,11 +26,11 @@ func (p *jekyllFeedPlugin) Initialize(s Site) error {
func (p *jekyllFeedPlugin) ConfigureTemplateEngine(e *liquid.Engine) error {
e.RegisterTag("feed_meta", p.feedMetaTag)
tmpl, err := e.ParseTemplate([]byte(feedTemplateSource))
tpl, err := e.ParseTemplate([]byte(feedTemplateSource))
if err != nil {
panic(err)
}
p.tpl = tmpl
p.tpl = tpl
return nil
}
@ -41,7 +41,6 @@ func (p *jekyllFeedPlugin) PostRead(s Site) error {
path = "/" + pp
}
}
d := feedDoc{s, p, path}
s.AddDocument(&d, true)
return nil

View File

@ -14,7 +14,6 @@ import (
"github.com/osteele/gojekyll/pages"
"github.com/osteele/gojekyll/utils"
"github.com/osteele/liquid"
"github.com/osteele/liquid/render"
)
// Site is the site interface that is available to a plugin.
@ -69,7 +68,6 @@ func register(name string, p Plugin) {
func init() {
register("jemoji", jekyllJemojiPlugin{})
register("jekyll-mentions", jekyllMentionsPlugin{})
register("jekyll-seo-tag", jekyllSEOTagPlugin{})
// the following plugins are always active
// no warning but effect; the server runs in this mode anyway
@ -102,29 +100,19 @@ func (p jekyllMentionsPlugin) PostRender(b []byte) []byte {
})
}
// jekyll-seo
type jekyllSEOTagPlugin struct{ plugin }
func (p jekyllSEOTagPlugin) ConfigureTemplateEngine(e *liquid.Engine) error {
p.stubbed("jekyll-seo-tag")
e.RegisterTag("seo", p.makeUnimplementedTag("jekyll-seo-tag"))
return nil
}
// helpers
func (p plugin) stubbed(name string) {
fmt.Printf("warning: gojekyll does not emulate the %s plugin. Some tags have been stubbed to prevent errors.\n", name)
}
// func (p plugin) stubbed(name string) {
// fmt.Printf("warning: gojekyll does not emulate the %s plugin. Some tags have been stubbed to prevent errors.\n", name)
// }
func (p plugin) makeUnimplementedTag(pluginName string) liquid.Renderer {
warned := false
return func(ctx render.Context) (string, error) {
if !warned {
fmt.Printf("The %q tag in the %q plugin has not been implemented.\n", ctx.TagName(), pluginName)
warned = true
}
return fmt.Sprintf(`<!-- unimplemented tag: %q -->`, ctx.TagName()), nil
}
}
// func (p plugin) makeUnimplementedTag(pluginName string) liquid.Renderer {
// warned := false
// return func(ctx render.Context) (string, error) {
// if !warned {
// fmt.Printf("The %q tag in the %q plugin has not been implemented.\n", ctx.TagName(), pluginName)
// warned = true
// }
// return fmt.Sprintf(`<!-- unimplemented tag: %q -->`, ctx.TagName()), nil
// }
// }

219
plugins/seo_tag.go Normal file
View File

@ -0,0 +1,219 @@
package plugins
import (
"fmt"
"regexp"
"github.com/osteele/gojekyll/utils"
"github.com/osteele/liquid"
"github.com/osteele/liquid/render"
)
type jekyllSEOTagPlugin struct {
plugin
site Site
tpl *liquid.Template
}
func init() {
register("jekyll-seo-tag", &jekyllSEOTagPlugin{})
}
func (p *jekyllSEOTagPlugin) Initialize(s Site) error {
p.site = s
return nil
}
func (p *jekyllSEOTagPlugin) ConfigureTemplateEngine(e *liquid.Engine) error {
e.RegisterTag("seo", p.seoTag)
tpl, err := e.ParseTemplate([]byte(seoTagTemplateSource))
if err != nil {
panic(err)
}
p.tpl = tpl
return nil
}
var seoSiteFields = []string{"description", "url", "twitter", "facebook", "logo", "social", "google_site_verification", "lang"}
var seoPageOrSiteFields = []string{"author", "description", "image", "author", "lang"}
var seoTagMultipleLinesPattern = regexp.MustCompile(`( *\n)+`)
func (p *jekyllSEOTagPlugin) seoTag(ctx render.Context) (string, error) {
var (
site = liquid.FromDrop(ctx.Get("site")).(map[string]interface{})
page = liquid.FromDrop(ctx.Get("page")).(map[string]interface{})
pageTitle = page["title"]
siteTitle = site["title"]
canonicalURL = fmt.Sprintf("%s%s", site["url"], page["url"])
)
if siteTitle == nil && site["name"] != nil {
siteTitle = site["name"]
}
seoTag := map[string]interface{}{
"title?": true,
"title": siteTitle,
// the following are not doc'ed, but evident from inspection:
// FIXME canonical w|w/out site.url and site.prefix
"canonical_url": canonicalURL,
"page_lang": "en_US",
"page_title": pageTitle,
}
copyFields(seoTag, site, append(seoSiteFields, seoPageOrSiteFields...))
copyFields(seoTag, page, seoPageOrSiteFields)
if pageTitle != nil && siteTitle != nil && pageTitle != siteTitle {
seoTag["title"] = fmt.Sprintf("%s | %s", pageTitle, siteTitle)
}
if author, ok := seoTag["author"].(string); ok {
if data, _ := utils.FollowDots(site, []string{"data", "authors", author}); data != nil {
seoTag["author"] = data
}
}
seoTag["json_ld"] = makeJSONLD(seoTag)
bindings := map[string]interface{}{
"page": page,
"site": site,
"seo_tag": seoTag,
}
b, err := p.tpl.Render(bindings)
if err != nil {
return "", err
}
return string(seoTagMultipleLinesPattern.ReplaceAll(b, []byte{'\n'})), nil
}
func copyFields(to, from map[string]interface{}, fields []string) {
for _, name := range fields {
if value := from[name]; value != nil {
to[name] = value
}
}
}
func makeJSONLD(seoTag map[string]interface{}) interface{} {
var authorRecord interface{}
if author := seoTag["author"]; author != nil {
if m, ok := author.(map[string]interface{}); ok {
author = m["name"]
}
authorRecord = map[string]interface{}{
"@type": "Person",
"name": author,
}
}
return map[string]interface{}{
// TODO publisher
"@context": "http://schema.org",
"@type": "WebPage",
"author": authorRecord,
"headline": seoTag["page_title"],
"description": seoTag["description"],
"url": seoTag["canonical_url"],
}
}
// Taken verbatim from https://github.com/jekyll/jekyll-seo-tag/
const seoTagTemplateSource = `<!-- Begin emulated Jekyll SEO tag -->
<!-- Adapted from github.com/jekyll/jekyll-seo-tag. Used according to the MIT License. -->
{% if seo_tag.title? %}
<title>{{ seo_tag.title }}</title>
{% endif %}
{% if seo_tag.page_title %}
<meta property="og:title" content="{{ seo_tag.page_title }}" />
{% endif %}
{% if seo_tag.author.name %}
<meta name="author" content="{{ seo_tag.author.name }}" />
{% endif %}
<meta property="og:locale" content="{{ seo_tag.page_lang | replace:'-','_' }}" />
{% if seo_tag.description %}
<meta name="description" content="{{ seo_tag.description }}" />
<meta property="og:description" content="{{ seo_tag.description }}" />
{% endif %}
{% if site.url %}
<link rel="canonical" href="{{ seo_tag.canonical_url }}" />
<meta property="og:url" content="{{ seo_tag.canonical_url }}" />
{% endif %}
{% if seo_tag.site_title %}
<meta property="og:site_name" content="{{ seo_tag.site_title }}" />
{% endif %}
{% if seo_tag.image %}
<meta property="og:image" content="{{ seo_tag.image.path }}" />
{% if seo_tag.image.height %}
<meta property="og:image:height" content="{{ seo_tag.image.height }}" />
{% endif %}
{% if seo_tag.image.width %}
<meta property="og:image:width" content="{{ seo_tag.image.width }}" />
{% endif %}
{% endif %}
{% if page.date %}
<meta property="og:type" content="article" />
<meta property="article:published_time" content="{{ page.date | date_to_xmlschema }}" />
{% endif %}
{% if paginator.previous_page %}
<link rel="prev" href="{{ paginator.previous_page_path | absolute_url }}">
{% endif %}
{% if paginator.next_page %}
<link rel="next" href="{{ paginator.next_page_path | absolute_url }}">
{% endif %}
{% if site.twitter %}
{% if seo_tag.image %}
<meta name="twitter:card" content="summary_large_image" />
{% else %}
<meta name="twitter:card" content="summary" />
{% endif %}
<meta name="twitter:site" content="@{{ site.twitter.username | replace:"@","" }}" />
{% if seo_tag.author.twitter %}
<meta name="twitter:creator" content="@{{ seo_tag.author.twitter }}" />
{% endif %}
{% endif %}
{% if site.facebook %}
{% if site.facebook.admins %}
<meta property="fb:admins" content="{{ site.facebook.admins }}" />
{% endif %}
{% if site.facebook.publisher %}
<meta property="article:publisher" content="{{ site.facebook.publisher }}" />
{% endif %}
{% if site.facebook.app_id %}
<meta property="fb:app_id" content="{{ site.facebook.app_id }}" />
{% endif %}
{% endif %}
{% if site.webmaster_verifications %}
{% if site.webmaster_verifications.google %}
<meta name="google-site-verification" content="{{ site.webmaster_verifications.google }}">
{% endif %}
{% if site.webmaster_verifications.bing %}
<meta name="msvalidate.01" content="{{ site.webmaster_verifications.bing }}">
{% endif %}
{% if site.webmaster_verifications.alexa %}
<meta name="alexaVerifyID" content="{{ site.webmaster_verifications.alexa }}">
{% endif %}
{% if site.webmaster_verifications.yandex %}
<meta name="yandex-verification" content="{{ site.webmaster_verifications.yandex }}">
{% endif %}
{% elsif site.google_site_verification %}
<meta name="google-site-verification" content="{{ site.google_site_verification }}" />
{% endif %}
<script type="application/ld+json">
{{ seo_tag.json_ld | jsonify }}
</script>
<!-- End emulated Jekyll SEO tag -->`

26
utils/liquid.go Normal file
View File

@ -0,0 +1,26 @@
package utils
import (
"fmt"
"reflect"
"github.com/osteele/liquid"
)
// FollowDots applied to a property list ["a", "b", "c"] is equivalent to
// the Liquid data expression "data.a.b.c", except without special treatment
// of "first", "last", and "size".
func FollowDots(data interface{}, props []string) (interface{}, error) {
for _, name := range props {
data = liquid.FromDrop(data)
if reflect.TypeOf(data).Kind() == reflect.Map {
item := reflect.ValueOf(data).MapIndex(reflect.ValueOf(name))
if item.IsValid() && !item.IsNil() && item.CanInterface() {
data = item.Interface()
continue
}
}
return nil, fmt.Errorf("no such property: %q", name)
}
return data, nil
}