spree_storefront 5.4.1 → 5.4.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76c18fe5cbf2c1619cfda1f9ae3209b1bc6e83eef396cadd669d685cb5e11aff
4
- data.tar.gz: fa94ea40e743864d935d550e178a710fa6aad34b264efdfe9faaa3bb85e7cbb1
3
+ metadata.gz: 86f9eaec23e1403d7ca88d9ad9888265d90b70749aa0fbc38b5cfb80208111b7
4
+ data.tar.gz: 141654121255c1801335bcc1bc6854aeca3824bae3523f888599f1d94ea58659
5
5
  SHA512:
6
- metadata.gz: 4916d85ad37a17a36c442c0bb953d5bf4cc80055add3a74fa9c294c8f9b2f057cfc5b41357de110863c074bdbb83723485170486299c06283370fb91228b2247
7
- data.tar.gz: e9d6530f6e12c1646558028f2bdec15cca750bec1dcc4385840aa78af00cffdd97cf6c43476c574fef4192a8625a1322bae757be4ec3b547cdea07ed2fccd585
6
+ metadata.gz: 47f847c1933bdfe5e7a41692146f76c913df33f7cec7fa5f8eb3c4e73eb5d9f07f99c4703fdd57b9d2e0ae563ddde3c91979ffc26a1ac25fedd853d14403cd9b
7
+ data.tar.gz: 1498db74ea682506648e9a22bf4324f3658a0eabb34614a64470f618ed895e342f7e65daa9773b89f6ae559b608a92332ee4823a69061729b9a76b9ec7364006
@@ -0,0 +1,12 @@
1
+ module Spree
2
+ module JsonLdHelper
3
+ # spree_api globally sets ActiveSupport::JSON::Encoding.escape_html_entities_in_json = false,
4
+ # so a literal `</script>` in any field would break out of the surrounding script tag.
5
+ # ERB::Util.json_escape post-processes the encoded JSON to escape `<`, `>`, `&` regardless
6
+ # of that global flag.
7
+ def json_ld_script(data, **html_attrs)
8
+ json = ERB::Util.json_escape(data.to_json)
9
+ content_tag(:script, json.html_safe, type: 'application/ld+json', **html_attrs)
10
+ end
11
+ end
12
+ end
@@ -225,6 +225,46 @@ module Spree
225
225
  json_ld
226
226
  end
227
227
 
228
+ def product_json_ld_data(product, selected_variant: nil)
229
+ first_or_default_variant = product.first_or_default_variant(current_currency)
230
+ data = {
231
+ '@context' => 'https://schema.org/',
232
+ '@type' => 'Product',
233
+ 'name' => product.name,
234
+ 'url' => spree.product_url(product, host: current_store.url_or_custom_domain)
235
+ }
236
+ data['image'] = [spree_image_url(product.primary_media, variant: :large)] if product.has_images?
237
+ data['description'] = strip_tags(product.description) if product.description.present?
238
+
239
+ sku_variant = product.has_variants? ? selected_variant : first_or_default_variant
240
+ data['sku'] = sku_variant.sku if sku_variant&.sku.present?
241
+
242
+ if product.brand_taxon
243
+ data['brand'] = { '@type' => 'Brand', 'name' => product.brand_taxon.name }
244
+ end
245
+
246
+ data['offers'] = if product.has_variants?
247
+ product.variants.map { |variant| product_json_ld_variant_offer(product, variant) }
248
+ else
249
+ [product_json_ld_variant_offer(product, first_or_default_variant)]
250
+ end
251
+ data
252
+ end
253
+
254
+ def product_json_ld_variant_offer(product, variant)
255
+ Rails.cache.fetch(['json-ld-variant-hash', *spree_base_cache_key, variant.cache_key_with_version]) do
256
+ offer = {
257
+ '@type' => 'Offer',
258
+ 'availability' => "http://schema.org/#{variant.available? ? 'InStock' : 'OutOfStock'}",
259
+ 'price' => variant.amount_in(current_currency),
260
+ 'priceCurrency' => current_currency,
261
+ 'url' => spree.product_url(product, variant_id: variant.id, host: current_store.url_or_custom_domain)
262
+ }
263
+ offer['sku'] = variant.sku if variant.sku.present?
264
+ offer
265
+ end
266
+ end
267
+
228
268
  def option_type_colors_preview_styles(option_type)
