Module: Www::SeoHelper

Extended by:
SeoHelper
Includes:
ActionView::Helpers::TagHelper
Included in:
ApplicationHelper, SeoHelper, VideoCardComponent, VideoSectionComponent
Defined in:
app/helpers/www/seo_helper.rb

Constant Summary collapse

COMPANY_NAME =

Company constants

'WarmlyYours'
COMPANY_EMAIL =
'info@warmlyyours.com'
COMPANY_LOGO =
'https://ik.warmlyyours.com/img/logo-red-5087d4.png'
COMPANY_SLOGAN =
'Radiant Heating Experts since 1999'
US_FOUNDING_DATE =
'1999-04-29'
CA_FOUNDING_DATE =
'2004-11-18'
US_GLOBAL_LOCATION_NUMBER =

Business identifiers

'1200180629131'
CA_GLOBAL_LOCATION_NUMBER =
'1200180334295'
GS1_COMPANY_PREFIX =
'0881308'
ISO6523_CODE =
'GS1'
'WarmlyYours.com, Inc.'
'WARMLY YOURS INC.'
US_TAX_ID =

Tax information (US uses taxID, Canada uses vatID)

'36-429-3383'
CA_VAT_ID =

Example - replace with actual US tax ID

'GST/HST: 86138 7777 RT0001'
PRIMARY_NAICS =

NAICS codes (North American Industry Classification System)

'444190'
SECONDARY_NAICS =

Other Building Material Dealers

'423610'
RETURN_POLICY_CATEGORY =

Return policy information

'https://schema.org/MerchantReturnUnlimitedWindow'
RETURN_METHOD =
['https://schema.org/ReturnByMail']
RETURN_FEES =
'https://schema.org/FreeReturn'
REFUND_TYPE =
['https://schema.org/FullRefund']
PHONE_NUMBER =

Phone numbers (E.164 format for consistency)

'+1-800-875-5285'
FAX_NUMBER =
'+1-800-408-1100'
US_ADDRESS =

Addresses

{
  streetAddress: '590 Telser Rd Ste B',
  addressLocality: 'Lake Zurich',
  addressRegion: 'IL',
  postalCode: '60047',
  addressCountry: 'US'
}
CA_ADDRESS =
{
  streetAddress: '300 Granton Drive 4A',
  addressLocality: 'Richmond Hill',
  addressRegion: 'ON',
  postalCode: 'L4B1H7',
  addressCountry: 'CA'
}
US_URL =

URLs

'https://www.warmlyyours.com/en-US'
CA_URL =
'https://www.warmlyyours.com/en-CA'
US_IMAGE =

Images (location-specific)

'https://ik.warmlyyours.com/img/roof-and-gutter-installation-with-constant-wattage-during-winter-789284.jpeg?tr=w-1280'
US_DESCRIPTION =

Descriptions

'Certified electric radiant heating systems (UL, CSA, TUV) including floor heating, snow melting, towel warmers, and more. Serving customers across the United States since 1999 with 25-year warranties and 24/7 support.'
CA_DESCRIPTION =
'Certified electric radiant heating systems (UL, CSA, TUV)including floor heating, snow melting, towel warmers, and more. Serving customers across Canada since 1999 with 25-year warranties and 24/7 support.'
US_BUSINESS_HOURS =

Business hours (country-specific)
Format: Mo-Fr HH:MM-HH:MM (ISO 8601 time format)

[
  'Mo-Fr 07:30-17:30'  # Mon-Fri, 7:30 a.m.-5:30 p.m. CST
]
CA_BUSINESS_HOURS =
[
  'Mo-Fr 08:30-17:30'  # Mon-Fri, 8:30 a.m.-5:30 p.m. EST
]
US_WAREHOUSE_HOURS =

Warehouse hours (country-specific)

[
  'Mo-Fr 09:00-16:00'  # Mon-Fri, 9:00 a.m.-4:00 p.m. CST
]
CA_WAREHOUSE_HOURS =
[
  'Mo-Fr 09:00-16:00'  # Mon-Fri, 9:00 a.m.-4:00 p.m. EST
]
PAYMENT_METHODS =

Payment methods

[
  'Cash',
  'Credit Card',
  'Debit Card',
  'Check',
  'Bank Transfer',
  'PayPal'
]
US_CURRENCIES =

Currencies accepted (country-specific)

['USD']
CA_CURRENCIES =
['CAD']
US_SERVICE_AREA =

