Class: SiteMap

Inherits:
ApplicationRecord show all
Includes:
Models::Embeddable, PgSearch::Model
Defined in:
app/models/site_map.rb

Overview

== Schema Information

Table name: site_maps
Database name: primary

id :integer not null, primary key
category :string
change_frequency :string default("monthly")
extracted_at :datetime
extracted_content :text
extracted_title :string
google_coverage_state :string
google_inspected_at :datetime
google_last_crawled_at :datetime
hide :boolean default(FALSE), not null
image_properties :jsonb
last_mod :datetime
last_status :string
last_status_datetime :datetime
legacy_url :string
locale :string
path :string
preserve :boolean default(FALSE), not null
priority :decimal(2, 1) default(0.5)
rendered_schema :jsonb
rendered_schema_at :datetime
resource_type :string
seo_clicks :integer
seo_keywords_count :integer
seo_report :jsonb
seo_synced_at :datetime
seo_top_keyword :string
seo_top_position :integer
seo_traffic :integer
seo_traffic_value :integer
state :string default("active"), not null
target_query :string
visit_count_30d :integer
created_at :datetime not null
updated_at :datetime not null
resource_id :integer

Indexes

index_site_maps_on_category (category)
index_site_maps_on_extracted_at (extracted_at)
index_site_maps_on_legacy_url (legacy_url) UNIQUE
index_site_maps_on_locale_and_path (locale,path) UNIQUE
index_site_maps_on_path (path)
index_site_maps_on_path_trigram (path) USING gin
index_site_maps_on_rendered_schema (rendered_schema) USING gin
index_site_maps_on_resource_type_and_resource_id (resource_type,resource_id)
index_site_maps_on_seo_synced_at (seo_synced_at)
index_site_maps_on_seo_traffic (seo_traffic)
index_site_maps_on_state (state)
locale_category (locale,category)

Constant Summary collapse

CHANGE_FREQUENCIES =
%w[always hourly daily weekly monthly yearly never].freeze
EMBEDDABLE_CATEGORIES =

Categories that have extracted content for embedding

%w[static_page].freeze
LOCALES =

Valid locales for site maps - must match LocaleUtility::SITE_LOCALES

LocaleUtility::SITE_LOCALES.map(&:to_s).freeze
STALE_ANALYSIS_SQL =

Scopes for filtering by analysis freshness (index filter).
Stale = has analysis but page recrawled after it ran.

<<~SQL.squish.freeze
  seo_report->>'analyzed_at' IS NOT NULL
  AND (
    (rendered_schema_at IS NOT NULL AND rendered_schema_at > (seo_report->>'analyzed_at')::timestamptz)
    OR (extracted_at IS NOT NULL AND extracted_at > (seo_report->>'analyzed_at')::timestamptz)
  )
SQL

Constants included from Models::Embeddable

Models::Embeddable::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH

Instance Attribute Summary collapse

Belongs to collapse

Has many collapse

Methods included from Models::Embeddable

#content_embeddings

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Embeddable

embeddable_content_types, #embeddable_locales, #embedding_content_hash, embedding_partition_class, #embedding_stale?, #embedding_type_name, #embedding_vector, #find_content_embedding, #find_similar, #generate_all_embeddings!, #generate_chunked_embeddings!, #generate_embedding!, #has_embedding?, #needs_chunking?, regenerate_all_embeddings, semantic_search

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#last_modObject (readonly)



87
# File 'app/models/site_map.rb', line 87

validates :path, :locale, :last_mod, presence: true

#localeObject (readonly)



87
# File 'app/models/site_map.rb', line 87

validates :path, :locale, :last_mod, presence: true

#pathObject (readonly)



87
# File 'app/models/site_map.rb', line 87

validates :path, :locale, :last_mod, presence: true

Class Method Details

.by_trafficActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are by traffic. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



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

scope :by_traffic,      -> { order(seo_traffic: :desc) }

.cacheableActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are cacheable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



94
# File 'app/models/site_map.rb', line 94

scope :cacheable,        -> { active.where.not(category: %w[publication video]) }

.categories_for_selectObject



202
203
204
# File 'app/models/site_map.rb', line 202

def self.categories_for_select
  %w[faqs floor_plan form post product product_line publication showcase static_page support tech_article towel_warmer_filter video]
end

.embeddableActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are embeddable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



97
# File 'app/models/site_map.rb', line 97

scope :embeddable,       -> { active.where(category: EMBEDDABLE_CATEGORIES).with_extracted_content }

.extract_path_from_url(full_url) ⇒ String

Extract path from a full URL, stripping domain and locale

Parameters:

Returns:

  • (String)

    Path like '/products/foo'



190
191
192
193
194
195
196
197
198
199
200
# File 'app/models/site_map.rb', line 190

def self.extract_path_from_url(full_url)
  return '/' if full_url.blank?

  # Remove protocol and domain
  uri_path = URI.parse(full_url).path
  # Remove locale prefix (e.g., /en-US/)
  uri_path.sub(%r{^/[a-z]{2}-[A-Z]{2}}, '')
          .then { |p| p.presence || '/' }
rescue URI::InvalidURIError
  '/'
end

.high_trafficActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are high traffic. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



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

scope :high_traffic,    ->(threshold = 100) { where(seo_traffic: threshold..) }

.needs_extractionActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are needs extraction. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



96
# File 'app/models/site_map.rb', line 96

scope :needs_extraction, -> { active.where(category: EMBEDDABLE_CATEGORIES, extracted_at: nil) }

.needs_seo_syncActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are needs seo sync. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



101
# File 'app/models/site_map.rb', line 101

scope :needs_seo_sync,  -> { active.where(seo_synced_at: nil).or(active.where(seo_synced_at: ..7.days.ago)) }

.purge_edge_cache_by_pattern(pattern, async: false, delay: nil) ⇒ Object

Pattern can be e.g "/floor-heating/" for everything floor heating related
Purges the edge cache for all URLs matching the given pattern.
The pattern can contain * as a wildcard. Converts * to % for the SQL pattern matching.
Fetches all matching URLs from the database and purges them from the edge cache.
Can run asynchronously by queueing jobs, with optional delay.
Logs any errors to AppSignal.



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'app/models/site_map.rb', line 212

def self.purge_edge_cache_by_pattern(pattern, async: false, delay: nil)
  return :disabled unless Cache::EdgeCacheUtility.edge_cache_enabled?

  sql_pattern = pattern.tr('*', '%')
  # Match against path column instead of full URL
  records = where(SiteMap[:path].matches(sql_pattern))
  urls = records.map(&:url)
  begin
    if async
      if delay
        EdgeCacheWorker.perform_in(delay, 'urls' => urls)
      else
        EdgeCacheWorker.perform_async('urls' => urls)
      end
    else
      Cache::EdgeCacheUtility.instance.purge_url(urls)
    end
  rescue StandardError => e
    ErrorReporting.error e, url: urls.first
  end
end

.schema_staleActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are schema stale. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



123
# File 'app/models/site_map.rb', line 123

scope :schema_stale, ->(since = 30.days.ago) { where(rendered_schema_at: nil).or(where(rendered_schema_at: ..since)) }

.seo_analysis_freshActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are seo analysis fresh. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



381
382
383
384
# File 'app/models/site_map.rb', line 381

scope :seo_analysis_fresh, -> {
  where("seo_report->>'analyzed_at' IS NOT NULL")
    .where.not(STALE_ANALYSIS_SQL)
}

.seo_analysis_noneActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are seo analysis none. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



385
386
387
388
389
# File 'app/models/site_map.rb', line 385

scope :seo_analysis_none, -> {
  where(seo_report: nil)
    .or(where(seo_report: {}))
    .or(where("seo_report->>'analyzed_at' IS NULL"))
}

