diff --git a/.rspec b/.rspec
index 8c18f1a..83e16f8 100644
--- a/.rspec
+++ b/.rspec
@@ -1,2 +1,2 @@
---format documentation
--color
+--require spec_helper
diff --git a/lib/jekyll-seo-tag.rb b/lib/jekyll-seo-tag.rb
index 7d3a736..6d3e4b8 100644
--- a/lib/jekyll-seo-tag.rb
+++ b/lib/jekyll-seo-tag.rb
@@ -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
diff --git a/lib/jekyll-seo-tag/drop.rb b/lib/jekyll-seo-tag/drop.rb
new file mode 100644
index 0000000..e997536
--- /dev/null
+++ b/lib/jekyll-seo-tag/drop.rb
@@ -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 `
` 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
diff --git a/lib/jekyll-seo-tag/filters.rb b/lib/jekyll-seo-tag/filters.rb
new file mode 100644
index 0000000..2874aed
--- /dev/null
+++ b/lib/jekyll-seo-tag/filters.rb
@@ -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
diff --git a/lib/jekyll-seo-tag/json_ld.rb b/lib/jekyll-seo-tag/json_ld.rb
new file mode 100644
index 0000000..318685f
--- /dev/null
+++ b/lib/jekyll-seo-tag/json_ld.rb
@@ -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
diff --git a/lib/template.html b/lib/template.html
index e0f673f..d51169b 100755
--- a/lib/template.html
+++ b/lib/template.html
@@ -1,143 +1,39 @@
-
-{% if page.url == "/" or page.url == "/about/" %}
- {% assign seo_homepage_or_about = true %}
+{% if seo_tag.title? %}
+ {{ seo_tag.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 %}
+
{% 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 %}
+
{% endif %}
-{% if seo_title %}
- {% assign seo_title = seo_title | smartify | strip_html | normalize_whitespace | escape_once %}
+
+
+{% if 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 %}
+
+
{% endif %}
-{% if seo_page_title %}
- {% assign seo_page_title = seo_page_title | smartify | strip_html | normalize_whitespace | escape_once %}
+{% if 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 %}
+
+ {% if 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 %}
- {{ seo_title }}
-{% endif %}
-
-{% if seo_page_title %}
-
-{% endif %}
-
-{% if seo_author_name %}
-
-{% endif %}
-
-
-
-{% if seo_description %}
-
-
-{% endif %}
-
-{% if page.url %}
-
-
-{% endif %}
-
-{% if seo_site_title %}
-
-{% endif %}
-
-{% if seo_page_image %}
-
- {% if page.image.height %}
-
- {% endif %}
- {% if page.image.width %}
-
+ {% if 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 %}
{% else %}
@@ -162,8 +58,8 @@
- {% if seo_author_twitter %}
-
+ {% if seo_tag.author.twitter %}
+
{% endif %}
{% endif %}
@@ -185,12 +81,15 @@
{% if site.webmaster_verifications.google %}
{% endif %}
+
{% if site.webmaster_verifications.bing %}
{% endif %}
+
{% if site.webmaster_verifications.alexa %}
{% endif %}
+
{% if site.webmaster_verifications.yandex %}
{% endif %}
@@ -198,81 +97,8 @@
{% endif %}
-
diff --git a/spec/jekyll_seo_tag/drop_spec.rb b/spec/jekyll_seo_tag/drop_spec.rb
new file mode 100644
index 0000000..d61e7b2
--- /dev/null
+++ b/spec/jekyll_seo_tag/drop_spec.rb
@@ -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
diff --git a/spec/jekyll_seo_tag/filters_spec.rb b/spec/jekyll_seo_tag/filters_spec.rb
new file mode 100644
index 0000000..c3cf1c2
--- /dev/null
+++ b/spec/jekyll_seo_tag/filters_spec.rb
@@ -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
diff --git a/spec/jekyll_seo_tag/json_ld_spec.rb b/spec/jekyll_seo_tag/json_ld_spec.rb
new file mode 100644
index 0000000..88b987a
--- /dev/null
+++ b/spec/jekyll_seo_tag/json_ld_spec.rb
@@ -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
diff --git a/spec/jekyll_seo_tag_spec.rb b/spec/jekyll_seo_tag_integration_spec.rb
similarity index 97%
rename from spec/jekyll_seo_tag_spec.rb
rename to spec/jekyll_seo_tag_integration_spec.rb
index 230563d..a0f3ca8 100755
--- a/spec/jekyll_seo_tag_spec.rb
+++ b/spec/jekyll_seo_tag_integration_spec.rb
@@ -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!!
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!!
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!!
expect(output).to match(expected)