Service areas

['US']
CA_SERVICE_AREA =
['CA']
SOCIAL_PROFILES =

Social media profiles

[
  'https://www.facebook.com/WarmlyYours',
  'https://www.instagram.com/warmlyyours',
  'https://twitter.com/warmlyyours',
  'https://www.youtube.com/user/WarmlyYoursRadiant',
  'https://www.linkedin.com/company/warmlyyours/',
  'https://www.houzz.com/professionals/appliances/warmlyyours-radiant-pfvwus-pf~663708328',
  'https://www.pinterest.com/warmlyyours/'
]
AWARDS =

Awards and certifications

[
  'UL Listed',
  'CSA Certified',
  'TUV Certified',
  'Intertek Certified',
  '25-Year Warranty',
  '24/7 Customer Support',
  'BBB A+ Rating'
]
EXPERTISE =

Areas of expertise

[
  'Electric Radiant Floor Heating',
  'Snow Melting Systems',
  'Roof and Gutter Deicing',
  'Pipe Freeze Protection',
  'Towel Warmers',
  'LED Mirrors',
  'Radiant Panel Heaters'
]
US_CONTACT_POINT =

Contact points

SchemaDotOrg::ContactPoint.new(
  telephone: PHONE_NUMBER,
  email: COMPANY_EMAIL,
  contactType: 'customer service',
  availableLanguage: ['English'],
  areaServed: US_SERVICE_AREA
)
CA_CONTACT_POINT =
SchemaDotOrg::ContactPoint.new(
  telephone: PHONE_NUMBER,
  email: COMPANY_EMAIL,
  contactType: 'customer service',
  availableLanguage: %w[English French],
  areaServed: CA_SERVICE_AREA
)
US_RETURN_POLICY =

Return policy schemas

SchemaDotOrg::MerchantReturnPolicy.new(
  applicableCountry: US_SERVICE_AREA,
  returnPolicyCategory: RETURN_POLICY_CATEGORY,
  merchantReturnDays: nil,  # Unlimited window
  returnMethod: RETURN_METHOD,
  returnFees: RETURN_FEES,
  refundType: REFUND_TYPE
)
CA_RETURN_POLICY =
SchemaDotOrg::MerchantReturnPolicy.new(
  applicableCountry: CA_SERVICE_AREA,
  returnPolicyCategory: RETURN_POLICY_CATEGORY,
  merchantReturnDays: nil,  # Unlimited window
  returnMethod: RETURN_METHOD,
  returnFees: RETURN_FEES,
  refundType: REFUND_TYPE
)
US_ONLINE_STORE =

OnlineStore schema for US (extends Organization)

SchemaDotOrg::OnlineStore.new(
  name: COMPANY_NAME,
  url: US_URL,
  logo: COMPANY_LOGO,
  image: [US_IMAGE],
  email: COMPANY_EMAIL,
  faxNumber: FAX_NUMBER,
  telephone: PHONE_NUMBER,
  address: SchemaDotOrg::PostalAddress.new(US_ADDRESS),
  description: US_DESCRIPTION,
  slogan: COMPANY_SLOGAN,
  foundingDate: US_FOUNDING_DATE,
  knowsAbout: EXPERTISE,
  award: AWARDS,
  contactPoint: US_CONTACT_POINT,
  sameAs: SOCIAL_PROFILES,
  hasMerchantReturnPolicy: US_RETURN_POLICY,
  legalName: US_LEGAL_NAME,
  taxID: US_TAX_ID
)
CA_ONLINE_STORE =

OnlineStore schema for Canada (extends Organization)

SchemaDotOrg::OnlineStore.new(
  name: COMPANY_NAME,
  url: CA_URL,
  logo: COMPANY_LOGO,
  email: COMPANY_EMAIL,
  faxNumber: FAX_NUMBER,
  telephone: PHONE_NUMBER,
  address: SchemaDotOrg::PostalAddress.new(CA_ADDRESS),
  description: CA_DESCRIPTION,
  slogan: COMPANY_SLOGAN,
  foundingDate: CA_FOUNDING_DATE,
  knowsAbout: EXPERTISE,
  award: AWARDS,
  contactPoint: CA_CONTACT_POINT,
  sameAs: SOCIAL_PROFILES,
  hasMerchantReturnPolicy: CA_RETURN_POLICY,
  legalName: CA_LEGAL_NAME,
  vatID: CA_VAT_ID
)
US_SALES_DEPARTMENT =