.seo_analysis_staleActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are seo analysis stale. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



380
# File 'app/models/site_map.rb', line 380

scope :seo_analysis_stale, -> { where(STALE_ANALYSIS_SQL) }

.with_extracted_contentActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are with extracted content. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



95
# File 'app/models/site_map.rb', line 95

scope :with_extracted_content, -> { where.not(extracted_content: nil) }

.with_rendered_schemaActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are with rendered schema. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



120
# File 'app/models/site_map.rb', line 120

scope :with_rendered_schema, -> { where.not(rendered_schema: nil) }

.with_schema_typeActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are with schema type. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



122
# File 'app/models/site_map.rb', line 122

scope :with_schema_type, ->(type) { where("rendered_schema @> ?", [{ '@type' => type }].to_json) }

.with_seo_dataActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are with seo data. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



100
# File 'app/models/site_map.rb', line 100

scope :with_seo_data,   -> { where.not(seo_synced_at: nil) }

.without_rendered_schemaActiveRecord::Relation<SiteMap>

A relation of SiteMaps that are without rendered schema. Active Record Scope

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



121
# File 'app/models/site_map.rb', line 121

scope :without_rendered_schema, -> { where(rendered_schema: nil) }

Instance Method Details

#cannibalization_risksArray<Hash>

Check for keyword cannibalization

Returns:

  • (Array<Hash>)

    Pages competing for same keywords



447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'app/models/site_map.rb', line 447

def cannibalization_risks
  competing = []
  seo_page_keywords.at_risk.each do |kw|
    kw.competing_pages.at_risk.each do |competing_kw|
      competing << {
        keyword: kw.keyword,
        this_position: kw.position,
        competing_url: competing_kw.site_map.url,
        competing_position: competing_kw.position
      }
    end
  end
  competing.uniq { |c| [c[:keyword], c[:competing_url]] }
end

#content_for_embedding(_content_type = :primary) ⇒ String?

Generate content for semantic search embedding
For static pages, uses extracted content from crawler
For other categories, delegates to the linked resource

Parameters:

  • _content_type (Symbol) (defaults to: :primary)

    Content type (ignored, single type for pages)

Returns:

  • (String, nil)

    Content to embed



306
307
308
309
310
311
312
313
314
315
# File 'app/models/site_map.rb', line 306

def content_for_embedding(_content_type = :primary)
  case category
  when 'static_page'
    build_static_page_content
  else
    # For other categories (post, video, etc.), the resource model handles embedding
    # This prevents duplicate embeddings
    nil
  end
end

#data_pointsActiveRecord::Relation<SiteMapDataPoint>

Returns:

See Also:



76
# File 'app/models/site_map.rb', line 76

has_many :data_points, class_name: 'SiteMapDataPoint', dependent: :destroy

#embedding_content_changed?Boolean

Check if embedding content has changed

Returns:

  • (Boolean)


318
319
320
# File 'app/models/site_map.rb', line 318

def embedding_content_changed?
  saved_change_to_extracted_content? || saved_change_to_extracted_title?
end

#extract_content!(force: false) ⇒ Object

Crawl this page and extract content

Parameters:

  • force (Boolean) (defaults to: false)

    Re-extract even if already extracted



332
333
334
335
336
337
338
# File 'app/models/site_map.rb', line 332

def extract_content!(force: false)
  return extracted_content if extracted_content.present? && !force

  Cache::SiteCrawler.new.process(pages: SiteMap.where(id: id), extract_content: true)
  reload
  extracted_content
end

#has_rendered_schema_type?(type) ⇒ Boolean

Check if the rendered page has a specific schema type

Parameters:

  • type (String)

    e.g., "FAQPage", "HowTo", "Article"

Returns:

  • (Boolean)


275
276
277
# File 'app/models/site_map.rb', line 275

