Module: Presenters::ImageTag

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

Overview

Renders responsive <img> tags for an Image presenter: native lazy
loading, srcset across viewport widths, schema.org output, and automatic
width/height attributes from the image's aspect ratio.

See Also:

Constant Summary collapse

BLANK_PNG_PIXEL_IMG_SRC =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
VIEWPORT_WIDTHS =

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.



13
14
15
# File 'app/concerns/presenters/image_tag.rb', line 13

def href
  @href
end

#imagesizesObject (readonly)

Returns the value of attribute imagesizes.



13
14
15
# File 'app/concerns/presenters/image_tag.rb', line 13

def imagesizes
  @imagesizes
end

#imagesrcsetObject (readonly)

Returns the value of attribute imagesrcset.



13
14
15
# File 'app/concerns/presenters/image_tag.rb', line 13

def imagesrcset
  @imagesrcset
end

Instance Method Details

#image_tag(options = {}) ⇒ String

Renders an <img> tag for the image, stripping request-level keys first.

Parameters:

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

    options forwarded to #image_tag_html

Options Hash (options):

  • request_format (Object)

    ignored; removed before rendering

  • amp (Object)

    ignored; removed before rendering

Returns:

  • (String)

    HTML img tag



45
46
47
48
49
# File 'app/concerns/presenters/image_tag.rb', line 45

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

#image_tag_html(options) ⇒ String

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.

Parameters:

  • options (Hash)

    tag attributes and image transformation options; keys
    in Image::VALID_IMAGE_URL_OPTIONS (plus :relative, :hostname, :cache,
    :webp, :optimize, :thumbnail, :background, :focus, crop and border
    options) are forwarded to Image#image_url

Options Hash (options):

  • class (String)

    CSS classes for the img tag

  • style (String)

    inline style attribute

  • itemprop (String)

    schema.org itemprop attribute

  • fetchpriority (String)

    fetchpriority attribute (only applied when not lazy loading)

  • data (Hash)

    data attributes (merged with iid of the image)

  • lazyload (Boolean)

    emit loading="lazy" (default: true)

  • alt (String)

    alt text (defaults to image seo title or title)

  • title (String)

    title attribute (defaults to image title or alt)

  • include_srcset (Boolean)

    build a srcset (default: true, forced on when :sizes given)

  • crop_aspect_ratio (Float)

    height/width ratio applied to every srcset entry

  • sizes (String)

    sizes attribute for the srcset

  • viewport_widths (Array<Integer>)

    widths to include in the srcset

  • width (Integer)

    requested image width

  • height (Integer)

    requested image height

  • size (String)

    requested dimensions, e.g. '150x150'

  • encode_format (String)

    output format for the image url

  • tag_width (Integer)

    explicit width attribute on the img tag

  • tag_height (Integer)

    explicit height attribute on the img tag

  • no_native_size_in_tag (Boolean)

    skip default width/height attributes

Returns:

  • (String)

    HTML img tag



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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'app/concerns/presenters/image_tag.rb', line 81

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 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.filter_map(&:presence).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_orgSchemaDotOrg::ImageObject

Builds a schema.org ImageObject for the underlying image (url, dimensions,
MIME type) for structured-data output.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'app/concerns/presenters/image_tag.rb', line 24

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