Module: Models::SchemaMarkup

Extended by:
ActiveSupport::Concern
Included in:
Article
Defined in:
app/concerns/models/schema_markup.rb

Overview

ActiveSupport::Concern mixin: schema markup.

Instance Method Summary collapse

Instance Method Details

#add_schema(schema) ⇒ Object

Schema markup helper methods



9
10
11
12
13
14
15
# File 'app/concerns/models/schema_markup.rb', line 9

def add_schema(schema)
  return unless schema.is_a?(Hash)

  schema['@context'] = 'https://schema.org' unless schema['@context']
  self.schema_markup ||= []
  self.schema_markup << schema
end

#clear_schema_markupObject



126
127
128
# File 'app/concerns/models/schema_markup.rb', line 126

def clear_schema_markup
  self.schema_markup = []
end

#consolidated_faq_page_schemaObject

Build a single FAQPage schema merging: (1) FAQPage mainEntity from schema_markup, (2) FAQs from embedded blocks (by ID via FaqPresenter).
Deduplicates by question name. Returns a hash suitable for JSON-LD or nil if nothing to output.



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
# File 'app/concerns/models/schema_markup.rb', line 72

def consolidated_faq_page_schema
  embedded_ids = embedded_faq_ids_from_content
  extracted_faq_schemas = (schema_markup || []).select { |s| s['@type'] == 'FAQPage' }
  extracted_entities = extracted_faq_schemas.flat_map { |s| (s['mainEntity'] || s[:mainEntity] || []).map(&:with_indifferent_access) }

  embedded_entities = if embedded_ids.any?
                        ordered_ids = embedded_ids.uniq
                        faqs = ArticleFaq
                               .where(id: ordered_ids)
                               .published
                               .order(Arel.sql("ARRAY_POSITION(ARRAY[#{ordered_ids.join(',')}]::integer[], id)"))
                        if faqs.empty?
                          []
                        else
                          FaqPresenter.new(faqs).schema_dot_org_structure.mainEntity.map(&:to_json_struct)
                        end
                      else
                        []
                      end

  all_entities = (extracted_entities + embedded_entities)
  return nil if all_entities.empty?

  # Deduplicate by question name (first occurrence wins)
  seen = Set.new
  main_entity = all_entities.filter_map do |entity|
    name = entity['name'] || entity[:name]
    next if name.blank? || seen.include?(name)

    seen.add(name)
    entity
  end

  return nil if main_entity.empty?

  {
    '@context' => 'https://schema.org',
    '@type' => 'FAQPage',
    'mainEntity' => main_entity
  }
end

#content_has_embedded_faq_schema?Boolean

Check if content has embedded FAQ oEmbed blocks for extractor/rules that need to skip or strip that block

Returns:

  • (Boolean)


115
116
117
118
119
120
121
122
123
124
# File 'app/concerns/models/schema_markup.rb', line 115

def content_has_embedded_faq_schema?
  return false unless respond_to?(:localized_solution)

  rendered = localized_solution.to_s

  return true if rendered.match?(/wy-faq-embed|data-wy-oembed="faq"/i)
  return true if rendered.include?('application/ld+json') && rendered.include?('"FAQPage"')

  false
end

#embedded_faq_ids_from_contentObject

Collect FAQ IDs from embedded oEmbed blocks (data-faq-ids), in document order.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'app/concerns/models/schema_markup.rb', line 51

def embedded_faq_ids_from_content
  return [] unless respond_to?(:localized_solution)

  content = localized_solution.to_s
  return [] if content.blank?

  ids = []

  doc = Nokogiri::HTML(content)
  doc.css('figure.wy-faq-embed, figure[data-wy-oembed="faq"]').each do |figure|
    figure['data-faq-ids'].to_s.split(',').each do |id_str|
      id = id_str.strip.to_i
      ids << id if id.positive?
    end
  end

  ids.uniq
end

#has_schema_type?(type) ⇒ Boolean

Returns:

  • (Boolean)


23
24
25
# File 'app/concerns/models/schema_markup.rb', line 23

def has_schema_type?(type)
  schema_types.include?(type)
end

#render_schema_markupObject



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'app/concerns/models/schema_markup.rb', line 33

def render_schema_markup
  # Build one consolidated FAQPage (extracted schema_markup FAQ + embedded oEmbed FAQs), then other schemas
  consolidated_faq = consolidated_faq_page_schema
  other_schemas = (schema_markup || []).reject { |s| s['@type'] == 'FAQPage' }

  parts = []
  parts << consolidated_faq if consolidated_faq.present?
  parts.concat(other_schemas)

  return '' if parts.empty?

  parts.map do |schema|
    hash = schema.is_a?(Hash) ? schema : schema.with_indifferent_access
    "<script type=\"application/ld+json\">#{hash.to_json}</script>"
  end.join("\n").html_safe
end

#schema_countObject



130
131
132
# File 'app/concerns/models/schema_markup.rb', line 130

def schema_count
  schema_markup&.length || 0
end

#schema_typesObject



17
18
19
20
21
# File 'app/concerns/models/schema_markup.rb', line 17

def schema_types
  return [] if schema_markup.blank?

  schema_markup.pluck('@type').compact.uniq
end

#schemas_by_type(type) ⇒ Object



27
28
29
30
31
# File 'app/concerns/models/schema_markup.rb', line 27

def schemas_by_type(type)
  return [] if schema_markup.blank?

  schema_markup.select { |schema| schema['@type'] == type }
end