def has_rendered_schema_type?(type)
  rendered_schema_types.include?(type)
end

#historical_urlsArray<String>

Get all historical URLs for this resource (for matching external data)
Uses FriendlyId slug history when available

Returns:

  • (Array<String>)


519
520
521
522
523
524
525
526
527
528
# File 'app/models/site_map.rb', line 519

def historical_urls
  return [] unless resource.present?
  return [] unless resource.respond_to?(:slugs)

  resource.slugs.map do |slug_record|
    url.sub(resource.slug, slug_record.slug)
  end
rescue StandardError
  []
end

Returns:

See Also:



83
# File 'app/models/site_map.rb', line 83

has_many :inbound_links,  class_name: 'SiteMapLink', foreign_key: :to_site_map_id,   dependent: :nullify,    inverse_of: :to_site_map

#locale_for_embeddingString

Locale for embedding - uses the SiteMap's locale column
Preserves full locale (en-US, en-CA) to allow region-specific search

Returns:

  • (String)

    Full locale code



326
327
328
# File 'app/models/site_map.rb', line 326

def locale_for_embedding
  locale.to_s.presence || 'en'
end

Internal link graph

Returns:

See Also:



82
# File 'app/models/site_map.rb', line 82

has_many :outbound_links, class_name: 'SiteMapLink', foreign_key: :from_site_map_id, dependent: :delete_all, inverse_of: :from_site_map

#production_urlString

Always returns the production URL regardless of environment.
Use this for SEO analysis, external tools, and display purposes.

Returns:

  • (String)

    Production URL



151
152
153
# File 'app/models/site_map.rb', line 151

def production_url
  build_url('https://www.warmlyyours.com')
end

#purge_edge_cache(async: true, extra_urls: []) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'app/models/site_map.rb', line 241

def purge_edge_cache(async: true, extra_urls: [])
  return :disabled unless Cache::EdgeCacheUtility.edge_cache_enabled?

  urls = ([url] + extra_urls).compact.uniq
  begin
    if async
      EdgeCacheWorker.perform_async('urls' => urls)
    else
      urls.each { |url| Cache::EdgeCacheUtility.instance.purge_url(url) }
    end
  rescue StandardError => e
    ErrorReporting.error e, url: url
  end
end

#ranking_keywords_countInteger

Get count of ranking keywords (position 1-100)
Uses actual records rather than cached counter for accuracy

Returns:

  • (Integer)


441
442
443
# File 'app/models/site_map.rb', line 441

def ranking_keywords_count
  seo_page_keywords.ranking.count
end

#recommendationsActiveRecord::Relation<SiteMapRecommendation>

SEO recommendations extracted from seo_report

Returns:

See Also:



79
# File 'app/models/site_map.rb', line 79

has_many :recommendations, class_name: 'SiteMapRecommendation', dependent: :destroy

#rendered_faq_countInteger

Count of FAQ questions in rendered FAQPage schema

Returns:

  • (Integer)


290
291
292
293
294
# File 'app/models/site_map.rb', line 290

def rendered_faq_count
  rendered_schemas_by_type('FAQPage')
    .flat_map { |s| Array(s['mainEntity']) }
    .size
end

#rendered_schema_typesArray<String>

Schema types found on the rendered page (e.g., ["FAQPage", "Article", "BreadcrumbList"])

Returns:

  • (Array<String>)


267
268
269
270
271
# File 'app/models/site_map.rb', line 267

def rendered_schema_types
  return [] unless rendered_schema.present?

  rendered_schema.flat_map { |s| Array(s['@type']) }.compact.uniq
end

#rendered_schemas_by_type(type) ⇒ Array<Hash>

Get schemas of a specific type from the rendered page

Parameters:

  • type (String)

    e.g., "FAQPage"

Returns:

  • (Array<Hash>)


282
283
284
285
286
# File 'app/models/site_map.rb', line 282

