# frozen_string_literal: true
module MetaTags
# This class is used by MetaTags gems to render HTML meta tags into page.
class Renderer
attr_reader :meta_tags, :normalized_meta_tags
# Initialized a new instance of Renderer.
#
# @param [MetaTagsCollection] meta_tags meta tags object to render.
#
def initialize(meta_tags)
@meta_tags = meta_tags
@normalized_meta_tags = {}
end
# Renders meta tags on the page.
#
# @param [ActionView::Base] view Rails view object.
def render(view)
tags = []
render_charset(tags)
render_title(tags)
render_icon(tags)
render_with_normalization(tags, :description)
render_with_normalization(tags, :keywords)
render_refresh(tags)
render_canonical_link(tags)
render_noindex(tags)
render_alternate(tags)
render_open_search(tags)
render_links(tags)
render_hashes(tags)
render_custom(tags)
tags.tap(&:compact!).map! { |tag| tag.render(view) }
view.safe_join tags, MetaTags.config.minify_output ? "" : "\n"
end
protected
# Renders charset tag.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_charset(tags)
charset = meta_tags.extract(:charset)
tags << Tag.new(:meta, charset: charset) if charset.present?
end
# Renders title tag.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_title(tags)
normalized_meta_tags[:title] = meta_tags.page_title
normalized_meta_tags[:site] = meta_tags[:site]
title = meta_tags.extract_full_title
normalized_meta_tags[:full_title] = title
default_attributes = MetaTags.config.title_tag_attributes || {}
if title.present?
tags << ContentTag.new(:title, {content: title}.with_defaults(default_attributes))
end
end
# Renders icon(s) tag.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_icon(tags)
icon = meta_tags.extract(:icon)
return unless icon
# String? Value is an href
icon = [{href: icon}] if icon.is_a?(String)
# Hash? Single icon instead of a list of icons
icon = [icon] if icon.is_a?(Hash)
icon.each do |icon_params|
icon_params = {rel: "icon", type: "image/x-icon"}.with_indifferent_access.merge(icon_params)
tags << Tag.new(:link, icon_params)
end
end
# Renders meta tag with normalization (should have a corresponding normalize_
# method in TextNormalizer).
#
# @param [Array<Tag>] tags a buffer object to store tag in.
# @see TextNormalizer
#
def render_with_normalization(tags, name)
value = TextNormalizer.public_send(:"normalize_#{name}", meta_tags.extract(name))
normalized_meta_tags[name] = value
tags << Tag.new(:meta, name: name, content: value) if value.present?
end
# Renders noindex and nofollow meta tags.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_noindex(tags)
meta_tags.extract_robots.each do |name, content|
tags << Tag.new(:meta, name: name, content: content) if content.present?
end
end
# Renders refresh meta tag.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_refresh(tags)
refresh = meta_tags.extract(:refresh)
tags << Tag.new(:meta, "http-equiv" => "refresh", :content => refresh.to_s) if refresh.present?
end
# Renders alternate link tags.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_alternate(tags)
alternate = meta_tags.extract(:alternate)
return unless alternate
if alternate.is_a?(Hash)
alternate.each do |hreflang, href|
tags << Tag.new(:link, rel: "alternate", href: href, hreflang: hreflang) if href.present?
end
elsif alternate.is_a?(Array)
alternate.each do |link_params|
tags << Tag.new(:link, {rel: "alternate"}.with_indifferent_access.merge(link_params))
end
end
end
# Renders open_search link tag.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_open_search(tags)
open_search = meta_tags.extract(:open_search)
return unless open_search
href = open_search[:href]
title = open_search[:title]
type = "application/opensearchdescription+xml"
tags << Tag.new(:link, rel: "search", type: type, href: href, title: title) if href.present?
end
# Renders links.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_links(tags)
[:amphtml, :prev, :next, :image_src, :manifest].each do |tag_name|
href = meta_tags.extract(tag_name)
if href.present?
@normalized_meta_tags[tag_name] = href
tags << Tag.new(:link, rel: tag_name, href: href)
end
end
end
# Renders canonical link
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_canonical_link(tags)
href = meta_tags.extract(:canonical) # extract, so its not used anywhere else
return if MetaTags.config.skip_canonical_links_on_noindex && meta_tags[:noindex]
return if href.blank?
@normalized_meta_tags[:canonical] = href
tags << Tag.new(:link, rel: :canonical, href: href)
end
# Renders complex hash objects.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_hashes(tags, **opts)
meta_tags.meta_tags.each_key do |property|
render_hash(tags, property, **opts)
end
end
# Renders a complex hash object by key.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_hash(tags, key, **opts)
data = meta_tags.meta_tags[key]
return unless data.is_a?(Hash)
process_hash(tags, key, data, **opts)
meta_tags.extract(key)
end
# Renders custom meta tags.
#
# @param [Array<Tag>] tags a buffer object to store tag in.
#
def render_custom(tags)
meta_tags.meta_tags.each do |name, data|
Array(data).each do |val|
tags << Tag.new(:meta, configured_name_key(name) => name, :content => val)
end
meta_tags.extract(name)
end
end
# Recursive method to process all the hashes and arrays on meta tags
#
# @param [Array<Tag>] tags a buffer object to store tag in.
# @param [String, Symbol] property a Hash or a String to render as meta tag.
# @param [Hash, Array, String, Symbol] content text content or a symbol reference to
# top-level meta tag.
#
def process_tree(tags, property, content, itemprop: nil, **opts)
method = case content
when Hash
:process_hash
when Array
:process_array
else
iprop = itemprop
:render_tag
end
__send__(method, tags, property, content, itemprop: iprop, **opts)
end
# Recursive method to process a hash with meta tags
#
# @param [Array<Tag>] tags a buffer object to store tag in.
# @param [String, Symbol] property a Hash or a String to render as meta tag.
# @param [Hash] content nested meta tag attributes.
#
def process_hash(tags, property, content, **opts)
itemprop = content.delete(:itemprop)
content.each do |key, value|
if key.to_s == "_"
iprop = itemprop
key = property
else
key = "#{property}:#{key}"
end
normalized_value = if value.is_a?(Symbol)
normalized_meta_tags[value]
else
value
end
process_tree(tags, key, normalized_value, **opts.merge(itemprop: iprop))
end
end
# Recursive method to process a hash with meta tags
#
# @param [Array<Tag>] tags a buffer object to store tag in.
# @param [String, Symbol] property a Hash or a String to render as meta tag.
# @param [Array] content array of nested meta tag attributes or values.
#
def process_array(tags, property, content, **opts)
content.each { |v| process_tree(tags, property, v, **opts) }
end
# Recursive method to process a hash with meta tags
#
# @param [Array<Tag>] tags a buffer object to store tag in.
# @param [String, Symbol] name a Hash or a String to render as meta tag.
# @param [String, Symbol] value text content or a symbol reference to
# top-level meta tag.
# @param [String, Symbol] itemprop value of the itemprop attribute.
#
def render_tag(tags, name, value, itemprop: nil)
name_key ||= configured_name_key(name)
tags << Tag.new(:meta, name_key => name.to_s, :content => value, :itemprop => itemprop) if value.present?
end
# Returns meta tag property name for a give meta tag based on the
# configured list of property tags in MetaTags::Configuration#property_tags.
#
# @param [String, Symbol] name tag key.
# @return [Symbol] meta tag attribute name (:property or :name).
#
def configured_name_key(name)
is_property_tag = MetaTags.config.property_tags.any? do |tag_name|
name.to_s.match(/^#{Regexp.escape(tag_name.to_s)}\b/)
end
is_property_tag ? :property : :name
end
end
end