Department schemas for US

SchemaDotOrg::Department.new(
  name: "#{COMPANY_NAME} - Sales, Customer Service & Technical Support",
  address: SchemaDotOrg::PostalAddress.new(US_ADDRESS),
  telephone: PHONE_NUMBER,
  url: US_URL,
  description: 'Sales, customer service, and technical support department',
  openingHours: US_BUSINESS_HOURS,
  parentOrganization: US_ONLINE_STORE
)
US_WAREHOUSE_DEPARTMENT =
SchemaDotOrg::Department.new(
  name: "#{COMPANY_NAME} - Warehouse & Pick Up/Returns",
  address: SchemaDotOrg::PostalAddress.new(US_ADDRESS),
  telephone: PHONE_NUMBER,
  url: US_URL,
  description: 'Warehouse operations, pick up and returns department',
  openingHours: US_WAREHOUSE_HOURS,
  parentOrganization: US_ONLINE_STORE
)
CA_SALES_DEPARTMENT =

Department schemas for Canada

SchemaDotOrg::Department.new(
  name: "#{COMPANY_NAME} - Sales, Customer Service & Technical Support",
  address: SchemaDotOrg::PostalAddress.new(CA_ADDRESS),
  telephone: PHONE_NUMBER,
  url: CA_URL,
  description: 'Sales, customer service, and technical support department',
  openingHours: CA_BUSINESS_HOURS,
  parentOrganization: CA_ONLINE_STORE
)
CA_WAREHOUSE_DEPARTMENT =
SchemaDotOrg::Department.new(
  name: "#{COMPANY_NAME} - Warehouse & Pick Up/Returns",
  address: SchemaDotOrg::PostalAddress.new(CA_ADDRESS),
  telephone: PHONE_NUMBER,
  url: CA_URL,
  description: 'Warehouse operations, pick up and returns department',
  openingHours: CA_WAREHOUSE_HOURS,
  parentOrganization: CA_ONLINE_STORE
)
US_LOCAL_BUSINESS =
SchemaDotOrg::LocalBusiness.new(
  name: COMPANY_NAME,
  address: SchemaDotOrg::PostalAddress.new(US_ADDRESS),
  telephone: PHONE_NUMBER,
  url: US_URL,
  description: US_DESCRIPTION,
  email: COMPANY_EMAIL,
  faxNumber: FAX_NUMBER,
  logo: COMPANY_LOGO,
  image: [US_IMAGE],
  openingHours: US_BUSINESS_HOURS,
  areaServed: US_SERVICE_AREA,
  paymentAccepted: PAYMENT_METHODS,
  currenciesAccepted: US_CURRENCIES,
  priceRange: '$$',
  foundingDate: US_FOUNDING_DATE,
  slogan: COMPANY_SLOGAN,
  knowsAbout: EXPERTISE,
  award: AWARDS,
  contactPoint: US_CONTACT_POINT,
  sameAs: SOCIAL_PROFILES,
  department: [US_SALES_DEPARTMENT, US_WAREHOUSE_DEPARTMENT],

  # Enhanced fields for better merchant listings
  hasMap: "https://maps.google.com/?q=#{US_ADDRESS[:streetAddress]},#{US_ADDRESS[:addressLocality]},#{US_ADDRESS[:addressRegion]}",
  latitude: 42.1961, # Lake Zurich, IL coordinates
  longitude: -88.0934,
  geo: SchemaDotOrg::GeoCoordinates.new(
    latitude: 42.1961,
    longitude: -88.0934
  )
)
CA_LOCAL_BUSINESS =

LocalBusiness schema for Canada