def rendered_schemas_by_type(type)
  return [] unless rendered_schema.present?

  rendered_schema.select { |s| Array(s['@type']).include?(type) }
end

#resourceResource

Returns:

  • (Resource)

See Also:



72
# File 'app/models/site_map.rb', line 72

belongs_to :resource, polymorphic: true, optional: true

#seo_analysis_stale?Boolean

Whether the AI analysis is stale (page was recrawled after analysis ran).
Matches Crm::SeoDashboardComponent#report_stale?

Returns:

  • (Boolean)


353
354
355
356
357
358
359
# File 'app/models/site_map.rb', line 353

def seo_analysis_stale?
  analyzed_at = seo_report_analyzed_at
  return false unless analyzed_at

  (rendered_schema_at.present? && rendered_schema_at > analyzed_at) ||
    (extracted_at.present? && extracted_at > analyzed_at)
end

#seo_avg_positionBigDecimal?

GSC average search position (28-day window)

Returns:

  • (BigDecimal, nil)


410
411
412
# File 'app/models/site_map.rb', line 410

def seo_avg_position
  latest_data_point_value(:gsc_avg_position)
end

#seo_ctrBigDecimal?

GSC click-through rate (28-day window)

Returns:

  • (BigDecimal, nil)


404
405
406
# File 'app/models/site_map.rb', line 404

def seo_ctr
  latest_data_point_value(:gsc_ctr)
end

#seo_data?Boolean

Check if SEO data has been synced or analyzed

Returns:

  • (Boolean)


345
346
347
# File 'app/models/site_map.rb', line 345

def seo_data?
  seo_synced_at.present? || seo_report.present? || data_points.exists?
end

#seo_impressionsInteger?

GSC impressions (28-day window)

Returns:

  • (Integer, nil)


398
399
400
# File 'app/models/site_map.rb', line 398

def seo_impressions
  latest_data_point_value(:gsc_impressions)&.to_i
end

#seo_page_keywordsActiveRecord::Relation<SeoPageKeyword>

SEO metrics associations

Returns:

See Also:



75
# File 'app/models/site_map.rb', line 75

has_many :seo_page_keywords, dependent: :destroy

#seo_report_analyzed_atTime?

Parsed analyzed_at from seo_report (ISO 8601 string).

Returns:

  • (Time, nil)


363
364
365
366
367
368
# File 'app/models/site_map.rb', line 363

def seo_report_analyzed_at
  raw = seo_report&.dig('analyzed_at')
  raw.present? ? Time.zone.parse(raw) : nil
rescue ArgumentError, TypeError
  nil
end

#seo_traffic_trendSymbol

Get traffic trend based on historical data

Returns:

  • (Symbol)

    :growing, :stable, :declining, or :unknown



422
423
424
425
426
427
428
429
# File 'app/models/site_map.rb', line 422

def seo_traffic_trend
  # Use Ahrefs traffic from data_points if available, fall back to legacy
  if data_points.for_metric(:ahrefs_traffic).exists?
    data_points.trend_direction(:ahrefs_traffic)
  else
    :unknown
  end
end

#sibling_site_mapsActiveRecord::Relation<SiteMap>

Other locales for the same path (e.g. en-CA when this is en-US).
Used so SEO analysis and Sunny fix prompts have all-country context for shared content (e.g. blogs).

Returns:

  • (ActiveRecord::Relation<SiteMap>)


173
174
175
176
177
# File 'app/models/site_map.rb', line 173

def sibling_site_maps
  return SiteMap.none if path.blank?

  SiteMap.active.where(path: path).where.not(id: id).order(:locale)
end

#skip_cache_warmup?Boolean

Those resources do not need cache warmup

Returns:

  • (Boolean)


257
258
259
# File 'app/models/site_map.rb', line 257

def skip_cache_warmup?
  category&.in?&.[]('publication', 'video')
end

#suggested_keyword_target_for(keyword, search_volume: nil) ⇒ String

