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,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,134 @@
|
|
|
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,
|
|
83
|
+
sugar_content: nil, **other)
|
|
84
|
+
nutrition_hash = {
|
|
85
|
+
"@type" => "NutritionInformation"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
nutrition_hash["calories"] = calories if calories
|
|
89
|
+
nutrition_hash["fatContent"] = fat_content if fat_content
|
|
90
|
+
nutrition_hash["carbohydrateContent"] = carbohydrate_content if carbohydrate_content
|
|
91
|
+
nutrition_hash["proteinContent"] = protein_content if protein_content
|
|
92
|
+
nutrition_hash["sugarContent"] = sugar_content if sugar_content
|
|
93
|
+
|
|
94
|
+
other.each do |key, value|
|
|
95
|
+
nutrition_hash[key.to_s.camelize(:lower)] = value if value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
set(:nutrition, nutrition_hash)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Aggregate rating
|
|
102
|
+
def aggregate_rating(rating_value:, review_count:, best_rating: 5, worst_rating: 1)
|
|
103
|
+
rating = {
|
|
104
|
+
"@type" => "AggregateRating",
|
|
105
|
+
"ratingValue" => rating_value,
|
|
106
|
+
"reviewCount" => review_count,
|
|
107
|
+
"bestRating" => best_rating,
|
|
108
|
+
"worstRating" => worst_rating
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
set(:aggregateRating, rating)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Override to_h to include ingredients and instructions
|
|
115
|
+
def to_h
|
|
116
|
+
hash = super
|
|
117
|
+
|
|
118
|
+
hash["recipeIngredient"] = @ingredients if @ingredients.any?
|
|
119
|
+
|
|
120
|
+
if @instructions.any?
|
|
121
|
+
hash["recipeInstructions"] = @instructions.map.with_index(1) do |instruction, index|
|
|
122
|
+
{
|
|
123
|
+
"@type" => "HowToStep",
|
|
124
|
+
"position" => index,
|
|
125
|
+
"text" => instruction
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
hash
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|