SchemaDotOrg::LocalBusiness.new(
  name: COMPANY_NAME,
  address: SchemaDotOrg::PostalAddress.new(CA_ADDRESS),
  telephone: PHONE_NUMBER,
  url: CA_URL,
  description: CA_DESCRIPTION,
  email: COMPANY_EMAIL,
  faxNumber: FAX_NUMBER,
  logo: COMPANY_LOGO,
  openingHours: CA_BUSINESS_HOURS,
  areaServed: CA_SERVICE_AREA,
  paymentAccepted: PAYMENT_METHODS,
  currenciesAccepted: CA_CURRENCIES,
  priceRange: '$$',
  foundingDate: CA_FOUNDING_DATE,
  slogan: COMPANY_SLOGAN,
  knowsAbout: EXPERTISE,
  award: AWARDS,
  contactPoint: CA_CONTACT_POINT,
  sameAs: SOCIAL_PROFILES,
  department: [CA_SALES_DEPARTMENT, CA_WAREHOUSE_DEPARTMENT],

  # Enhanced fields for better merchant listings
  hasMap: "https://maps.google.com/?q=#{CA_ADDRESS[:streetAddress]},#{CA_ADDRESS[:addressLocality]},#{CA_ADDRESS[:addressRegion]}",
  latitude: 43.8828, # Richmond Hill, ON coordinates
  longitude: -79.4403,
  geo: SchemaDotOrg::GeoCoordinates.new(
    latitude: 43.8828,
    longitude: -79.4403
  )
)

Instance Method Summary collapse

Instance Method Details

#add_page_schema(schema_type, object, options = {}) ⇒ Object

Add schema to the page's schema collection



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'app/helpers/www/seo_helper.rb', line 456

def add_page_schema(schema_type, object, options = {})
  @schema_json_objects ||= []
  @schema_json_webpage_objects_collection ||= []

  case schema_type
  when :video
    begin
      presenter = Www::VideoPresenter.new(object)
      @schema_json_objects << presenter.schema_dot_org_structure
    rescue StandardError => e
      Rails.logger.error "Failed to add video schema: #{e.message}"
      Rails.logger.error "Object: #{object.inspect}"
      Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

      if defined?(ErrorReporting)
        ErrorReporting.error(e, 'Failed to add video schema',
          object: object.inspect)
      end
    end
  when :product
    begin
      presenter = Www::ProductCatalogPresenter.new(object)
      @schema_json_objects << presenter.schema_dot_org_structure
    rescue StandardError => e
      Rails.logger.error "Failed to add product schema: #{e.message}"
      Rails.logger.error "Object: #{object.inspect}"
      Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

      if defined?(ErrorReporting)
        ErrorReporting.error(e, 'Failed to add product schema',
          object: object.inspect)
      end
    end
  when :review
    # Reviews migrated to Reviews.io - schema handled by widget
    nil
  when :faq
    begin
      presenter = FaqPresenter.new(object)
      @schema_json_objects << presenter.schema_dot_org_structure
    rescue StandardError => e
      Rails.logger.error "Failed to add FAQ schema: #{e.message}"
      Rails.logger.error "Object: #{object.inspect}"
      Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

      if defined?(ErrorReporting)
        ErrorReporting.error(e, 'Failed to add FAQ schema',
          object: object.inspect)
      end
    end
  when :blog_posting
    begin
      presenter = PostPresenter.new(object)
      @schema_json_objects << presenter.schema_dot_org_structure
    rescue StandardError => e
      Rails.logger.error "Failed to add blog posting schema: #{e.message}"
      Rails.logger.error "Object: #{object.inspect}"
      Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

      if defined?(ErrorReporting)
        ErrorReporting.error(e, 'Failed to add blog posting schema',
          object: object.inspect)
      end
    end
  when :collection_page_item
    # Collect individual items for a CollectionPage schema
    @collection_page_items ||= []
    begin
      candidate = if object.respond_to?(:name) && object.respond_to?(:url)
                    object
                  else
                    OpenStruct.new({ name: options[:name], url: options[:url] })
                  end

      # Only append if URL present
      @collection_page_items << candidate if candidate&.url.present?
    rescue StandardError => e
      # Log the error but don't crash the page
      Rails.logger.error "Failed to add collection page item schema: #{e.message}"
      Rails.logger.error "Object: #{object.inspect}"
      Rails.logger.error "Options: #{options.inspect}"
      Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

      # Send to error reporter if available
      if defined?(ErrorReporting)
        ErrorReporting.error(e, 'Failed to add collection page item schema',
          object: object.inspect,
          options: options.inspect)
      end
    end
  when :profile_page
    begin
      # Adding a ProfilePage should replace default WebPage/CollectionPage rendering
      @suppress_default_page_schema = true
      @schema_json_objects << object
    rescue StandardError => e
      Rails.logger.error "Failed to add profile_page schema: #{e.message}"
      Rails.logger.error "Object: #{object.inspect}"
      Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

      if defined?(ErrorReporting)
        ErrorReporting.error(e, 'Failed to add profile_page schema',
          object: object.inspect)
      end
    end
  when :how_to
    begin
      @schema_json_objects << object
    rescue StandardError => e
      Rails.logger.error "Failed to add HowTo schema: #{e.message}"
      Rails.logger.error "Object: #{object.inspect}"
      Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

      if defined?(ErrorReporting)
        ErrorReporting.error(e, 'Failed to add HowTo schema',
          object: object.inspect)
      end
    end
  else
    # For custom schema objects, add them directly
    begin
      @schema_json_objects << object
    rescue StandardError => e
      Rails.logger.error "Failed to add custom schema object: #{e.message}"
      Rails.logger.error "Object: #{object.inspect}"
      Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

      if defined?(ErrorReporting)
        ErrorReporting.error(e, 'Failed to add custom schema object',
          object: object.inspect)
      end
    end
  end
  nil
