Merge pull request #184 from jekyll/drop

Convert seo_author_* logic to a drop
This commit is contained in:
Ben Balter 2017-04-11 16:35:25 -05:00 committed by GitHub
commit 6a373369e8
10 changed files with 1048 additions and 224 deletions

2
.rspec
View File

@ -1,2 +1,2 @@
--format documentation
--color
--require spec_helper

View File

@ -1,7 +1,12 @@
require "jekyll"
require "jekyll-seo-tag/version"
module Jekyll
class SeoTag < Liquid::Tag
autoload :JSONLD, "jekyll-seo-tag/json_ld"
autoload :Drop, "jekyll-seo-tag/drop"
autoload :Filters, "jekyll-seo-tag/filters"
attr_accessor :context
# Matches all whitespace that follows either
@ -39,12 +44,12 @@ module Jekyll
"page" => context.registers[:page],
"site" => context.registers[:site].site_payload["site"],
"paginator" => context["paginator"],
"seo_tag" => options,
"seo_tag" => drop,
}
end
def title?
@text !~ %r!title=false!i
def drop
@drop ||= Jekyll::SeoTag::Drop.new(@text, @context)
end
def info

241
lib/jekyll-seo-tag/drop.rb Normal file
View File

@ -0,0 +1,241 @@
module Jekyll
class SeoTag
class Drop < Jekyll::Drops::Drop
include Jekyll::SeoTag::JSONLD
TITLE_SEPARATOR = " | ".freeze
FORMAT_STRING_METHODS = %i[
markdownify strip_html normalize_whitespace escape_once
].freeze
HOMEPAGE_OR_ABOUT_REGEX = %r!^/(about/)?(index.html?)?$!
def initialize(text, context)
@obj = {}
@mutations = {}
@text = text
@context = context
end
def version
Jekyll::SeoTag::VERSION
end
# Should the `<title>` tag be generated for this page?
def title?
return false unless title
return @display_title if defined?(@display_title)
@display_title = (@text !~ %r!title=false!i)
end
def site_title
@site_title ||= format_string(site["title"] || site["name"])
end
# Page title without site title or description appended
def page_title
@page_title ||= format_string(page["title"] || site_title)
end
# Page title with site title or description appended
def title
@title ||= begin
if page["title"] && site_title
page_title + TITLE_SEPARATOR + site_title
elsif site["description"] && site_title
site_title + TITLE_SEPARATOR + format_string(site["description"])
else
page_title || site_title
end
end
end
def name
return @name if defined?(@name)
@name = if seo_name
seo_name
elsif !homepage_or_about?
nil
elsif site["social"] && site["social"]["name"]
format_string site["social"]["name"]
elsif site_title
format_string site_title
end
end
def description
@description ||= format_string(
page["description"] || page["excerpt"] || site["description"]
)
end
# Returns a nil or a hash representing the author
# Author name will be pulled from:
#
# 1. The `author` key, if the key is a string
# 2. The first author in the `authors` key
# 3. The `author` key in the site config
#
# If the result from the name search is a string, we'll also check
# to see if the author exists in `site.data.authors`
def author
@author ||= begin
return if author_string_or_hash.to_s.empty?
author = if author_string_or_hash.is_a?(String)
author_hash(author_string_or_hash)
else
author_string_or_hash
end
author["twitter"] ||= author["name"]
author["twitter"].delete! "@"
author.to_liquid
end
end
def date_modified
@date_modified ||= begin
date = if page["seo"] && page["seo"]["date_modified"]
page["seo"]["date_modified"]
else
page["last_modified_at"] || page["date"]
end
filters.date_to_xmlschema(date) if date
end
end
def date_published
@date_published ||= filters.date_to_xmlschema(page["date"]) if page["date"]
end
def type
@type ||= begin
if page["seo"] && page["seo"]["type"]
page["seo"]["type"]
elsif homepage_or_about?
"WebSite"
elsif page["date"]
"BlogPosting"
else
"WebPage"
end
end
end
def links
@links ||= begin
if page["seo"] && page["seo"]["links"]
page["seo"]["links"]
elsif homepage_or_about? && site["social"] && site["social"]["links"]
site["social"]["links"]
end
end
end
def logo
@logo ||= begin
return unless site["logo"]
if absolute_url? site["logo"]
filters.uri_escape site["logo"]
else
filters.uri_escape filters.absolute_url site["logo"]
end
end
end
# Returns nil or a hash representing the page image
# The image hash will always contain a path, pulled from:
#
# 1. The `image` key if it's a string
# 2. The `image.path` key if it's a hash
# 3. The `image.facebook` key
# 4. The `image.twitter` key
#
# The resulting path is always an absolute URL
def image
return @image if defined?(@image)
image = page["image"]
return @image = nil unless image
image = { "path" => image } if image.is_a?(String)
image["path"] ||= image["facebook"] || image["twitter"]
unless absolute_url? image["path"]
image["path"] = filters.absolute_url image["path"]
end
image["path"] = filters.uri_escape image["path"]
@image = image.to_liquid
end
def page_lang
@page_lang ||= page["lang"] || site["lang"] || "en_US"
end
def canonical_url
@canonical_url ||= filters.absolute_url(page["url"]).gsub(%r!/index\.html$!, "/")
end
private
def filters
@filters ||= Jekyll::SeoTag::Filters.new(@context)
end
def page
@page ||= @context.registers[:page].to_liquid
end
def site
@site ||= @context.registers[:site].site_payload["site"].to_liquid
end
def homepage_or_about?
page["url"] =~ HOMEPAGE_OR_ABOUT_REGEX
end
attr_reader :context
def fallback_data
@fallback_data ||= {}
end
def absolute_url?(string)
Addressable::URI.parse(string).absolute?
end
def format_string(string)
string = FORMAT_STRING_METHODS.reduce(string) do |memo, method|
filters.public_send(method, memo)
end
string unless string.empty?
end
def author_string_or_hash
@author_string_or_hash ||= begin
author = page["author"]
author = page["authors"][0] if author.to_s.empty? && page["authors"]
author = site["author"] if author.to_s.empty?
author
end
end
def author_hash(author_string)
if site.data["authors"] && site.data["authors"][author_string]
hash = site.data["authors"][author_string]
hash["twitter"] ||= author_string
hash
else
{ "name" => author_string }
end
end
def seo_name
@seo_name ||= format_string(page["seo"]["name"]) if page["seo"]
end
end
end
end

