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 @@ <!-- 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 }} {% 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)