end

#canada?Boolean

Returns:

  • (Boolean)


12
13
14
# File 'app/helpers/www/seo_helper.rb', line 12

def canada?
  %i[en-CA fr-CA].include?(I18n.locale)
end

Public company social links keyed by platform symbol
Platforms: :facebook, :instagram, :twitter, :youtube, :linkedin, :houzz, :pinterest



677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# File 'app/helpers/www/seo_helper.rb', line 677

def company_social_links
  links = {}
  Array(SOCIAL_PROFILES).each do |u|
    next if u.blank?

    case u
    when /facebook\.com/i then links[:facebook] = u
    when /instagram\.com/i then links[:instagram] = u
    when /twitter\.com|x\.com/i then links[:twitter] = u
    when /youtube\.com/i then links[:youtube] = u
    when /linkedin\.com/i then links[:linkedin] = u
    when /houzz\.com/i then links[:houzz] = u
    when /pinterest\.com/i then links[:pinterest] = u
    end
  end
  links
end

#ensure_context_json(schema_or_json) ⇒ Object

Ensure every JSON-LD block has a @context; accepts SchemaType, Hash, or JSON String



773
774
775
776
777
778
779
780
781
782
783
784
785
# File 'app/helpers/www/seo_helper.rb', line 773

def ensure_context_json(schema_or_json)
  h = case schema_or_json
      when String
        JSON.parse(schema_or_json)
      else
        JSON.parse(schema_or_json.to_json)
      end
  h['@context'] ||= 'https://schema.org'
  h.to_json
rescue StandardError
  # If parsing fails, fallback to original string
  schema_or_json.is_a?(String) ? schema_or_json : schema_or_json.to_json
end

#json_ld_script_tag(schema_or_json) ⇒ Object

Wrap a schema object, hash, or pre-serialized JSON string in a



768
769
770
# File 'app/helpers/www/seo_helper.rb', line 768

def json_ld_script_tag(schema_or_json)
  "<script type=\"application/ld+json\">#{ensure_context_json(schema_or_json)}</script>".html_safe
end

#local_business_schemaObject



350
351
352
353
354
355
356
# File 'app/helpers/www/seo_helper.rb', line 350

def local_business_schema
  if usa?
    US_LOCAL_BUSINESS
  elsif canada?
    CA_LOCAL_BUSINESS
  end
end

#online_store_idObject

Convenience: get the canonical id for the online store



414
415
416
417
418
419
420
# File 'app/helpers/www/seo_helper.rb', line 414

def online_store_id
  if usa?
    US_URL
  elsif canada?
    CA_URL
  end
end

#online_store_schemaObject



339
340
341
342
343
344
345
346
347
348
# File 'app/helpers/www/seo_helper.rb', line 339

def online_store_schema
  store = if usa?
            US_ONLINE_STORE
          elsif canada?
            CA_ONLINE_STORE
          end
  # Ensure a stable @id so other entities can reference it
  store.id_iri ||= online_store_id if store.respond_to?(:id_iri)
  store
end

#page_main_entity(entity) ⇒ Object

View helper to set the WebPage main entity for the current page
Accepts a String URL, a SchemaType with id_iri/url, or a presenter with url



424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'app/helpers/www/seo_helper.rb', line 424

def page_main_entity(entity)
  id = case entity
       when String
         entity
       else
         if entity.respond_to?(:id_iri) && entity.id_iri.present?
           entity.id_iri
         elsif entity.respond_to?(:url)
           entity.url
         end
       end
  content_for :webpage_main_entity_id, id if id.present?
  id
end

#page_main_entity_json(json) ⇒ Object

