Class: SiteMapLink

Inherits:
ApplicationRecord show all
Defined in:
app/models/site_map_link.rb

Overview

Represents a directed hyperlink from one SiteMap page to another.

link_type values:
editorial — link placed inside main page content (passes topical signal)
navigation — link found in or .navbar / menu (sitewide template)
footer — link found in (sitewide template)
sidebar — link in an aside / sidebar widget

Template links (navigation, footer) are stored but excluded from gap analysis
and SEO recommendations since they don't carry contextual authority.
== Schema Information

Table name: site_map_links
Database name: primary

id :bigint not null, primary key
anchor_text :string
context_snippet :string
first_seen_at :datetime not null
last_seen_at :datetime not null
link_type :string default("editorial"), not null
to_path :string not null
created_at :datetime not null
updated_at :datetime not null
from_site_map_id :integer not null
to_site_map_id :integer

Indexes

index_site_map_links_on_from_to_path_type (from_site_map_id,to_path,link_type) UNIQUE
index_site_map_links_on_to_path_and_type (to_path,link_type)
index_site_map_links_on_to_site_map_id (to_site_map_id)

Foreign Keys

fk_rails_... (from_site_map_id => site_maps.id) ON DELETE => cascade
fk_rails_... (to_site_map_id => site_maps.id) ON DELETE => cascade

Constant Summary collapse

%w[editorial navigation footer sidebar].freeze

Instance Attribute Summary collapse

Belongs to collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#from_site_map_idObject (readonly)



47
# File 'app/models/site_map_link.rb', line 47

validates :from_site_map_id, uniqueness: { scope: %i[to_path link_type] }


46
# File 'app/models/site_map_link.rb', line 46

validates :link_type, inclusion: { in: LINK_TYPES }

#to_pathObject (readonly)



45
# File 'app/models/site_map_link.rb', line 45

validates :to_path,   presence: true

Class Method Details

.editorialActiveRecord::Relation<SiteMapLink>

A relation of SiteMapLinks that are editorial. Active Record Scope

Returns:

See Also:



51
# File 'app/models/site_map_link.rb', line 51

scope :editorial,   -> { where(link_type: 'editorial') }

.editorial_to_pathActiveRecord::Relation<SiteMapLink>

A relation of SiteMapLinks that are editorial to path. Active Record Scope

Returns:

See Also:



57
# File 'app/models/site_map_link.rb', line 57

scope :editorial_to_path, ->(path) { editorial.where(to_path: path) }

.inbound_toActiveRecord::Relation<SiteMapLink>

A relation of SiteMapLinks that are inbound to. Active Record Scope

Returns:

See Also:



53
# File 'app/models/site_map_link.rb', line 53

scope :inbound_to,  ->(site_map) { where(to_site_map_id: site_map.id) }

.outbound_fromActiveRecord::Relation<SiteMapLink>

A relation of SiteMapLinks that are outbound from. Active Record Scope

Returns:

See Also:



54
# File 'app/models/site_map_link.rb', line 54

scope :outbound_from, ->(site_map) { where(from_site_map_id: site_map.id) }

.templateActiveRecord::Relation<SiteMapLink>

A relation of SiteMapLinks that are template. Active Record Scope

Returns:

See Also:



52
# File 'app/models/site_map_link.rb', line 52

scope :template,    -> { where(link_type: %w[navigation footer]) }

.upsert_for_page!(from_site_map, links) ⇒ Object

Bulk upsert links for a page. Existing rows are updated (anchor, last_seen_at);
new rows are inserted. Rows no longer present in the crawl are not deleted —
they get stale naturally and can be pruned separately if needed.

Parameters:

  • from_site_map (SiteMap)
  • links (Array<Hash>)

    Each hash: { to_path:, anchor_text:, link_type:, context_snippet: }



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
# File 'app/models/site_map_link.rb', line 67

def self.upsert_for_page!(from_site_map, links)
  return if links.blank?

  now = Time.current

  # Resolve to_site_map_id for each target path (batch lookup)
  paths = links.map { |l| l[:to_path] }.uniq.compact
  path_to_id = SiteMap.where(path: paths, locale: from_site_map.locale)
                      .pluck(:path, :id)
                      .to_h

  rows = links.map do |link|
    {
      from_site_map_id: from_site_map.id,
      to_path:          link[:to_path],
      to_site_map_id:   path_to_id[link[:to_path]],
      anchor_text:      link[:anchor_text].to_s.truncate(255),
      link_type:        link[:link_type] || 'editorial',
      context_snippet:  link[:context_snippet].to_s.truncate(200),
      first_seen_at:    now,
      last_seen_at:     now,
      created_at:       now,
      updated_at:       now
    }
  end

  upsert_all(
    rows,
    unique_by:    %i[from_site_map_id to_path link_type],
    update_only:  %i[to_site_map_id anchor_text context_snippet last_seen_at]
  )
end

Instance Method Details

#editorial?Boolean

─── Helpers ───────────────────────────────────────────────────────────────

Returns:

  • (Boolean)


102
# File 'app/models/site_map_link.rb', line 102

def editorial?   = link_type == 'editorial'

#from_site_mapSiteMap

Returns:

See Also:



42
# File 'app/models/site_map_link.rb', line 42

belongs_to :from_site_map, class_name: 'SiteMap'

#template?Boolean

Returns:

  • (Boolean)


103
# File 'app/models/site_map_link.rb', line 103

def template?    = %w[navigation footer].include?(link_type)

#to_site_mapSiteMap

Returns:

See Also:



43
# File 'app/models/site_map_link.rb', line 43

belongs_to :to_site_map,   class_name: 'SiteMap', optional: true