aemi-seo-tag 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/aemi-seo-tag.gemspec +37 -0
- data/lib/aemi-seo-tag/author_drop.rb +93 -0
- data/lib/aemi-seo-tag/drop.rb +262 -0
- data/lib/aemi-seo-tag/filters.rb +14 -0
- data/lib/aemi-seo-tag/image_drop.rb +78 -0
- data/lib/aemi-seo-tag/json_ld.rb +31 -0
- data/lib/aemi-seo-tag/json_ld_drop.rb +104 -0
- data/lib/aemi-seo-tag/url_helper.rb +23 -0
- data/lib/aemi-seo-tag/version.rb +10 -0
- data/lib/aemi-seo-tag.rb +94 -0
- data/lib/template.html +138 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4e9bd5aa03e132d5ac70d4d1467ab6ae2954e720cb567d3b4e24f9b8b0f22bee
|
4
|
+
data.tar.gz: 48746be5b30ccef44f1ee6137867c2dd7ab1f447b40bb0883087cc81e65f2d0e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bc1b664eb653217feb33fed4687210770202d441e535181344ca35736d536c80b5c430d63151771f7e07e726d83c9748ddbfed75b0328bc679bbe42e3f75d62d
|
7
|
+
data.tar.gz: ba1053115e4cc817473ecf48a75ebfde754eb29bc77eada43897a63288923072496412ee5829f4617355ede0d291f5bb18b8a42cb95f7438685a2ddbe0e91cbe
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/aemi-seo-tag/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "aemi-seo-tag"
|
7
|
+
spec.version = Jekyll::AemiSeoTag::VERSION
|
8
|
+
spec.authors = ["Guillaume COQUARD"]
|
9
|
+
spec.email = ["public@gcqd.fr"]
|
10
|
+
spec.summary = "A Jekyll plugin to add metadata tags for search engines and social networks to better index and display your site's content. Almost the same as aemi-seo-tag. But adapted to aemi-jekyll-theme's needs."
|
11
|
+
spec.homepage = "https://github.com/aemi-dev/aemi-seo-tag"
|
12
|
+
spec.license = "MIT"
|
13
|
+
|
14
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
15
|
+
# delete this section to allow pushing this gem to any host.
|
16
|
+
if spec.respond_to?(:metadata)
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
else
|
19
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
20
|
+
end
|
21
|
+
|
22
|
+
spec.required_ruby_version = ">= 2.5.0"
|
23
|
+
|
24
|
+
spec.files = Dir["lib/*.rb"]
|
25
|
+
spec.files += Dir["lib/*.html"]
|
26
|
+
spec.files += Dir["lib/**/*.rb"]
|
27
|
+
spec.files += Dir["aemi-seo-tag.gemspec"]
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r!^exe/!) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_dependency "jekyll", ">= 3.8", "< 5.0"
|
33
|
+
spec.add_development_dependency "bundler", ">= 1.15"
|
34
|
+
spec.add_development_dependency "html-proofer", "~> 3.7"
|
35
|
+
spec.add_development_dependency "webrick"
|
36
|
+
spec.add_development_dependency "aemi"
|
37
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jekyll
|
4
|
+
class AemiSeoTag
|
5
|
+
# A drop representing the current page's author
|
6
|
+
#
|
7
|
+
# Author name will be pulled from:
|
8
|
+
#
|
9
|
+
# 1. The page's `author` key
|
10
|
+
# 2. The first author in the page's `authors` key
|
11
|
+
# 3. The `author` key in the site config
|
12
|
+
#
|
13
|
+
# If the result from the name search is a string, we'll also check
|
14
|
+
# for additional author metadata in `site.data.authors`
|
15
|
+
class AuthorDrop < Jekyll::Drops::Drop
|
16
|
+
# Initialize a new AuthorDrop
|
17
|
+
#
|
18
|
+
# page - The page hash (e.g., Page#to_liquid)
|
19
|
+
# site - The Jekyll::Drops::SiteDrop
|
20
|
+
def initialize(page: nil, site: nil)
|
21
|
+
raise ArgumentError unless page && site
|
22
|
+
|
23
|
+
@mutations = {}
|
24
|
+
@page = page
|
25
|
+
@site = site
|
26
|
+
end
|
27
|
+
|
28
|
+
# AuthorDrop#to_s should return name, allowing the author drop to safely
|
29
|
+
# replace `page.author`, if necessary, and remain backwards compatible
|
30
|
+
def name
|
31
|
+
author_hash["name"]
|
32
|
+
end
|
33
|
+
alias_method :to_s, :name
|
34
|
+
|
35
|
+
def twitter
|
36
|
+
return @twitter if defined? @twitter
|
37
|
+
|
38
|
+
twitter = author_hash["twitter"] || author_hash["name"]
|
39
|
+
@twitter = twitter.is_a?(String) ? twitter.sub(%r!^@!, "") : nil
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :site, :page
|
45
|
+
|
46
|
+
# Finds the page author in the page.author, page.authors, or site.author
|
47
|
+
#
|
48
|
+
# Returns a string or hash representing the author
|
49
|
+
def resolved_author
|
50
|
+
return @resolved_author if defined? @resolved_author
|
51
|
+
|
52
|
+
sources = [page["author"]]
|
53
|
+
sources << page["authors"].first if page["authors"].is_a?(Array)
|
54
|
+
sources << site["author"]
|
55
|
+
@resolved_author = sources.find { |s| !s.to_s.empty? }
|
56
|
+
end
|
57
|
+
|
58
|
+
# If resolved_author is a string, attempts to find coresponding author
|
59
|
+
# metadata in `site.data.authors`
|
60
|
+
#
|
61
|
+
# Returns a hash representing additional metadata or an empty hash
|
62
|
+
def site_data_hash
|
63
|
+
@site_data_hash ||= begin
|
64
|
+
return {} unless resolved_author.is_a?(String)
|
65
|
+
return {} unless site.data["authors"].is_a?(Hash)
|
66
|
+
|
67
|
+
author_hash = site.data["authors"][resolved_author]
|
68
|
+
author_hash.is_a?(Hash) ? author_hash : {}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the normalized author hash representing the page author,
|
73
|
+
# including site-wide metadata if the author is provided as a string,
|
74
|
+
# or an empty hash, if the author cannot be resolved
|
75
|
+
def author_hash
|
76
|
+
@author_hash ||= begin
|
77
|
+
case resolved_author
|
78
|
+
when Hash
|
79
|
+
resolved_author
|
80
|
+
when String
|
81
|
+
{ "name" => resolved_author }.merge!(site_data_hash)
|
82
|
+
else
|
83
|
+
{}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Since author_hash is aliased to fallback_data, any values in the hash
|
89
|
+
# will be exposed via the drop, allowing support for arbitrary metadata
|
90
|
+
alias_method :fallback_data, :author_hash
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jekyll
|
4
|
+
class AemiSeoTag
|
5
|
+
class Drop < Jekyll::Drops::Drop
|
6
|
+
include Jekyll::AemiSeoTag::UrlHelper
|
7
|
+
|
8
|
+
TITLE_SEPARATOR = " | "
|
9
|
+
FORMAT_STRING_METHODS = [
|
10
|
+
:markdownify, :strip_html, :normalize_whitespace, :escape_once,
|
11
|
+
].freeze
|
12
|
+
HOMEPAGE_OR_ABOUT_REGEX = %r!^/(about/)?(index.html?)?$!.freeze
|
13
|
+
|
14
|
+
EMPTY_READ_ONLY_HASH = {}.freeze
|
15
|
+
private_constant :EMPTY_READ_ONLY_HASH
|
16
|
+
|
17
|
+
def initialize(text, context)
|
18
|
+
@obj = EMPTY_READ_ONLY_HASH
|
19
|
+
@mutations = {}
|
20
|
+
@text = text
|
21
|
+
@context = context
|
22
|
+
end
|
23
|
+
|
24
|
+
def version
|
25
|
+
Jekyll::AemiSeoTag::VERSION
|
26
|
+
end
|
27
|
+
|
28
|
+
def generator
|
29
|
+
return false unless generator
|
30
|
+
return true if generator
|
31
|
+
end
|
32
|
+
|
33
|
+
def comments
|
34
|
+
return false unless comments
|
35
|
+
return true if comments
|
36
|
+
end
|
37
|
+
|
38
|
+
# Should the `<title>` tag be generated for this page?
|
39
|
+
def title?
|
40
|
+
return false unless title
|
41
|
+
return @display_title if defined?(@display_title)
|
42
|
+
|
43
|
+
@display_title = (@text !~ %r!title=false!i)
|
44
|
+
end
|
45
|
+
|
46
|
+
def site_title
|
47
|
+
@site_title ||= format_string(site["title"] || site["name"])
|
48
|
+
end
|
49
|
+
|
50
|
+
def site_tagline
|
51
|
+
@site_tagline ||= format_string site["tagline"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def site_description
|
55
|
+
@site_description ||= format_string site["description"]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Page title without site title or description appended
|
59
|
+
def page_title
|
60
|
+
@page_title ||= format_string(page["title"]) || site_title
|
61
|
+
end
|
62
|
+
|
63
|
+
def site_tagline_or_description
|
64
|
+
site_tagline || site_description
|
65
|
+
end
|
66
|
+
|
67
|
+
# Page title with site title or description appended
|
68
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
69
|
+
def title
|
70
|
+
@title ||= begin
|
71
|
+
if site_title && page_title != site_title
|
72
|
+
page_title + TITLE_SEPARATOR + site_title
|
73
|
+
elsif site_description && site_title
|
74
|
+
site_title + TITLE_SEPARATOR + site_tagline_or_description
|
75
|
+
else
|
76
|
+
page_title || site_title
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
return page_number + @title if page_number
|
81
|
+
|
82
|
+
@title
|
83
|
+
end
|
84
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
85
|
+
|
86
|
+
def name
|
87
|
+
return @name if defined?(@name)
|
88
|
+
|
89
|
+
@name = if seo_name
|
90
|
+
seo_name
|
91
|
+
elsif !homepage_or_about?
|
92
|
+
nil
|
93
|
+
elsif site_social["name"]
|
94
|
+
format_string site_social["name"]
|
95
|
+
elsif site_title
|
96
|
+
site_title
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def description
|
101
|
+
@description ||= begin
|
102
|
+
format_string(page["description"] || page["excerpt"]) || site_description
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# A drop representing the page author
|
107
|
+
def author
|
108
|
+
@author ||= AuthorDrop.new(:page => page, :site => site)
|
109
|
+
end
|
110
|
+
|
111
|
+
def jsonld
|
112
|
+
return true unless generator
|
113
|
+
return false if generator == false
|
114
|
+
end
|
115
|
+
|
116
|
+
# A drop representing the JSON-LD output
|
117
|
+
def json_ld
|
118
|
+
@json_ld ||= JSONLDDrop.new(self)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns a Drop representing the page's image
|
122
|
+
# Returns nil if the image has no path, to preserve backwards compatability
|
123
|
+
def image
|
124
|
+
@image ||= ImageDrop.new(:page => page, :context => @context)
|
125
|
+
@image if @image.path
|
126
|
+
end
|
127
|
+
|
128
|
+
def date_modified
|
129
|
+
@date_modified ||= begin
|
130
|
+
date = page_seo["date_modified"] || page["last_modified_at"].to_liquid || page["date"]
|
131
|
+
filters.date_to_xmlschema(date) if date
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def date_published
|
136
|
+
@date_published ||= filters.date_to_xmlschema(page["date"]) if page["date"]
|
137
|
+
end
|
138
|
+
|
139
|
+
def type
|
140
|
+
@type ||= begin
|
141
|
+
if page_seo["type"]
|
142
|
+
page_seo["type"]
|
143
|
+
elsif homepage_or_about?
|
144
|
+
"WebSite"
|
145
|
+
elsif page["date"]
|
146
|
+
"BlogPosting"
|
147
|
+
else
|
148
|
+
"WebPage"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def links
|
154
|
+
@links ||= begin
|
155
|
+
if page_seo["links"]
|
156
|
+
page_seo["links"]
|
157
|
+
elsif homepage_or_about? && site_social["links"]
|
158
|
+
site_social["links"]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def logo
|
164
|
+
@logo ||= begin
|
165
|
+
return unless site["logo"]
|
166
|
+
|
167
|
+
if absolute_url? site["logo"]
|
168
|
+
filters.uri_escape site["logo"]
|
169
|
+
else
|
170
|
+
filters.uri_escape filters.absolute_url site["logo"]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def page_lang
|
176
|
+
@page_lang ||= page["lang"] || site["lang"] || "en_US"
|
177
|
+
end
|
178
|
+
|
179
|
+
def page_locale
|
180
|
+
@page_locale ||= (page["locale"] || site["locale"] || page_lang).tr("-", "_")
|
181
|
+
end
|
182
|
+
|
183
|
+
def canonical_url
|
184
|
+
@canonical_url ||= begin
|
185
|
+
if page["canonical_url"].to_s.empty?
|
186
|
+
filters.absolute_url(page["url"]).to_s.gsub(%r!/index\.html$!, "/")
|
187
|
+
else
|
188
|
+
page["canonical_url"]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def filters
|
196
|
+
@filters ||= Jekyll::AemiSeoTag::Filters.new(@context)
|
197
|
+
end
|
198
|
+
|
199
|
+
def page
|
200
|
+
@page ||= @context.registers[:page].to_liquid
|
201
|
+
end
|
202
|
+
|
203
|
+
def site
|
204
|
+
@site ||= @context.registers[:site].site_payload["site"].to_liquid
|
205
|
+
end
|
206
|
+
|
207
|
+
def homepage_or_about?
|
208
|
+
page["url"] =~ HOMEPAGE_OR_ABOUT_REGEX
|
209
|
+
end
|
210
|
+
|
211
|
+
def page_number
|
212
|
+
return unless @context["paginator"] && @context["paginator"]["page"]
|
213
|
+
|
214
|
+
current = @context["paginator"]["page"]
|
215
|
+
total = @context["paginator"]["total_pages"]
|
216
|
+
paginator_message = site["seo_paginator_message"] || "Page %<current>s of %<total>s for "
|
217
|
+
|
218
|
+
format(paginator_message, :current => current, :total => total) if current > 1
|
219
|
+
end
|
220
|
+
|
221
|
+
attr_reader :context
|
222
|
+
|
223
|
+
def fallback_data
|
224
|
+
@fallback_data ||= {}
|
225
|
+
end
|
226
|
+
|
227
|
+
def format_string(string)
|
228
|
+
string = FORMAT_STRING_METHODS.reduce(string) do |memo, method|
|
229
|
+
filters.public_send(method, memo)
|
230
|
+
end
|
231
|
+
|
232
|
+
string unless string.empty?
|
233
|
+
end
|
234
|
+
|
235
|
+
def seo_name
|
236
|
+
@seo_name ||= format_string(page_seo["name"]) if page_seo["name"]
|
237
|
+
end
|
238
|
+
|
239
|
+
def page_seo
|
240
|
+
@page_seo ||= sub_hash(page, "seo")
|
241
|
+
end
|
242
|
+
|
243
|
+
def site_social
|
244
|
+
@site_social ||= sub_hash(site, "social")
|
245
|
+
end
|
246
|
+
|
247
|
+
# Safely returns a sub hash
|
248
|
+
#
|
249
|
+
# hash - the parent hash
|
250
|
+
# key - the key in the parent hash
|
251
|
+
#
|
252
|
+
# Returns the sub hash or an empty hash, if it does not exist
|
253
|
+
def sub_hash(hash, key)
|
254
|
+
if hash[key].is_a?(Hash)
|
255
|
+
hash[key]
|
256
|
+
else
|
257
|
+
EMPTY_READ_ONLY_HASH
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jekyll
|
4
|
+
class AemiSeoTag
|
5
|
+
# A drop representing the page image
|
6
|
+
# The image path will be pulled from:
|
7
|
+
#
|
8
|
+
# 1. The `image` key if it's a string
|
9
|
+
# 2. The `image.path` key if it's a hash
|
10
|
+
# 3. The `image.facebook` key
|
11
|
+
# 4. The `image.twitter` key
|
12
|
+
class ImageDrop < Jekyll::Drops::Drop
|
13
|
+
include Jekyll::AemiSeoTag::UrlHelper
|
14
|
+
|
15
|
+
# Initialize a new ImageDrop
|
16
|
+
#
|
17
|
+
# page - The page hash (e.g., Page#to_liquid)
|
18
|
+
# context - the Liquid::Context
|
19
|
+
def initialize(page: nil, context: nil)
|
20
|
+
raise ArgumentError unless page && context
|
21
|
+
|
22
|
+
@mutations = {}
|
23
|
+
@page = page
|
24
|
+
@context = context
|
25
|
+
end
|
26
|
+
|
27
|
+
# Called path for backwards compatability, this is really
|
28
|
+
# the escaped, absolute URL representing the page's image
|
29
|
+
# Returns nil if no image path can be determined
|
30
|
+
def path
|
31
|
+
@path ||= filters.uri_escape(absolute_url) if absolute_url
|
32
|
+
end
|
33
|
+
alias_method :to_s, :path
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_accessor :page, :context
|
38
|
+
|
39
|
+
# The normalized image hash with a `path` key (which may be nil)
|
40
|
+
def image_hash
|
41
|
+
@image_hash ||= begin
|
42
|
+
image_meta = page["image"]
|
43
|
+
|
44
|
+
case image_meta
|
45
|
+
when Hash
|
46
|
+
{ "path" => nil }.merge!(image_meta)
|
47
|
+
when String
|
48
|
+
{ "path" => image_meta }
|
49
|
+
else
|
50
|
+
{ "path" => nil }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
alias_method :fallback_data, :image_hash
|
55
|
+
|
56
|
+
def raw_path
|
57
|
+
@raw_path ||= begin
|
58
|
+
image_hash["path"] || image_hash["facebook"] || image_hash["twitter"]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def absolute_url
|
63
|
+
return unless raw_path
|
64
|
+
return @absolute_url if defined? @absolute_url
|
65
|
+
|
66
|
+
@absolute_url = if raw_path.is_a?(String) && absolute_url?(raw_path) == false
|
67
|
+
filters.absolute_url raw_path
|
68
|
+
else
|
69
|
+
raw_path
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def filters
|
74
|
+
@filters ||= Jekyll::AemiSeoTag::Filters.new(context)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jekyll
|
4
|
+
class AemiSeoTag
|
5
|
+
# This module is deprecated, but is included in the Gem to avoid a breaking
|
6
|
+
# change and should be removed at the next major version bump
|
7
|
+
module JSONLD
|
8
|
+
METHODS_KEYS = {
|
9
|
+
:json_context => "@context",
|
10
|
+
:type => "@type",
|
11
|
+
:name => "name",
|
12
|
+
:page_title => "headline",
|
13
|
+
:json_author => "author",
|
14
|
+
:json_image => "image",
|
15
|
+
:date_published => "datePublished",
|
16
|
+
:date_modified => "dateModified",
|
17
|
+
:description => "description",
|
18
|
+
:publisher => "publisher",
|
19
|
+
:main_entity => "mainEntityOfPage",
|
20
|
+
:links => "sameAs",
|
21
|
+
:canonical_url => "url",
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
# Self should be a Jekyll::AemiSeoTag::Drop instance (when extending the module)
|
25
|
+
def json_ld
|
26
|
+
Jekyll.logger.warn "Jekyll::AemiSeoTag::JSONLD is deprecated"
|
27
|
+
@json_ld ||= JSONLDDrop.new(self)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jekyll
|
4
|
+
class AemiSeoTag
|
5
|
+
class JSONLDDrop < Jekyll::Drops::Drop
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegator :page_drop, :name, :name
|
9
|
+
def_delegator :page_drop, :description, :description
|
10
|
+
def_delegator :page_drop, :canonical_url, :url
|
11
|
+
def_delegator :page_drop, :page_title, :headline
|
12
|
+
def_delegator :page_drop, :date_modified, :dateModified
|
13
|
+
def_delegator :page_drop, :date_published, :datePublished
|
14
|
+
def_delegator :page_drop, :links, :sameAs
|
15
|
+
def_delegator :page_drop, :logo, :logo
|
16
|
+
def_delegator :page_drop, :type, :type
|
17
|
+
|
18
|
+
# Expose #type and #logo as private methods and #@type as a public method
|
19
|
+
alias_method :@type, :type
|
20
|
+
private :type, :logo
|
21
|
+
|
22
|
+
VALID_ENTITY_TYPES = %w(BlogPosting CreativeWork).freeze
|
23
|
+
VALID_AUTHOR_TYPES = %w(Organization Person).freeze
|
24
|
+
private_constant :VALID_ENTITY_TYPES, :VALID_AUTHOR_TYPES
|
25
|
+
|
26
|
+
# page_drop should be an instance of Jekyll::AemiSeoTag::Drop
|
27
|
+
def initialize(page_drop)
|
28
|
+
@mutations = {}
|
29
|
+
@page_drop = page_drop
|
30
|
+
end
|
31
|
+
|
32
|
+
def fallback_data
|
33
|
+
@fallback_data ||= {
|
34
|
+
"@context" => "https://schema.org",
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def author
|
39
|
+
return unless page_drop.author["name"]
|
40
|
+
|
41
|
+
author_type = page_drop.author["type"]
|
42
|
+
return if author_type && !VALID_AUTHOR_TYPES.include?(author_type)
|
43
|
+
|
44
|
+
hash = {
|
45
|
+
"@type" => author_type || "Person",
|
46
|
+
"name" => page_drop.author["name"],
|
47
|
+
}
|
48
|
+
|
49
|
+
author_url = page_drop.author["url"]
|
50
|
+
hash["url"] = author_url if author_url
|
51
|
+
|
52
|
+
hash
|
53
|
+
end
|
54
|
+
|
55
|
+
def image
|
56
|
+
return unless page_drop.image
|
57
|
+
return page_drop.image.path if page_drop.image.keys.length == 1
|
58
|
+
|
59
|
+
hash = page_drop.image.to_h
|
60
|
+
hash["url"] = hash.delete("path")
|
61
|
+
hash["@type"] = "imageObject"
|
62
|
+
hash
|
63
|
+
end
|
64
|
+
|
65
|
+
def publisher
|
66
|
+
return unless logo
|
67
|
+
|
68
|
+
output = {
|
69
|
+
"@type" => "Organization",
|
70
|
+
"logo" => {
|
71
|
+
"@type" => "ImageObject",
|
72
|
+
"url" => logo,
|
73
|
+
},
|
74
|
+
}
|
75
|
+
output["name"] = page_drop.author.name if page_drop.author.name
|
76
|
+
output
|
77
|
+
end
|
78
|
+
|
79
|
+
def main_entity
|
80
|
+
return unless VALID_ENTITY_TYPES.include?(type)
|
81
|
+
|
82
|
+
{
|
83
|
+
"@type" => "WebPage",
|
84
|
+
"@id" => page_drop.canonical_url,
|
85
|
+
}
|
86
|
+
end
|
87
|
+
alias_method :mainEntityOfPage, :main_entity
|
88
|
+
private :main_entity
|
89
|
+
|
90
|
+
# Returns a JSON-encoded object containing the JSON-LD data.
|
91
|
+
# Keys are sorted.
|
92
|
+
def to_json(state = nil)
|
93
|
+
keys.sort.each_with_object({}) do |(key, _), result|
|
94
|
+
v = self[key]
|
95
|
+
result[key] = v unless v.nil?
|
96
|
+
end.to_json(state)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
attr_reader :page_drop
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jekyll
|
4
|
+
class AemiSeoTag
|
5
|
+
# Mixin to share common URL-related methods between class
|
6
|
+
module UrlHelper
|
7
|
+
private
|
8
|
+
|
9
|
+
# Determines if the given string is an absolute URL
|
10
|
+
#
|
11
|
+
# Returns true if an absolute URL
|
12
|
+
# Returns false if it's a relative URL
|
13
|
+
# Returns nil if it is not a string or can't be parsed as a URL
|
14
|
+
def absolute_url?(string)
|
15
|
+
return unless string
|
16
|
+
|
17
|
+
Addressable::URI.parse(string).absolute?
|
18
|
+
rescue Addressable::URI::InvalidURIError
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/aemi-seo-tag.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jekyll"
|
4
|
+
require "aemi-seo-tag/version"
|
5
|
+
|
6
|
+
module Jekyll
|
7
|
+
class AemiSeoTag < Liquid::Tag
|
8
|
+
autoload :JSONLD, "aemi-seo-tag/json_ld"
|
9
|
+
autoload :AuthorDrop, "aemi-seo-tag/author_drop"
|
10
|
+
autoload :ImageDrop, "aemi-seo-tag/image_drop"
|
11
|
+
autoload :JSONLDDrop, "aemi-seo-tag/json_ld_drop"
|
12
|
+
autoload :UrlHelper, "aemi-seo-tag/url_helper"
|
13
|
+
autoload :Drop, "aemi-seo-tag/drop"
|
14
|
+
autoload :Filters, "aemi-seo-tag/filters"
|
15
|
+
|
16
|
+
attr_accessor :context
|
17
|
+
|
18
|
+
# Matches all whitespace that follows either
|
19
|
+
# 1. A '}', which closes a Liquid tag
|
20
|
+
# 2. A '{', which opens a JSON block
|
21
|
+
# 3. A '>' followed by a newline, which closes an XML tag or
|
22
|
+
# 4. A ',' followed by a newline, which ends a JSON line
|
23
|
+
# We will strip all of this whitespace to minify the template
|
24
|
+
# We will not strip any whitespace if the next character is a '-'
|
25
|
+
# so that we do not interfere with the HTML comment at the
|
26
|
+
# very begining
|
27
|
+
MINIFY_REGEX = %r!(?<=[{}]|[>,]\n)\s+(?\!-)!.freeze
|
28
|
+
|
29
|
+
def initialize(_tag_name, text, _tokens)
|
30
|
+
super
|
31
|
+
@text = text
|
32
|
+
end
|
33
|
+
|
34
|
+
def render(context)
|
35
|
+
@context = context
|
36
|
+
AemiSeoTag.template.render!(payload, info)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def options
|
42
|
+
{
|
43
|
+
"version" => Jekyll::AemiSeoTag::VERSION,
|
44
|
+
"title" => title?,
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def payload
|
49
|
+
# site_payload is an instance of UnifiedPayloadDrop. See https://github.com/jekyll/jekyll/blob/22f2724a1f117a94cc16d18c499a93d5915ede4f/lib/jekyll/site.rb#L261-L276
|
50
|
+
context.registers[:site].site_payload.tap do |site_payload|
|
51
|
+
site_payload["page"] = context.registers[:page]
|
52
|
+
site_payload["paginator"] = context["paginator"]
|
53
|
+
site_payload["seo_tag"] = drop
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def drop
|
58
|
+
if context.registers[:site].liquid_renderer.respond_to?(:cache)
|
59
|
+
Jekyll::AemiSeoTag::Drop.new(@text, @context)
|
60
|
+
else
|
61
|
+
@drop ||= Jekyll::AemiSeoTag::Drop.new(@text, @context)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def info
|
66
|
+
{
|
67
|
+
:registers => context.registers,
|
68
|
+
:filters => [Jekyll::Filters],
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
class << self
|
73
|
+
def template
|
74
|
+
@template ||= Liquid::Template.parse template_contents
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def template_contents
|
80
|
+
@template_contents ||= begin
|
81
|
+
File.read(template_path).gsub(MINIFY_REGEX, "")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def template_path
|
86
|
+
@template_path ||= begin
|
87
|
+
File.expand_path "./template.html", File.dirname(__FILE__)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
Liquid::Template.register_tag("seo", Jekyll::AemiSeoTag)
|
data/lib/template.html
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
|
2
|
+
{% if seo_tag.comments? %}
|
3
|
+
<!-- Begin Jekyll SEO tag v{{ seo_tag.version }} -->
|
4
|
+
{% endif %}
|
5
|
+
|
6
|
+
{% if seo_tag.title? %}
|
7
|
+
<title>{{ seo_tag.title }}</title>
|
8
|
+
{% endif %}
|
9
|
+
|
10
|
+
{% if seo_tag.generator? %}
|
11
|
+
<meta name="generator" content="Jekyll v{{ jekyll.version }}" />
|
12
|
+
{% endif %}
|
13
|
+
|
14
|
+
{% if seo_tag.page_title %}
|
15
|
+
<meta property="og:title" content="{{ seo_tag.page_title }}" />
|
16
|
+
{% endif %}
|
17
|
+
|
18
|
+
{% if seo_tag.author.name %}
|
19
|
+
<meta name="author" content="{{ seo_tag.author.name }}" />
|
20
|
+
{% endif %}
|
21
|
+
|
22
|
+
<meta property="og:locale" content="{{ seo_tag.page_locale }}" />
|
23
|
+
|
24
|
+
{% if seo_tag.description %}
|
25
|
+
<meta name="description" content="{{ seo_tag.description }}" />
|
26
|
+
<meta property="og:description" content="{{ seo_tag.description }}" />
|
27
|
+
{% endif %}
|
28
|
+
|
29
|
+
{% if site.url %}
|
30
|
+
<link rel="canonical" href="{{ seo_tag.canonical_url }}" />
|
31
|
+
<meta property="og:url" content="{{ seo_tag.canonical_url }}" />
|
32
|
+
{% endif %}
|
33
|
+
|
34
|
+
{% if seo_tag.site_title %}
|
35
|
+
<meta property="og:site_name" content="{{ seo_tag.site_title }}" />
|
36
|
+
{% endif %}
|
37
|
+
|
38
|
+
{% if seo_tag.image %}
|
39
|
+
<meta property="og:image" content="{{ seo_tag.image.path }}" />
|
40
|
+
{% if seo_tag.image.height %}
|
41
|
+
<meta property="og:image:height" content="{{ seo_tag.image.height }}" />
|
42
|
+
{% endif %}
|
43
|
+
{% if seo_tag.image.width %}
|
44
|
+
<meta property="og:image:width" content="{{ seo_tag.image.width }}" />
|
45
|
+
{% endif %}
|
46
|
+
{% if seo_tag.image.alt %}
|
47
|
+
<meta property="og:image:alt" content="{{ seo_tag.image.alt }}" />
|
48
|
+
{% endif %}
|
49
|
+
{% endif %}
|
50
|
+
|
51
|
+
{% if page.date %}
|
52
|
+
<meta property="og:type" content="article" />
|
53
|
+
<meta property="article:published_time" content="{{ page.date | date_to_xmlschema }}" />
|
54
|
+
{% else %}
|
55
|
+
<meta property="og:type" content="website" />
|
56
|
+
{% endif %}
|
57
|
+
|
58
|
+
{% if paginator.previous_page %}
|
59
|
+
<link rel="prev" href="{{ paginator.previous_page_path | absolute_url }}" />
|
60
|
+
{% endif %}
|
61
|
+
{% if paginator.next_page %}
|
62
|
+
<link rel="next" href="{{ paginator.next_page_path | absolute_url }}" />
|
63
|
+
{% endif %}
|
64
|
+
|
65
|
+
{% if seo_tag.image %}
|
66
|
+
<meta name="twitter:card" content="{{ page.twitter.card | default: site.twitter.card | default: "summary_large_image" }}" />
|
67
|
+
<meta property="twitter:image" content="{{ seo_tag.image.path }}" />
|
68
|
+
{% else %}
|
69
|
+
<meta name="twitter:card" content="summary" />
|
70
|
+
{% endif %}
|
71
|
+
|
72
|
+
{% if seo_tag.image.alt %}
|
73
|
+
<meta name="twitter:image:alt" content="{{ seo_tag.image.alt }}" />
|
74
|
+
{% endif %}
|
75
|
+
|
76
|
+
{% if seo_tag.page_title %}
|
77
|
+
<meta property="twitter:title" content="{{ seo_tag.page_title }}" />
|
78
|
+
{% endif %}
|
79
|
+
|
80
|
+
{% if site.twitter %}
|
81
|
+
<meta name="twitter:site" content="@{{ site.twitter.username | remove:'@' }}" />
|
82
|
+
|
83
|
+
{% if seo_tag.author.twitter %}
|
84
|
+
<meta name="twitter:creator" content="@{{ seo_tag.author.twitter | remove:'@' }}" />
|
85
|
+
{% endif %}
|
86
|
+
{% endif %}
|
87
|
+
|
88
|
+
{% if site.facebook %}
|
89
|
+
{% if site.facebook.admins %}
|
90
|
+
<meta property="fb:admins" content="{{ site.facebook.admins }}" />
|
91
|
+
{% endif %}
|
92
|
+
|
93
|
+
{% if site.facebook.publisher %}
|
94
|
+
<meta property="article:publisher" content="{{ site.facebook.publisher }}" />
|
95
|
+
{% endif %}
|
96
|
+
|
97
|
+
{% if site.facebook.app_id %}
|
98
|
+
<meta property="fb:app_id" content="{{ site.facebook.app_id }}" />
|
99
|
+
{% endif %}
|
100
|
+
{% endif %}
|
101
|
+
|
102
|
+
{% if site.webmaster_verifications %}
|
103
|
+
{% if site.webmaster_verifications.google %}
|
104
|
+
<meta name="google-site-verification" content="{{ site.webmaster_verifications.google }}" />
|
105
|
+
{% endif %}
|
106
|
+
|
107
|
+
{% if site.webmaster_verifications.bing %}
|
108
|
+
<meta name="msvalidate.01" content="{{ site.webmaster_verifications.bing }}" />
|
109
|
+
{% endif %}
|
110
|
+
|
111
|
+
{% if site.webmaster_verifications.alexa %}
|
112
|
+
<meta name="alexaVerifyID" content="{{ site.webmaster_verifications.alexa }}" />
|
113
|
+
{% endif %}
|
114
|
+
|
115
|
+
{% if site.webmaster_verifications.yandex %}
|
116
|
+
<meta name="yandex-verification" content="{{ site.webmaster_verifications.yandex }}" />
|
117
|
+
{% endif %}
|
118
|
+
|
119
|
+
{% if site.webmaster_verifications.baidu %}
|
120
|
+
<meta name="baidu-site-verification" content="{{ site.webmaster_verifications.baidu }}" />
|
121
|
+
{% endif %}
|
122
|
+
|
123
|
+
{% if site.webmaster_verifications.facebook %}
|
124
|
+
<meta name="facebook-domain-verification" content="{{ site.webmaster_verifications.facebook }}" />
|
125
|
+
{% endif %}
|
126
|
+
{% elsif site.google_site_verification %}
|
127
|
+
<meta name="google-site-verification" content="{{ site.google_site_verification }}" />
|
128
|
+
{% endif %}
|
129
|
+
|
130
|
+
{% if seo_tag.jsonld? != false %}
|
131
|
+
<script type="application/ld+json">
|
132
|
+
{{ seo_tag.json_ld | jsonify }}
|
133
|
+
</script>
|
134
|
+
{% endif %}
|
135
|
+
|
136
|
+
{% if seo_tag.comments? %}
|
137
|
+
<!-- End Jekyll SEO tag -->
|
138
|
+
{% endif %}
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aemi-seo-tag
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Guillaume COQUARD
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-06-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: jekyll
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.8'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '5.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.8'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '5.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bundler
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.15'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.15'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: html-proofer
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.7'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.7'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: webrick
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: aemi
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
description:
|
90
|
+
email:
|
91
|
+
- public@gcqd.fr
|
92
|
+
executables: []
|
93
|
+
extensions: []
|
94
|
+
extra_rdoc_files: []
|
95
|
+
files:
|
96
|
+
- aemi-seo-tag.gemspec
|
97
|
+
- lib/aemi-seo-tag.rb
|
98
|
+
- lib/aemi-seo-tag/author_drop.rb
|
99
|
+
- lib/aemi-seo-tag/drop.rb
|
100
|
+
- lib/aemi-seo-tag/filters.rb
|
101
|
+
- lib/aemi-seo-tag/image_drop.rb
|
102
|
+
- lib/aemi-seo-tag/json_ld.rb
|
103
|
+
- lib/aemi-seo-tag/json_ld_drop.rb
|
104
|
+
- lib/aemi-seo-tag/url_helper.rb
|
105
|
+
- lib/aemi-seo-tag/version.rb
|
106
|
+
- lib/template.html
|
107
|
+
homepage: https://github.com/aemi-dev/aemi-seo-tag
|
108
|
+
licenses:
|
109
|
+
- MIT
|
110
|
+
metadata:
|
111
|
+
allowed_push_host: https://rubygems.org
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: 2.5.0
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubygems_version: 3.3.11
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: A Jekyll plugin to add metadata tags for search engines and social networks
|
131
|
+
to better index and display your site's content. Almost the same as aemi-seo-tag.
|
132
|
+
But adapted to aemi-jekyll-theme's needs.
|
133
|
+
test_files: []
|