Module: Presenters::ImageTag

Extended by:
ActiveSupport::Concern
Included in:
Crm::ImagePresenter, Www::ImagePresenter
Defined in:
app/concerns/presenters/image_tag.rb

Constant Summary collapse

BLANK_PNG_PIXEL_IMG_SRC =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
VIEWPORT_WIDTHS =
[3840, 2048, 1920, 1600, 1200, 1080, 915, 750, 500, 320, 160].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#hrefObject (readonly)

Returns the value of attribute href.



6
7
8
# File 'app/concerns/presenters/image_tag.rb', line 6

def href
  @href
end

#imagesizesObject (readonly)

Returns the value of attribute imagesizes.



6
7
8
# File 'app/concerns/presenters/image_tag.rb', line 6

def imagesizes
  @imagesizes
end

#imagesrcsetObject (readonly)

Returns the value of attribute imagesrcset.



6
7
8
# File 'app/concerns/presenters/image_tag.rb', line 6

def imagesrcset
  @imagesrcset
end

Instance Method Details

#image_tag(options = {}) ⇒ Object



33
34
35
36
37
# File 'app/concerns/presenters/image_tag.rb', line 33

def image_tag(options = {})
  options.delete(:request_format)
  options.delete(:amp)
  image_tag_html(options)
end

#image_tag_html(options) ⇒ Object



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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'app/concerns/presenters/image_tag.rb', line 39

def image_tag_html(options)
  img_options = options.dup
  # Extract tag specific param here
  # img_options[:relative] = false if img_options[:relative].nil?
  css_class = img_options.delete(:class)
  style = img_options.delete(:style)
  itemprop = img_options.delete(:itemprop)
  fetchpriority = img_options.delete(:fetchpriority)
  custom_data = img_options.delete(:data)
  lazyload = img_options.delete(:lazyload)
  lazyload = true if lazyload.nil?
  alt = img_options.delete(:alt) || image.try(:seo_title) || image.title
  title = img_options.delete(:title) || image.title.presence || alt
  include_srcset = img_options.delete(:include_srcset)
  # crop_aspect_ratio: height/width float (e.g. 600.0/1920 for a hero banner).
  # Applies a proportional height to every srcset entry without specifying a fixed
  # width, so the srcset covers all viewport sizes including Retina (3840w etc.).
  crop_aspect_ratio = img_options.delete(:crop_aspect_ratio)
  @imagesizes = sizes = img_options.delete(:sizes)
  ratio = nil
  include_srcset = if sizes.present?
                     true
                   elsif include_srcset.nil?
                     true
                   else
                     include_srcset.to_b
                   end
  viewport_widths = img_options.delete(:viewport_widths) || VIEWPORT_WIDTHS.dup
  img_options[:encode_format] ||= @options&.dig(:img_options, :encode_format)
  img_url_options = img_options.slice(*Image::VALID_IMAGE_URL_OPTIONS)
  # extracted requested width if any to reduce our srcset
  requested_width = img_options[:width]
  requested_width ||= img_options[:size]&.scan(/\A(\d+)x?/)&.flatten&.first
  requested_width = requested_width.to_i if requested_width

  requested_height = img_options[:height]
  requested_height ||= img_options[:size]&.scan(/\A(\d+)x(\d+)?/)&.flatten&.last
  requested_height = requested_height.to_i if requested_height

  ratio = requested_height.to_f / requested_width.to_f if requested_width && requested_height && requested_width > 0

  if requested_width # Include the requested width into the srcset
    if requested_width < 150 # don't bother with a srcset on an image this small
      include_srcset = false
    else
      viewport_widths << requested_width
      viewport_widths = viewport_widths.map(&:presence).compact.map(&:to_i).uniq.sort.reverse
    end
  end
  img_url_options = img_url_options.compact
  url = image.image_url(**img_url_options)
  srcsets = []
  if include_srcset
    # Based on https://medium.com/hceverything/applying-srcset-choosing-the-right-sizes-for-responsive-images-at-different-breakpoints-a0433450a4a3
    target_viewport_widths = viewport_widths.reject { |w| requested_width && w > requested_width }
    # The width returned are for a full width viewport.  Remember to pair with a sizes attribute to give the
    # browser some guidance on the final rendering of the image or use the lazy option.
    srcsets = target_viewport_widths.map do |width|
      # Create a new transform set with the viewport width and passing key transformations to preserve
      preserved_options = img_options.slice(:relative, :encode_format, :crop_x, :crop_y, :crop_w, :crop_last, :crop_mode,
                                            :crop_h, :borderx, :bordery, :rotate, :relative, :hostname, :cache, :webp, :optimize, :thumbnail, :background,
                                            :focus)
      img_srcset_options = { width: width, **preserved_options }
      # Apply explicit ratio (from width+height args) or crop_aspect_ratio for a
      # proportional height crop at every srcset breakpoint (e.g. hero banners).
      effective_ratio = ratio || crop_aspect_ratio
      img_srcset_options[:height] = (width * effective_ratio).round if effective_ratio

      img_srcset_options = img_srcset_options.compact
      "#{image.image_url(**img_srcset_options)} #{width}w"
    end
  end
  @imagesrcset = strset_str = srcsets.join(',').presence
  params = { alt: alt, title: title, class: css_class, itemprop: itemprop, style: style }
  css_class_ary = css_class&.split(' ') || []

  # Set image source and srcset
  params[:src] = url
  if strset_str.present?
    params[:srcset] = strset_str
    params[:sizes] = sizes if sizes.present?
  end

  # Native browser lazy loading (default: true)
  # Use lazyload: false for above-fold/LCP images
  params[:loading] = 'lazy' if lazyload

  css_class_ary = css_class_ary.uniq.compact
  if css_class_ary.present?
    params[:class] = css_class_ary.join(' ')
  else
    params.delete(:class)
  end
  params[:width] ||= img_options[:tag_width] if img_options[:tag_width]
  params[:height] ||= img_options[:tag_height] if img_options[:tag_height]
  if params[:width].nil? && params[:height].nil?
    if requested_width && requested_height
      # Both dimensions explicitly requested (e.g., size: '150x150')
      params[:width] = requested_width
      params[:height] = requested_height
    elsif requested_width && image.aspect_ratio
      # Only width requested - calculate height from aspect ratio
      params[:width] = requested_width
      params[:height] = (requested_width.to_f / image.aspect_ratio).round
    elsif !img_options[:no_native_size_in_tag].to_b && attachment_width && image.aspect_ratio
      params[:width] = [attachment_width, 1920].min
      params[:height] = (params[:width].to_f / image.aspect_ratio).round
    end
  end
  params[:data] = (custom_data || {}).merge(iid: image.id)
  @href = params[:src]

  # Add fetchpriority for LCP images (only makes sense without lazy loading)
  params[:fetchpriority] = fetchpriority if fetchpriority.present? && !lazyload

  h.tag(:img, **params)
end

#to_schema_dot_orgObject

Lazy loading: Uses browser's native loading="lazy" by default

  • true (default) - Native browser lazy loading (loads when near viewport)
  • false - No lazy loading, loads immediately (use for LCP/above-fold images)

Native lazy loading has 95%+ browser support and requires no JavaScript.



18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'app/concerns/presenters/image_tag.rb', line 18

def to_schema_dot_org
  url = image.image_url

  width = image.attachment_width
  height = image.attachment_height
  content_type = image.determine_mime_type

  SchemaDotOrg::ImageObject.new(
    url: url,
    width: width,
    height: height,
    encodingFormat: content_type
  )
end