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:
parent
e0b451cf2f
commit
e200d0c98c
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
219
plugins/seo_tag.go
Normal 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
26
utils/liquid.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user