229
269
  return unless option_type.color_swatch?
230
270
 
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class ColorsPreviewStylesPresenter
3
+ UNSAFE_CSS_CHARS = /[<>"'\\;{}\r\n]/
4
+
3
5
  def initialize(colors)
4
6
  @colors = colors.compact_blank.map do |color|
5
7
  case color
@@ -17,11 +19,11 @@ module Spree
17
19
  end
18
20
 
19
21
  def to_s
20
- @to_s ||= if colors.any?
22
+ @to_s ||= if renderable_colors.any?
21
23
  css = ['<style>']
22
24
 
23
- colors.each do |color|
24
- css_color = css_colors_hash[color[:filter_name]] || color[:filter_name].gsub(' ', '')
25
+ renderable_colors.each do |color|
26
+ css_color = css_colors_hash[color[:filter_name]]
25
27
  color_name = color[:name]
26
28
  css << <<~CSS
27
29
  @supports(background: #{css_color}) {
@@ -48,6 +50,17 @@ module Spree
48
50
 
49
51
  attr_reader :colors
50
52
 
53
+ # We only emit CSS rules for colors we recognise via Spree::ColorNames AND whose
54
+ # name contains no characters that could break out of the CSS string/selector
55
+ # context (admins control these names; #to_s output is rendered with raw()).
56
+ def renderable_colors
57
+ @renderable_colors ||= colors.reject do |color|
58
+ css_colors_hash[color[:filter_name]].nil? ||
59
+ color[:name].match?(UNSAFE_CSS_CHARS) ||
60
+ color[:filter_name].match?(UNSAFE_CSS_CHARS)
61
+ end
62
+ end
63
+
51
64
  def css_colors_hash
52
65
  @css_colors_hash ||= begin
53
66
  colors_hash = {}
@@ -61,7 +74,7 @@ module Spree
61
74
  colors_hash[color_name] = generate_css_color(hex_colors)
62
75
  elsif (subcolors = color_name.split.compact) && subcolors.length > 1
63
76
  subcolors = subcolors.map(&method(:find_color)).compact
64
- colors_hash[color_name] = generate_css_color(subcolors)
77
+ colors_hash[color_name] = generate_css_color(subcolors) if subcolors.any?
65
78
  end
66
79
  end
67
80
 
@@ -28,7 +28,7 @@
28
28
  <%= hidden_field_tag :section_id, section.id %>
29
29
  <%= f.email_field :email, placeholder: block.preferred_placeholder, required: true, class: 'focus:border-primary focus:ring-primary pr-36 text-sm bg-accent rounded-input border-accent py-2 px-4 w-full relative z-30 !leading-[46px]' %>
30
30
  <div class="absolute right-2 top-1/2 transform -translate-y-1/2 z-40">
31
- <%= f.submit block.preferred_button_text.html_safe, class: "#{block.preferred_button_style == 'secondary' ? 'btn-secondary' : 'btn-primary'} content-center" %>
31
+ <%= f.submit block.preferred_button_text, class: "#{block.preferred_button_style == 'secondary' ? 'btn-secondary' : 'btn-primary'} content-center" %>
32
32
  </div>
33
33
  </div>
34
34
  </div>
@@ -1,20 +1,16 @@
1
- <script type="application/ld+json" data-test-id="post-json-ld">
2
- {
3
- "@context": "https://schema.org",
4
- "@type": "BlogPosting",
5
- "headline": "<%= post.title %>",
6
- "image": <%= post.image.attached? ? [spree_image_url(post.image, width: 1200, height: 675)].to_json.html_safe : [].to_json.html_safe %>,
7
- "datePublished": "<%= post.published_at&.iso8601 %>",
8
- "dateModified": "<%= post.updated_at&.iso8601 %>",
9
- "author": [
10
- {
11
- "@type": "Person",
12
- "name": "<%= post.author_name %>"
13
- }
14
- ]
15
- }
16
- </script>
1
+ <%= json_ld_script({
2
+ '@context' => 'https://schema.org',
3
+ '@type' => 'BlogPosting',
4
+ 'headline' => post.title,
5
+ 'image' => post.image.attached? ? [spree_image_url(post.image, width: 1200, height: 675)] : [],
6
+ 'datePublished' => post.published_at&.iso8601,
7
+ 'dateModified' => post.updated_at&.iso8601,
8
+ 'author' => [
9
+ {
10
+ '@type' => 'Person',
11
+ 'name' => post.author_name
12
+ }
13
+ ]
14
+ }, data: { test_id: 'post-json-ld' }) %>
17
15
 
18
- <script type="application/ld+json" data-test-id="post-breadcrumbs-json-ld">
19
- <%= posts_json_ld_breadcrumbs(post).to_json.html_safe %>
20
- </script>
16
+ <%= json_ld_script(posts_json_ld_breadcrumbs(post), data: { test_id: 'post-breadcrumbs-json-ld' }) %>
@@ -2,7 +2,7 @@
2
2
  <% description_text = strip_tags(product.storefront_description) %>
3
3
  <div data-controller="read-more" class="flex flex-col gap-4" data-read-more-more-text-value="<%= Spree.t(:read_more) %>" data-read-more-less-text-value="Read less">
4
4
  <div class="prose product-description text-sm <%= 'product-description-truncated' if description_text.size > 250 %>" data-read-more-target="content">
5
- <%= raw(product.storefront_description) %>
5
+ <%= sanitize(product.storefront_description) %>
6
6
  </div>
7
7
  <% if description_text.size > 250 %>
8
8
  <%= button_tag Spree.t(:read_more), type: 'button', data: { action: "read-more#toggle" }, class: "font-bold underline text-sm" %>
@@ -3,7 +3,7 @@
3
3
  <% description_text = strip_tags(product.storefront_description) %>
4
4
  <div data-controller="read-more" class="py-4 flex flex-col gap-4" data-read-more-more-text-value="<%= Spree.t(:read_more) %>" data-read-more-less-text-value="Read less">
5
5
  <div class="prose product-description text-sm <%= 'product-description-truncated' if description_text.size > 250 %>" data-read-more-target="content">
6
- <%= raw(product.storefront_description) %>
6
+ <%= sanitize(product.storefront_description) %>
7
7
  </div>
8
8
  <% if description_text.size > 250 %>
9
9
  <%= button_tag Spree.t(:read_more), type: 'button', data: { action: "read-more#toggle" }, class: "font-bold underline text-sm" %>
@@ -14,7 +14,7 @@
14
14
  "w-full group-hover:opacity-0 transition-opacity duration-500 z-10 relative h-full bg-background product-card !max-h-full #{object_class}" :
15
15
  "w-full h-full !max-h-full #{object_class}" %>
16
16
  <%= spree_image_tag(object.primary_media, width: width, height: height, variant: :medium, loading: :lazy, alt: "#{object.name} #{Spree.t('storefront.products.primary_image', default: 'primary image')}", class: image_hover_class) %>
17
- <% if object.has_images? && object.image_count > 1 && object.secondary_image.present? && object.secondary_image.attached? %>
17
+ <% if object.has_images? && object.secondary_image.present? && object.secondary_image.attached? %>
18
18
  <% secondary_image_class = "w-full absolute top-0 left-0 opacity-100 h-full !max-h-full #{object_class}" %>
19
19
  <%= spree_image_tag(object.secondary_image, width: width, height: height, variant: :medium, loading: :lazy, alt: "#{object.name} #{Spree.t('storefront.products.secondary_image', default: 'secondary image')}", class: secondary_image_class) %>
20
20
  <% end %>
@@ -1,40 +1,2 @@
1
- <script type="application/ld+json" data-test-id="product-json-ld">
2
- <% first_or_default_variant = product.first_or_default_variant(current_currency) %>
3
- {
4
- "@context": "https://schema.org/",
5
- "@type": "Product",
6
- "name": <%= product.name.to_json.html_safe %>,
7
- "url": <%= spree.product_url(product, host: current_store.url_or_custom_domain).to_json.html_safe %>,
8
- <% if product.has_images? %>
9
- "image": [
10
- <%= spree_image_url(product.primary_media, variant: :large).to_json.html_safe %>
11
- ],
12
- <% end %>
13
- <% if product.description.present? %>
14
- "description": <%= strip_tags(product.description).to_json.html_safe %>,
15
- <% end %>
16
- <% if !product.has_variants? %>
17
- <% if first_or_default_variant.sku.present? %>"sku": <%= first_or_default_variant.sku.to_json.html_safe %>,<% end %>
18
- <% elsif selected_variant %>
19
- <% if selected_variant.sku.present? %>"sku": <%= selected_variant.sku.to_json.html_safe %>,<% end %>
20
- <% end %>
21
- <% if product.brand_taxon %>
22
- "brand": {
23
- "@type": "Brand",
24
- "name": <%= product.brand_taxon.name.to_json.html_safe %>
25
- },
26
- <% end %>
27
- "offers": [
28
- <% if product.has_variants? %>
29
- <%= raw(product.variants.map do |variant|
30
- render partial: "spree/products/json_ld_variant", locals: { product: product, variant: variant }
31
- end.join(",\n")) %>
32
- <% else %>
33
- <%= render "spree/products/json_ld_variant", product: product, variant: first_or_default_variant %>
34
- <% end %>
35
- ]
36
- }
37
- </script>
38
- <script type="application/ld+json">
39
- <%= product_json_ld_breadcrumbs(product).to_json.html_safe %>
40
- </script>
1
+ <%= json_ld_script(product_json_ld_data(product, selected_variant: selected_variant), data: { test_id: 'product-json-ld' }) %>
2
+ <%= json_ld_script(product_json_ld_breadcrumbs(product)) %>
@@ -1,9 +1,7 @@
1
1
  <% cache [spree_base_cache_key, products,'json-ld-list'], expires_in: 1.day do %>
2
- <script type="application/ld+json" data-test-id="product-list-json-ld">
3
- {
4
- "@context": "https://schema.org",
5
- "@type": "ItemList",
6
- "itemListElement": <%= product_list_json_ld_elements(products.to_a.pluck(:slug)).to_json.html_safe %>
7
- }
8
- </script>
9
- <% end %>
2
+ <%= json_ld_script({
3
+ '@context' => 'https://schema.org',
4
+ '@type' => 'ItemList',
5
+ 'itemListElement' => product_list_json_ld_elements(products.to_a.pluck(:slug))
6
+ }, data: { test_id: 'product-list-json-ld' }) %>
7
+ <% end %>
@@ -1,32 +1,29 @@
1
1
  <% cache spree_storefront_base_cache_scope.call(current_store) do %>
2
- <script type="application/ld+json">
3
- {
4
- "@context": "https://schema.org",
5
- "@type": "Organization",
6
- "url": <%= current_store.formatted_url_or_custom_domain.to_json.html_safe %>,
7
- "sameAs": <%= current_store.social_links.to_json.html_safe %>,
8
- <% if current_store.logo && current_store.logo&.attached? && current_store.logo&.variable? %>
9
- "logo": <%= spree_image_url(current_store.logo, width: 250, height: 250).to_json.html_safe %>,
10
- <% end %>
11
- "name": <%= current_store.name.to_json.html_safe %>,
12
- "email": <%= current_store.customer_support_email.to_json.html_safe %>
13
- }
14
- </script>
2
+ <% organization = {
3
+ '@context' => 'https://schema.org',
4
+ '@type' => 'Organization',
5
+ 'url' => current_store.formatted_url_or_custom_domain,
6
+ 'sameAs' => current_store.social_links,
7
+ 'name' => current_store.name,
8
+ 'email' => current_store.customer_support_email
9
+ } %>
10
+ <% if current_store.logo && current_store.logo&.attached? && current_store.logo&.variable? %>
11
+ <% organization['logo'] = spree_image_url(current_store.logo, width: 250, height: 250) %>
12
+ <% end %>
13
+ <%= json_ld_script(organization) %>
15
14
  <% end %>
16
15
 
17
16
  <% if canonical_path.include?('search') %>
18
17
  <% potential_action_target = "#{canonical_href(current_store.url_or_custom_domain)}?q={search_term_string}" %>
19
- <script type="application/ld+json">
20
- {
21
- "@context": "https://schema.org",
22
- "@type": "WebSite",
23
- "name": <%= current_store.name.to_json.html_safe %>,
24
- "potentialAction": {
25
- "@type": "SearchAction",
26
- "target": <%= potential_action_target.to_json.html_safe %>,
27
- "query-input": "required name=search_term_string"
28
- },
29
- "url": <%= current_store.formatted_url_or_custom_domain.to_json.html_safe %>
30
- }
31
- </script>
18
+ <%= json_ld_script({
19
+ '@context' => 'https://schema.org',
20
+ '@type' => 'WebSite',
21
+ 'name' => current_store.name,
22
+ 'potentialAction' => {
23
+ '@type' => 'SearchAction',
24
+ 'target' => potential_action_target,
25
+ 'query-input' => 'required name=search_term_string'
26
+ },
27
+ 'url' => current_store.formatted_url_or_custom_domain
28
+ }) %>
32
29
  <% end %>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_storefront
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.1
4
+ version: 5.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 5.4.1
18
+ version: 5.4.3
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 5.4.1
25
+ version: 5.4.3
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: active_link_to
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -247,6 +247,7 @@ files:
247
247
  - app/helpers/spree/checkout_helper.rb
248
248
  - app/helpers/spree/filters_helper.rb
249
249
  - app/helpers/spree/fonts_helper.rb
250
+ - app/helpers/spree/json_ld_helper.rb
250
251
  - app/helpers/spree/orders_helper.rb
251
252
  - app/helpers/spree/page_helper.rb
252
253
  - app/helpers/spree/payment_methods_helper.rb
@@ -471,7 +472,6 @@ files:
471
472
  - app/views/themes/default/spree/products/_filters.html.erb
472
473
  - app/views/themes/default/spree/products/_json_ld.html.erb
473
474
  - app/views/themes/default/spree/products/_json_ld_list.html.erb
474
- - app/views/themes/default/spree/products/_json_ld_variant.html.erb
475
475
  - app/views/themes/default/spree/products/_label.html.erb
476
476
  - app/views/themes/default/spree/products/_media_gallery.html.erb
477
477
  - app/views/themes/default/spree/products/_metafields.html.erb
@@ -592,9 +592,9 @@ licenses:
592
592
  - MIT
593
593
  metadata:
594
594
  bug_tracker_uri: https://github.com/spree/spree-rails-storefront/issues
595
- changelog_uri: https://github.com/spree/spree-rails-storefront/releases/tag/v5.4.1
595
+ changelog_uri: https://github.com/spree/spree-rails-storefront/releases/tag/v5.4.3
596
596
  documentation_uri: https://docs.spreecommerce.org/
597
- source_code_uri: https://github.com/spree/spree-rails-storefront/tree/v5.4.1
597
+ source_code_uri: https://github.com/spree/spree-rails-storefront/tree/v5.4.3
598
598
  rdoc_options: []
599
599
  require_paths:
600
600
  - lib
@@ -1,12 +0,0 @@
1
- <% cache ["json-ld-variant", spree_base_cache_scope, variant.cache_key_with_version] do %>
2
- {
3
- "@type": "Offer",
4
- <% if variant.sku.present? %>
5
- "sku": <%= variant.sku.to_json.html_safe %>,
6
- <% end %>
7
- "availability": "http://schema.org/<%= variant.available? ? 'InStock' : 'OutOfStock' %>",
8
- "price": <%= variant.amount_in(current_currency).to_json.html_safe %>,
9
- "priceCurrency": <%= current_currency.to_json.html_safe %>,
10
- "url": <%= spree.product_url(product, variant_id: variant.id, host: current_store.url_or_custom_domain).to_json.html_safe %>
11
- }
12
- <% end %>