better_seo 0.14.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +297 -179
  4. data/docs/00_OVERVIEW.md +472 -0
  5. data/docs/01_CORE_AND_CONFIGURATION.md +913 -0
  6. data/docs/02_META_TAGS_AND_OPEN_GRAPH.md +251 -0
  7. data/docs/03_STRUCTURED_DATA.md +140 -0
  8. data/docs/04_SITEMAP_AND_ROBOTS.md +131 -0
  9. data/docs/05_RAILS_INTEGRATION.md +175 -0
  10. data/docs/06_I18N_PAGE_GENERATOR.md +233 -0
  11. data/docs/07_IMAGE_OPTIMIZATION.md +260 -0
  12. data/docs/DEPENDENCIES.md +383 -0
  13. data/docs/README.md +180 -0
  14. data/docs/TESTING_STRATEGY.md +663 -0
  15. data/lib/better_seo/analytics/google_analytics.rb +83 -0
  16. data/lib/better_seo/analytics/google_tag_manager.rb +74 -0
  17. data/lib/better_seo/configuration.rb +316 -0
  18. data/lib/better_seo/dsl/base.rb +86 -0
  19. data/lib/better_seo/dsl/meta_tags.rb +55 -0
  20. data/lib/better_seo/dsl/open_graph.rb +109 -0
  21. data/lib/better_seo/dsl/twitter_cards.rb +131 -0
  22. data/lib/better_seo/errors.rb +31 -0
  23. data/lib/better_seo/generators/amp_generator.rb +83 -0
  24. data/lib/better_seo/generators/breadcrumbs_generator.rb +126 -0
  25. data/lib/better_seo/generators/canonical_url_manager.rb +106 -0
  26. data/lib/better_seo/generators/meta_tags_generator.rb +100 -0
  27. data/lib/better_seo/generators/open_graph_generator.rb +110 -0
  28. data/lib/better_seo/generators/robots_txt_generator.rb +102 -0
  29. data/lib/better_seo/generators/twitter_cards_generator.rb +102 -0
  30. data/lib/better_seo/image/optimizer.rb +143 -0
  31. data/lib/better_seo/rails/helpers/controller_helpers.rb +118 -0
  32. data/lib/better_seo/rails/helpers/seo_helper.rb +176 -0
  33. data/lib/better_seo/rails/helpers/structured_data_helper.rb +123 -0
  34. data/lib/better_seo/rails/model_helpers.rb +62 -0
  35. data/lib/better_seo/rails/railtie.rb +22 -0
  36. data/lib/better_seo/sitemap/builder.rb +65 -0
  37. data/lib/better_seo/sitemap/generator.rb +57 -0
  38. data/lib/better_seo/sitemap/sitemap_index.rb +73 -0
  39. data/lib/better_seo/sitemap/url_entry.rb +157 -0
  40. data/lib/better_seo/structured_data/article.rb +55 -0
  41. data/lib/better_seo/structured_data/base.rb +73 -0
  42. data/lib/better_seo/structured_data/breadcrumb_list.rb +49 -0
  43. data/lib/better_seo/structured_data/event.rb +207 -0
  44. data/lib/better_seo/structured_data/faq_page.rb +55 -0
  45. data/lib/better_seo/structured_data/generator.rb +75 -0
  46. data/lib/better_seo/structured_data/how_to.rb +96 -0
  47. data/lib/better_seo/structured_data/local_business.rb +94 -0
  48. data/lib/better_seo/structured_data/organization.rb +67 -0
  49. data/lib/better_seo/structured_data/person.rb +51 -0
  50. data/lib/better_seo/structured_data/product.rb +123 -0
  51. data/lib/better_seo/structured_data/recipe.rb +135 -0
  52. data/lib/better_seo/validators/seo_recommendations.rb +165 -0
  53. data/lib/better_seo/validators/seo_validator.rb +195 -0
  54. data/lib/better_seo/version.rb +1 -1
  55. data/lib/better_seo.rb +1 -0
  56. data/lib/generators/better_seo/install_generator.rb +21 -0
  57. data/lib/generators/better_seo/templates/README +29 -0
  58. data/lib/generators/better_seo/templates/better_seo.rb +40 -0
  59. metadata +55 -2
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterSeo
4
+ module StructuredData
5
+ class HowTo < Base
6
+ attr_reader :steps
7
+
8
+ def initialize(**properties)
9
+ super("HowTo", **properties)
10
+ @steps = []
11
+ end
12
+
13
+ # Basic properties
14
+ def name(value)
15
+ set(:name, value)
16
+ end
17
+
18
+ def description(value)
19
+ set(:description, value)
20
+ end
21
+
22
+ def image(value)
23
+ set(:image, value)
24
+ end
25
+
26
+ def total_time(value)
27
+ set(:totalTime, value)
28
+ end
29
+
30
+ def supply(value)
31
+ set(:supply, value)
32
+ end
33
+
34
+ def tool(value)
35
+ set(:tool, value)
36
+ end
37
+
38
+ # Add a single step
39
+ def add_step(name:, text:, image: nil, url: nil, position: nil)
40
+ position ||= @steps.size + 1
41
+ @steps << {
42
+ name: name,
43
+ text: text,
44
+ image: image,
45
+ url: url,
46
+ position: position
47
+ }
48
+ self
49
+ end
50
+
51
+ # Add multiple steps from an array
52
+ def add_steps(steps_array)
53
+ steps_array.each do |step|
54
+ add_step(
55
+ name: step[:name],
56
+ text: step[:text],
57
+ image: step[:image],
58
+ url: step[:url],
59
+ position: step[:position]
60
+ )
61
+ end
62
+ self
63
+ end
64
+
65
+ # Clear all steps
66
+ def clear
67
+ @steps = []
68
+ @properties.delete(:step)
69
+ self
70
+ end
71
+
72
+ # Override to_h to include steps
73
+ def to_h
74
+ hash = super
75
+
76
+ if @steps.any?
77
+ hash["step"] = @steps.map do |step|
78
+ step_hash = {
79
+ "@type" => "HowToStep",
80
+ "name" => step[:name],
81
+ "text" => step[:text],
82
+ "position" => step[:position]
83
+ }
84
+
85
+ step_hash["image"] = step[:image] if step[:image]
86
+ step_hash["url"] = step[:url] if step[:url]
87
+
88
+ step_hash
89
+ end
90
+ end
91
+
92
+ hash
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterSeo
4
+ module StructuredData
5
+ class LocalBusiness < Base
6
+ def initialize(**properties)
7
+ super("LocalBusiness", **properties)
8
+ end
9
+
10
+ # Basic properties
11
+ def name(value)
12
+ set(:name, value)
13
+ end
14
+
15
+ def description(value)
16
+ set(:description, value)
17
+ end
18
+
19
+ def url(value)
20
+ set(:url, value)
21
+ end
22
+
23
+ def telephone(value)
24
+ set(:telephone, value)
25
+ end
26
+
27
+ def email(value)
28
+ set(:email, value)
29
+ end
30
+
31
+ def image(value)
32
+ set(:image, value)
33
+ end
34
+
35
+ def price_range(value)
36
+ set(:priceRange, value)
37
+ end
38
+
39
+ # Address with PostalAddress schema
40
+ def address(street: nil, city: nil, region: nil, postal_code: nil, country: nil)
41
+ address_hash = {
42
+ "@type" => "PostalAddress"
43
+ }
44
+
45
+ address_hash["streetAddress"] = street if street
46
+ address_hash["addressLocality"] = city if city
47
+ address_hash["addressRegion"] = region if region
48
+ address_hash["postalCode"] = postal_code if postal_code
49
+ address_hash["addressCountry"] = country if country
50
+
51
+ set(:address, address_hash)
52
+ end
53
+
54
+ # Geographic coordinates
55
+ def geo(latitude:, longitude:)
56
+ geo_hash = {
57
+ "@type" => "GeoCoordinates",
58
+ "latitude" => latitude,
59
+ "longitude" => longitude
60
+ }
61
+
62
+ set(:geo, geo_hash)
63
+ end
64
+
65
+ # Opening hours - accepts string or array
66
+ def opening_hours(value)
67
+ set(:openingHours, value)
68
+ end
69
+
70
+ # Opening hours specification (structured format)
71
+ def opening_hours_specification(value)
72
+ set(:openingHoursSpecification, value)
73
+ end
74
+
75
+ # Aggregate rating
76
+ def aggregate_rating(rating_value:, review_count:, best_rating: 5, worst_rating: 1)
77
+ rating = {
78
+ "@type" => "AggregateRating",
79
+ "ratingValue" => rating_value,
80
+ "reviewCount" => review_count,
81
+ "bestRating" => best_rating,
82
+ "worstRating" => worst_rating
83
+ }
84
+
85
+ set(:aggregateRating, rating)
86
+ end
87
+
88
+ # Serves cuisine (for restaurants)
89
+ def serves_cuisine(value)
90
+ set(:servesCuisine, value)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterSeo
4
+ module StructuredData
5
+ class Organization < Base
6
+ def initialize(**properties)
7
+ super("Organization", **properties)
8
+ end
9
+
10
+ def name(value)
11
+ set(:name, value)
12
+ end
13
+
14
+ def url(value)
15
+ set(:url, value)
16
+ end
17
+
18
+ def logo(value)
19
+ set(:logo, value)
20
+ end
21
+
22
+ def description(value)
23
+ set(:description, value)
24
+ end
25
+
26
+ def email(value)
27
+ set(:email, value)
28
+ end
29
+
30
+ def telephone(value)
31
+ set(:telephone, value)
32
+ end
33
+
34
+ def address(value)
35
+ if value.is_a?(Hash)
36
+ postal_address = {
37
+ "@type" => "PostalAddress",
38
+ "streetAddress" => value[:street],
39
+ "addressLocality" => value[:city],
40
+ "addressRegion" => value[:region],
41
+ "postalCode" => value[:postal_code],
42
+ "addressCountry" => value[:country]
43
+ }.compact
44
+ set(:address, postal_address)
45
+ else
46
+ set(:address, value)
47
+ end
48
+ end
49
+
50
+ def same_as(value)
51
+ set(:sameAs, value)
52
+ end
53
+
54
+ def founding_date(value)
55
+ set(:foundingDate, value)
56
+ end
57
+
58
+ def founder(value)
59
+ set(:founder, value)
60
+ end
61
+
62
+ def founders(value)
63
+ set(:founder, value)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterSeo
4
+ module StructuredData
5
+ class Person < Base
6
+ def initialize(**properties)
7
+ super("Person", **properties)
8
+ end
9
+
10
+ def name(value)
11
+ set(:name, value)
12
+ end
13
+
14
+ def given_name(value)
15
+ set(:givenName, value)
16
+ end
17
+
18
+ def family_name(value)
19
+ set(:familyName, value)
20
+ end
21
+
22
+ def email(value)
23
+ set(:email, value)
24
+ end
25
+
26
+ def url(value)
27
+ set(:url, value)
28
+ end
29
+
30
+ def image(value)
31
+ set(:image, value)
32
+ end
33
+
34
+ def job_title(value)
35
+ set(:jobTitle, value)
36
+ end
37
+
38
+ def works_for(value)
39
+ set(:worksFor, value)
40
+ end
41
+
42
+ def telephone(value)
43
+ set(:telephone, value)
44
+ end
45
+
46
+ def same_as(value)
47
+ set(:sameAs, value)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterSeo
4
+ module StructuredData
5
+ class Product < Base
6
+ AVAILABILITY_MAPPING = {
7
+ "InStock" => "https://schema.org/InStock",
8
+ "OutOfStock" => "https://schema.org/OutOfStock",
9
+ "PreOrder" => "https://schema.org/PreOrder",
10
+ "Discontinued" => "https://schema.org/Discontinued",
11
+ "LimitedAvailability" => "https://schema.org/LimitedAvailability"
12
+ }.freeze
13
+
14
+ def initialize(**properties)
15
+ super("Product", **properties)
16
+ end
17
+
18
+ def name(value)
19
+ set(:name, value)
20
+ end
21
+
22
+ def description(value)
23
+ set(:description, value)
24
+ end
25
+
26
+ def image(value)
27
+ set(:image, value)
28
+ end
29
+
30
+ def brand(value)
31
+ set(:brand, value)
32
+ end
33
+
34
+ def sku(value)
35
+ set(:sku, value)
36
+ end
37
+
38
+ def gtin(value)
39
+ set(:gtin, value)
40
+ end
41
+
42
+ def mpn(value)
43
+ set(:mpn, value)
44
+ end
45
+
46
+ def offers(value)
47
+ if value.is_a?(Array)
48
+ set(:offers, value.map { |v| build_offer(v) })
49
+ elsif value.is_a?(Hash)
50
+ set(:offers, build_offer(value))
51
+ else
52
+ set(:offers, value)
53
+ end
54
+ end
55
+
56
+ def aggregate_rating(rating_value:, review_count:, best_rating: 5, worst_rating: 1)
57
+ rating = {
58
+ "@type" => "AggregateRating",
59
+ "ratingValue" => rating_value,
60
+ "reviewCount" => review_count,
61
+ "bestRating" => best_rating,
62
+ "worstRating" => worst_rating
63
+ }
64
+ set(:aggregateRating, rating)
65
+ end
66
+
67
+ def review(author:, rating_value:, review_body: nil, date_published: nil)
68
+ review_data = build_review(
69
+ author: author,
70
+ rating_value: rating_value,
71
+ review_body: review_body,
72
+ date_published: date_published
73
+ )
74
+ set(:review, review_data)
75
+ end
76
+
77
+ def reviews(reviews_array)
78
+ reviews_data = reviews_array.map do |review_hash|
79
+ build_review(
80
+ author: review_hash[:author],
81
+ rating_value: review_hash[:rating_value],
82
+ review_body: review_hash[:review_body],
83
+ date_published: review_hash[:date_published]
84
+ )
85
+ end
86
+ set(:review, reviews_data)
87
+ end
88
+
89
+ private
90
+
91
+ def build_offer(offer_hash)
92
+ availability = offer_hash[:availability] || "InStock"
93
+ availability_url = AVAILABILITY_MAPPING[availability] || "https://schema.org/#{availability}"
94
+
95
+ offer = {
96
+ "@type" => "Offer",
97
+ "price" => offer_hash[:price],
98
+ "priceCurrency" => offer_hash[:price_currency],
99
+ "availability" => availability_url
100
+ }
101
+ offer["url"] = offer_hash[:url] if offer_hash[:url]
102
+ offer.compact
103
+ end
104
+
105
+ def build_review(author:, rating_value:, review_body: nil, date_published: nil)
106
+ review = {
107
+ "@type" => "Review",
108
+ "author" => {
109
+ "@type" => "Person",
110
+ "name" => author
111
+ },
112
+ "reviewRating" => {
113
+ "@type" => "Rating",
114
+ "ratingValue" => rating_value
115
+ }
116
+ }
117
+ review["reviewBody"] = review_body if review_body
118
+ review["datePublished"] = date_published if date_published
119
+ review
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterSeo
4
+ module StructuredData
5
+ class Recipe < Base
6
+ attr_reader :ingredients, :instructions
7
+
8
+ def initialize(**properties)
9
+ super("Recipe", **properties)
10
+ @ingredients = []
11
+ @instructions = []
12
+ end
13
+
14
+ # Basic properties
15
+ def name(value)
16
+ set(:name, value)
17
+ end
18
+
19
+ def description(value)
20
+ set(:description, value)
21
+ end
22
+
23
+ def image(value)
24
+ set(:image, value)
25
+ end
26
+
27
+ def author(value)
28
+ set(:author, value)
29
+ end
30
+
31
+ def prep_time(value)
32
+ set(:prepTime, value)
33
+ end
34
+
35
+ def cook_time(value)
36
+ set(:cookTime, value)
37
+ end
38
+
39
+ def total_time(value)
40
+ set(:totalTime, value)
41
+ end
42
+
43
+ def recipe_yield(value)
44
+ set(:recipeYield, value)
45
+ end
46
+
47
+ def recipe_category(value)
48
+ set(:recipeCategory, value)
49
+ end
50
+
51
+ def recipe_cuisine(value)
52
+ set(:recipeCuisine, value)
53
+ end
54
+
55
+ def keywords(value)
56
+ set(:keywords, value)
57
+ end
58
+
59
+ # Ingredients
60
+ def add_ingredient(ingredient)
61
+ @ingredients << ingredient
62
+ self
63
+ end
64
+
65
+ def add_ingredients(ingredients_array)
66
+ @ingredients.concat(ingredients_array)
67
+ self
68
+ end
69
+
70
+ # Instructions
71
+ def add_instruction(instruction)
72
+ @instructions << instruction
73
+ self
74
+ end
75
+
76
+ def add_instructions(instructions_array)
77
+ @instructions.concat(instructions_array)
78
+ self
79
+ end
80
+
81
+ # Nutrition information
82
+ def nutrition(calories: nil, fat_content: nil, carbohydrate_content: nil, protein_content: nil, sugar_content: nil, **other)
83
+ nutrition_hash = {
84
+ "@type" => "NutritionInformation"
85
+ }
86
+
87
+ nutrition_hash["calories"] = calories if calories
88
+ nutrition_hash["fatContent"] = fat_content if fat_content
89
+ nutrition_hash["carbohydrateContent"] = carbohydrate_content if carbohydrate_content
90
+ nutrition_hash["proteinContent"] = protein_content if protein_content
91
+ nutrition_hash["sugarContent"] = sugar_content if sugar_content
92
+
93
+ other.each do |key, value|
94
+ nutrition_hash[key.to_s.camelize(:lower)] = value if value
95
+ end
96
+
97
+ set(:nutrition, nutrition_hash)
98
+ end
99
+
100
+ # Aggregate rating
101
+ def aggregate_rating(rating_value:, review_count:, best_rating: 5, worst_rating: 1)
102
+ rating = {
103
+ "@type" => "AggregateRating",
104
+ "ratingValue" => rating_value,
105
+ "reviewCount" => review_count,
106
+ "bestRating" => best_rating,
107
+ "worstRating" => worst_rating
108
+ }
109
+
110
+ set(:aggregateRating, rating)
111
+ end
112
+
113
+ # Override to_h to include ingredients and instructions
114
+ def to_h
115
+ hash = super
116
+
117
+ if @ingredients.any?
118
+ hash["recipeIngredient"] = @ingredients
119
+ end
120
+
121
+ if @instructions.any?
122
+ hash["recipeInstructions"] = @instructions.map.with_index(1) do |instruction, index|
123
+ {
124
+ "@type" => "HowToStep",
125
+ "position" => index,
126
+ "text" => instruction
127
+ }
128
+ end
129
+ end
130
+
131
+ hash
132
+ end
133
+ end
134
+ end
135
+ end