Intent-based suggestion: compare keyword intent with this page's intent and with other
pages that rank for the same keyword. Returns 'desired', 'undesired', or 'ignore'.

  • ignore: noise keyword, or very low search volume, or no clear signal
  • desired: this page already has keyword_target='desired' for this keyword,
    or the target_query matches
  • undesired: another page (same locale) has keyword_target='desired' for this keyword

Parameters:

  • keyword (String)
  • search_volume (Integer, nil) (defaults to: nil)

    optional, for ignore suggestion when very low

Returns:

  • (String)

    'desired', 'undesired', or 'ignore'



473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'app/models/site_map.rb', line 473

def suggested_keyword_target_for(keyword, search_volume: nil)
  return 'ignore' if keyword.blank?

  normalized = keyword.to_s.strip.downcase
  return 'ignore' if normalized.blank?

  # Prefer AI-generated suggestions from SEO analysis when present
  suggestions = seo_report.is_a?(Hash) && seo_report['keyword_suggestions'].is_a?(Hash) ? seo_report['keyword_suggestions'] : nil
  if suggestions.present?
    ai_value = suggestions[normalized] || suggestions[keyword]
    return ai_value if ai_value.present? && SeoPageKeyword.keyword_targets.key?(ai_value.to_s)
  end

  return 'ignore' if SeoPageKeyword.noise?(keyword)
  return 'ignore' if search_volume.present? && search_volume.to_i < 10

  # This page already has this keyword marked as desired
  if seo_page_keywords.desired.exists?(keyword: keyword)
    return 'desired'
  end
  if target_query.present? && target_query.strip.downcase == normalized
    return 'desired'
  end

  # Another page (same locale) has this keyword marked as desired → we shouldn't compete
  other_owns = SeoPageKeyword
    .joins(:site_map)
    .where(keyword: keyword, keyword_target: :desired)
    .where.not(site_map_id: id)
    .merge(SiteMap.where(locale: locale))
    .exists?
  return 'undesired' if other_owns

  'ignore'
end

#top_keywords(limit: 10) ⇒ Array<SeoPageKeyword>

Get top keywords for this page

Parameters:

  • limit (Integer) (defaults to: 10)

    Max keywords to return

Returns:



434
435
436
# File 'app/models/site_map.rb', line 434

def top_keywords(limit: 10)
  seo_page_keywords.ranking.by_traffic.limit(limit)
end

#urlString

Constructs the full URL from WEB_URL + locale + path
Example: locale='en-US', path='/products/foo' => 'https://www.warmlyyours.com/en-US/products/foo'

Returns:

  • (String)

    Full URL (uses current environment's host)



141
142
143
144
145
# File 'app/models/site_map.rb', line 141

def url
  return legacy_url if path.blank? && legacy_url.present? # Fallback during migration

  build_url(WEB_URL)
end

#url=(full_url) ⇒ Object

Alias for backward compatibility - some code may use url=



180
181
182
183
184
185
# File 'app/models/site_map.rb', line 180

def url=(full_url)
  return if full_url.blank?

  # Extract path from full URL
  self.path = self.class.extract_path_from_url(full_url)
end

#url_pathString

Extract URL path for matching with external data
Now simply returns the stored path

Returns:

  • (String)

    Path without locale prefix



512
513
514
# File 'app/models/site_map.rb', line 512

def url_path
  path
end

#visit_count_90dInteger?

Visit count over 90-day window

Returns:

  • (Integer, nil)


416
417
418
# File 'app/models/site_map.rb', line 416

def visit_count_90d
  latest_data_point_value(:visits_90d)&.to_i
end

#warm_cacheObject



234
235
236
237
238
239
# File 'app/models/site_map.rb', line 234

def warm_cache
  return :disabled unless Cache::EdgeCacheUtility.edge_cache_enabled?

  # Pass path (not full url) since SiteCrawler filters against the path column
  Cache::SiteCrawler.new.process(url: path)
end