View File

@ -0,0 +1,12 @@
module Jekyll
class SeoTag
class Filters
include Jekyll::Filters
include Liquid::StandardFilters
def initialize(context)
@context = context
end
end
end
end

View File

@ -0,0 +1,79 @@
module Jekyll
class SeoTag
module JSONLD
# A hash of instance methods => key in resulting JSON-LD hash
METHODS_KEYS = {
:json_context => "@context",
:type => "@type",
:name => "name",
:page_title => "headline",
:json_author => "author",
:json_image => "image",
:date_published => "datePublished",
:date_modified => "dateModified",
:description => "description",
:publisher => "publisher",
:main_entity => "mainEntityOfPage",
:links => "sameAs",
:canonical_url => "url",
}.freeze
def json_ld
@json_ld ||= begin
output = {}
METHODS_KEYS.each do |method, key|
value = send(method)
output[key] = value unless value.nil?
end
output
end
end
private
def json_context
"http://schema.org"
end
def json_author
return unless author
{
"@type" => "Person",
"name" => author["name"],
}
end
def json_image
return unless image
return image["path"] if image.length == 1
hash = image.dup
hash["url"] = hash.delete("path")
hash["@type"] = "imageObject"
hash
end
def publisher
return unless logo
output = {
"@type" => "Organization",
"logo" => {
"@type" => "ImageObject",
"url" => logo,
},
}
output["name"] = author["name"] if author
output
end
def main_entity
return unless %w(BlogPosting CreativeWork).include?(type)
{
"@type" => "WebPage",
"@id" => canonical_url,
}
end
end
end
end

View File

