initial commit

This commit is contained in:
Ben Balter 2015-10-25 17:21:46 -04:00
commit 0c2ca4e07a
20 changed files with 543 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

2
.rspec Normal file
View File

@ -0,0 +1,2 @@
--format documentation
--color

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
rvm:
- 2.2.3
before_install: gem install bundler
langauage: ruby
script: script/cibuild
sudo: false
cache: bundler

6
Gemfile Normal file
View File

@ -0,0 +1,6 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in jekyll_seo_tags.gemspec
gemspec
gem 'github-pages'

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Ben Balter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# Jekyll SEO Tag
A Jekyll plugin to add metadata tags for search engines and social networks to better index and display your site's content.
## What it does
Jekyll SEO Tag adds the following meta tags to your site:
* Pages title (with site title appended when available)
* Page description
* Canonical URL
* Next and previous URLs for posts
* [JSON-LD Site and post metadata](https://developers.google.com/structured-data/) for richer indexing
* [Open graph](http://ogp.me/) title, description, site title, and URL (for Facebook, LinkedIn, etc.)
* [Twitter summary card](https://dev.twitter.com/cards/overview) metadata
## What it doesn't do
Jekyll SEO tag is designed to output machine-readable metadata for search engines and social networks to index and display. If you're looking for something to analyze your Jekyll site's structure and content (e.g., more traditional SEO optimization), take a look at [The Jekyll SEO Gem](https://github.com/pmarsceill/jekyll-seo-gem).
## Installation
1. Add the following to your site's `Gemfile`:
```ruby
gem 'jekyll-seo-tag'
```
2. Add the following to your site's `_config.yml`:
```yml
gems:
- jekyll-seo-tag
```
3. Add the following right before `</head>` in your site's template(s):
```liquid
{% seo %}
```
## Usage
The SEO tag will respect any of the following if included in your site's `_config.yml` (and simply not include them if they're not defined):
* `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.
* `twitter:username` - The site's Twitter handle. You'll want to describe it like so:
```yml
twitter:
username: benbalter
```
* `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`
* `name` - If the user or organization name differs from the site's name
* `links` - An array of links to social media profiles.
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

6
Rakefile Normal file
View File

@ -0,0 +1,6 @@
require "bundler/gem_tasks"
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)
task :default => :spec

14
bin/console Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env ruby
require "bundler/setup"
require "jekyll_seo_tags"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start

7
bin/setup Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
bundle install
# Do any other automated setup that you need to do here

31
jekyll-seo-tag.gemspec Normal file
View File

@ -0,0 +1,31 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
Gem::Specification.new do |spec|
spec.name = "jekyll-seo-tag"
spec.version = "0.1.0"
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"
# 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'] = "TODO: Set to 'http://mygemserver.com'"
else
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.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_dependency "jekyll", ">= 2.4"
spec.add_development_dependency "bundler", "~> 1.10"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec"
end

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

@ -0,0 +1,37 @@
module Jekyll
class SeoTag < Liquid::Tag
attr_accessor :context
def render(context)
@context = context
Liquid::Template.parse(template_contents).render!(payload, info).gsub(/[\n\s]{2,}/, "\n")
end
private
def payload
{
"page" => context.registers[:page],
"site" => context.registers[:site].site_payload["site"]
}
end
def info
{
:registers => context.registers,
:filters => [Jekyll::Filters]
}
end
def template_contents
@template_contents ||= File.read(template_path)
end
def template_path
@template_path ||= File.expand_path "./template.html", File.dirname(__FILE__)
end
end
end
Liquid::Template.register_tag('seo', Jekyll::SeoTag)

117
lib/template.html Normal file
View File

@ -0,0 +1,117 @@
<!-- Begin Jekyll SEO tag -->
{% if site.url %}
{% assign seo_url = site.url %}
{% elsif site.github.url %}
{% assign seo_url = site.github.url %}
{% endif %}
{% if page.title %}
{% assign seo_title = page.title %}
{% if site.title %}
{% assign seo_title = seo_title | append:" - " | append: site.title %}
{% endif %}
{% elsif site.title %}
{% assign seo_title = site.title %}
{% endif %}
{% if seo_title %}
{% assign seo_title = seo_title | escape | markdownify | strip_html | strip_newlines %}
{% endif %}
{% if page.description %}
{% assign seo_description = page.description %}
{% elsif site.description %}
{% assign seo_description = site.description %}
{% endif %}
{% if seo_description %}
{% assign seo_description = seo_description | escape | markdownify | strip_html | strip_newlines %}
{% endif %}
{% if seo_title %}
<title>{{ seo_title }}</title>
<meta property="og:title" content="{{ seo_title }}" />
{% endif %}
{% if seo_description %}
<meta name="description" content="{{ seo_description }}" />
<meta property='og:description' content="{{ seo_description }}" />
{% endif %}
{% if seo_url %}
<link rel="canonical" href="{{ seo_url }}{{ page.url }}" itemprop="url" />
<meta property='og:url' content='{{ seo_url }}{{ page.url }}' />
{% endif %}
{% if site.title %}
<meta property="og:site_name" content="{{ site.title }}" />
<script type="application/ld+json">
{
"@context" : "http://schema.org",
"@type" : "WebSite",
"name" : {{ site.title | jsonify }},
"url" : {{ seo_url | jsonify }}
}
</script>
{% endif %}
{% if page.image %}
<meta property="og:image" content="{{ page.image }}" />
{% endif %}
{% if page.date %}
<meta property="og:type" content="article" />
{% if page.next.url %}
<link rel="next" href="{{ seo_url }}{{ page.next.url }}" title="{{ page.next.title | escape }}" />
{% endif %}
{% if page.previous.url %}
<link rel="prev" href="{{ seo_url }}{{ page.previous.url }}" title="{{ page.previous.title | escape }}" />
{% endif %}
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "NewsArticle",
"headline": {{ page.title | jsonify }},
"image": {{ page.image | jsonify }},
"datePublished": {{ page.date | date_to_xmlschema | jsonify }},
"description": {{ page.description | jsonify }}
}
</script>
{% endif %}
{% if site.twitter %}
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@{{ site.twitter.username | replace:"@","" }}" />
<meta name="twitter:title" content="{{ seo_title }}" />
<meta name="twitter:description" content="{{ seo_description }}" />
{% if page.image %}
<meta name="twitter:image" content="{{ page.image | escape }}" />
{% endif %}
{% if page.author %}
<meta name="twitter:creator" content="@{{ page.author | replace:"@","" }}" />
{% endif %}
{% endif %}
{% if site.logo %}
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "Organization",
"url": {{ seo_url | jsonify }},
"logo": {{ site.logo | prepend: "/" | prepend: seo_url | jsonify }}
}
</script>
{% endif %}
{% if site.social %}
<script type="application/ld+json">
{
"@context" : "http://schema.org",
"@type" : "{% if site.social.type %}{{ site.social.type }}{% else %}person{% endif %}",
"name" : "{% if site.social.name %}{{ site.social.name }}{% else %}{{ site.title }}{% endif %}",
"url" : {{ seo_url | jsonify }},
"sameAs" : {{ site.social.links | jsonify }}
}
</script>
{% endif %}
<!-- End Jekyll SEO tag -->

5
script/bootstrap Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
set -ex
bundle install

6
script/cibuild Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
set -ex
bundle exec rake spec
bundle exec gem build jekyll-seo-tag.gemspec

38
script/release Executable file
View File

@ -0,0 +1,38 @@
#!/bin/sh
# Tag and push a release.
set -e
# Make sure we're in the project root.
cd $(dirname "$0")/..
# Build a new gem archive.
rm -rf jekyll-seo-tag-*.gem
gem build -q jekyll-seo-tag.gemspec
# Make sure we're on the master branch.
(git branch | grep -q '* master') || {
echo "Only release from the master branch."
exit 1
}
# Figure out what version we're releasing.
tag=v`ls jekyll-seo-tag-*.gem | sed 's/^jekyll-seo-tag-\(.*\)\.gem$/\1/'`
# Make sure we haven't released this version before.
git fetch -t origin
(git tag -l | grep -q "$tag") && {
echo "Whoops, there's already a '${tag}' tag."
exit 1
}
# Tag it and bag it.
gem push jekyll-seo-tag-*.gem && git tag "$tag" &&
git push origin master && git push origin "$tag"

View File

View File

4
spec/fixtures/page.md vendored Normal file
View File

@ -0,0 +1,4 @@
---
---
# Test

138
spec/jekyll_seo_tag_spec.rb Normal file
View File

@ -0,0 +1,138 @@
require 'spec_helper'
describe Jekyll::SeoTag do
subject { Jekyll::SeoTag.new("seo", nil, nil) }
it "builds" do
expect(subject.render(context)).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(/<title>foo<\/title>/)
expect(subject.render(context)).to match(/<meta property="og:title" content="foo" \/>/)
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>/)
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(/<title>foo<\/title>/)
end
it "escapes titles" do
site = site({"title" => "Jekyll & Hyde"})
context = context({ :site => site })
expect(subject.render(context)).to match(/<title>Jekyll &amp; Hyde<\/title>/)
end
it "uses the page description" do
page = page({"description" => "foo"})
context = context({ :page => page })
expect(subject.render(context)).to match(/<meta name="description" content="foo" \/>/)
expect(subject.render(context)).to match(/<meta property='og:description' content="foo" \/>/)
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(/<meta name="description" content="foo" \/>/)
expect(subject.render(context)).to match(/<meta property='og:description' content="foo" \/>/)
end
it "uses the site url to build the seo url" do
site = site({"url" => "http://example.invalid"})
context = context({ :site => site })
expected = /<link rel="canonical" href="http:\/\/example.invalid\/page.html" itemprop="url" \/>/
expect(subject.render(context)).to match(expected)
expected = /<meta property='og:url' content='http:\/\/example.invalid\/page.html' \/>/
expect(subject.render(context)).to match(expected)
end
it "uses site.github.url to build the seo url" do
site = site({"github" => { "url" => "http://example.invalid" }} )
context = context({ :site => site })
expected = /<link rel="canonical" href="http:\/\/example.invalid\/page.html" itemprop="url" \/>/
expect(subject.render(context)).to match(expected)
expected = /<meta property='og:url' content='http:\/\/example.invalid\/page.html' \/>/
expect(subject.render(context)).to match(expected)
end
it "outputs the site title meta" do
site = site({"title" => "Foo", "url" => "http://example.invalid"})
context = context({ :site => site })
output = subject.render(context)
expect(output).to match(/<meta property="og:site_name" content="Foo" \/>/)
data = output.match(/<script type=\"application\/ld\+json\">(.*)<\/script>/m)[1]
data = JSON.parse(data)
expect(data["name"]).to eql("Foo")
expect(data["url"]).to eql("http://example.invalid")
end
it "outputs post meta" do
post = post({"title" => "post", "description" => "description", "image" => "/img.png" })
context = context({ :page => post })
output = subject.render(context)
expected = /<meta property="og:type" content="article" \/>/
expect(output).to match(expected)
data = output.match(/<script type=\"application\/ld\+json\">(.*)<\/script>/m)[1]
data = JSON.parse(data)
expect(data["headline"]).to eql("post")
expect(data["description"]).to eql("description")
expect(data["image"]).to eql("/img.png")
end
it "outputs twitter card meta" do
site = site({"twitter" => { "username" => "jekyllrb" }})
page = page({"author" => "benbalter"})
context = context({ :site => site, :page => page })
expected = /<meta name="twitter:site" content="@jekyllrb" \/>/
expect(subject.render(context)).to match(expected)
expected = /<meta name="twitter:creator" content="@benbalter" \/>/
expect(subject.render(context)).to match(expected)
end
it "outputs social meta" do
links = ["http://foo.invalid", "http://bar.invalid"]
site = site({"social" => { "name" => "Ben", "links" => links }})
context = context({ :site => site })
output = subject.render(context)
data = output.match(/<script type=\"application\/ld\+json\">(.*)<\/script>/m)[1]
data = JSON.parse(data)
expect(data["@type"]).to eql("person")
expect(data["name"]).to eql("Ben")
expect(data["sameAs"]).to eql(links)
end
it "outputs the logo" do
site = site({"logo" => "logo.png", "url" => "http://example.invalid" })
context = context({ :site => site })
output = subject.render(context)
data = output.match(/<script type=\"application\/ld\+json\">(.*)<\/script>/m)[1]
data = JSON.parse(data)
expect(data["logo"]).to eql("http://example.invalid/logo.png")
expect(data["url"]).to eql("http://example.invalid")
end
it "outputs the image" do
page = page({"image" => "http://foo.invalid/foo.png"})
context = context({ :page => page })
expected = /<meta property="og:image" content="http:\/\/foo.invalid\/foo.png" \/>/
expect(subject.render(context)).to match(expected)
end
end

30
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,30 @@
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'jekyll'
require 'jekyll-seo-tag'
CONFIG_DEFAULTS = {
"source" => File.expand_path("./fixtures", File.dirname(__FILE__)),
"destination" => File.expand_path("../tmp/dest", File.dirname(__FILE__)),
"gems" => ["jekyll-seo-tag"]
}
def page(options={})
page = Jekyll::Page.new site, CONFIG_DEFAULTS["source"], "", "page.md"
page.data = options
page
end
def post(options={})
page = Jekyll::Post.new site, CONFIG_DEFAULTS["source"], "", "2015-01-01-post.md"
page.data = options
page
end
def site(options={})
config = Jekyll.configuration CONFIG_DEFAULTS.merge(options)
Jekyll::Site.new(config)
end
def context(registers={})
Liquid::Context.new({}, {}, { :site => site, :page => page }.merge(registers))
end