Class: SiteMap
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- SiteMap
- 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
- #last_mod ⇒ Object readonly
- #locale ⇒ Object readonly
- #path ⇒ Object readonly
Belongs to collapse
Has many collapse
- #data_points ⇒ ActiveRecord::Relation<SiteMapDataPoint>
- #inbound_links ⇒ ActiveRecord::Relation<SiteMapLink>
-
#outbound_links ⇒ ActiveRecord::Relation<SiteMapLink>
Internal link graph.
-
#recommendations ⇒ ActiveRecord::Relation<SiteMapRecommendation>
SEO recommendations extracted from seo_report.
-
#seo_page_keywords ⇒ ActiveRecord::Relation<SeoPageKeyword>
SEO metrics associations.
Methods included from Models::Embeddable
Class Method Summary collapse
-
.by_traffic ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are by traffic.
-
.cacheable ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are cacheable.
- .categories_for_select ⇒ Object
-
.embeddable ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are embeddable.
-
.extract_path_from_url(full_url) ⇒ String
Extract path from a full URL, stripping domain and locale.
-
.high_traffic ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are high traffic.
-
.needs_extraction ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are needs extraction.
-
.needs_seo_sync ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are needs seo sync.
-
.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.
-
.schema_stale ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are schema stale.
-
.seo_analysis_fresh ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are seo analysis fresh.
-
.seo_analysis_none ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are seo analysis none.
-
.seo_analysis_stale ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are seo analysis stale.
-
.with_extracted_content ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are with extracted content.
-
.with_rendered_schema ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are with rendered schema.
-
.with_schema_type ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are with schema type.
-
.with_seo_data ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are with seo data.
-
.without_rendered_schema ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are without rendered schema.
Instance Method Summary collapse
-
#cannibalization_risks ⇒ Array<Hash>
Check for keyword cannibalization.
-
#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.
-
#embedding_content_changed? ⇒ Boolean
Check if embedding content has changed.
-
#extract_content!(force: false) ⇒ Object
Crawl this page and extract content.
-
#has_rendered_schema_type?(type) ⇒ Boolean
Check if the rendered page has a specific schema type.
-
#historical_urls ⇒ Array<String>
Get all historical URLs for this resource (for matching external data) Uses FriendlyId slug history when available.
-
#locale_for_embedding ⇒ String
Locale for embedding - uses the SiteMap's locale column Preserves full locale (en-US, en-CA) to allow region-specific search.
-
#production_url ⇒ String
Always returns the production URL regardless of environment.
- #purge_edge_cache(async: true, extra_urls: []) ⇒ Object
-
#ranking_keywords_count ⇒ Integer
Get count of ranking keywords (position 1-100) Uses actual records rather than cached counter for accuracy.
-
#rendered_faq_count ⇒ Integer
Count of FAQ questions in rendered FAQPage schema.
-
#rendered_schema_types ⇒ Array<String>
Schema types found on the rendered page (e.g., ["FAQPage", "Article", "BreadcrumbList"]).
-
#rendered_schemas_by_type(type) ⇒ Array<Hash>
Get schemas of a specific type from the rendered page.
-
#seo_analysis_stale? ⇒ Boolean
Whether the AI analysis is stale (page was recrawled after analysis ran).
-
#seo_avg_position ⇒ BigDecimal?
GSC average search position (28-day window).
-
#seo_ctr ⇒ BigDecimal?
GSC click-through rate (28-day window).
-
#seo_data? ⇒ Boolean
Check if SEO data has been synced or analyzed.
-
#seo_impressions ⇒ Integer?
GSC impressions (28-day window).
-
#seo_report_analyzed_at ⇒ Time?
Parsed analyzed_at from seo_report (ISO 8601 string).
-
#seo_traffic_trend ⇒ Symbol
Get traffic trend based on historical data.
-
#sibling_site_maps ⇒ ActiveRecord::Relation<SiteMap>
Other locales for the same path (e.g. en-CA when this is en-US).
-
#skip_cache_warmup? ⇒ Boolean
Those resources do not need cache warmup.
-
#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.
-
#top_keywords(limit: 10) ⇒ Array<SeoPageKeyword>
Get top keywords for this page.
-
#url ⇒ String
Constructs the full URL from WEB_URL + locale + path Example: locale='en-US', path='/products/foo' => 'https://www.warmlyyours.com/en-US/products/foo'.
-
#url=(full_url) ⇒ Object
Alias for backward compatibility - some code may use url=.
-
#url_path ⇒ String
Extract URL path for matching with external data Now simply returns the stored path.
-
#visit_count_90d ⇒ Integer?
Visit count over 90-day window.
- #warm_cache ⇒ Object
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
Instance Attribute Details
#last_mod ⇒ Object (readonly)
87 |
# File 'app/models/site_map.rb', line 87 validates :path, :locale, :last_mod, presence: true |
#locale ⇒ Object (readonly)
87 |
# File 'app/models/site_map.rb', line 87 validates :path, :locale, :last_mod, presence: true |
#path ⇒ Object (readonly)
87 |
# File 'app/models/site_map.rb', line 87 validates :path, :locale, :last_mod, presence: true |
Class Method Details
.by_traffic ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are by traffic. Active Record Scope
102 |
# File 'app/models/site_map.rb', line 102 scope :by_traffic, -> { order(seo_traffic: :desc) } |
.cacheable ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are cacheable. Active Record Scope
94 |
# File 'app/models/site_map.rb', line 94 scope :cacheable, -> { active.where.not(category: %w[publication video]) } |
.categories_for_select ⇒ Object
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 |
.embeddable ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are embeddable. Active Record Scope
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
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_traffic ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are high traffic. Active Record Scope
103 |
# File 'app/models/site_map.rb', line 103 scope :high_traffic, ->(threshold = 100) { where(seo_traffic: threshold..) } |
.needs_extraction ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are needs extraction. Active Record Scope
96 |
# File 'app/models/site_map.rb', line 96 scope :needs_extraction, -> { active.where(category: EMBEDDABLE_CATEGORIES, extracted_at: nil) } |
.needs_seo_sync ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are needs seo sync. Active Record Scope
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_stale ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are schema stale. Active Record Scope
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_fresh ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are seo analysis fresh. Active Record Scope
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_none ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are seo analysis none. Active Record Scope
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_stale ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are seo analysis stale. Active Record Scope
380 |
# File 'app/models/site_map.rb', line 380 scope :seo_analysis_stale, -> { where(STALE_ANALYSIS_SQL) } |
.with_extracted_content ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are with extracted content. Active Record Scope
95 |
# File 'app/models/site_map.rb', line 95 scope :with_extracted_content, -> { where.not(extracted_content: nil) } |
.with_rendered_schema ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are with rendered schema. Active Record Scope
120 |
# File 'app/models/site_map.rb', line 120 scope :with_rendered_schema, -> { where.not(rendered_schema: nil) } |
.with_schema_type ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are with schema type. Active Record Scope
122 |
# File 'app/models/site_map.rb', line 122 scope :with_schema_type, ->(type) { where("rendered_schema @> ?", [{ '@type' => type }].to_json) } |
.with_seo_data ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are with seo data. Active Record Scope
100 |
# File 'app/models/site_map.rb', line 100 scope :with_seo_data, -> { where.not(seo_synced_at: nil) } |
.without_rendered_schema ⇒ ActiveRecord::Relation<SiteMap>
A relation of SiteMaps that are without rendered schema. Active Record Scope
121 |
# File 'app/models/site_map.rb', line 121 scope :without_rendered_schema, -> { where(rendered_schema: nil) } |
Instance Method Details
#cannibalization_risks ⇒ Array<Hash>
Check for keyword cannibalization
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
306 307 308 309 310 311 312 313 314 315 |
# File 'app/models/site_map.rb', line 306 def (_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_points ⇒ ActiveRecord::Relation<SiteMapDataPoint>
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
318 319 320 |
# File 'app/models/site_map.rb', line 318 def saved_change_to_extracted_content? || saved_change_to_extracted_title? end |
#extract_content!(force: false) ⇒ Object
Crawl this page and extract content
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
275 276 277 |
# File 'app/models/site_map.rb', line 275 def has_rendered_schema_type?(type) rendered_schema_types.include?(type) end |
#historical_urls ⇒ Array<String>
Get all historical URLs for this resource (for matching external data)
Uses FriendlyId slug history when available
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 |
#inbound_links ⇒ ActiveRecord::Relation<SiteMapLink>
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_embedding ⇒ String
Locale for embedding - uses the SiteMap's locale column
Preserves full locale (en-US, en-CA) to allow region-specific search
326 327 328 |
# File 'app/models/site_map.rb', line 326 def locale.to_s.presence || 'en' end |
#outbound_links ⇒ ActiveRecord::Relation<SiteMapLink>
Internal link graph
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_url ⇒ String
Always returns the production URL regardless of environment.
Use this for SEO analysis, external tools, and display purposes.
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_count ⇒ Integer
Get count of ranking keywords (position 1-100)
Uses actual records rather than cached counter for accuracy
441 442 443 |
# File 'app/models/site_map.rb', line 441 def ranking_keywords_count seo_page_keywords.ranking.count end |
#recommendations ⇒ ActiveRecord::Relation<SiteMapRecommendation>
SEO recommendations extracted from seo_report
79 |
# File 'app/models/site_map.rb', line 79 has_many :recommendations, class_name: 'SiteMapRecommendation', dependent: :destroy |
#rendered_faq_count ⇒ Integer
Count of FAQ questions in rendered FAQPage schema
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_types ⇒ Array<String>
Schema types found on the rendered page (e.g., ["FAQPage", "Article", "BreadcrumbList"])
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
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 |
#resource ⇒ Resource
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?
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_position ⇒ BigDecimal?
GSC average search position (28-day window)
410 411 412 |
# File 'app/models/site_map.rb', line 410 def seo_avg_position latest_data_point_value(:gsc_avg_position) end |
#seo_ctr ⇒ BigDecimal?
GSC click-through rate (28-day window)
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
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_impressions ⇒ Integer?
GSC impressions (28-day window)
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_keywords ⇒ ActiveRecord::Relation<SeoPageKeyword>
SEO metrics associations
75 |
# File 'app/models/site_map.rb', line 75 has_many :seo_page_keywords, dependent: :destroy |
#seo_report_analyzed_at ⇒ Time?
Parsed analyzed_at from seo_report (ISO 8601 string).
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_trend ⇒ Symbol
Get traffic trend based on historical data
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_maps ⇒ ActiveRecord::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).
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
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
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
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 |
#url ⇒ String
Constructs the full URL from WEB_URL + locale + path
Example: locale='en-US', path='/products/foo' => 'https://www.warmlyyours.com/en-US/products/foo'
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_path ⇒ String
Extract URL path for matching with external data
Now simply returns the stored path
512 513 514 |
# File 'app/models/site_map.rb', line 512 def url_path path end |
#visit_count_90d ⇒ Integer?
Visit count over 90-day window
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_cache ⇒ Object
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 |