Class: Oembed::ProductProvider

Inherits:
Object
  • Object
show all
Defined in:
app/services/oembed/product_provider.rb

Overview

Custom oEmbed-like provider for WarmlyYours Products
Returns rendered product HTML for embedding in Redactor editor

This allows products to be visually previewed in the editor while maintaining
the ability to edit which product is displayed via context menu.

The output includes:

  • Magazine-style product card with image, title, SKU, price
  • Locale-aware pricing (US/Canada)
  • Add to cart button
  • Product JSON-LD structured data for SEO
  • Data attributes for editing in Redactor

Products are automatically refreshed by the overnight job to:

  • Update prices when they change
  • Hide products that become discontinued or unavailable

Usage:
provider = Oembed::ProductProvider.new
result = provider.get(sku: 'TRT512-KIT', locale: 'en-US')
result[:html] # => Rendered product HTML with embedded JSON-LD schema

Defined Under Namespace

Classes: ProductNotFoundError, ProductUnavailableError

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.curl_typographic_quotes(text) ⇒ String

Convert straight ASCII quotes to curly typographic quotes per the
project's curly-quote convention
(https://typographyforlawyers.com/straight-and-curly-quotes.html).

Kept as a thin shim around Heatwave::TypographicQuotes.curl_plain
so existing call sites continue to work; new callers should use
the module method directly.

Parameters:

  • text (String)

    Plain-text input with straight " / ' quotes.

Returns:

  • (String)

    Same text with opening quotes (start of string or
    after whitespace/open-bracket) converted to / and remaining
    quotes to /.



319
320
321
# File 'app/services/oembed/product_provider.rb', line 319

def self.curl_typographic_quotes(text)
  Heatwave::TypographicQuotes.curl_plain(text)
end

.sanitize_schema_text(text) ⇒ String?

Prepare description text for embedding in a JSON-LD <script> tag.

Two concerns:

  1. Word boundaries — replace block-level tags with spaces before
    stripping so <p>foo</p><p>bar</p> becomes foo bar, not
    foobar.
  2. Quote safety — convert straight ASCII quotes to curly typographic
    quotes via Heatwave::TypographicQuotes.curl_plain. Even with
    to_json escaping straight " as \", any tool that round-trips
    the JSON-LD as HTML text (Redactor, Nokogiri-based refreshers, etc.)
    can unescape \" back to ", leaving invalid JSON inside the
    script tag. Curly quotes are different code points entirely and
    survive every round-trip intact.

Idempotent: re-running on already-sanitized text yields the same
output (curly quotes pass through, block tags are already gone).

Parameters:

  • text (String, nil)

    Raw description text — may include HTML
    tags, HTML entities, and straight quotes.

Returns:

  • (String, nil)

    Plain-text description with words spaced,
    entities decoded, whitespace squished, and quotes curled. Returns
    the input unchanged when blank.



298
299
300
301
302
303
304
305
# File 'app/services/oembed/product_provider.rb', line 298

def self.sanitize_schema_text(text)
  return text if text.blank?

  spaced = text.to_s.gsub(BLOCK_TAG_REGEX, ' ')
  sanitized = ActionView::Base.full_sanitizer.sanitize(spaced)
  decoded = CGI.unescapeHTML(sanitized).squish
  Heatwave::TypographicQuotes.curl_plain(decoded)
end

Instance Method Details

#get(options = {}) ⇒ Hash

Get rendered product HTML

Parameters:

  • options (Hash) (defaults to: {})

    Options hash

Options Hash (options):

  • :sku (String)

    Product SKU (required)

  • :locale (String)

    Locale for pricing ('en-US' or 'en-CA')

  • :embedded_asset_uuid (String)

    EmbeddedAsset UUID for tracking (optional)

Returns:

  • (Hash)

    oEmbed-like response with :html, :type, :provider_name, etc.

Raises:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'app/services/oembed/product_provider.rb', line 39

def get(options = {})
  sku = options[:sku].to_s.strip
  locale = options[:locale].to_s.presence || 'en-US'
  embedded_asset_uuid = options[:embedded_asset_uuid].presence

  raise ProductNotFoundError, 'No SKU provided' if sku.blank?

  # Try exact match first, then case-insensitive
  item = Item.find_by(sku: sku) || Item.where('UPPER(sku) = ?', sku.upcase).first
  raise ProductNotFoundError, "Product not found: #{sku}" if item.nil?

  # For CRM editor preview, show the product with locale-aware pricing
  # The frontend rendering will handle final availability checks
  build_response(item, sku, locale, embedded_asset_uuid: embedded_asset_uuid)
end

#render_for_locale(sku, locale = 'en-US', include_schema: true) ⇒ String?

Render product embed HTML for the frontend (blog display)
This is called during content refresh and normal rendering
Handles locale-aware pricing and availability checks

Parameters:

  • sku (String)

    Product SKU

  • locale (String, Symbol) (defaults to: 'en-US')

    Locale ('en-US' or 'en-CA')

  • include_schema (Boolean) (defaults to: true)

    Whether to include JSON-LD schema (default: true)

Returns:

  • (String, nil)

    HTML string or nil if product unavailable



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'app/services/oembed/product_provider.rb', line 63

def render_for_locale(sku, locale = 'en-US', include_schema: true)
  sku_stripped = sku.to_s.strip
  item = Item.find_by(sku: sku_stripped) || Item.where('UPPER(sku) = ?', sku_stripped.upcase).first
  return nil if item.nil?

  catalog_id = catalog_id_for_locale(locale)
  catalog_item = item.catalog_items.find_by(catalog_id: catalog_id)

  # Check availability - return nil if product shouldn't be displayed
  return nil unless product_available?(item, catalog_item)

  # Render the product card
  card_html = render_product_card(item, catalog_item, locale)
  return nil if card_html.nil?

  # Optionally include JSON-LD schema for SEO
  if include_schema
    schema_html = render_product_schema(item, catalog_item, locale)
    "#{card_html}#{schema_html}"
  else
    card_html
  end
end