package plugins import ( "bytes" "fmt" "text/template" "github.com/danog/gojekyll/utils" "github.com/danog/liquid" "github.com/danog/liquid/render" "github.com/danog/liquid/tags" "github.com/tdewolff/minify" "github.com/tdewolff/minify/html" ) type jekyllSEOTagPlugin struct { plugin site Site tpl *liquid.Template } func init() { register("jekyll-seo-tag", &jekyllSEOTagPlugin{}) } func (p *jekyllSEOTagPlugin) AfterInitSite(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 } func (p *jekyllSEOTagPlugin) seoTag(ctx render.Context) (string, error) { buf := new(bytes.Buffer) e := seoTagTemplate.Execute(buf, seoTag{p.tpl, ctx}) return buf.String(), e } type seoTag struct { tpl *liquid.Template ctx render.Context } var seoSiteFields = []string{"url", "twitter", "facebook", "logo", "social", "google_site_verification", "lang"} var seoPageOrSiteFields = []string{"author", "description", "image", "author", "lang"} func (p seoTag) TagBody() (string, error) { var ( ctx = p.ctx site = liquid.FromDrop(ctx.Get("site")).(tags.IterationKeyedMap) page = liquid.FromDrop(ctx.Get("page")).(tags.IterationKeyedMap) pageTitle = page["title"] siteTitle = site["title"] canonicalURL = fmt.Sprintf("%s%s", site["url"], page["url"]) ) if siteTitle == 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, "site_title": siteTitle, } copyFields(seoTag, site, append(seoSiteFields, seoPageOrSiteFields...)) copyFields(seoTag, page, seoPageOrSiteFields) // seoTag["description"] = page["excerpt"] FIXME appears to behave something like following, but excerpt should not be processed 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 } m := minify.New() m.AddFunc("text/html", html.Minify) min := bytes.NewBuffer(make([]byte, 0, len(b))) if err := m.Minify("text/html", min, bytes.NewBuffer(b)); err != nil { return "", err } return min.String(), 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{} { jsonLD := map[string]interface{}{ // TODO dateModified, datePublished, publisher, mainEntityOfPage "@context": "http://schema.org", "@type": "WebPage", // FIXME this is BlogPosting for a blog page "headline": seoTag["page_title"], "description": seoTag["description"], "url": seoTag["canonical_url"], } if author := seoTag["author"]; author != nil { if m, ok := author.(map[string]interface{}); ok { author = m["name"] } jsonLD["author"] = map[string]interface{}{ "@type": "Person", "name": author, } } return jsonLD } // This is a separate template so it isn't minimized away. var seoTagTemplate = template.Must(template.New("SEO tag").Parse( ` {{.TagBody}} `)) // Taken verbatim from https://github.com/jekyll/jekyll-seo-tag/ // Adapted from github.com/jekyll/jekyll-seo-tag. Used according to the MIT License. const seoTagTemplateSource = `{% if seo_tag.title? %}