better_seo 0.14.0 → 1.0.0.2
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/.rubocop.yml +30 -0
- data/.rubocop_todo.yml +360 -0
- data/CHANGELOG.md +105 -0
- data/README.md +280 -180
- 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 +322 -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 +105 -0
- data/lib/better_seo/dsl/twitter_cards.rb +129 -0
- data/lib/better_seo/errors.rb +31 -0
- data/lib/better_seo/generators/amp_generator.rb +77 -0
- data/lib/better_seo/generators/breadcrumbs_generator.rb +127 -0
- data/lib/better_seo/generators/canonical_url_manager.rb +100 -0
- data/lib/better_seo/generators/meta_tags_generator.rb +101 -0
- data/lib/better_seo/generators/open_graph_generator.rb +110 -0
- data/lib/better_seo/generators/robots_txt_generator.rb +96 -0
- data/lib/better_seo/generators/twitter_cards_generator.rb +102 -0
- data/lib/better_seo/image/optimizer.rb +145 -0
- data/lib/better_seo/rails/helpers/controller_helpers.rb +120 -0
- data/lib/better_seo/rails/helpers/seo_helper.rb +172 -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 +155 -0
- data/lib/better_seo/structured_data/article.rb +55 -0
- data/lib/better_seo/structured_data/base.rb +74 -0
- data/lib/better_seo/structured_data/breadcrumb_list.rb +49 -0
- data/lib/better_seo/structured_data/event.rb +205 -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 +134 -0
- data/lib/better_seo/validators/seo_recommendations.rb +165 -0
- data/lib/better_seo/validators/seo_validator.rb +205 -0
- data/lib/better_seo/version.rb +1 -1
- data/lib/better_seo.rb +1 -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 +57 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterSeo
|
|
4
|
+
module Generators
|
|
5
|
+
class OpenGraphGenerator
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def generate
|
|
11
|
+
tags = []
|
|
12
|
+
tags << meta_tag("og:title", @config[:title])
|
|
13
|
+
tags << meta_tag("og:description", @config[:description])
|
|
14
|
+
tags << meta_tag("og:type", @config[:type])
|
|
15
|
+
tags << meta_tag("og:url", @config[:url])
|
|
16
|
+
tags.concat(image_tags)
|
|
17
|
+
tags << meta_tag("og:site_name", @config[:site_name])
|
|
18
|
+
tags << meta_tag("og:locale", @config[:locale])
|
|
19
|
+
tags.concat(locale_alternate_tags)
|
|
20
|
+
tags.concat(article_tags)
|
|
21
|
+
tags.concat(video_tags)
|
|
22
|
+
tags << meta_tag("og:audio", @config[:audio])
|
|
23
|
+
|
|
24
|
+
tags.compact.join("\n")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def meta_tag(property, content)
|
|
30
|
+
return nil unless content
|
|
31
|
+
|
|
32
|
+
%(<meta property="#{property}" content="#{escape(content)}">)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def image_tags
|
|
36
|
+
image = @config[:image]
|
|
37
|
+
return [] unless image
|
|
38
|
+
|
|
39
|
+
tags = []
|
|
40
|
+
|
|
41
|
+
if image.is_a?(Hash)
|
|
42
|
+
tags << meta_tag("og:image", image[:url])
|
|
43
|
+
tags << meta_tag("og:image:width", image[:width])
|
|
44
|
+
tags << meta_tag("og:image:height", image[:height])
|
|
45
|
+
tags << meta_tag("og:image:alt", image[:alt])
|
|
46
|
+
else
|
|
47
|
+
tags << meta_tag("og:image", image)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
tags.compact
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def locale_alternate_tags
|
|
54
|
+
alternates = @config[:locale_alternate]
|
|
55
|
+
return [] unless alternates&.any?
|
|
56
|
+
|
|
57
|
+
Array(alternates).map do |locale|
|
|
58
|
+
meta_tag("og:locale:alternate", locale)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def article_tags
|
|
63
|
+
article = @config[:article]
|
|
64
|
+
return [] unless article
|
|
65
|
+
|
|
66
|
+
tags = []
|
|
67
|
+
tags << meta_tag("article:author", article[:author])
|
|
68
|
+
tags << meta_tag("article:published_time", article[:published_time])
|
|
69
|
+
tags << meta_tag("article:modified_time", article[:modified_time])
|
|
70
|
+
tags << meta_tag("article:expiration_time", article[:expiration_time])
|
|
71
|
+
tags << meta_tag("article:section", article[:section])
|
|
72
|
+
|
|
73
|
+
# Handle multiple tags
|
|
74
|
+
if article[:tag]
|
|
75
|
+
Array(article[:tag]).each do |tag|
|
|
76
|
+
tags << meta_tag("article:tag", tag)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
tags.compact
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def video_tags
|
|
84
|
+
video = @config[:video]
|
|
85
|
+
return [] unless video
|
|
86
|
+
|
|
87
|
+
tags = []
|
|
88
|
+
|
|
89
|
+
if video.is_a?(Hash)
|
|
90
|
+
tags << meta_tag("og:video", video[:url])
|
|
91
|
+
tags << meta_tag("og:video:width", video[:width])
|
|
92
|
+
tags << meta_tag("og:video:height", video[:height])
|
|
93
|
+
tags << meta_tag("og:video:type", video[:type])
|
|
94
|
+
else
|
|
95
|
+
tags << meta_tag("og:video", video)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
tags.compact
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def escape(text)
|
|
102
|
+
text.to_s
|
|
103
|
+
.gsub("&", "&")
|
|
104
|
+
.gsub('"', """)
|
|
105
|
+
.gsub("<", "<")
|
|
106
|
+
.gsub(">", ">")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module BetterSeo
|
|
6
|
+
module Generators
|
|
7
|
+
class RobotsTxtGenerator
|
|
8
|
+
attr_reader :rules, :sitemaps
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@rules = []
|
|
12
|
+
@sitemaps = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Add rule for user agent
|
|
16
|
+
def add_rule(user_agent, allow: nil, disallow: nil)
|
|
17
|
+
rule = { user_agent: user_agent }
|
|
18
|
+
rule[:allow] = Array(allow).compact if allow
|
|
19
|
+
rule[:disallow] = Array(disallow).compact if disallow
|
|
20
|
+
@rules << rule
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Set crawl delay for user agent
|
|
25
|
+
def set_crawl_delay(user_agent, seconds)
|
|
26
|
+
rule = @rules.find { |r| r[:user_agent] == user_agent }
|
|
27
|
+
|
|
28
|
+
if rule
|
|
29
|
+
rule[:crawl_delay] = seconds
|
|
30
|
+
else
|
|
31
|
+
@rules << { user_agent: user_agent, crawl_delay: seconds }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Add sitemap URL
|
|
38
|
+
def add_sitemap(url)
|
|
39
|
+
@sitemaps << url
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Clear all rules and sitemaps
|
|
44
|
+
def clear
|
|
45
|
+
@rules = []
|
|
46
|
+
@sitemaps = []
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Generate robots.txt content
|
|
51
|
+
def to_text
|
|
52
|
+
return "" if @rules.empty? && @sitemaps.empty?
|
|
53
|
+
|
|
54
|
+
lines = []
|
|
55
|
+
|
|
56
|
+
# Generate rules for each user agent
|
|
57
|
+
@rules.each_with_index do |rule, index|
|
|
58
|
+
lines << "User-agent: #{rule[:user_agent]}"
|
|
59
|
+
|
|
60
|
+
# Allow directives
|
|
61
|
+
rule[:allow]&.each do |path|
|
|
62
|
+
lines << "Allow: #{path}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Disallow directives
|
|
66
|
+
rule[:disallow]&.each do |path|
|
|
67
|
+
lines << "Disallow: #{path}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Crawl delay
|
|
71
|
+
lines << "Crawl-delay: #{rule[:crawl_delay]}" if rule[:crawl_delay]
|
|
72
|
+
|
|
73
|
+
# Add blank line between user agent sections (except last)
|
|
74
|
+
lines << "" if index < @rules.size - 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Add blank line before sitemaps if there are rules
|
|
78
|
+
lines << "" if @rules.any? && @sitemaps.any?
|
|
79
|
+
|
|
80
|
+
# Add sitemaps
|
|
81
|
+
@sitemaps.each do |sitemap|
|
|
82
|
+
lines << "Sitemap: #{sitemap}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
lines.join("\n")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Write robots.txt to file
|
|
89
|
+
def write_to_file(path)
|
|
90
|
+
directory = File.dirname(path)
|
|
91
|
+
FileUtils.mkdir_p(directory) unless File.directory?(directory)
|
|
92
|
+
File.write(path, to_text)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterSeo
|
|
4
|
+
module Generators
|
|
5
|
+
class TwitterCardsGenerator
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def generate
|
|
11
|
+
tags = []
|
|
12
|
+
tags << meta_tag("twitter:card", @config[:card])
|
|
13
|
+
tags << meta_tag("twitter:site", @config[:site])
|
|
14
|
+
tags << meta_tag("twitter:creator", @config[:creator])
|
|
15
|
+
tags << meta_tag("twitter:title", @config[:title])
|
|
16
|
+
tags << meta_tag("twitter:description", @config[:description])
|
|
17
|
+
tags.concat(image_tags)
|
|
18
|
+
tags.concat(player_tags)
|
|
19
|
+
tags.concat(app_tags)
|
|
20
|
+
|
|
21
|
+
tags.compact.join("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def meta_tag(name, content)
|
|
27
|
+
return nil unless content
|
|
28
|
+
|
|
29
|
+
%(<meta name="#{name}" content="#{escape(content)}">)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def image_tags
|
|
33
|
+
image = @config[:image]
|
|
34
|
+
return [] unless image
|
|
35
|
+
|
|
36
|
+
tags = []
|
|
37
|
+
|
|
38
|
+
if image.is_a?(Hash)
|
|
39
|
+
tags << meta_tag("twitter:image", image[:url])
|
|
40
|
+
tags << meta_tag("twitter:image:alt", image[:alt])
|
|
41
|
+
else
|
|
42
|
+
tags << meta_tag("twitter:image", image)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Add separate image_alt if present
|
|
46
|
+
tags << meta_tag("twitter:image:alt", @config[:image_alt]) if @config[:image_alt]
|
|
47
|
+
|
|
48
|
+
tags.compact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def player_tags
|
|
52
|
+
return [] unless @config[:player]
|
|
53
|
+
|
|
54
|
+
tags = []
|
|
55
|
+
tags << meta_tag("twitter:player", @config[:player])
|
|
56
|
+
tags << meta_tag("twitter:player:width", @config[:player_width])
|
|
57
|
+
tags << meta_tag("twitter:player:height", @config[:player_height])
|
|
58
|
+
tags << meta_tag("twitter:player:stream", @config[:player_stream])
|
|
59
|
+
|
|
60
|
+
tags.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def app_tags
|
|
64
|
+
tags = []
|
|
65
|
+
|
|
66
|
+
# App name tags
|
|
67
|
+
if @config[:app_name]
|
|
68
|
+
app_name = @config[:app_name]
|
|
69
|
+
tags << meta_tag("twitter:app:name:iphone", app_name[:iphone])
|
|
70
|
+
tags << meta_tag("twitter:app:name:ipad", app_name[:ipad])
|
|
71
|
+
tags << meta_tag("twitter:app:name:googleplay", app_name[:googleplay])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# App ID tags
|
|
75
|
+
if @config[:app_id]
|
|
76
|
+
app_id = @config[:app_id]
|
|
77
|
+
tags << meta_tag("twitter:app:id:iphone", app_id[:iphone])
|
|
78
|
+
tags << meta_tag("twitter:app:id:ipad", app_id[:ipad])
|
|
79
|
+
tags << meta_tag("twitter:app:id:googleplay", app_id[:googleplay])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# App URL tags
|
|
83
|
+
if @config[:app_url]
|
|
84
|
+
app_url = @config[:app_url]
|
|
85
|
+
tags << meta_tag("twitter:app:url:iphone", app_url[:iphone])
|
|
86
|
+
tags << meta_tag("twitter:app:url:ipad", app_url[:ipad])
|
|
87
|
+
tags << meta_tag("twitter:app:url:googleplay", app_url[:googleplay])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
tags.compact
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def escape(text)
|
|
94
|
+
text.to_s
|
|
95
|
+
.gsub("&", "&")
|
|
96
|
+
.gsub('"', """)
|
|
97
|
+
.gsub("<", "<")
|
|
98
|
+
.gsub(">", ">")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mini_magick"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mini_magick is optional
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module BetterSeo
|
|
10
|
+
module Image
|
|
11
|
+
class Optimizer
|
|
12
|
+
SUPPORTED_FORMATS = %w[jpg jpeg png webp gif].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :quality
|
|
15
|
+
|
|
16
|
+
def initialize(quality: 85)
|
|
17
|
+
@quality = quality
|
|
18
|
+
check_imagemagick!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Validate image format
|
|
22
|
+
def validate_format!(path)
|
|
23
|
+
ext = File.extname(path).downcase.delete(".")
|
|
24
|
+
unless SUPPORTED_FORMATS.include?(ext)
|
|
25
|
+
raise ImageError, "Unsupported format: #{ext}. Supported: #{SUPPORTED_FORMATS.join(", ")}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Convert image to WebP
|
|
32
|
+
def convert_to_webp(source, destination)
|
|
33
|
+
check_imagemagick!
|
|
34
|
+
validate_source!(source)
|
|
35
|
+
|
|
36
|
+
image = MiniMagick::Image.open(source)
|
|
37
|
+
image.format "webp"
|
|
38
|
+
image.quality @quality
|
|
39
|
+
image.write destination
|
|
40
|
+
|
|
41
|
+
destination
|
|
42
|
+
rescue MiniMagick::Error => e
|
|
43
|
+
raise ImageError, "Failed to convert to WebP: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resize image
|
|
47
|
+
def resize(source, destination, width: nil, height: nil)
|
|
48
|
+
check_imagemagick!
|
|
49
|
+
validate_source!(source)
|
|
50
|
+
|
|
51
|
+
raise ImageError, "Either width or height must be specified" if width.nil? && height.nil?
|
|
52
|
+
|
|
53
|
+
image = MiniMagick::Image.open(source)
|
|
54
|
+
|
|
55
|
+
if width && height
|
|
56
|
+
image.resize "#{width}x#{height}!"
|
|
57
|
+
elsif width
|
|
58
|
+
image.resize "#{width}x"
|
|
59
|
+
else
|
|
60
|
+
image.resize "x#{height}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
image.write destination
|
|
64
|
+
|
|
65
|
+
destination
|
|
66
|
+
rescue MiniMagick::Error => e
|
|
67
|
+
raise ImageError, "Failed to resize: #{e.message}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Compress image
|
|
71
|
+
def compress(source, destination)
|
|
72
|
+
check_imagemagick!
|
|
73
|
+
validate_source!(source)
|
|
74
|
+
|
|
75
|
+
image = MiniMagick::Image.open(source)
|
|
76
|
+
image.quality @quality
|
|
77
|
+
image.write destination
|
|
78
|
+
|
|
79
|
+
destination
|
|
80
|
+
rescue MiniMagick::Error => e
|
|
81
|
+
raise ImageError, "Failed to compress: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Generate responsive image sizes
|
|
85
|
+
def generate_responsive(source, output_dir, sizes: {}, prefix: "responsive")
|
|
86
|
+
check_imagemagick!
|
|
87
|
+
validate_source!(source)
|
|
88
|
+
|
|
89
|
+
FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
|
|
90
|
+
|
|
91
|
+
results = {}
|
|
92
|
+
ext = File.extname(source)
|
|
93
|
+
|
|
94
|
+
sizes.each do |name, width|
|
|
95
|
+
output_path = File.join(output_dir, "#{prefix}_#{name}_#{width}w#{ext}")
|
|
96
|
+
resize(source, output_path, width: width)
|
|
97
|
+
results[name] = output_path
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
results
|
|
101
|
+
rescue MiniMagick::Error => e
|
|
102
|
+
raise ImageError, "Failed to generate responsive images: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Optimize image (resize + compress)
|
|
106
|
+
def optimize(source, destination, resize: nil)
|
|
107
|
+
check_imagemagick!
|
|
108
|
+
validate_source!(source)
|
|
109
|
+
|
|
110
|
+
original_size = File.size(source)
|
|
111
|
+
|
|
112
|
+
if resize
|
|
113
|
+
resize(source, destination, **resize)
|
|
114
|
+
else
|
|
115
|
+
compress(source, destination)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
optimized_size = File.size(destination)
|
|
119
|
+
reduction = ((original_size - optimized_size).to_f / original_size * 100).round(2)
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
original_size: original_size,
|
|
123
|
+
optimized_size: optimized_size,
|
|
124
|
+
reduction_percent: reduction
|
|
125
|
+
}
|
|
126
|
+
rescue MiniMagick::Error => e
|
|
127
|
+
raise ImageError, "Failed to optimize: #{e.message}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def check_imagemagick!
|
|
133
|
+
return if defined?(MiniMagick)
|
|
134
|
+
|
|
135
|
+
raise ImageError, "ImageMagick is not available. Please install ImageMagick and the mini_magick gem."
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def validate_source!(source)
|
|
139
|
+
raise ImageError, "Source file not found: #{source}" unless File.exist?(source)
|
|
140
|
+
|
|
141
|
+
validate_format!(source)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterSeo
|
|
4
|
+
module Rails
|
|
5
|
+
module Helpers
|
|
6
|
+
module ControllerHelpers
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
helper_method :better_seo_data if respond_to?(:helper_method)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Set meta tags data
|
|
14
|
+
def set_meta_tags(data = nil, &block)
|
|
15
|
+
if block_given?
|
|
16
|
+
meta_data = {}
|
|
17
|
+
yield(meta_data)
|
|
18
|
+
merge_seo_data(:meta, meta_data)
|
|
19
|
+
else
|
|
20
|
+
merge_seo_data(:meta, data)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Set Open Graph tags data
|
|
25
|
+
def set_og_tags(data = nil, &block)
|
|
26
|
+
if block_given?
|
|
27
|
+
og_data = {}
|
|
28
|
+
yield(og_data)
|
|
29
|
+
merge_seo_data(:og, og_data)
|
|
30
|
+
else
|
|
31
|
+
merge_seo_data(:og, data)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Set Twitter Card tags data
|
|
36
|
+
def set_twitter_tags(data = nil, &block)
|
|
37
|
+
if block_given?
|
|
38
|
+
twitter_data = {}
|
|
39
|
+
yield(twitter_data)
|
|
40
|
+
merge_seo_data(:twitter, twitter_data)
|
|
41
|
+
else
|
|
42
|
+
merge_seo_data(:twitter, data)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Set canonical URL
|
|
47
|
+
def set_canonical(url)
|
|
48
|
+
merge_seo_data(:meta, { canonical: url })
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Set page title with optional prefix/suffix
|
|
52
|
+
def set_page_title(title, prefix: nil, suffix: nil)
|
|
53
|
+
full_title = ""
|
|
54
|
+
full_title += prefix if prefix
|
|
55
|
+
full_title += title
|
|
56
|
+
full_title += suffix if suffix
|
|
57
|
+
|
|
58
|
+
merge_seo_data(:meta, { title: full_title })
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Set page description with optional truncation
|
|
62
|
+
def set_page_description(description, max_length: nil)
|
|
63
|
+
final_description = description
|
|
64
|
+
|
|
65
|
+
# rubocop:disable Style/IfUnlessModifier
|
|
66
|
+
if max_length && description.length > max_length
|
|
67
|
+
final_description = "#{description[0...(max_length - 3)]}..."
|
|
68
|
+
end
|
|
69
|
+
# rubocop:enable Style/IfUnlessModifier
|
|
70
|
+
|
|
71
|
+
merge_seo_data(:meta, { description: final_description })
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Set page keywords (array or comma-separated string)
|
|
75
|
+
def set_page_keywords(keywords)
|
|
76
|
+
keywords_array = if keywords.is_a?(String)
|
|
77
|
+
keywords.split(",").map(&:strip)
|
|
78
|
+
else
|
|
79
|
+
keywords
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
merge_seo_data(:meta, { keywords: keywords_array })
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Set page image for both OG and Twitter
|
|
86
|
+
def set_page_image(url, width: nil, height: nil)
|
|
87
|
+
og_data = { image: url }
|
|
88
|
+
og_data[:image_width] = width if width
|
|
89
|
+
og_data[:image_height] = height if height
|
|
90
|
+
|
|
91
|
+
merge_seo_data(:og, og_data)
|
|
92
|
+
merge_seo_data(:twitter, { image: url })
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Set noindex robot directive
|
|
96
|
+
def set_noindex(nofollow: false)
|
|
97
|
+
robots = "noindex"
|
|
98
|
+
robots += ", nofollow" if nofollow
|
|
99
|
+
|
|
100
|
+
merge_seo_data(:meta, { robots: robots })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get all SEO data stored in controller
|
|
104
|
+
def better_seo_data
|
|
105
|
+
@_better_seo_data ||= {}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def merge_seo_data(category, data)
|
|
111
|
+
return unless data
|
|
112
|
+
|
|
113
|
+
@_better_seo_data ||= {}
|
|
114
|
+
@_better_seo_data[category] ||= {}
|
|
115
|
+
@_better_seo_data[category].merge!(data)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|