Allow setting a full mainEntity object (hash/string JSON) if needed



440
441
442
# File 'app/helpers/www/seo_helper.rb', line 440

def page_main_entity_json(json)
  content_for :webpage_main_entity_json, json
end

#render_auto_collection_page_schema(page_title, page_description, page_url) ⇒ Object

Auto-generate CollectionPage schema if products were auto-collected



731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
# File 'app/helpers/www/seo_helper.rb', line 731

def render_auto_collection_page_schema(page_title, page_description, page_url)
  return '' unless @collection_page_items&.any?

  begin
    presenter = Www::CollectionPagePresenter.new(
      page_title,
      page_description,
      page_url,
      @collection_page_items,
      nil
    )

    ensure_context_json(presenter.schema_dot_org_structure)
  rescue StandardError => e
    Rails.logger.error "Failed to render auto collection page schema: #{e.message}"
    Rails.logger.error "Page title: #{page_title}"
    Rails.logger.error "Page description: #{page_description}"
    Rails.logger.error "Page URL: #{page_url}"
    Rails.logger.error "Collection page items: #{@collection_page_items&.inspect}"
    Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

    if defined?(ErrorReporting)
      ErrorReporting.error(e, 'Failed to render auto collection page schema',
        page_title: page_title,
        page_description: page_description,
        page_url: page_url,
        collection_page_items: @collection_page_items&.inspect)
    end

    # Return empty string to prevent page crash
    ''
  end
end

#render_collection_page_schema(page_title, page_description, page_url, main_entity_json: nil) ⇒ Object

Render a CollectionPage schema for category/listing pages
Uses SchemaDotOrg::CollectionPage with ItemList as mainEntity



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'app/helpers/www/seo_helper.rb', line 393

def render_collection_page_schema(page_title, page_description, page_url, main_entity_json: nil)
  cp = SchemaDotOrg::CollectionPage.new(
    name: page_title,
    description: page_description,
    url: page_url
  )

  if main_entity_json.present?
    parsed = main_entity_json
    begin
      parsed = JSON.parse(main_entity_json) if main_entity_json.is_a?(String)
    rescue StandardError
      parsed = main_entity_json
    end
    cp.mainEntity = parsed
  end

  ensure_context_json(cp)
end

#render_local_business_schemaObject



362
363
364
# File 'app/helpers/www/seo_helper.rb', line 362

def render_local_business_schema
  ensure_context_json(local_business_schema) if local_business_schema
end

#render_online_store_schemaObject



358
359
360
# File 'app/helpers/www/seo_helper.rb', line 358

def render_online_store_schema
  ensure_context_json(online_store_schema) if online_store_schema
end

#render_page_schemas(title: nil, description: nil, url: nil) ⇒ Object

Render all collected schemas at once, including business schemas and conditional WebPage/CollectionPage schema



593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'app/helpers/www/seo_helper.rb', line 593

def render_page_schemas(title: nil, description: nil, url: nil)
  schemas = []

  # Add all collected page schemas
  if @schema_json_objects&.any?
    schemas.concat(@schema_json_objects.map { |schema| json_ld_script_tag(schema) })
  end

  # Add business schemas (always included unless suppressed)
  unless @suppress_default_page_schema
    schemas << json_ld_script_tag(render_local_business_schema)
    schemas << json_ld_script_tag(render_online_store_schema)
  end

  # If a ProfilePage was explicitly added, suppress default WebPage/CollectionPage fallback
  unless @suppress_default_page_schema
    # If we have collection page items, render a CollectionPage schema
    # Otherwise, render a WebPage schema
    if @collection_page_items&.any?
      collection_page_schema = render_auto_collection_page_schema(
        title || 'Radiant Heating Systems by WarmlyYours',
        description || 'Expert electric radiant heating systems including floor heating, snow melting, towel warmers, and more.',
        url || cms_link(request.fullpath, host: WEB_HOSTNAME_WITHOUT_PORT, port: APP_PORT, strip_params: true)
      )
      schemas << json_ld_script_tag(collection_page_schema)
    else
      # Prefer a page-specific main entity when provided via page_main_entity/page_main_entity_json
      main_entity_id = nil
      main_entity_json = nil
      begin
        main_entity_id = content_for(:webpage_main_entity_id) if content_for?(:webpage_main_entity_id)
      rescue StandardError
        main_entity_id = nil
      end
      begin
        main_entity_json = content_for(:webpage_main_entity_json) if content_for?(:webpage_main_entity_json)
      rescue StandardError
        main_entity_json = nil
      end

      webpage_schema = render_webpage_schema(
        title || 'Radiant Heating Systems by WarmlyYours',
        description || 'Expert electric radiant heating systems including floor heating, snow melting, towel warmers, and more.',
        url || cms_link(request.fullpath, host: WEB_HOSTNAME_WITHOUT_PORT, port: APP_PORT, strip_params: true),
        main_entity_id: main_entity_id.presence || online_store_id,
        main_entity_json: main_entity_json
      )
      schemas << json_ld_script_tag(webpage_schema)
    end
  end

  safe_join(schemas)
