diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..9408010 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,10 @@ +Metrics/LineLength: + Exclude: + - spec/**/* + - jekyll-seo-tag.gemspec + +Style/Documentation: + Enabled: false + +Style/FileName: + Enabled: false diff --git a/Gemfile b/Gemfile index 1475b55..a215af7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,16 @@ source 'https://rubygems.org' +require 'json' +require 'open-uri' -# Specify your gem's dependencies in jekyll_seo_tags.gemspec gemspec -gem 'github-pages' +group :development, :test do + versions = JSON.parse(open('https://pages.github.com/versions.json').read) + versions.delete('ruby') + versions.delete('jekyll-seo-tag') + versions.delete('github-pages') + + versions.each do |dep, version| + gem dep, version + end +end diff --git a/README.md b/README.md index c4ee3b3..9e5b38d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Jekyll plugin to add metadata tags for search engines and social networks to better index and display your site's content. -[![Gem Version](https://badge.fury.io/rb/jekyll-seo-tag.svg)](https://badge.fury.io/rb/jekyll-seo-tag) [![Build Status](https://travis-ci.org/benbalter/jekyll-seo-tag.svg)](https://travis-ci.org/benbalter/jekyll-seo-tag) +[![Gem Version](https://badge.fury.io/rb/jekyll-seo-tag.svg)](https://badge.fury.io/rb/jekyll-seo-tag) [![Build Status](https://travis-ci.org/benbalter/jekyll-seo-tag.svg)](https://travis-ci.org/benbalter/jekyll-seo-tag) ## What it does @@ -52,6 +52,7 @@ The SEO tag will respect any of the following if included in your site's `_confi * `title` - Your site's title (e.g., Ben's awesome site, The GitHub Blog, etc.) * `description` - A short description (e.g., A blog dedicated to reviewing cat gifs) * `url` - The full URL to your site. Note: `site.github.url` will be used by default. +* `author` - global author information (see below) * `twitter:username` - The site's Twitter handle. You'll want to describe it like so: ```yml @@ -59,15 +60,80 @@ The SEO tag will respect any of the following if included in your site's `_confi username: benbalter ``` +* `facebook:app_id` (A Facebook app ID for Facebook insights), and/or `facebook:publisher` (A Facebook page URL or ID of the publishing entity). You'll want to describe one or both like so: + + ```yml + facebook: + app_id: 1234 + publisher: 1234 + ``` + * `logo` - Relative URL to a site-wide logo (e.g., `assets/your-company-logo.png`) * `social` - For [specifying social profiles](https://developers.google.com/structured-data/customize/social-profiles). The following properties are available: * `type` - Either `person` or `organization` (defaults to `person`) * `name` - If the user or organization name differs from the site's name * `links` - An array of links to social media profiles. +* `google_site_verification` for verifying ownership via Google webmaster tools The SEO tag will respect the following YAML front matter if included in a post, page, or document: * `title` - The title of the post, page, or document * `description` - A short description of the page's content -* `image` - The absolute URL to an image that should be associated with the post, page, or document -* `author` - The username of the post, page, or document author +* `image` - Relative URL to an image associated with the post, page, or document (e.g., `assets/page-pic.jpg`) +* `author` - Page-, post-, or document-specific author information (see below) + +### Author information + +Author information is used to propagate the `creator` field of Twitter summary cards. This is should be an author-specific, not site-wide Twitter handle (the site-wide username be stored as `site.twitter.username`). + +*TL;DR: In most cases, put `author: [your Twitter handle]` in the document's front matter, for sites with multiple authors. If you need something more complicated, read on.* + +There are several ways to convey this author-specific information. Author information is found in the following order of priority: + +1. An `author` object, in the documents's front matter, e.g.: + + ```yml + author: + twitter: benbalter + ``` + +2. An `author` object, in the site's `_config.yml`, e.g.: + + ```yml + author: + twitter: benbalter + ``` + +3. `site.data.authors[author]`, if an author is specified in the document's front matter, and a corresponding key exists in `site.data.authors`. E.g., you have the following in the document's front matter: + + ```yml + author: benbalter + ``` + + And you have the following in `_data/authors.yml`: + + ```yml + benbalter: + picture: /img/benbalter.png + twitter: jekyllrb + + potus: + picture: /img/potus.png + twitter: whitehouse + ``` + + In the above example, the author `benbalter`'s Twitter handle will be resolved to `@jekyllrb`. This allows you to centralize author information in a single `_data/authors` file for site with many authors that require more than just the author's username. + + *Pro-tip: If `authors` is present in the document's front matter as an array (and `author` is not), the plugin will use the first author listed, as Twitter supports only one author.* + +4. An author in the document's front matter (the simplest way), e.g.: + + ```yml + author: benbalter + ``` + +5. An author in the site's `_config.yml`, e.g.: + + ```yml + author: benbalter + ``` diff --git a/Rakefile b/Rakefile index b7e9ed5..4c774a2 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ -require "bundler/gem_tasks" -require "rspec/core/rake_task" +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -task :default => :spec +task default: :spec diff --git a/jekyll-seo-tag.gemspec b/jekyll-seo-tag.gemspec index bc207b0..9ba99bb 100644 --- a/jekyll-seo-tag.gemspec +++ b/jekyll-seo-tag.gemspec @@ -4,31 +4,31 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'jekyll-seo-tag/version' Gem::Specification.new do |spec| - spec.name = "jekyll-seo-tag" + spec.name = 'jekyll-seo-tag' spec.version = Jekyll::SeoTag::VERSION - spec.authors = ["Ben Balter"] - spec.email = ["ben.balter@github.com"] - spec.summary = %q{A Jekyll plugin to add metadata tags for search engines and social networks to better index and display your site's content.} - spec.homepage = "https://github.com/benbalter/jekyll-seo-tag" - spec.license = "MIT" + spec.authors = ['Ben Balter'] + spec.email = ['ben.balter@github.com'] + spec.summary = "A Jekyll plugin to add metadata tags for search engines and social networks to better index and display your site's content." + spec.homepage = 'https://github.com/benbalter/jekyll-seo-tag' + spec.license = 'MIT' # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or # delete this section to allow pushing this gem to any host. if spec.respond_to?(:metadata) - spec.metadata['allowed_push_host'] = "https://rubygems.org" + spec.metadata['allowed_push_host'] = 'https://rubygems.org' else - raise "RubyGems 2.0 or newer is required to protect against public gem pushes." + raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' end spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] - - spec.add_dependency "jekyll", ">= 2.0" - spec.add_development_dependency "bundler", "~> 1.10" - spec.add_development_dependency "rake", "~> 10.0" - spec.add_development_dependency "rspec", "~> 3.3" - spec.add_development_dependency "html-proofer", "~> 2.5" + spec.require_paths = ['lib'] + spec.add_dependency 'jekyll', '>= 2.0' + spec.add_development_dependency 'bundler', '~> 1.10' + spec.add_development_dependency 'rake', '~> 10.0' + spec.add_development_dependency 'rspec', '~> 3.3' + spec.add_development_dependency 'html-proofer', '~> 2.5' + spec.add_development_dependency 'rubocop', '~> 0.37' end diff --git a/lib/jekyll-seo-tag.rb b/lib/jekyll-seo-tag.rb index a926ddb..69561dc 100644 --- a/lib/jekyll-seo-tag.rb +++ b/lib/jekyll-seo-tag.rb @@ -1,36 +1,42 @@ +require 'jekyll-seo-tag/filters' + module Jekyll class SeoTag < Liquid::Tag - attr_accessor :context - def initialize(_, markup, _) + MINIFY_REGEX = /(>\n|[%}]})\s+(<|{[{%])/ + + def initialize(_tag_name, text, _tokens) super - @options = { - "title" => !(markup =~ /title\s*:\s*false/i) - } + @text = text end def render(context) @context = context - output = template.render!(payload, info) - - output + template.render!(payload, info) end private + def options + { + 'version' => VERSION, + "title" => !(@text =~ /title\s*:\s*false/i) + } + end + def payload { - "page" => context.registers[:page], - "site" => context.registers[:site].site_payload["site"], - "seo" => @options + 'page' => context.registers[:page], + 'site' => context.registers[:site].site_payload['site'], + 'seo_tag' => options } end def info { - :registers => context.registers, - :filters => [Jekyll::Filters] + registers: context.registers, + filters: [Jekyll::Filters, JekyllSeoTag::Filters] } end @@ -39,11 +45,15 @@ module Jekyll end def template_contents - @template_contents ||= File.read(template_path).gsub(/(>\n|[%}]})\s+(<|{[{%])/,'\1\2').chomp + @template_contents ||= begin + File.read(template_path).gsub(MINIFY_REGEX, '\1\2').chomp + end end def template_path - @template_path ||= File.expand_path "./template.html", File.dirname(__FILE__) + @template_path ||= begin + File.expand_path './template.html', File.dirname(__FILE__) + 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..24856c6 --- /dev/null +++ b/lib/jekyll-seo-tag/filters.rb @@ -0,0 +1,13 @@ +module JekyllSeoTag + module Filters + # This is available in Liquid from version 3 which is required by Jekyll 3 + # Provided here for compatibility with Jekyll 2.x + def default(input, default_value = ''.freeze) + if !input || input.respond_to?(:empty?) && input.empty? + default_value + else + input + end + end + end +end diff --git a/lib/jekyll-seo-tag/version.rb b/lib/jekyll-seo-tag/version.rb index 8b59136..01f9676 100644 --- a/lib/jekyll-seo-tag/version.rb +++ b/lib/jekyll-seo-tag/version.rb @@ -3,6 +3,6 @@ module Liquid; class Tag; end; end module Jekyll class SeoTag < Liquid::Tag - VERSION = "0.1.4" + VERSION = '1.1.0'.freeze end end diff --git a/lib/template.html b/lib/template.html index b8c8096..8191b3f 100644 --- a/lib/template.html +++ b/lib/template.html @@ -1,50 +1,59 @@ - + {% if site.url %} {% assign seo_url = site.url | append: site.baseurl %} -{% elsif site.github.url %} - {% assign seo_url = site.github.url %} -{% endif %} - -{% if site.title %} - {% assign seo_site_title = site.title %} -{% elsif site.name %} - {% assign seo_site_title = site.name %} {% endif %} +{% assign seo_url = seo_url | default: site.github.url %} +{% assign seo_site_title = site.title | default: site.name %} {% if page.title %} {% assign seo_title = page.title %} {% assign seo_page_title = page.title %} + {% if seo_site_title %} {% assign seo_title = seo_title | append:" - " | append: seo_site_title %} {% endif %} {% elsif seo_site_title %} {% assign seo_title = seo_site_title %} {% assign seo_page_title = seo_site_title %} + {% if site.description %} {% assign seo_title = seo_title | append:" - " | append: site.description %} {% endif %} {% endif %} + {% if seo_title %} {% assign seo_title = seo_title | markdownify | strip_html | strip_newlines | escape_once %} {% endif %} + {% if seo_site_title %} {% assign seo_site_title = seo_site_title | markdownify | strip_html | strip_newlines | escape_once %} {% endif %} + {% if seo_page_title %} {% assign seo_page_title = seo_page_title | markdownify | strip_html | strip_newlines | escape_once %} {% endif %} -{% if page.description %} - {% assign seo_description = page.description %} -{% elsif site.description %} - {% assign seo_description = site.description %} -{% endif %} +{% assign seo_description = page.description | default: page.excerpt | default: site.description %} {% if seo_description %} {% assign seo_description = seo_description | markdownify | strip_html | strip_newlines | escape_once %} {% endif %} -{% if seo_title and seo.title %} +{% assign seo_author = page.author | default: page.authors[0] | default: site.author %} +{% if seo_author %} + {% 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 seo_tag.title and seo_title %} {{ seo_title }} {% endif %} @@ -58,7 +67,7 @@ {% endif %} {% if seo_url %} - + {% endif %} @@ -75,17 +84,21 @@ {% endif %} {% if page.image %} - + {% endif %} {% if page.date %} + + {% if page.next.url %} {% endif %} + {% if page.previous.url %} {% endif %} + {% endif %} @@ -103,14 +116,25 @@ + {% if page.image %} {% endif %} - {% if page.author %} - + + {% if seo_author_twitter %} + {% endif %} {% endif %} +{% if site.facebook %} + + +{% endif %} + +{% if site.google_site_verification %} + +{% endif %} + {% if site.logo %} }m)[1] } + let(:json_data) { JSON.parse(json) } before do Jekyll.logger.log_level = :error end - it "builds" do - expect(subject.render(context)).to match(/Jekyll SEO tag/i) + it 'builds' do + expect(output).to match(/Jekyll SEO tag/i) end - it "builds the title with a page title only" do - page = page({"title" => "foo"}) - context = context({ :page => page }) - expect(subject.render(context)).to match(/foo<\/title>/) - expect(subject.render(context)).to match(/<meta property="og:title" content="foo" \/>/) + it 'outputs the plugin version' do + version = Jekyll::SeoTag::VERSION + expect(output).to match(/Jekyll SEO tag v#{version}/i) end - it "builds the title with a page title and site title" do - page = page({"title" => "foo"}) - site = site({"title" => "bar"}) - context = context({ :page => page, :site => site }) - expect(subject.render(context)).to match(/<title>foo - bar<\/title>/) + context 'with page.title' do + let(:page) { make_page('title' => 'foo') } + + it 'builds the title with a page title only' do + expect(output).to match(%r{<title>foo}) + expected = %r{} + expect(output).to match(expected) + end + + context 'with site.title' do + let(:site) { make_site('title' => 'bar') } + + it 'builds the title with a page title and site title' do + expect(output).to match(%r{foo - bar}) + end + end end - it "builds the title with only a site title" do - site = site({"title" => "foo"}) - context = context({ :site => site }) - expect(subject.render(context)).to match(/foo<\/title>/) + context 'with site.title' do + let(:site) { make_site('title' => 'Site title') } + + it 'builds the title with only a site title' do + expect(output).to match(%r{<title>Site title}) + end end - it "uses the page description" do - page = page({"description" => "foo"}) - context = context({ :page => page }) - expect(subject.render(context)).to match(//) - expect(subject.render(context)).to match(//) + context 'with page.description' do + let(:page) { make_page('description' => 'foo') } + + it 'uses the page description' do + expect(output).to match(%r{}) + expect(output).to match(%r{}) + end end - it "uses the site description when no page description exists" do - site = site({"description" => "foo"}) - context = context({ :site => site }) - expect(subject.render(context)).to match(//) - expect(subject.render(context)).to match(//) + context 'with page.excerpt' do + let(:page) { make_page('excerpt' => 'foo') } + + it 'uses the page excerpt when no page description exists' do + expect(output).to match(%r{}) + expect(output).to match(%r{}) + end end - it "uses the site url to build the seo url" do - site = site({"url" => "http://example.invalid"}) - context = context({ :site => site }) - expected = // - expect(subject.render(context)).to match(expected) - expected = // - expect(subject.render(context)).to match(expected) + context 'with site.description' do + let(:site) { make_site('description' => 'foo') } + + it 'uses the site description when no page description nor excerpt exist' do + expect(output).to match(%r{}) + expect(output).to match(%r{}) + end end - it "uses site.github.url to build the seo url" do - site = site({"github" => { "url" => "http://example.invalid" }} ) - context = context({ :site => site }) - expected = // - expect(subject.render(context)).to match(expected) - expected = // - expect(subject.render(context)).to match(expected) + context 'with site.url' do + let(:site) { make_site('url' => 'http://example.invalid') } + + it 'uses the site url to build the seo url' do + expected = %r{} + expect(output).to match(expected) + expected = %r{} + expect(output).to match(expected) + end + + context 'with page.permalink' do + let(:page) { make_page('permalink' => '/page/index.html') } + + it "uses replaces '/index.html' with '/'" do + expected = %r{} + expect(output).to match(expected) + + expected = %r{} + expect(output).to match(expected) + end + end + + context 'with site.baseurl' do + let(:site) { make_site('url' => 'http://example.invalid', 'baseurl' => '/foo') } + it 'uses baseurl to build the seo url' do + expected = %r{} + expect(output).to match(expected) + expected = %r{} + expect(output).to match(expected) + end + end + + context 'with page.image' do + let(:page) { make_page('image' => 'foo.png') } + + it 'outputs the image' do + expected = %r{} + expect(output).to match(expected) + end + end + + context 'with site.logo' do + let(:site) { make_site('logo' => 'logo.png', 'url' => 'http://example.invalid') } + + it 'outputs the logo' do + expect(json_data['logo']).to eql('http://example.invalid/logo.png') + expect(json_data['url']).to eql('http://example.invalid') + end + end + + context 'with site.title' do + let(:site) { make_site('title' => 'Foo', 'url' => 'http://example.invalid') } + + it 'outputs the site title meta' do + expect(output).to match(%r{}) + expect(json_data['name']).to eql('Foo') + expect(json_data['url']).to eql('http://example.invalid') + end + end end - it "uses replaces '/index.html' with '/'" do - page = page({ "permalink" => "/page/index.html" }) - site = site({ "url" => "http://example.invalid" }) - context = context({ :page => page, :site => site }) - expected = %r!! - expected = %r!! - expect(subject.render(context)).to match(expected) + context 'with site.github.url' do + let(:github_namespace) { { 'url' => 'http://example.invalid' } } + let(:site) { make_site('github' => github_namespace) } + + it 'uses site.github.url to build the seo url' do + expected = %r{} + expect(output).to match(expected) + expected = %r{} + expect(output).to match(expected) + end end - it "uses baseurl to build the seo url" do - site = site({ "url" => "http://example.invalid", "baseurl" => "/foo" }) - context = context({ :site => site }) - expected = %r!! - expect(subject.render(context)).to match(expected) - expected = %r!! - expect(subject.render(context)).to match(expected) + context 'posts' do + context 'with post meta' do + let(:meta) do + { + 'title' => 'post', + 'description' => 'description', + 'image' => '/img.png' + } + end + let(:page) { make_post(meta) } + + it 'outputs post meta' do + expected = %r{} + expect(output).to match(expected) + + expect(json_data['headline']).to eql('post') + expect(json_data['description']).to eql('description') + expect(json_data['image']).to eql('/img.png') + end + end end - it "outputs the site title meta" do - site = site({"title" => "Foo", "url" => "http://example.invalid"}) - context = context({ :site => site }) - output = subject.render(context) + context 'twitter' do + context 'with site.twitter.username' do + let(:site_twitter) { { 'username' => 'jekyllrb' } } + let(:site) { make_site('twitter' => site_twitter) } - expect(output).to match(//) - data = output.match(/