@ -1,143 +1,39 @@
<!-- Begin Jekyll SEO tag v{{ seo_tag.version }} -->
{% if page.url == "/" or page.url == "/about/" %}
{% assign seo_homepage_or_about = true %}
{% if seo_tag.title? %}
<title>{{ seo_tag.title }}</title>
{% endif %}
{% assign seo_site_title = site.title | default: site.name %}
{% assign seo_page_title = page.title | default: seo_site_title %}
{% assign seo_title = page.title | default: seo_site_title %}
{% if page.title and seo_site_title %}
{% assign seo_title = page.title | append:" | " | append: seo_site_title %}
{% elsif site.description and seo_site_title %}
{% assign seo_title = seo_site_title | append:" | " | append: site.description %}
{% if seo_tag.page_title %}
<meta property="og:title" content="{{ seo_tag.page_title }}" />
{% endif %}
{% if page.seo and page.seo.name %}
{% assign seo_name = page.seo.name %}
{% elsif seo_homepage_or_about and site.social and site.social.name %}
{% assign seo_name = site.social.name %}
{% elsif seo_homepage_or_about and seo_site_title %}
{% assign seo_name = seo_site_title %}
{% endif %}
{% if seo_name %}
{% assign seo_name = seo_name | smartify | strip_html | normalize_whitespace | escape_once %}
{% if seo_tag.author.name %}
<meta name="author" content="{{ seo_tag.author.name }}" />
{% endif %}
{% if seo_title %}
{% assign seo_title = seo_title | smartify | strip_html | normalize_whitespace | escape_once %}
<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 seo_site_title %}
{% assign seo_site_title = seo_site_title | smartify | strip_html | normalize_whitespace | escape_once %}
{% if site.url %}
<link rel="canonical" href="{{ seo_tag.canonical_url }}" />
<meta property="og:url" content="{{ seo_tag.canonical_url }}" />
{% endif %}
{% if seo_page_title %}
{% assign seo_page_title = seo_page_title | smartify | strip_html | normalize_whitespace | escape_once %}
{% if seo_tag.site_title %}
<meta property="og:site_name" content="{{ seo_tag.site_title }}" />
{% endif %}
{% assign seo_description = page.description | default: page.excerpt | default: site.description %}
{% if seo_description %}
{% assign seo_description = seo_description | markdownify | strip_html | normalize_whitespace | escape_once %}
{% endif %}
{% assign seo_author = page.author | default: page.authors[0] | default: site.author %}
{% if seo_author %}
{% if seo_author.name %}
{% assign seo_author_name = seo_author.name %}
{% else %}
{% if site.data.authors and site.data.authors[seo_author] %}
{% assign seo_author_name = site.data.authors[seo_author].name %}
{% else %}
{% assign seo_author_name = seo_author %}
{% 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_author.twitter %}
{% assign seo_author_twitter = seo_author.twitter %}
{% else %}
{% if site.data.authors and site.data.authors[seo_author] %}
{% assign seo_author_twitter = site.data.authors[seo_author].twitter %}
{% else %}
{% assign seo_author_twitter = seo_author %}
{% endif %}
{% endif %}
{% assign seo_author_twitter = seo_author_twitter | replace:"@","" %}
{% endif %}
{% if page.date_modified or page.last_modified_at or page.date %}
{% assign seo_date_modified = page.seo.date_modified | default: page.last_modified_at %}
{% endif %}
{% if page.seo and page.seo.type %}
{% assign seo_type = page.seo.type %}
{% elsif seo_homepage_or_about %}
{% assign seo_type = "WebSite" %}
{% elsif page.date %}
{% assign seo_type = "BlogPosting" %}
{% else %}
{% assign seo_type = "WebPage" %}
{% endif %}
{% if page.seo and page.seo.links %}
{% assign seo_links = page.seo.links %}
{% elsif seo_homepage_or_about and site.social and site.social.links %}
{% assign seo_links = site.social.links %}
{% endif %}
{% if site.logo %}
{% assign seo_site_logo = site.logo %}
{% unless seo_site_logo contains "://" %}
{% assign seo_site_logo = seo_site_logo | absolute_url %}
{% endunless %}
{% assign seo_site_logo = seo_site_logo | escape %}
{% endif %}
{% if page.image %}
{% assign seo_page_image = page.image.path | default: page.image.facebook | default: page.image.twitter | default: page.image %}
{% unless seo_page_image contains "://" %}
{% assign seo_page_image = seo_page_image | absolute_url %}
{% endunless %}
{% assign seo_page_image = seo_page_image | escape %}
{% endif %}
{% assign seo_page_lang = page.lang | default: site.lang | default: "en_US" %}
{% if seo_tag.title and seo_title %}
<title>{{ seo_title }}</title>
{% endif %}
{% if seo_page_title %}
<meta property="og:title" content="{{ seo_page_title }}" />
{% endif %}
{% if seo_author_name %}
<meta name="author" content="{{ seo_author_name }}" />
{% endif %}
<meta property="og:locale" content="{{ seo_page_lang | replace:'-','_' }}" />
{% if seo_description %}
<meta name="description" content="{{ seo_description }}" />
<meta property="og:description" content="{{ seo_description }}" />
{% endif %}
{% if page.url %}
<link rel="canonical" href="{{ page.url | replace:'/index.html','/' | absolute_url }}" />
<meta property="og:url" content="{{ page.url | replace:'/index.html','/' | absolute_url }}" />
{% endif %}
{% if seo_site_title %}
<meta property="og:site_name" content="{{ seo_site_title }}" />
{% endif %}
{% if seo_page_image %}
<meta property="og:image" content="{{ seo_page_image }}" />
{% if page.image.height %}
<meta property="og:image:height" content="{{ page.image.height }}" />
{% endif %}
{% if page.image.width %}
<meta property="og:image:width" content="{{ page.image.width }}" />
{% if seo_tag.image.width %}
<meta property="og:image:width" content="{{ seo_tag.image.width }}" />
{% endif %}
{% endif %}
@ -154,7 +50,7 @@
{% endif %}
{% if site.twitter %}
{% if seo_page_image or page.image.twitter %}
{% if seo_tag.image %}
<meta name="twitter:card" content="summary_large_image" />
{% else %}
<meta name="twitter:card" content="summary" />
@ -162,8 +58,8 @@
<meta name="twitter:site" content="@{{ site.twitter.username | replace:"@","" }}" />
{% if seo_author_twitter %}
<meta name="twitter:creator" content="@{{ seo_author_twitter }}" />
{% if seo_tag.author.twitter %}
<meta name="twitter:creator" content="@{{ seo_tag.author.twitter }}" />
{% endif %}
{% endif %}
@ -185,12 +81,15 @@
{% 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 %}
@ -198,81 +97,8 @@
<meta name="google-site-verification" content="{{ site.google_site_verification }}" />
{% endif %}
<script type="application/ld+json">
{
"@context": "http://schema.org",
{% if seo_type %}
"@type": {{ seo_type | jsonify }},
{% endif %}
{% if seo_name %}
"name": {{ seo_name | jsonify }},
{% endif %}
{% if seo_page_title %}
"headline": {{ seo_page_title | jsonify }},
{% endif %}
{% if seo_author %}
"author": {
"@type": "Person",
"name": {{ seo_author | jsonify }}
},
{% endif %}
{% if seo_page_image %}
{% if page.image.height && page.image.width %}
"image": {
"@type": "ImageObject",
"url": {{ seo_page_image | jsonify }},
"height": {{ page.image.height | jsonify }},
"width": {{ page.image.width | jsonify }}
},
{% else %}
"image": {{ seo_page_image | jsonify }},
{% endif %}
{% endif %}
{% if page.date %}
"datePublished": {{ page.date | date_to_xmlschema | jsonify }},
{% endif %}
{% if seo_date_modified %}
"dateModified": {{ seo_date_modified | date_to_xmlschema | jsonify }},
{% endif %}
{% if seo_description %}
"description": {{ seo_description | jsonify }},
{% endif %}
{% if seo_site_logo %}
"publisher": {
"@type": "Organization",
{% if seo_author %}
"name": {{ seo_author | jsonify }},
{% endif %}
"logo": {
"@type": "ImageObject",
"url": {{ seo_site_logo | jsonify }}
}
},
{% endif %}
{% if seo_type == "BlogPosting" or seo_type == "CreativeWork"%}
"mainEntityOfPage": {
"@type": "WebPage",
"@id": {{ page.url | replace:'/index.html','/' | absolute_url | jsonify }}
},
{% endif %}
{% if seo_links %}
"sameAs": {{ seo_links | jsonify }},
{% endif %}
"url": {{ page.url | replace:'/index.html','/' | absolute_url | jsonify }}
}
{{ seo_tag.json_ld | jsonify }}
</script>
<!-- End Jekyll SEO tag -->