rescue StandardError => e
  Rails.logger.error "Failed to render page schemas: #{e.message}"
  Rails.logger.error "Title: #{title}"
  Rails.logger.error "Description: #{description}"
  Rails.logger.error "URL: #{url}"
  Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

  if defined?(ErrorReporting)
    ErrorReporting.error(e, 'Failed to render page schemas',
      title: title,
      description: description,
      url: url)
  end

  # Return basic business schemas to prevent complete failure (unless suppressed)
  if @suppress_default_page_schema
    # Return only collected schemas if suppression is enabled
    if @schema_json_objects&.any?
      safe_join(@schema_json_objects.map { |schema| json_ld_script_tag(schema) })
    else
      ''.html_safe
    end
  else
    safe_join([
                json_ld_script_tag(render_local_business_schema),
                json_ld_script_tag(render_online_store_schema)
              ])
  end
end

#render_page_video_schemasObject

Render Video schema for all videos collected on the page



445
446
447
448
449
450
451
452
453
# File 'app/helpers/www/seo_helper.rb', line 445

def render_page_video_schemas
  return '' unless @page_videos&.any?

  safe_join(@page_videos.map do |video|
    present video, Www::VideoPresenter do |vp|
      json_ld_script_tag(vp.schema_dot_org_structure)
    end
  end)
end

#render_webpage_schema(page_title, page_description, page_url, main_entity_id: nil, main_entity_json: nil) ⇒ Object

Render a WebPage schema; optionally include a mainEntity when provided.
Uses SchemaDotOrg::WebPage for a unified structure.
Pass either main_entity_id (preferred) or a full main_entity (SchemaType/hash).



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'app/helpers/www/seo_helper.rb', line 369

def render_webpage_schema(page_title, page_description, page_url, main_entity_id: nil, main_entity_json: nil)
  wp = SchemaDotOrg::WebPage.new(
    name: page_title,
    description: page_description,
    url: page_url
  )

  if main_entity_id.present?
    wp.mainEntity = { '@id' => main_entity_id }
  elsif main_entity_json.present?
    parsed = main_entity_json
    begin
      parsed = JSON.parse(main_entity_json) if main_entity_json.is_a?(String)
    rescue StandardError
      parsed = main_entity_json
    end
    wp.mainEntity = parsed
  end

  ensure_context_json(wp)
end

#render_webpage_schema_with_collections(title, description, url) ⇒ Object

Render WebPage schema with collected collection objects as mainEntity



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
# File 'app/helpers/www/seo_helper.rb', line 696

def render_webpage_schema_with_collections(title, description, url)
  return '' unless @schema_json_webpage_objects_collection&.any?

  begin
    presenter = Www::WebPagePresenter.new(
      title,
      description,
      url,
      @schema_json_webpage_objects_collection,
      nil
    )

    json_ld_script_tag(presenter.schema_dot_org_structure)
  rescue StandardError => e
    Rails.logger.error "Failed to render webpage schema with collections: #{e.message}"
    Rails.logger.error "Title: #{title}"
    Rails.logger.error "Description: #{description}"
    Rails.logger.error "URL: #{url}"
    Rails.logger.error "Collection objects: #{@schema_json_webpage_objects_collection&.inspect}"
    Rails.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"

    if defined?(ErrorReporting)
      ErrorReporting.error(e, 'Failed to render webpage schema with collections',
        title: title,
        description: description,
        url: url,
        collection_objects: @schema_json_webpage_objects_collection&.inspect)
    end

    # Return empty string to prevent page crash
    ''
  end
end

#usa?Boolean

Returns:

  • (Boolean)


8
9
10
# File 'app/helpers/www/seo_helper.rb', line 8

def usa?
  I18n.locale == :'en-US'
end