better_seo 0.13.0 → 1.0.0.1
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 +4 -4
- data/CHANGELOG.md +121 -3
- data/README.md +299 -181
- data/docs/00_OVERVIEW.md +472 -0
- data/docs/01_CORE_AND_CONFIGURATION.md +913 -0
- data/docs/02_META_TAGS_AND_OPEN_GRAPH.md +251 -0
- data/docs/03_STRUCTURED_DATA.md +140 -0
- data/docs/04_SITEMAP_AND_ROBOTS.md +131 -0
- data/docs/05_RAILS_INTEGRATION.md +175 -0
- data/docs/06_I18N_PAGE_GENERATOR.md +233 -0
- data/docs/07_IMAGE_OPTIMIZATION.md +260 -0
- data/docs/DEPENDENCIES.md +383 -0
- data/docs/README.md +180 -0
- data/docs/TESTING_STRATEGY.md +663 -0
- data/lib/better_seo/analytics/google_analytics.rb +83 -0
- data/lib/better_seo/analytics/google_tag_manager.rb +74 -0
- data/lib/better_seo/configuration.rb +316 -0
- data/lib/better_seo/dsl/base.rb +86 -0
- data/lib/better_seo/dsl/meta_tags.rb +55 -0
- data/lib/better_seo/dsl/open_graph.rb +109 -0
- data/lib/better_seo/dsl/twitter_cards.rb +131 -0
- data/lib/better_seo/errors.rb +31 -0
- data/lib/better_seo/generators/amp_generator.rb +83 -0
- data/lib/better_seo/generators/breadcrumbs_generator.rb +126 -0
- data/lib/better_seo/generators/canonical_url_manager.rb +106 -0
- data/lib/better_seo/generators/meta_tags_generator.rb +100 -0
- data/lib/better_seo/generators/open_graph_generator.rb +110 -0
- data/lib/better_seo/generators/robots_txt_generator.rb +102 -0
- data/lib/better_seo/generators/twitter_cards_generator.rb +102 -0
- data/lib/better_seo/image/optimizer.rb +143 -0
- data/lib/better_seo/rails/helpers/controller_helpers.rb +118 -0
- data/lib/better_seo/rails/helpers/seo_helper.rb +176 -0
- data/lib/better_seo/rails/helpers/structured_data_helper.rb +123 -0
- data/lib/better_seo/rails/model_helpers.rb +62 -0
- data/lib/better_seo/rails/railtie.rb +22 -0
- data/lib/better_seo/sitemap/builder.rb +65 -0
- data/lib/better_seo/sitemap/generator.rb +57 -0
- data/lib/better_seo/sitemap/sitemap_index.rb +73 -0
- data/lib/better_seo/sitemap/url_entry.rb +157 -0
- data/lib/better_seo/structured_data/article.rb +55 -0
- data/lib/better_seo/structured_data/base.rb +73 -0
- data/lib/better_seo/structured_data/breadcrumb_list.rb +49 -0
- data/lib/better_seo/structured_data/event.rb +207 -0
- data/lib/better_seo/structured_data/faq_page.rb +55 -0
- data/lib/better_seo/structured_data/generator.rb +75 -0
- data/lib/better_seo/structured_data/how_to.rb +96 -0
- data/lib/better_seo/structured_data/local_business.rb +94 -0
- data/lib/better_seo/structured_data/organization.rb +67 -0
- data/lib/better_seo/structured_data/person.rb +51 -0
- data/lib/better_seo/structured_data/product.rb +123 -0
- data/lib/better_seo/structured_data/recipe.rb +135 -0
- data/lib/better_seo/validators/seo_recommendations.rb +165 -0
- data/lib/better_seo/validators/seo_validator.rb +195 -0
- data/lib/better_seo/version.rb +1 -1
- data/lib/better_seo.rb +5 -0
- data/lib/generators/better_seo/install_generator.rb +21 -0
- data/lib/generators/better_seo/templates/README +29 -0
- data/lib/generators/better_seo/templates/better_seo.rb +40 -0
- metadata +69 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterSeo
|
|
4
|
+
module Rails
|
|
5
|
+
module Helpers
|
|
6
|
+
module SeoHelper
|
|
7
|
+
# Generate meta tags from configuration or block
|
|
8
|
+
def seo_meta_tags(config = nil, &block)
|
|
9
|
+
if block_given?
|
|
10
|
+
builder = BetterSeo::DSL::MetaTags.new
|
|
11
|
+
builder.evaluate(&block)
|
|
12
|
+
config = builder.build
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
generator = BetterSeo::Generators::MetaTagsGenerator.new(config || {})
|
|
16
|
+
raw(generator.generate)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generate Open Graph tags from configuration or block
|
|
20
|
+
def seo_open_graph_tags(config = nil, &block)
|
|
21
|
+
if block_given?
|
|
22
|
+
builder = BetterSeo::DSL::OpenGraph.new
|
|
23
|
+
builder.evaluate(&block)
|
|
24
|
+
config = builder.build
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
generator = BetterSeo::Generators::OpenGraphGenerator.new(config || {})
|
|
28
|
+
raw(generator.generate)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Generate Twitter Card tags from configuration or block
|
|
32
|
+
def seo_twitter_tags(config = nil, &block)
|
|
33
|
+
if block_given?
|
|
34
|
+
builder = BetterSeo::DSL::TwitterCards.new
|
|
35
|
+
builder.evaluate(&block)
|
|
36
|
+
config = builder.build
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
generator = BetterSeo::Generators::TwitterCardsGenerator.new(config || {})
|
|
40
|
+
raw(generator.generate)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Generate all SEO tags (meta + og + twitter) from configuration or block
|
|
44
|
+
def seo_tags(config = nil, &block)
|
|
45
|
+
if block_given?
|
|
46
|
+
context = SeoTagsContext.new
|
|
47
|
+
yield(context)
|
|
48
|
+
config = context.to_h
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
config ||= {}
|
|
52
|
+
|
|
53
|
+
# Merge with defaults from BetterSeo configuration
|
|
54
|
+
merged_config = build_seo_config(config)
|
|
55
|
+
|
|
56
|
+
tags = []
|
|
57
|
+
|
|
58
|
+
if merged_config[:meta]
|
|
59
|
+
tags << seo_meta_tags(merged_config[:meta])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if merged_config[:og] && BetterSeo.configuration.open_graph_enabled?
|
|
63
|
+
tags << seo_open_graph_tags(merged_config[:og])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if merged_config[:twitter] && BetterSeo.configuration.twitter_enabled?
|
|
67
|
+
tags << seo_twitter_tags(merged_config[:twitter])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
raw(tags.compact.join("\n"))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Build SEO configuration by merging controller data with defaults
|
|
76
|
+
def build_seo_config(controller_data)
|
|
77
|
+
config = BetterSeo.configuration
|
|
78
|
+
result = {}
|
|
79
|
+
|
|
80
|
+
# Meta tags
|
|
81
|
+
meta = {}
|
|
82
|
+
|
|
83
|
+
# Start with controller data
|
|
84
|
+
if controller_data[:meta]
|
|
85
|
+
meta = controller_data[:meta].dup
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Use default title if not set
|
|
89
|
+
meta[:title] ||= config.meta_tags.default_title
|
|
90
|
+
|
|
91
|
+
# Apply site_name to title if configured
|
|
92
|
+
if meta[:title] && config.meta_tags.append_site_name && config.site_name
|
|
93
|
+
separator = config.meta_tags.title_separator || " | "
|
|
94
|
+
meta[:title] = "#{meta[:title]}#{separator}#{config.site_name}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Use defaults for missing values
|
|
98
|
+
meta[:description] ||= config.meta_tags.default_description
|
|
99
|
+
meta[:keywords] ||= config.meta_tags.default_keywords
|
|
100
|
+
meta[:author] ||= config.meta_tags.default_author
|
|
101
|
+
|
|
102
|
+
result[:meta] = meta if meta.any?
|
|
103
|
+
|
|
104
|
+
# Open Graph tags
|
|
105
|
+
if config.open_graph_enabled?
|
|
106
|
+
og = controller_data[:og]&.dup || {}
|
|
107
|
+
|
|
108
|
+
# Use meta title/description as fallbacks
|
|
109
|
+
og[:title] ||= meta[:title] || config.meta_tags.default_title
|
|
110
|
+
og[:description] ||= meta[:description]
|
|
111
|
+
og[:type] ||= config.open_graph.default_type
|
|
112
|
+
og[:locale] ||= config.open_graph.default_locale
|
|
113
|
+
og[:site_name] ||= config.open_graph.site_name || config.site_name
|
|
114
|
+
|
|
115
|
+
# Default image
|
|
116
|
+
if og[:image].nil? && config.open_graph.default_image&.url
|
|
117
|
+
og[:image] = config.open_graph.default_image.url
|
|
118
|
+
og[:image_width] = config.open_graph.default_image.width
|
|
119
|
+
og[:image_height] = config.open_graph.default_image.height
|
|
120
|
+
og[:image_alt] = config.open_graph.default_image.alt if config.open_graph.default_image.respond_to?(:alt)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
result[:og] = og if og.any?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Twitter Card tags
|
|
127
|
+
if config.twitter_enabled?
|
|
128
|
+
twitter = controller_data[:twitter]&.dup || {}
|
|
129
|
+
|
|
130
|
+
twitter[:card] ||= config.twitter.card_type
|
|
131
|
+
twitter[:site] ||= config.twitter.site
|
|
132
|
+
twitter[:creator] ||= config.twitter.creator
|
|
133
|
+
twitter[:title] ||= meta[:title] || config.meta_tags.default_title
|
|
134
|
+
twitter[:description] ||= meta[:description]
|
|
135
|
+
|
|
136
|
+
# Use OG image as fallback
|
|
137
|
+
twitter[:image] ||= result.dig(:og, :image)
|
|
138
|
+
|
|
139
|
+
result[:twitter] = twitter if twitter.any?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
result
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Context class for building all SEO tags with block syntax
|
|
146
|
+
class SeoTagsContext
|
|
147
|
+
def initialize
|
|
148
|
+
@config = {}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def meta(&block)
|
|
152
|
+
builder = BetterSeo::DSL::MetaTags.new
|
|
153
|
+
builder.evaluate(&block)
|
|
154
|
+
@config[:meta] = builder.build
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def og(&block)
|
|
158
|
+
builder = BetterSeo::DSL::OpenGraph.new
|
|
159
|
+
builder.evaluate(&block)
|
|
160
|
+
@config[:og] = builder.build
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def twitter(&block)
|
|
164
|
+
builder = BetterSeo::DSL::TwitterCards.new
|
|
165
|
+
builder.evaluate(&block)
|
|
166
|
+
@config[:twitter] = builder.build
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def to_h
|
|
170
|
+
@config
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterSeo
|
|
4
|
+
module Rails
|
|
5
|
+
module Helpers
|
|
6
|
+
module StructuredDataHelper
|
|
7
|
+
TYPE_MAPPING = {
|
|
8
|
+
organization: BetterSeo::StructuredData::Organization,
|
|
9
|
+
article: BetterSeo::StructuredData::Article,
|
|
10
|
+
person: BetterSeo::StructuredData::Person,
|
|
11
|
+
product: BetterSeo::StructuredData::Product,
|
|
12
|
+
breadcrumb_list: BetterSeo::StructuredData::BreadcrumbList,
|
|
13
|
+
local_business: BetterSeo::StructuredData::LocalBusiness,
|
|
14
|
+
event: BetterSeo::StructuredData::Event,
|
|
15
|
+
faq_page: BetterSeo::StructuredData::FAQPage,
|
|
16
|
+
how_to: BetterSeo::StructuredData::HowTo,
|
|
17
|
+
recipe: BetterSeo::StructuredData::Recipe
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def structured_data_tag(type_or_object, **properties, &block)
|
|
21
|
+
sd_object = if type_or_object.is_a?(Symbol)
|
|
22
|
+
create_structured_data(type_or_object, properties, &block)
|
|
23
|
+
else
|
|
24
|
+
type_or_object
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
raw(sd_object.to_script_tag)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def structured_data_tags(objects = nil, &block)
|
|
31
|
+
array = block_given? ? yield : objects
|
|
32
|
+
return "" if array.nil? || array.empty?
|
|
33
|
+
|
|
34
|
+
tags = array.map(&:to_script_tag).join("\n\n")
|
|
35
|
+
raw(tags)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def organization_sd(**properties, &block)
|
|
39
|
+
structured_data_tag(:organization, **properties, &block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def article_sd(**properties, &block)
|
|
43
|
+
structured_data_tag(:article, **properties, &block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def person_sd(**properties, &block)
|
|
47
|
+
structured_data_tag(:person, **properties, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def product_sd(**properties, &block)
|
|
51
|
+
structured_data_tag(:product, **properties, &block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def breadcrumb_list_sd(items: nil, &block)
|
|
55
|
+
if block_given?
|
|
56
|
+
structured_data_tag(:breadcrumb_list, &block)
|
|
57
|
+
elsif items
|
|
58
|
+
structured_data_tag(:breadcrumb_list) do |breadcrumb|
|
|
59
|
+
breadcrumb.add_items(items)
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
structured_data_tag(:breadcrumb_list)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def local_business_sd(**properties, &block)
|
|
67
|
+
structured_data_tag(:local_business, **properties, &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def event_sd(**properties, &block)
|
|
71
|
+
structured_data_tag(:event, **properties, &block)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def faq_page_sd(questions: nil, &block)
|
|
75
|
+
if block_given?
|
|
76
|
+
structured_data_tag(:faq_page, &block)
|
|
77
|
+
elsif questions
|
|
78
|
+
structured_data_tag(:faq_page) do |faq|
|
|
79
|
+
faq.add_questions(questions)
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
structured_data_tag(:faq_page)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def how_to_sd(steps: nil, &block)
|
|
87
|
+
if block_given?
|
|
88
|
+
structured_data_tag(:how_to, &block)
|
|
89
|
+
elsif steps
|
|
90
|
+
structured_data_tag(:how_to) do |how_to|
|
|
91
|
+
how_to.add_steps(steps)
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
structured_data_tag(:how_to)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def recipe_sd(ingredients: nil, instructions: nil, &block)
|
|
99
|
+
if block_given?
|
|
100
|
+
structured_data_tag(:recipe, &block)
|
|
101
|
+
else
|
|
102
|
+
structured_data_tag(:recipe) do |recipe|
|
|
103
|
+
recipe.add_ingredients(ingredients) if ingredients
|
|
104
|
+
recipe.add_instructions(instructions) if instructions
|
|
105
|
+
yield(recipe) if block_given?
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def create_structured_data(type, properties, &block)
|
|
113
|
+
klass = TYPE_MAPPING[type]
|
|
114
|
+
raise ArgumentError, "Unknown structured data type: #{type}" unless klass
|
|
115
|
+
|
|
116
|
+
sd_object = klass.new(**properties)
|
|
117
|
+
yield(sd_object) if block_given?
|
|
118
|
+
sd_object
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterSeo
|
|
4
|
+
module Rails
|
|
5
|
+
module ModelHelpers
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
# Define SEO attribute mappings
|
|
10
|
+
# @example
|
|
11
|
+
# seo_attributes(
|
|
12
|
+
# title: :post_title,
|
|
13
|
+
# description: :excerpt,
|
|
14
|
+
# keywords: -> { tags.map(&:name).join(", ") },
|
|
15
|
+
# image: :featured_image_url
|
|
16
|
+
# )
|
|
17
|
+
def seo_attributes(mappings = {})
|
|
18
|
+
@_seo_attribute_mappings = mappings
|
|
19
|
+
|
|
20
|
+
# Define accessor methods for each SEO attribute
|
|
21
|
+
mappings.each do |seo_attr, source|
|
|
22
|
+
define_method("seo_#{seo_attr}") do
|
|
23
|
+
evaluate_seo_attribute(source)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def seo_attribute_mappings
|
|
29
|
+
@_seo_attribute_mappings || {}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Convert model to SEO hash
|
|
34
|
+
def to_seo_hash
|
|
35
|
+
mappings = self.class.seo_attribute_mappings
|
|
36
|
+
return {} if mappings.empty?
|
|
37
|
+
|
|
38
|
+
hash = {}
|
|
39
|
+
|
|
40
|
+
mappings.each_key do |seo_attr|
|
|
41
|
+
value = send("seo_#{seo_attr}")
|
|
42
|
+
hash[seo_attr] = value if value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
hash
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def evaluate_seo_attribute(source)
|
|
51
|
+
case source
|
|
52
|
+
when Proc
|
|
53
|
+
instance_exec(&source)
|
|
54
|
+
when Symbol
|
|
55
|
+
send(source) if respond_to?(source)
|
|
56
|
+
else
|
|
57
|
+
source
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module BetterSeo
|
|
6
|
+
module Rails
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
initializer "better_seo.controller_helpers" do
|
|
9
|
+
ActiveSupport.on_load(:action_controller) do
|
|
10
|
+
include BetterSeo::Rails::Helpers::ControllerHelpers
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer "better_seo.view_helpers" do
|
|
15
|
+
ActiveSupport.on_load(:action_view) do
|
|
16
|
+
include BetterSeo::Rails::Helpers::SeoHelper
|
|
17
|
+
include BetterSeo::Rails::Helpers::StructuredDataHelper
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterSeo
|
|
4
|
+
module Sitemap
|
|
5
|
+
class Builder
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :urls
|
|
9
|
+
|
|
10
|
+
def initialize(urls: [])
|
|
11
|
+
@urls = urls
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add_url(location, lastmod: nil, changefreq: "weekly", priority: 0.5)
|
|
15
|
+
@urls << UrlEntry.new(location, lastmod: lastmod, changefreq: changefreq, priority: priority)
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add_urls(locations, lastmod: nil, changefreq: "weekly", priority: 0.5)
|
|
20
|
+
locations.each do |location|
|
|
21
|
+
add_url(location, lastmod: lastmod, changefreq: changefreq, priority: priority)
|
|
22
|
+
end
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def remove_url(location)
|
|
27
|
+
@urls.reject! { |url| url.loc == location }
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clear
|
|
32
|
+
@urls = []
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_xml
|
|
37
|
+
xml = []
|
|
38
|
+
xml << '<?xml version="1.0" encoding="UTF-8"?>'
|
|
39
|
+
xml << '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
|
40
|
+
@urls.each do |url|
|
|
41
|
+
xml << url.to_xml
|
|
42
|
+
end
|
|
43
|
+
xml << "</urlset>"
|
|
44
|
+
xml.join("\n")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate!
|
|
48
|
+
@urls.each(&:validate!)
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def size
|
|
53
|
+
@urls.size
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def empty?
|
|
57
|
+
@urls.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def each(&block)
|
|
61
|
+
@urls.each(&block)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module BetterSeo
|
|
6
|
+
module Sitemap
|
|
7
|
+
class Generator
|
|
8
|
+
class << self
|
|
9
|
+
def generate(&block)
|
|
10
|
+
builder = Builder.new
|
|
11
|
+
yield(builder) if block_given?
|
|
12
|
+
builder.to_xml
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate_from(urls, lastmod: nil, changefreq: "weekly", priority: 0.5)
|
|
16
|
+
builder = Builder.new
|
|
17
|
+
builder.add_urls(urls, lastmod: lastmod, changefreq: changefreq, priority: priority)
|
|
18
|
+
builder.to_xml
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def generate_from_collection(collection, url: nil, lastmod: nil, changefreq: "weekly", priority: 0.5)
|
|
22
|
+
raise ArgumentError, "url option is required" if url.nil?
|
|
23
|
+
raise ArgumentError, "url option must be callable" unless url.respond_to?(:call)
|
|
24
|
+
|
|
25
|
+
builder = Builder.new
|
|
26
|
+
|
|
27
|
+
collection.each do |item|
|
|
28
|
+
url_value = url.call(item)
|
|
29
|
+
lastmod_value = lastmod.respond_to?(:call) ? lastmod.call(item) : lastmod
|
|
30
|
+
changefreq_value = changefreq.respond_to?(:call) ? changefreq.call(item) : changefreq
|
|
31
|
+
priority_value = priority.respond_to?(:call) ? priority.call(item) : priority
|
|
32
|
+
|
|
33
|
+
builder.add_url(
|
|
34
|
+
url_value,
|
|
35
|
+
lastmod: lastmod_value,
|
|
36
|
+
changefreq: changefreq_value,
|
|
37
|
+
priority: priority_value
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
builder.to_xml
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def write_to_file(file_path, &block)
|
|
45
|
+
xml = generate(&block)
|
|
46
|
+
|
|
47
|
+
# Create parent directories if they don't exist
|
|
48
|
+
dir = File.dirname(file_path)
|
|
49
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
50
|
+
|
|
51
|
+
File.write(file_path, xml)
|
|
52
|
+
file_path
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module BetterSeo
|
|
7
|
+
module Sitemap
|
|
8
|
+
class SitemapIndex
|
|
9
|
+
attr_reader :sitemaps
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@sitemaps = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Add a sitemap to the index
|
|
16
|
+
def add_sitemap(loc, lastmod: nil)
|
|
17
|
+
sitemap = { loc: loc }
|
|
18
|
+
sitemap[:lastmod] = format_date(lastmod) if lastmod
|
|
19
|
+
@sitemaps << sitemap
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Generate XML for sitemap index
|
|
24
|
+
def to_xml
|
|
25
|
+
xml = []
|
|
26
|
+
xml << '<?xml version="1.0" encoding="UTF-8"?>'
|
|
27
|
+
xml << '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
|
28
|
+
|
|
29
|
+
@sitemaps.each do |sitemap|
|
|
30
|
+
xml << " <sitemap>"
|
|
31
|
+
xml << " <loc>#{escape_xml(sitemap[:loc])}</loc>"
|
|
32
|
+
xml << " <lastmod>#{sitemap[:lastmod]}</lastmod>" if sitemap[:lastmod]
|
|
33
|
+
xml << " </sitemap>"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
xml << "</sitemapindex>"
|
|
37
|
+
xml.join("\n")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Write sitemap index to file
|
|
41
|
+
def write_to_file(path)
|
|
42
|
+
directory = File.dirname(path)
|
|
43
|
+
FileUtils.mkdir_p(directory) unless File.directory?(directory)
|
|
44
|
+
|
|
45
|
+
File.write(path, to_xml)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def format_date(value)
|
|
51
|
+
case value
|
|
52
|
+
when String
|
|
53
|
+
value
|
|
54
|
+
when Date
|
|
55
|
+
value.strftime("%Y-%m-%d")
|
|
56
|
+
when Time, DateTime
|
|
57
|
+
value.strftime("%Y-%m-%d")
|
|
58
|
+
else
|
|
59
|
+
value.to_s
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def escape_xml(text)
|
|
64
|
+
text.to_s
|
|
65
|
+
.gsub("&", "&")
|
|
66
|
+
.gsub("<", "<")
|
|
67
|
+
.gsub(">", ">")
|
|
68
|
+
.gsub('"', """)
|
|
69
|
+
.gsub("'", "'")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|