View File

@ -0,0 +1,500 @@
RSpec.describe Jekyll::SeoTag::Drop do
let(:config) { { "title" => "site title" } }
let(:page_meta) { { "title" => "page title" } }
let(:page) { make_page(page_meta) }
let(:site) { make_site(config) }
let(:context) { make_context(:page => page, :site => site) }
let(:text) { "" }
subject { described_class.new(text, context) }
before do
Jekyll.logger.log_level = :error
end
# Drop includes liquid filters which expect arguments
# By default, in drops, `to_h` will call each public method with no arugments
# Here, that would cause the filters to explode. This test ensures that all
# public methods don't explode when called without arguments. Don't explode.
it "doesn't blow up on to_h" do
expect { subject.to_h }.to_not raise_error
end
it "returns the version" do
expect(subject.version).to eql(Jekyll::SeoTag::VERSION)
end
context "title?" do
it "knows to include the title" do
expect(subject.title?).to be_truthy
end
context "with title=false" do
let(:text) { "title=false" }
it "knows not to include the title" do
expect(subject.title?).to be_falsy
end
end
context "site title" do
it "knows the site title" do
expect(subject.site_title).to eql("site title")
end
context "with site.name" do
let(:config) { { "name" => "site title" } }
it "knows the site title" do
expect(subject.site_title).to eql("site title")
end
end
end
context "page title" do
it "knows the page title" do
expect(subject.page_title).to eql("page title")
end
context "without a page title" do
let(:page) { make_page }
it "knows the page title" do
expect(subject.page_title).to eql("site title")
end
end
end
context "title" do
context "with a page and site title" do
it "builds the title" do
expect(subject.title).to eql("page title | site title")
end
end
context "with a site description but no page title" do
let(:page) { make_page }
let(:config) do
{ "title" => "site title", "description" => "site description" }
end
it "builds the title" do
expect(subject.title).to eql("site title | site description")
end
end
context "with just a page title" do
let(:site) { make_site }
it "builds the title" do
expect(subject.title).to eql("page title")
end
end
context "with just a site title" do
let(:page) { make_page }
it "builds the title" do
expect(subject.title).to eql("site title")
end
end
end
end
context "name" do
context "with seo.name" do
let(:page_meta) do
{ "seo" => { "name" => "seo name" } }
end
it "uses the seo name" do
expect(subject.name).to eql("seo name")
end
end
context "the index" do
let(:page_meta) { { "permalink" => "/" } }
context "with site.social.name" do
let(:config) { { "social" => { "name" => "social name" } } }
it "uses site.social.name" do
expect(subject.name).to eql("social name")
end
end
it "uses the site title" do
expect(subject.name).to eql("site title")
end
end
context "description" do
context "with a page description" do
let(:page_meta) { { "description"=> "page description" } }
it "uses the page description" do
expect(subject.description).to eql("page description")
end
end
context "with a page excerpt" do
let(:page_meta) { { "description"=> "page excerpt" } }
it "uses the page description" do
expect(subject.description).to eql("page excerpt")
end
end
context "with a site description" do
let(:config) { { "description"=> "site description" } }
it "uses the page description" do
expect(subject.description).to eql("site description")
end
end
end
context "author" do
let(:data) { {} }
let(:config) { { "author" => "author" } }
let(:site) do
site = make_site(config)
site.data = data
site
end
%i[with without].each do |site_data_type|
context "#{site_data_type} site.author data" do
let(:data) do
if site_data_type == :with
{
"authors" => {
"author" => { "name" => "Author" },
},
}
else
{}
end
end
{
:string => { "author" => "author" },
:array => { "authors" => %w(author author2) },
:empty_string => { "author" => "" },
:nil => { "author" => nil },
:hash => { "author" => { "name" => "author" } },
}.each do |author_type, data|
context "with author as #{author_type}" do
let(:page_meta) { data }
it "returns a hash" do
expect(subject.author).to be_a(Hash)
end
it "returns the name" do
if site_data_type == :with && author_type != :hash
expect(subject.author["name"]).to eql("Author")
else
expect(subject.author["name"]).to eql("author")
end
end
it "returns the twitter handle" do
expect(subject.author["twitter"]).to eql("author")
end
end
end
end
end
context "twitter" do
let(:page_meta) { { "author" => "author" } }
it "pulls the handle from the author" do
expect(subject.author["twitter"]).to eql("author")
end
context "with an @" do
let(:page_meta) do
{
"author" => {
"name" => "author",
"twitter" => "@twitter",
},
}
end
it "strips the @" do
expect(subject.author["twitter"]).to eql("twitter")
end
end
context "with an explicit handle" do
let(:page_meta) do
{
"author" => {
"name" => "author",
"twitter" => "twitter",
},
}
end
it "pulls the handle from the hash" do
expect(subject.author["twitter"]).to eql("twitter")
end
end
end
end
end
context "date published" do
let(:config) { { "timezone" => "America/New_York" } }
let(:page_meta) { { "date" => "2017-01-01" } }
it "uses page.date" do
expect(subject.date_published).to eql("2017-01-01T00:00:00-05:00")
end
end
context "date modified" do
let(:config) { { "timezone" => "America/New_York" } }
context "with seo.date_modified" do
let(:page_meta) { { "seo" => { "date_modified" => "2017-01-01" } } }
it "uses seo.date_modified" do
expect(subject.date_modified).to eql("2017-01-01T00:00:00-05:00")
end
end
context "with page.last_modified_at" do
let(:page_meta) { { "last_modified_at" => "2017-01-01" } }
it "uses page.last_modified_at" do
expect(subject.date_modified).to eql("2017-01-01T00:00:00-05:00")
end
end
context "date" do
let(:page_meta) { { "date" => "2017-01-01" } }
it "uses page.date" do
expect(subject.date_modified).to eql("2017-01-01T00:00:00-05:00")
end
end
end
context "type" do
context "with seo.type set" do
let(:page_meta) { { "seo" => { "type" => "test" } } }
it "uses seo.type" do
expect(subject.type).to eql("test")
end
end
context "the homepage" do
let(:page_meta) { { "permalink" => "/" } }
it "is a website" do
expect(subject.type).to eql("WebSite")
end
end
context "the about page" do
let(:page) { make_page({ "permalink" => "/about/" }) }
it "is a website" do
expect(subject.type).to eql("WebSite")
end
end
context "a blog post" do
let(:page_meta) { { "date" => "2017-01-01" } }
it "is a blog post" do
expect(subject.type).to eql("BlogPosting")
end
end
it "is a webpage" do
expect(subject.type).to eql("WebPage")
end
end
context "links" do
context "with seo.links" do
let(:page_meta) { { "seo" => { "links" => %w(foo bar) } } }
it "uses seo.links" do
expect(subject.links).to eql(%w(foo bar))
end
end
context "with site.social.links" do
let(:config) { { "social" => { "links"=> %w(a b) } } }
it "doesn't use site.social.links" do
expect(subject.links).to be_nil
end
context "the homepage" do
let(:page_meta) { { "permalink" => "/" } }
it "uses site.social.links" do
expect(subject.links).to eql(%w(a b))
end
end
end
end
context "logo" do
context "without site.logo" do
it "returns nothing" do
expect(subject.logo).to be_nil
end
end
context "with an absolute site.logo" do
let(:config) { { "logo" => "http://example.com/image.png" } }
it "uses site.logo" do
expect(subject.logo).to eql("http://example.com/image.png")
end
end
context "with a relative site.logo" do
let(:config) do
{
"logo" => "image.png",
"url" => "http://example.com",
}
end
it "uses site.logo" do
expect(subject.logo).to eql("http://example.com/image.png")
end
end
context "with a uri-escaped logo" do
let(:config) { { "logo" => "some image.png" } }
it "escapes the logo" do
expect(subject.logo).to eql("/some%20image.png")
end
end
end
context "image" do
let(:page_meta) { { "image" => image } }
context "with image as a string" do
let(:image) { "image.png" }
it "returns a hash" do
expect(subject.image).to be_a(Hash)
end
it "returns the image" do
expect(subject.image["path"]).to eql("/image.png")
end
context "with site.url" do
let(:config) { { "url" => "http://example.com" } }
it "makes the path absolute" do
expect(subject.image["path"]).to eql("http://example.com/image.png")
end
end
context "with a URL-escaped path" do
let(:image) { "some image.png" }
it "URL-escapes the image" do
expect(subject.image["path"]).to eql("/some%20image.png")
end
end
end
context "with image as a hash" do
context "with a path" do
let(:image) { { "path" => "image.png" } }
it "returns the image" do
expect(subject.image["path"]).to eql("/image.png")
end
end
context "with facebook" do
let(:image) { { "facebook" => "image.png" } }
it "returns the image" do
expect(subject.image["path"]).to eql("/image.png")
end
end
context "with twitter" do
let(:image) { { "twitter" => "image.png" } }
it "returns the image" do
expect(subject.image["path"]).to eql("/image.png")
end
end
context "with height and width" do
let(:image) { { "path" => "image.png", "height" => 5, "width" => 10 } }
it "returns the height and width" do
expect(subject.image["height"]).to eql(5)
expect(subject.image["width"]).to eql(10)
end
end
end
end
context "lang" do
context "with page.lang" do
let(:page_meta) { { "lang" => "en_GB" } }
it "uses page.lang" do
expect(subject.page_lang).to eql("en_GB")
end
end
context "with site.lang" do
let(:config) { { "lang" => "en_GB" } }
it "uses site.lang" do
expect(subject.page_lang).to eql("en_GB")
end
end
context "with nothing" do
it "defaults" do
expect(subject.page_lang).to eql("en_US")
end
end
end
context "homepage_or_about?" do
[
"/", "/index.html", "index.html", "/index.htm",
"/about/", "/about/index.html",
].each do |permalink|
context "when passed '#{permalink}' as a permalink" do
let(:page_meta) { { "permalink" => permalink } }
it "knows it's the home or about page" do
expect(subject.send(:homepage_or_about?)).to be_truthy
end
end
end
context "a random URL" do
let(:page_meta) { { "permalink" => "/about-foo/" } }
it "knows it's not the home or about page" do
expect(subject.send(:homepage_or_about?)).to be_falsy
end
end
end
end

