Class: Seo::KeywordSyncService
- Inherits:
-
BaseService
- Object
- BaseService
- Seo::KeywordSyncService
- Defined in:
- app/services/seo/keyword_sync_service.rb
Overview
Syncs organic keywords for a single SiteMap page.
Data source priority:
- Google Search Console (page_queries) — actual clicks/positions, no API quota per page
- Ahrefs (organic_keywords) — fallback when GSC returns nothing (new/unindexed pages)
Search volume enrichment: Google Keyword Planner (authoritative, already in stack).
After saving keywords, updates seo_report['cannibalization'] so the AI batch
analysis always has current cannibalization context without a separate pass.
Constant Summary collapse
- MAX_KEYWORDS =
100- GOOGLE_BATCH_SIZE =
20- GSC_LOOKBACK_DAYS =
90
Instance Method Summary collapse
-
#initialize(options = {}) ⇒ KeywordSyncService
constructor
A new instance of KeywordSyncService.
- #process ⇒ Object
Methods inherited from BaseService
#log_debug, #log_error, #log_info, #log_warning, #logger, #options, #tagged_logger
Constructor Details
#initialize(options = {}) ⇒ KeywordSyncService
Returns a new instance of KeywordSyncService.
23 24 25 26 27 28 29 30 |
# File 'app/services/seo/keyword_sync_service.rb', line 23 def initialize( = {}) super @site_map = [:site_map] @limit = [:limit] || MAX_KEYWORDS @skip_google = [:skip_google] || false @skip_ahrefs_fallback = [:skip_ahrefs_fallback] || false raise ArgumentError, 'site_map is required' unless @site_map end |
Instance Method Details
#process ⇒ Object
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'app/services/seo/keyword_sync_service.rb', line 32 def process @logger.info "[KeywordSyncService] Syncing #{@site_map.id}: #{@site_map.path}" # Step 1: Try GSC first (preferred — actual rankings, no per-page quota) keywords_data = fetch_keywords_from_gsc source = if keywords_data.present? @logger.info "[KeywordSyncService] GSC returned #{keywords_data.size} queries" 'gsc' elsif @skip_ahrefs_fallback @logger.info "[KeywordSyncService] GSC returned nothing, Ahrefs fallback disabled" return { keywords_synced: 0, source: 'none' } else @logger.info "[KeywordSyncService] GSC empty — falling back to Ahrefs" keywords_data = fetch_keywords_from_ahrefs if keywords_data.blank? @logger.warn "[KeywordSyncService] No keywords from Ahrefs either" return { keywords_synced: 0, error: 'No keywords from GSC or Ahrefs' } end @logger.info "[KeywordSyncService] Ahrefs returned #{keywords_data.size} keywords" 'ahrefs' end # Step 2: Enrich with authoritative search volume from Google Keyword Planner keywords_data = enrich_with_google_data(keywords_data) unless @skip_google # Step 3: Save to seo_page_keywords synced_count = save_keywords(keywords_data, source: source) # Step 3b: Enrich with Ahrefs SERP feature data (AI Overview, snippets, etc.) enrich_with_serp_features(source: source) # Step 4: Refresh cannibalization data in seo_report now that keywords are current update_cannibalization_in_report @site_map.update!(seo_synced_at: Time.current) @logger.info "[KeywordSyncService] Synced #{synced_count} keywords (#{source}) for #{@site_map.path}" { keywords_synced: synced_count, source: source } rescue Seo::AhrefsMcpClient::Error => e @logger.error "[KeywordSyncService] Ahrefs error: #{e.}" { keywords_synced: 0, error: e. } rescue StandardError => e @logger.error "[KeywordSyncService] Error: #{e.}" { keywords_synced: 0, error: e. } end |