Class: ShowcasePresenter

Inherits:
BasePresenter show all
Includes:
Memery
Defined in:
app/presenters/showcase_presenter.rb

Overview

Presenter: showcase presenter.

Instance Attribute Summary

Attributes inherited from BasePresenter

#current_account, #options, #url_helper

Instance Method Summary collapse

Methods inherited from BasePresenter

#can?, #capture, #concat, #content_tag, #fa_icon, #h, #initialize, #link_to, #number_to_currency, #present, presents, #r, #safe_present, #simple_format, #u

Constructor Details

This class inherits a constructor from BasePresenter

Instance Method Details

Quick links for the showcase sidebar, derived from breadcrumbs,
product-line slugs, flooring surfaces, and enabled CTA buttons.

Returns:

  • (Array<Hash>)

    link entries (:name, :url, :icon), sorted by name



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'app/presenters/showcase_presenter.rb', line 12

def quick_links_hash
  ql_array = (showcase.breadcrumbs_hash || []).dup
  urls = ql_array.map { |h| h[:url].to_s }

  if urls.empty?
    product_line_ids = Array(showcase.product_line_ids).compact
    item_ids = Array(showcase.item_ids).compact

    product_line_ids |= Item.joins(:product_lines).where(items: { id: item_ids }).pluck('product_lines.id') if item_ids.any?

    slugs = ProductLine.where(id: product_line_ids).pluck(:slug_ltree).filter_map(&:to_s)

    urls << h.cms_link('/floor-heating') if slugs.any? { |s| s.include?('floor_heating') }
    urls << h.cms_link('/snow-melting') if slugs.any? { |s| s.include?('snow_melting') }
    urls << h.cms_link('/roof-and-gutter-deicing') if slugs.any? { |s| s.include?('roof_and_gutter_deicing') }
    urls << h.cms_link('/pipe-freeze-protection') if slugs.any? { |s| s.include?('pipe_freeze') }
    urls << h.cms_link('/towel-warmer') if slugs.any? { |s| s.include?('towel_warmer') }
    urls << h.cms_link('/infrared-heating-panels') if slugs.any? { |s| s.include?('infrared_heating_panels') }
    urls << h.cms_link('/led-mirror') if slugs.any? { |s| s.include?('led_mirror') }
    urls << h.cms_link('/mirror-defogger') if slugs.any? { |s| s.include?('mirror_defogger') }
  end

  if urls.any? { |u| u.match?(%r{/(floor-heating|indoor-heating)}) }
    ql_array << { name: 'Floor Heating', url: h.cms_link('/floor-heating'), icon: 'flame' }
    ql_array << { name: 'Instant Quote', url: h.cms_link('/floor-heating/quote-builder'), icon: 'bolt' }
    ql_array << { name: 'Running Costs', url: h.cms_link('/floor-heating/cost-calculator'), icon: 'calculator' }
  end

  if urls.any? { |u| u.match?(%r{/(snow-melting|outdoor-heating)}) }
    ql_array << { name: 'Snow Melting', url: h.cms_link('/snow-melting'), icon: 'snowflake' }
    ql_array << { name: 'Instant Quote', url: h.cms_link('/snow-melting/quote-builder'), icon: 'bolt' }
    ql_array << { name: 'Running Costs', url: h.cms_link('/snow-melting/cost-calculator'), icon: 'calculator' }
  end

  # Optional: add base category links for other detected contexts
  ql_array << { name: 'Towel Warmers', url: h.cms_link('/towel-warmer'), icon: 'bath' } if urls.any? { |u| u.include?('/towel-warmer') }
  ql_array << { name: 'Infrared Heating Panels', url: h.cms_link('/infrared-heating-panels'), icon: 'sun' } if urls.any? { |u| u.include?('/infrared-heating-panels') }
  ql_array << { name: 'Roof & Gutter Deicing', url: h.cms_link('/roof-and-gutter-deicing'), icon: 'icicles' } if urls.any? { |u| u.include?('/roof-and-gutter-deicing') }
  ql_array << { name: 'Pipe Freeze Protection', url: h.cms_link('/pipe-freeze-protection'), icon: 'icicles' } if urls.any? { |u| u.include?('/pipe-freeze-protection') }

  # Floor Plan Gallery link - only for floor heating and snow melting showcases
  ql_array << { name: 'Floor Plan Gallery', url: h.cms_link('/floor-plans'), icon: 'th' } if urls.any? { |u| u.match?(%r{/(floor-heating|indoor-heating|snow-melting|outdoor-heating)}) }

  # Flooring/Surface driven links
  if showcase.respond_to?(:flooring_surface_types) && showcase.flooring_surface_types.present?
    showcase.flooring_surface_types.each do |name|
      next if name.blank?

      if (path = ShowcaseTaxonomies.flooring_surface_link_for(name)).present?
        ql_array << { name: name, url: h.cms_link(path), icon: 'chevron-right' }
      end
    end
  end

  # Merge enabled CTA buttons into quick links
  button_links = {
    'button_design_room'          => { name: 'Design Your Own Heated Space', url: h.cms_link('room_plans/new'), icon: 'paint-brush' },
    'button_floor_heating'        => { name: 'Floor Heating',               url: h.cms_link('/floor-heating'), icon: 'flame' },
    'button_floor_heating_quote'  => { name: 'Floor Heating Quote',         url: h.cms_link('/floor-heating/quote-builder'), icon: 'bolt' },
    'button_snow_melting'         => { name: 'Snow Melting',                url: h.cms_link('/snow-melting'), icon: 'snowflake' },
    'button_snow_melting_quote'   => { name: 'Snow Melting Quote',          url: h.cms_link('/snow-melting/quote-builder'), icon: 'bolt' },
    'button_roof_gutter_deicing'  => { name: 'Roof & Gutter Deicing',      url: h.cms_link('/roof-and-gutter-deicing'), icon: 'icicles' },
    'button_pipe_freeze_protection' => { name: 'Pipe Freeze Protection',   url: h.cms_link('/pipe-freeze-protection'), icon: 'icicles' },
    'button_customize_floor_plan' => { name: 'Customize Floor Plan', url: h.cms_link('/floor-plans'), icon: 'th' }
  }
  existing_urls = ql_array.map { |link| link[:url].to_s }
  Array(showcase.enabled_buttons).each do |btn|
    link = button_links[btn]
    next unless link
    next if existing_urls.include?(link[:url].to_s)

    ql_array << link
    existing_urls << link[:url].to_s
  end

  ql_array.uniq { |h| h[:url] }.sort_by { |h| h[:name].to_s }
end

#schema_dot_org_structureSchemaDotOrg::Article

JSON-LD for the project itself. The page already emits FAQPage and
BreadcrumbList, so the questions were machine-readable while the specs —
the part the AEO prompt calls the "agentic data table" — were only prose in
an HTML table. An AI agent could quote our FAQ answer but not the wattage.

Modeled as an Article about a SchemaDotOrg::Place: the specs live in the
Place's additionalProperty, which is where schema.org puts a spec table.

The pairs deliberately mirror the visible chip bar in
shared/_showcase.html.erb one for one — structured data that claims more
than the page shows is exactly what Google penalizes.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'app/presenters/showcase_presenter.rb', line 103

def schema_dot_org_structure
  article = SchemaDotOrg::Article.new(
    headline: showcase.name,
    name: showcase.name,
    description: showcase.seo_description.presence || showcase.short_description,
    url: canonical_url,
    mainEntityOfPage: canonical_url,
    inLanguage: I18n.locale.to_s.tr('_', '-'),
    datePublished: showcase.created_at,
    dateModified: showcase.updated_at,
    image: image_schemas,
    publisher: publisher_schema,
    about: project_place
  )
  article.id_iri = canonical_url
  article
end