View File

@ -0,0 +1,22 @@
RSpec.describe Jekyll::SeoTag::Filters do
let(:page) { make_page }
let(:site) { make_site }
let(:context) { make_context(:page => page, :site => site) }
subject { described_class.new(context) }
before do
Jekyll.logger.log_level = :error
end
it "stores the context" do
expect(subject.instance_variable_get("@context")).to be_a(Liquid::Context)
end
it "exposes jekyll filters" do
expect(subject).to respond_to(:markdownify)
end
it "exposes liquid standard filters" do
expect(subject).to respond_to(:normalize_whitespace)
end
end

View File

@ -0,0 +1,152 @@
RSpec.describe Jekyll::SeoTag::JSONLD do
before do
Jekyll.logger.log_level = :error
end
let(:author) { "author" }
let(:image) { "image" }
let(:metadata) do
{
"title" => "title",
"author" => author,
"image" => image,
"date" => "2017-01-01",
"description" => "description",
"seo" => {
"name" => "seo name",
"date_modified" => "2017-01-02",
"links" => %w(a b),
},
}
end
let(:config) do
{
"logo" => "logo",
"timezone" => "America/New_York",
}
end
let(:page) { make_page(metadata) }
let(:site) { make_site(config) }
let(:context) { make_context(:page => page, :site => site) }
subject { Jekyll::SeoTag::Drop.new("", context).json_ld }
it "returns the context" do
expect(subject).to have_key("@context")
expect(subject["@context"]).to eql("http://schema.org")
end
it "returns the type" do
expect(subject).to have_key("@type")
expect(subject["@type"]).to eql("BlogPosting")
end
it "returns the name" do
expect(subject).to have_key("name")
expect(subject["name"]).to eql("seo name")
end
it "returns the headline" do
expect(subject).to have_key("headline")
expect(subject["headline"]).to eql("title")
end
context "author" do
{
"string" => "author",
"hash" => { "name" => "author" },
}.each do |key, value|
context "when passed as a #{key}" do
let(:author) { value }
it "returns the author" do
expect(subject).to have_key("author")
expect(subject["author"]).to be_a(Hash)
expect(subject["author"]).to have_key("@type")
expect(subject["author"]["@type"]).to eql("Person")
expect(subject["author"]).to have_key("name")
expect(subject["author"]["name"]).to be_a(String)
expect(subject["author"]["name"]).to eql("author")
end
end
end
end
context "image" do
context "with image as a string" do
let(:image) { "image" }
it "returns the image as a string" do
expect(subject).to have_key("image")
expect(subject["image"]).to be_a(String)
expect(subject["image"]).to eql("/image")
end
end
context "with image as a hash" do
let(:image) { { "path" => "image", "height" => 5, "width" => 10 } }
it "returns the image as a hash" do
expect(subject).to have_key("image")
expect(subject["image"]).to be_a(Hash)
expect(subject["image"]).to eql({
"@type" => "imageObject",
"url" => "/image",
"height" => 5,
"width" => 10,
})
end
end
end
it "returns the datePublished" do
expect(subject).to have_key("datePublished")
expect(subject["datePublished"]).to eql("2017-01-01T00:00:00-05:00")
end
it "returns the dateModified" do
expect(subject).to have_key("dateModified")
expect(subject["dateModified"]).to eql("2017-01-02T00:00:00-05:00")
end
it "returns the description" do
expect(subject).to have_key("description")
expect(subject["description"]).to eql("description")
end
it "returns the publisher" do
expect(subject).to have_key("publisher")
publisher = subject["publisher"]
expect(publisher).to be_a(Hash)
expect(publisher).to have_key("@type")
expect(publisher["@type"]).to eql("Organization")
expect(publisher).to have_key("logo")
logo = publisher["logo"]
expect(logo).to have_key("@type")
expect(logo["@type"]).to eql("ImageObject")
expect(logo).to have_key("url")
expect(logo["url"]).to eql("/logo")
end
it "returns the main entity of page" do
expect(subject).to have_key("mainEntityOfPage")
expect(subject["mainEntityOfPage"]).to be_a(Hash)
expect(subject["mainEntityOfPage"]).to have_key("@type")
expect(subject["mainEntityOfPage"]["@type"]).to eql("WebPage")
expect(subject["mainEntityOfPage"]).to have_key("@id")
expect(subject["mainEntityOfPage"]["@id"]).to eql("/page.html")
end
it "returns sameAs" do
expect(subject).to have_key("sameAs")
expect(subject["sameAs"]).to be_a(Array)
expect(subject["sameAs"]).to eql(%w(a b))
end
it "returns the url" do
expect(subject).to have_key("url")
expect(subject["url"]).to eql("/page.html")
end
end

