jekyll-aeo 1.0.0
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/lib/jekyll-aeo/commands/validate.rb +128 -0
- data/lib/jekyll-aeo/config.rb +86 -0
- data/lib/jekyll-aeo/generators/domain_profile.rb +77 -0
- data/lib/jekyll-aeo/generators/llms_full_txt.rb +68 -0
- data/lib/jekyll-aeo/generators/llms_txt.rb +165 -0
- data/lib/jekyll-aeo/generators/markdown_page.rb +148 -0
- data/lib/jekyll-aeo/generators/robots_txt.rb +67 -0
- data/lib/jekyll-aeo/generators/url_map.rb +170 -0
- data/lib/jekyll-aeo/hooks.rb +32 -0
- data/lib/jekyll-aeo/link_tag.rb +27 -0
- data/lib/jekyll-aeo/schema/article.rb +56 -0
- data/lib/jekyll-aeo/schema/breadcrumb_list.rb +46 -0
- data/lib/jekyll-aeo/schema/faq_page.rb +33 -0
- data/lib/jekyll-aeo/schema/how_to.rb +40 -0
- data/lib/jekyll-aeo/schema/organization.rb +34 -0
- data/lib/jekyll-aeo/schema/speakable.rb +31 -0
- data/lib/jekyll-aeo/tags/aeo_json_ld.rb +33 -0
- data/lib/jekyll-aeo/utils/content_stripper.rb +122 -0
- data/lib/jekyll-aeo/utils/html_converter.rb +35 -0
- data/lib/jekyll-aeo/utils/md_url.rb +32 -0
- data/lib/jekyll-aeo/utils/skip_logic.rb +66 -0
- data/lib/jekyll-aeo/version.rb +5 -0
- data/lib/jekyll-aeo.rb +25 -0
- metadata +153 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllAeo
|
|
4
|
+
module Generators
|
|
5
|
+
class RobotsTxt < Jekyll::Generator
|
|
6
|
+
priority :low
|
|
7
|
+
safe true
|
|
8
|
+
|
|
9
|
+
def generate(site)
|
|
10
|
+
config = JekyllAeo::Config.from_site(site)
|
|
11
|
+
return if config["enabled"] == false
|
|
12
|
+
|
|
13
|
+
robots_config = config["robots_txt"] || {}
|
|
14
|
+
return if robots_config["enabled"] == false
|
|
15
|
+
return if user_robots_exists?(site)
|
|
16
|
+
|
|
17
|
+
content = build_content(robots_config, site)
|
|
18
|
+
page = Jekyll::PageWithoutAFile.new(site, site.source, "", "robots.txt")
|
|
19
|
+
page.content = content
|
|
20
|
+
page.data["layout"] = nil
|
|
21
|
+
site.pages << page
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def user_robots_exists?(site)
|
|
27
|
+
File.exist?(File.join(site.source, "robots.txt"))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build_content(robots_config, site)
|
|
31
|
+
lines = []
|
|
32
|
+
bot_rules(lines, robots_config["allow"] || [], "Allow")
|
|
33
|
+
bot_rules(lines, robots_config["disallow"] || [], "Disallow")
|
|
34
|
+
lines << "User-agent: *"
|
|
35
|
+
lines << "Allow: /"
|
|
36
|
+
lines << ""
|
|
37
|
+
custom_rules(lines, robots_config["custom_rules"] || [])
|
|
38
|
+
append_metadata_lines(lines, robots_config, site)
|
|
39
|
+
"#{lines.join("\n")}\n"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def bot_rules(lines, bots, directive)
|
|
43
|
+
bots.each do |bot|
|
|
44
|
+
lines << "User-agent: #{bot}"
|
|
45
|
+
lines << "#{directive}: /"
|
|
46
|
+
lines << ""
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def custom_rules(lines, rules)
|
|
51
|
+
rules.each do |rule|
|
|
52
|
+
lines << "User-agent: #{rule['user_agent']}"
|
|
53
|
+
lines << "Allow: #{rule['allow']}" if rule["allow"]
|
|
54
|
+
lines << "Disallow: #{rule['disallow']}" if rule["disallow"]
|
|
55
|
+
lines << ""
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def append_metadata_lines(lines, robots_config, site)
|
|
60
|
+
base_url = site.config["url"].to_s.chomp("/")
|
|
61
|
+
baseurl = site.config["baseurl"].to_s.chomp("/")
|
|
62
|
+
lines << "Sitemap: #{base_url}#{baseurl}/sitemap.xml" if robots_config["include_sitemap"] != false
|
|
63
|
+
lines << "Llms-txt: #{base_url}#{baseurl}/llms.txt" if robots_config["include_llms_txt"] != false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module JekyllAeo
|
|
6
|
+
module Generators
|
|
7
|
+
module UrlMap
|
|
8
|
+
COLUMN_HEADERS = {
|
|
9
|
+
"page_id" => "Page ID",
|
|
10
|
+
"url" => "URL",
|
|
11
|
+
"lang" => "Lang",
|
|
12
|
+
"layout" => "Layout",
|
|
13
|
+
"path" => "Path",
|
|
14
|
+
"redirects" => "Redirects",
|
|
15
|
+
"markdown_copy" => "Markdown Copy",
|
|
16
|
+
"skipped" => "Skipped"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def self.generate(site)
|
|
20
|
+
config = JekyllAeo::Config.from_site(site)
|
|
21
|
+
return if config["enabled"] == false
|
|
22
|
+
|
|
23
|
+
url_map_config = config["url_map"] || {}
|
|
24
|
+
return if url_map_config["enabled"] == false
|
|
25
|
+
|
|
26
|
+
columns = url_map_config["columns"] || COLUMN_HEADERS.keys
|
|
27
|
+
items = collect_all_items(site, config, columns)
|
|
28
|
+
sections = build_sections(items)
|
|
29
|
+
markdown = build_markdown(sections, columns, url_map_config)
|
|
30
|
+
|
|
31
|
+
output_file = File.join(project_root(site), url_map_config["output_filepath"] || "docs/Url-Map.md")
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
|
33
|
+
File.write(output_file, markdown)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.collect_all_items(site, config, columns)
|
|
37
|
+
items = []
|
|
38
|
+
needs_skip = columns.include?("skipped")
|
|
39
|
+
needs_md = columns.include?("markdown_copy")
|
|
40
|
+
baseurl = site.config["baseurl"].to_s.chomp("/")
|
|
41
|
+
|
|
42
|
+
site.documents.each do |doc|
|
|
43
|
+
next unless doc.output_ext == ".html"
|
|
44
|
+
next if doc.respond_to?(:collection) && doc.collection&.label == "assets"
|
|
45
|
+
|
|
46
|
+
items << build_item(doc, doc.collection&.label, site, config, needs_skip, needs_md, baseurl)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
site.pages.each do |page|
|
|
50
|
+
next unless page.output_ext == ".html"
|
|
51
|
+
|
|
52
|
+
items << build_item(page, nil, site, config, needs_skip, needs_md, baseurl)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
items
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.build_item(obj, collection_label, site, config, needs_skip, needs_md, baseurl = "")
|
|
59
|
+
redirect_from = obj.data["redirect_from"]
|
|
60
|
+
redirects_str = case redirect_from
|
|
61
|
+
when Array then redirect_from.join(", ")
|
|
62
|
+
when String then redirect_from
|
|
63
|
+
else ""
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
item = {
|
|
67
|
+
url: obj.url,
|
|
68
|
+
page_id: obj.data["page_id"] || "",
|
|
69
|
+
lang: obj.data["lang"] || "",
|
|
70
|
+
layout: obj.data["layout"] || "",
|
|
71
|
+
path: obj.relative_path,
|
|
72
|
+
redirects: redirects_str,
|
|
73
|
+
collection: collection_label
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
item[:skipped] = JekyllAeo::Utils::SkipLogic.skip_reason(obj, site, config) || "" if needs_skip
|
|
77
|
+
item[:markdown_copy] = md_url(obj.url, baseurl) if needs_md && (!needs_skip || item[:skipped].empty?)
|
|
78
|
+
item[:markdown_copy] ||= "" if needs_md
|
|
79
|
+
|
|
80
|
+
item
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.build_sections(items)
|
|
84
|
+
grouped = items.group_by { |item| item[:collection] }
|
|
85
|
+
sections = []
|
|
86
|
+
|
|
87
|
+
sections << { title: "Pages", items: grouped.delete(nil).sort_by { |i| i[:url] } } if grouped.key?(nil)
|
|
88
|
+
|
|
89
|
+
sorted_keys = grouped.keys.compact.sort
|
|
90
|
+
sorted_keys.each do |key|
|
|
91
|
+
sections << { title: titleize(key), items: grouped[key].sort_by { |i| i[:url] } }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sections
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.build_markdown(sections, columns, url_map_config)
|
|
98
|
+
lines = []
|
|
99
|
+
lines << "# Url Map"
|
|
100
|
+
if url_map_config["show_created_at"] != false
|
|
101
|
+
lines << "Generated by Jekyll-AEO on #{Time.now.strftime('%Y-%m-%d at %H:%M:%S')}"
|
|
102
|
+
end
|
|
103
|
+
lines << ""
|
|
104
|
+
|
|
105
|
+
sections.each do |section|
|
|
106
|
+
next if section[:items].empty?
|
|
107
|
+
|
|
108
|
+
lines << "## #{section[:title]}"
|
|
109
|
+
lines << ""
|
|
110
|
+
lines << table_header(columns)
|
|
111
|
+
lines << table_separator(columns)
|
|
112
|
+
|
|
113
|
+
section[:items].each do |item|
|
|
114
|
+
lines << table_row(item, columns)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
lines << ""
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
"#{lines.join("\n").rstrip}\n"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.table_header(columns)
|
|
124
|
+
cells = columns.map { |col| COLUMN_HEADERS[col] || col }
|
|
125
|
+
"| #{cells.join(' | ')} |"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.table_separator(columns)
|
|
129
|
+
cells = columns.map { |col| "-" * (COLUMN_HEADERS[col] || col).length }
|
|
130
|
+
"| #{cells.join(' | ')} |"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.table_row(item, columns)
|
|
134
|
+
cells = columns.map { |col| escape_pipe(item[col.to_sym].to_s) }
|
|
135
|
+
"| #{cells.join(' | ')} |"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.escape_pipe(value)
|
|
139
|
+
value.gsub("|", "\\|")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.md_url(url, baseurl = "")
|
|
143
|
+
JekyllAeo::Utils::MdUrl.for(url, baseurl)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.titleize(label)
|
|
147
|
+
case label
|
|
148
|
+
when "posts"
|
|
149
|
+
"Posts"
|
|
150
|
+
else
|
|
151
|
+
label.split(/[_-]/).map(&:capitalize).join(" ")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.project_root(site)
|
|
156
|
+
pwd = Dir.pwd
|
|
157
|
+
if %w[_config.yml _config.yaml _config.toml].any? { |f| File.exist?(File.join(pwd, f)) }
|
|
158
|
+
pwd
|
|
159
|
+
else
|
|
160
|
+
site.source
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private_class_method :collect_all_items, :build_item, :build_sections,
|
|
165
|
+
:build_markdown, :table_header, :table_separator,
|
|
166
|
+
:table_row, :escape_pipe, :md_url, :titleize,
|
|
167
|
+
:project_root
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Jekyll::Hooks.register :documents, :pre_render do |doc|
|
|
4
|
+
JekyllAeo::LinkTag.set_data(doc, doc.site)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
Jekyll::Hooks.register :pages, :pre_render do |page|
|
|
8
|
+
JekyllAeo::LinkTag.set_data(page, page.site)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Jekyll::Hooks.register :documents, :post_render do |doc|
|
|
12
|
+
JekyllAeo::LinkTag.inject(doc, doc.site)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Jekyll::Hooks.register :pages, :post_render do |page|
|
|
16
|
+
JekyllAeo::LinkTag.inject(page, page.site)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Jekyll::Hooks.register :documents, :post_write do |doc|
|
|
20
|
+
JekyllAeo::Generators::MarkdownPage.process(doc, doc.site)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Jekyll::Hooks.register :pages, :post_write do |page|
|
|
24
|
+
JekyllAeo::Generators::MarkdownPage.process(page, page.site)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Jekyll::Hooks.register :site, :post_write do |site|
|
|
28
|
+
JekyllAeo::Generators::LlmsTxt.generate(site)
|
|
29
|
+
JekyllAeo::Generators::LlmsFullTxt.generate(site)
|
|
30
|
+
JekyllAeo::Generators::UrlMap.generate(site)
|
|
31
|
+
JekyllAeo::Generators::DomainProfile.generate(site)
|
|
32
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllAeo
|
|
4
|
+
module LinkTag
|
|
5
|
+
def self.inject(obj, site)
|
|
6
|
+
config = JekyllAeo::Config.from_site(site)
|
|
7
|
+
dotmd_config = config["dotmd"]
|
|
8
|
+
return unless dotmd_config["link_tag"] == "auto"
|
|
9
|
+
return if JekyllAeo::Utils::SkipLogic.skip?(obj, site, config)
|
|
10
|
+
|
|
11
|
+
md_url = JekyllAeo::Utils::MdUrl.for(obj.url, site.config["baseurl"])
|
|
12
|
+
tag = %(<link rel="alternate" type="text/markdown" href="#{md_url}">)
|
|
13
|
+
obj.output = obj.output.sub("</head>", "#{tag}\n</head>")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.set_data(obj, site)
|
|
17
|
+
config = JekyllAeo::Config.from_site(site)
|
|
18
|
+
dotmd_config = config["dotmd"]
|
|
19
|
+
return unless dotmd_config["link_tag"] == "data"
|
|
20
|
+
return if JekyllAeo::Utils::SkipLogic.skip?(obj, site, config)
|
|
21
|
+
|
|
22
|
+
md_url = JekyllAeo::Utils::MdUrl.for(obj.url, site.config["baseurl"])
|
|
23
|
+
obj.data["md_url"] = md_url
|
|
24
|
+
obj.data["md_link_tag"] = %(<link rel="alternate" type="text/markdown" href="#{md_url}">)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllAeo
|
|
4
|
+
module Schema
|
|
5
|
+
module Article
|
|
6
|
+
def self.build(page, site_config, _aeo_config = {})
|
|
7
|
+
return nil unless page["date"]
|
|
8
|
+
return nil if seo_tag_present?
|
|
9
|
+
|
|
10
|
+
base_url = site_config["url"].to_s.chomp("/")
|
|
11
|
+
baseurl = site_config["baseurl"].to_s.chomp("/")
|
|
12
|
+
|
|
13
|
+
schema = {
|
|
14
|
+
"@context" => "https://schema.org",
|
|
15
|
+
"@type" => "Article",
|
|
16
|
+
"headline" => page["title"] || "",
|
|
17
|
+
"url" => "#{base_url}#{baseurl}#{page['url']}",
|
|
18
|
+
"datePublished" => format_date(page["date"])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
description = page["description"] || page["excerpt"]
|
|
22
|
+
schema["description"] = description.to_s if description
|
|
23
|
+
|
|
24
|
+
author = page["author"]
|
|
25
|
+
if author
|
|
26
|
+
schema["author"] = {
|
|
27
|
+
"@type" => "Person",
|
|
28
|
+
"name" => author
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
lm = page["last_modified_at"]
|
|
33
|
+
schema["dateModified"] = format_date(lm) if lm
|
|
34
|
+
|
|
35
|
+
schema
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.seo_tag_present?
|
|
39
|
+
!Liquid::Template.tags["seo"].nil?
|
|
40
|
+
rescue StandardError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.format_date(value)
|
|
45
|
+
case value
|
|
46
|
+
when Time, DateTime
|
|
47
|
+
value.iso8601
|
|
48
|
+
else
|
|
49
|
+
value.to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private_class_method :seo_tag_present?, :format_date
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllAeo
|
|
4
|
+
module Schema
|
|
5
|
+
module BreadcrumbList
|
|
6
|
+
def self.build(page, site_config, _aeo_config = {})
|
|
7
|
+
url = page["url"]
|
|
8
|
+
return nil if url.nil? || url == "/"
|
|
9
|
+
|
|
10
|
+
base_url = site_config["url"].to_s.chomp("/")
|
|
11
|
+
baseurl = site_config["baseurl"].to_s.chomp("/")
|
|
12
|
+
segments = url.split("/").reject(&:empty?)
|
|
13
|
+
return nil if segments.empty?
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
"@context" => "https://schema.org",
|
|
17
|
+
"@type" => "BreadcrumbList",
|
|
18
|
+
"itemListElement" => build_items(page, segments, base_url, baseurl)
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.build_items(page, segments, base_url, baseurl)
|
|
23
|
+
items = [{
|
|
24
|
+
"@type" => "ListItem", "position" => 1,
|
|
25
|
+
"name" => "Home", "item" => "#{base_url}#{baseurl}/"
|
|
26
|
+
}]
|
|
27
|
+
accumulated = ""
|
|
28
|
+
segments.each_with_index do |segment, index|
|
|
29
|
+
accumulated += "/#{segment}"
|
|
30
|
+
name = if page["title"] && index == segments.length - 1
|
|
31
|
+
page["title"]
|
|
32
|
+
else
|
|
33
|
+
segment.split(/[_-]/).map(&:capitalize).join(" ")
|
|
34
|
+
end
|
|
35
|
+
items << {
|
|
36
|
+
"@type" => "ListItem", "position" => index + 2,
|
|
37
|
+
"name" => name, "item" => "#{base_url}#{baseurl}#{accumulated}/"
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
items
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method :build_items
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllAeo
|
|
4
|
+
module Schema
|
|
5
|
+
module FaqPage
|
|
6
|
+
def self.build(page, _site_config, _aeo_config = {})
|
|
7
|
+
faq = page["faq"]
|
|
8
|
+
return nil unless faq.is_a?(Array) && faq.any?
|
|
9
|
+
|
|
10
|
+
entities = faq.filter_map do |item|
|
|
11
|
+
next unless item.is_a?(Hash) && item["q"] && item["a"]
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
"@type" => "Question",
|
|
15
|
+
"name" => item["q"],
|
|
16
|
+
"acceptedAnswer" => {
|
|
17
|
+
"@type" => "Answer",
|
|
18
|
+
"text" => item["a"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return nil if entities.empty?
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
"@context" => "https://schema.org",
|
|
27
|
+
"@type" => "FAQPage",
|
|
28
|
+
"mainEntity" => entities
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllAeo
|
|
4
|
+
module Schema
|
|
5
|
+
module HowTo
|
|
6
|
+
def self.build(page, _site_config, _aeo_config = {})
|
|
7
|
+
howto = page["howto"]
|
|
8
|
+
return nil unless howto.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
steps = howto["steps"]
|
|
11
|
+
return nil unless steps.is_a?(Array) && steps.any?
|
|
12
|
+
|
|
13
|
+
step_entities = steps.each_with_index.filter_map { |step, i| build_step(step, i) }
|
|
14
|
+
return nil if step_entities.empty?
|
|
15
|
+
|
|
16
|
+
schema = { "@context" => "https://schema.org", "@type" => "HowTo", "step" => step_entities }
|
|
17
|
+
add_optional_fields(schema, howto, page)
|
|
18
|
+
schema
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.build_step(step, index)
|
|
22
|
+
return unless step.is_a?(Hash) && step["text"]
|
|
23
|
+
|
|
24
|
+
entity = { "@type" => "HowToStep", "position" => index + 1, "text" => step["text"] }
|
|
25
|
+
entity["name"] = step["name"] if step["name"]
|
|
26
|
+
entity["url"] = step["url"] if step["url"]
|
|
27
|
+
entity["image"] = step["image"] if step["image"]
|
|
28
|
+
entity
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.add_optional_fields(schema, howto, page)
|
|
32
|
+
schema["name"] = howto["name"] || page["title"] if howto["name"] || page["title"]
|
|
33
|
+
schema["description"] = howto["description"] if howto["description"]
|
|
34
|
+
schema["totalTime"] = howto["totalTime"] if howto["totalTime"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private_class_method :build_step, :add_optional_fields
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllAeo
|
|
4
|
+
module Schema
|
|
5
|
+
module Organization
|
|
6
|
+
def self.build(page, site_config, aeo_config = {})
|
|
7
|
+
return nil unless page["url"] == "/"
|
|
8
|
+
|
|
9
|
+
name = site_config["title"] || site_config["name"]
|
|
10
|
+
return nil unless name
|
|
11
|
+
|
|
12
|
+
base_url = site_config["url"].to_s.chomp("/")
|
|
13
|
+
|
|
14
|
+
schema = {
|
|
15
|
+
"@context" => "https://schema.org",
|
|
16
|
+
"@type" => "Organization",
|
|
17
|
+
"name" => name,
|
|
18
|
+
"url" => base_url.empty? ? "/" : base_url
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
description = site_config["description"]
|
|
22
|
+
schema["description"] = description if description && !description.to_s.empty?
|
|
23
|
+
|
|
24
|
+
logo = aeo_config.dig("domain_profile", "logo")
|
|
25
|
+
schema["logo"] = logo if logo
|
|
26
|
+
|
|
27
|
+
same_as = aeo_config.dig("domain_profile", "jsonld", "sameAs")
|
|
28
|
+
schema["sameAs"] = same_as if same_as.is_a?(Array) && same_as.any?
|
|
29
|
+
|
|
30
|
+
schema
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllAeo
|
|
4
|
+
module Schema
|
|
5
|
+
module Speakable
|
|
6
|
+
def self.build(page, site_config, _aeo_config = {})
|
|
7
|
+
return nil unless page["speakable"] == true
|
|
8
|
+
|
|
9
|
+
base_url = site_config["url"].to_s.chomp("/")
|
|
10
|
+
baseurl = site_config["baseurl"].to_s.chomp("/")
|
|
11
|
+
url = "#{base_url}#{baseurl}#{page['url']}"
|
|
12
|
+
|
|
13
|
+
selectors = [
|
|
14
|
+
".post-title, .page-title, h1",
|
|
15
|
+
".post-content p:first-of-type, .page-content p:first-of-type"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
"@context" => "https://schema.org",
|
|
20
|
+
"@type" => "WebPage",
|
|
21
|
+
"name" => page["title"] || "",
|
|
22
|
+
"url" => url,
|
|
23
|
+
"speakable" => {
|
|
24
|
+
"@type" => "SpeakableSpecification",
|
|
25
|
+
"cssSelector" => selectors
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module JekyllAeo
|
|
6
|
+
module Tags
|
|
7
|
+
class AeoJsonLd < Liquid::Tag
|
|
8
|
+
BUILDERS = [
|
|
9
|
+
JekyllAeo::Schema::FaqPage,
|
|
10
|
+
JekyllAeo::Schema::HowTo,
|
|
11
|
+
JekyllAeo::Schema::BreadcrumbList,
|
|
12
|
+
JekyllAeo::Schema::Organization,
|
|
13
|
+
JekyllAeo::Schema::Speakable,
|
|
14
|
+
JekyllAeo::Schema::Article
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def render(context)
|
|
18
|
+
site = context.registers[:site]
|
|
19
|
+
page = context.registers[:page]
|
|
20
|
+
|
|
21
|
+
aeo_config = JekyllAeo::Config.from_site(site)
|
|
22
|
+
results = BUILDERS.filter_map { |builder| builder.build(page, site.config, aeo_config) }
|
|
23
|
+
|
|
24
|
+
results.map do |schema|
|
|
25
|
+
json = JSON.pretty_generate(schema).gsub("</", "<\\/")
|
|
26
|
+
"<script type=\"application/ld+json\">\n#{json}\n</script>"
|
|
27
|
+
end.join("\n")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Liquid::Template.register_tag("aeo_json_ld", JekyllAeo::Tags::AeoJsonLd)
|