View File

@ -1,6 +1,4 @@
require "spec_helper"
describe Jekyll::SeoTag do
RSpec.describe Jekyll::SeoTag do
let(:page) { make_page }
let(:site) { make_site }
let(:post) { make_post }
@ -195,10 +193,6 @@ describe Jekyll::SeoTag do
expected = %r!<meta property="og:image" content="http://example.invalid/img/foo.png" />!
expect(output).to match(expected)
end
it "outputs the image JSON item" do
expect(json_data["image"]).to eql("http://example.invalid/img/foo.png")
end
end
context "when given a facebook image" do
@ -229,12 +223,6 @@ describe Jekyll::SeoTag do
expected = %r!<meta property="og:image:width" content="2" />!
expect(output).to match(expected)
end
it "outputs the image JSON object with dimensions" do
expect(json_data["image"]["url"]).to eql("http://example.invalid/img/foo.png")
expect(json_data["image"]["height"]).to eql(1)
expect(json_data["image"]["width"]).to eql(2)
end
end
end
@ -341,10 +329,8 @@ EOS
end
it "minifies JSON-LD" do
expected = <<-EOS
{"@context": "http://schema.org",
"@type": "BlogPosting",
"headline": "post",
expected = <<-EOS.strip
{"@context":"http://schema.org","@type":"BlogPosting","headline":"post",
EOS
expect(output).to match(expected)
end
@ -413,6 +399,7 @@ EOS
context "with the author in site.data.authors" do
let(:author_data) { { "benbalter" => { "twitter" => "test" } } }
it "outputs the twitter card" do
expected = %r!<meta name="twitter:creator" content="@test" />!
expect(output).to match(expected)