Class: Crm::SiteMapsController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- Crm::SiteMapsController
- Defined in:
- app/controllers/crm/site_maps_controller.rb
Overview
SEO dashboard for SiteMap pages: search/filter, edge-cache purge,
sitemap regeneration, per-page AI analysis, and keyword/GSC/GA4 syncing.
Constant Summary collapse
- KEYWORD_LOOKUP_LIMIT =
Max results returned by #keyword_lookup's TomSelect endpoint.
20- PATH_PREDICATES =
Path predicates.
{ 'contains' => ->(val) { ['site_maps.path ILIKE ?', "%#{val}%"] }, 'starts_with' => ->(val) { ['site_maps.path ILIKE ?', "#{val}%"] }, 'ends_with' => ->(val) { ['site_maps.path ILIKE ?', "%#{val}"] }, 'equals' => ->(val) { ['site_maps.path = ?', val] } }.freeze
Constants included from Controllers::ReferenceFindable
Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS
Constants included from Controllers::AnalyticsEvents
Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY
Constants included from Controllers::ErrorRendering
Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES
Instance Method Summary collapse
-
#action_items ⇒ void
Lists open SEO recommendations, grouped by page path (with locale-specific vs. shared recs split out), paginated by distinct path.
-
#add_keyword ⇒ Object
POST /site_maps/:id/add_keyword — add a keyword to this page's list (ad-hoc, not from sync).
-
#analyze ⇒ void
Runs a full SEO analysis (including data syncs) on the page.
-
#analyze_only ⇒ void
Runs SEO analysis using existing data only, skipping GSC/GA4/keyword syncs.
-
#analyze_premium ⇒ void
Runs a premium-model (Opus) SEO analysis on the page.
-
#apply_suggestion_to_selected ⇒ Object
PATCH /site_maps/:id/apply_suggestion_to_selected — set keyword_target from intent-based suggestion for selected keywords.
-
#index ⇒ void
Lists site maps with dashboard stats, recommendation counts, and ad spend, running any queued auto-purge action.
-
#keyword_lookup ⇒ Object
GET /site_maps/keyword_lookup.json — TomSelect-compatible ranking keyword search.
-
#lookup ⇒ Object
GET /site_maps/lookup.json — TomSelect-compatible search for sitemap paths.
-
#perform ⇒ void
Runs the selected bulk action (cache warm, edge purge, or tag purge) on the checked site maps.
-
#recrawl ⇒ void
Re-crawls the page and re-extracts its rendered content/schema types.
-
#regenerate ⇒ void
Enqueues a background job to regenerate the XML sitemap.
-
#show ⇒ void
Shows a single site map's detail page.
-
#sync_ga4 ⇒ void
Syncs the page's last-30-day Google Analytics 4 metrics.
-
#sync_gsc ⇒ void
Syncs the page's last-28-day Google Search Console metrics.
-
#sync_keywords ⇒ void
Syncs ranking keywords for the page from GSC or Ahrefs.
-
#sync_visits ⇒ void
Syncs the page's 30-day visit counts.
-
#update_keyword_target ⇒ Object
PATCH /site_maps/:id/update_keyword_target?seo_page_keyword_id=123&keyword_target=desired.
-
#update_seo_targeting ⇒ void
Updates the page's target SEO query.
Methods inherited from CrmController
#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen
Methods inherited from ApplicationController
#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y
Methods included from Controllers::ReturnPathHandling
#check_for_return_path, #redirect_to_return_path_or_default
Methods included from Controllers::AnalyticsEvents
#consume_queued_analytics_events, #registration_lead_type, #track_event
Methods included from Controllers::DeviceDetection
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
Methods included from Controllers::TrackingDetection
#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?
Methods included from Controllers::AcceleratedFileSending
#send_file_accelerated, #send_upload_accelerated
Methods included from Controllers::ErrorRendering
#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback
Methods included from Controllers::TurnstileVerification
#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!
Methods included from Controllers::CloudflareCaching
edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session
Methods included from Controllers::Webpackable
#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?
Methods included from Controllers::Localizable
#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale
Methods included from Controllers::Authenticable
#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak
Methods included from UrlsHelper
#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?
Instance Method Details
#action_items ⇒ void
This method returns an undefined value.
Lists open SEO recommendations, grouped by page path (with
locale-specific vs. shared recs split out), paginated by distinct path.
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 |
# File 'app/controllers/crm/site_maps_controller.rb', line 365 def action_items :manage, SiteMap rec_scope = filtered_recommendations_scope paths_with_recs = rec_scope.joins(:site_map) .select('site_maps.path') .distinct .order('site_maps.path') .pluck('site_maps.path') @pagy, paginated_paths = pagy_paths(paths_with_recs, limit: 50) site_maps_for_paths = SiteMap.where(path: paginated_paths).order(:path, :locale).index_by(&:id) site_map_ids = site_maps_for_paths.keys recs = rec_scope.where(site_map_id: site_map_ids) .includes(:site_map) .by_priority .order(created_at: :desc) @path_groups = build_path_groups(paginated_paths, recs) respond_to do |format| format.html format.turbo_stream end end |
#add_keyword ⇒ Object
POST /site_maps/:id/add_keyword — add a keyword to this page's list (ad-hoc, not from sync)
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'app/controllers/crm/site_maps_controller.rb', line 230 def add_keyword :manage, SiteMap @site_map = SiteMap.find(params[:id]) raw = params[:keyword].to_s.strip if raw.blank? flash[:error] = 'Keyword cannot be blank.' redirect_to site_map_path(@site_map, anchor: 'sm-keywords') and return end if SeoPageKeyword.noise?(raw) flash[:error] = 'Keyword is too short or invalid.' redirect_to site_map_path(@site_map, anchor: 'sm-keywords') and return end existing = @site_map.seo_page_keywords.find_by('LOWER(keyword) = ?', raw.downcase) if existing flash[:info] = "Keyword already in list." redirect_to site_map_path(@site_map, anchor: 'sm-keywords') and return end @site_map.seo_page_keywords.create!(keyword: raw, position: nil) flash[:success] = "Added \"#{raw}\" to this page's keywords." redirect_to site_map_path(@site_map, anchor: 'sm-keywords') rescue ActiveRecord::RecordInvalid => e flash[:error] = e.record.errors..to_sentence redirect_to site_map_path(@site_map, anchor: 'sm-keywords') end |
#analyze ⇒ void
This method returns an undefined value.
Runs a full SEO analysis (including data syncs) on the page.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'app/controllers/crm/site_maps_controller.rb', line 104 def analyze :manage, SiteMap @site_map = SiteMap.find(params[:id]) = { return_path: site_map_path(@site_map) } job_id = SeoPageAnalysisWorker.perform_async(@site_map.id, ) if job_id flash[:info] = 'SEO analysis started. You will be redirected when complete.' redirect_to job_path(job_id) else flash[:warning] = 'An analysis for this page is already running. Please wait for it to finish.' redirect_to site_map_path(@site_map) end end |
#analyze_only ⇒ void
This method returns an undefined value.
Runs SEO analysis using existing data only, skipping GSC/GA4/keyword
syncs.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'app/controllers/crm/site_maps_controller.rb', line 124 def analyze_only :manage, SiteMap @site_map = SiteMap.find(params[:id]) = { return_path: site_map_path(@site_map), skip_syncs: true } job_id = SeoPageAnalysisWorker.perform_async(@site_map.id, ) if job_id flash[:info] = 'AI analysis started (using existing data). You will be redirected when complete.' redirect_to job_path(job_id) else flash[:warning] = 'An analysis for this page is already running. Please wait for it to finish.' redirect_to site_map_path(@site_map) end end |
#analyze_premium ⇒ void
This method returns an undefined value.
Runs a premium-model (Opus) SEO analysis on the page.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'app/controllers/crm/site_maps_controller.rb', line 143 def analyze_premium :manage, SiteMap @site_map = SiteMap.find(params[:id]) = { return_path: site_map_path(@site_map), premium: true } job_id = SeoPageAnalysisWorker.perform_async(@site_map.id, ) if job_id flash[:info] = 'Premium SEO analysis started (Opus). You will be redirected when complete.' redirect_to job_path(job_id) else flash[:warning] = 'An analysis for this page is already running. Please wait for it to finish.' redirect_to site_map_path(@site_map) end end |
#apply_suggestion_to_selected ⇒ Object
PATCH /site_maps/:id/apply_suggestion_to_selected — set keyword_target from intent-based suggestion for selected keywords
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'app/controllers/crm/site_maps_controller.rb', line 213 def apply_suggestion_to_selected :manage, SiteMap @site_map = SiteMap.find(params[:id]) ids = Array(params[:seo_page_keyword_ids]).filter_map(&:to_i).uniq updated = 0 @site_map.seo_page_keywords.where(id: ids).find_each do |pk| suggestion = @site_map.suggested_keyword_target_for(pk.keyword, search_volume: pk.search_volume) pk.update!(keyword_target: suggestion) updated += 1 end flash[:success] = "Applied suggestion to #{updated} keyword(s)." redirect_to site_map_path(@site_map, anchor: 'sm-keywords') end |
#index ⇒ void
This method returns an undefined value.
Lists site maps with dashboard stats, recommendation counts, and ad
spend, running any queued auto-purge action.
13 14 15 16 17 18 19 20 21 22 23 24 |
# File 'app/controllers/crm/site_maps_controller.rb', line 13 def index :manage, SiteMap @search_form = Crm::SiteMapSearchForm.new(params) @search_form.ransack_search.sorts = 'updated_at desc' if @search_form.ransack_search.sorts.empty? @pagy, @site_maps = pagy(:countless, @search_form.results, limit: 50) load_index_stats load_recommendation_counts load_ads_spend handle_auto_purge_action if @search_form.auto_purge? end |
#keyword_lookup ⇒ Object
GET /site_maps/keyword_lookup.json — TomSelect-compatible ranking keyword search
91 92 93 94 95 96 97 98 99 |
# File 'app/controllers/crm/site_maps_controller.rb', line 91 def keyword_lookup :manage, SiteMap scope = SeoPageKeyword.ranking_keyword_suggestions(params[:q]) render json: TomSelect.format_json_results(self, scope, params[:page], KEYWORD_LOOKUP_LIMIT) { |seo_page_keyword| { id: seo_page_keyword.keyword, text: seo_page_keyword.keyword } } end |
#lookup ⇒ Object
GET /site_maps/lookup.json — TomSelect-compatible search for sitemap paths
77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'app/controllers/crm/site_maps_controller.rb', line 77 def lookup :manage, SiteMap term = params[:q].to_s.strip scope = SiteMap.active.where(locale: 'en-US') scope = term.present? ? scope.lookup_search(term) : scope.order(:path) render json: TomSelect.format_json_results(self, scope, params[:page], params[:per_page]) { |sm| title = sm.extracted_title.presence { id: sm.path, text: title ? "#{sm.path} — #{title}" : sm.path } } end |
#perform ⇒ void
This method returns an undefined value.
Runs the selected bulk action (cache warm, edge purge, or tag purge) on
the checked site maps.
30 31 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 |
# File 'app/controllers/crm/site_maps_controller.rb', line 30 def perform :manage, SiteMap @search_form = Crm::SiteMapSearchForm.new(params) @site_maps = @search_form.results.where(id: params[:smids]) if params[:fetch].present? Cache::SiteCrawler.new.process(pages: @site_maps) flash[:info] = 'Cached warmed up for selected urls' @site_maps.reload elsif params[:purge].present? || params[:purge_all].present? # Include each product page's lazy section fragments (documents/reviews/…), # which Cloudflare caches as separate entries — purging the page URL alone # leaves them stale. See SiteMap#section_cache_urls. urls = @site_maps.flat_map { |sm| [sm.url, *sm.section_cache_urls] }.uniq EdgeCacheWorker.perform_async('urls' => urls) flash[:info] = "Edge Cache Purge queued for #{urls.size} url(s)" elsif params[:purge_by_tag].present? purge_selected_by_tag else flash[:warning] = 'No action selected' end # Redirect (PRG) instead of render :index — a 200 render on this POST is # dropped by Turbo Drive, and it lands flash[:info] above on the next page. # Matches the sibling #regenerate. redirect_to_return_path_or_default site_maps_path end |
#recrawl ⇒ void
This method returns an undefined value.
Re-crawls the page and re-extracts its rendered content/schema types.
343 344 345 346 347 348 349 350 351 |
# File 'app/controllers/crm/site_maps_controller.rb', line 343 def recrawl :manage, SiteMap @site_map = SiteMap.find(params[:id]) Cache::SiteCrawler.new.process(pages: SiteMap.where(id: @site_map.id), extract_content: true) @site_map.reload schema_summary = @site_map.rendered_schema_types.join(', ').presence || 'none' flash[:info] = "Recrawled #{@site_map.path} — Status: #{@site_map.last_status}, Schema: #{schema_summary}" redirect_back_or_to site_maps_path end |
#regenerate ⇒ void
This method returns an undefined value.
Enqueues a background job to regenerate the XML sitemap.
61 62 63 64 65 66 |
# File 'app/controllers/crm/site_maps_controller.rb', line 61 def regenerate :manage, SiteMap job_id = ServiceRunner.perform_async([Sitemap::SitemapGenerator.name]) flash[:info] = "Job #{job_id} was enqued" redirect_to_return_path_or_default site_maps_path end |
#show ⇒ void
This method returns an undefined value.
Shows a single site map's detail page.
71 72 73 74 |
# File 'app/controllers/crm/site_maps_controller.rb', line 71 def show :manage, SiteMap @site_map = SiteMap.find(params[:id]) end |
#sync_ga4 ⇒ void
This method returns an undefined value.
Syncs the page's last-30-day Google Analytics 4 metrics.
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'app/controllers/crm/site_maps_controller.rb', line 310 def sync_ga4 :manage, SiteMap @site_map = SiteMap.find(params[:id]) ga4 = Seo::Ga4ApiClient.new start_date = 30.days.ago.to_date end_date = Date.yesterday page_path = "/#{@site_map.locale}#{@site_map.path}" metrics = ga4.page_metrics(page_path: page_path, start_date: start_date, end_date: end_date) if metrics SiteMapDataPoint.bulk_record!( site_map: @site_map, metrics: { ga4_page_views: metrics[:screen_page_views], ga4_sessions: metrics[:sessions], ga4_users: metrics[:total_users], ga4_bounce_rate: (metrics[:bounce_rate].to_f * 100).round(2), ga4_engagement_rate: (metrics[:engagement_rate].to_f * 100).round(2), ga4_avg_session_duration: metrics[:average_session_duration]&.round(2) }, period_start: start_date, period_end: end_date ) flash[:success] = "GA4 synced — #{metrics[:screen_page_views]} page views, #{metrics[:sessions]} sessions" else flash[:info] = "No GA4 data found for #{page_path}" end redirect_back_or_to site_map_path(@site_map) rescue StandardError => e flash[:error] = "GA4 sync failed: #{e.}" redirect_back_or_to site_map_path(@site_map) end |
#sync_gsc ⇒ void
This method returns an undefined value.
Syncs the page's last-28-day Google Search Console metrics.
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'app/controllers/crm/site_maps_controller.rb', line 280 def sync_gsc :manage, SiteMap @site_map = SiteMap.find(params[:id]) gsc = Seo::McpClients.gsc start_date = 28.days.ago.to_date end_date = Date.yesterday metrics = gsc.page_metrics(page_url: @site_map.production_url, start_date: start_date, end_date: end_date) if metrics SiteMapDataPoint.bulk_record!( site_map: @site_map, metrics: { gsc_clicks: metrics[:clicks], gsc_impressions: metrics[:impressions], gsc_ctr: metrics[:ctr], gsc_avg_position: metrics[:position] }, period_start: start_date, period_end: end_date ) @site_map.update_columns(seo_clicks: metrics[:clicks]) flash[:success] = "GSC synced — #{metrics[:clicks]} clicks, #{metrics[:impressions]} impressions" else flash[:info] = "No GSC data found for #{@site_map.production_url}" end redirect_back_or_to site_map_path(@site_map) rescue StandardError => e flash[:error] = "GSC sync failed: #{e.}" redirect_back_or_to site_map_path(@site_map) end |
#sync_keywords ⇒ void
This method returns an undefined value.
Syncs ranking keywords for the page from GSC or Ahrefs.
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'app/controllers/crm/site_maps_controller.rb', line 162 def sync_keywords :manage, SiteMap @site_map = SiteMap.find(params[:id]) result = Seo::KeywordSyncService.new(site_map: @site_map).process if result[:error] flash[:error] = "Keyword sync failed: #{result[:error]}" else source = result[:source] == 'gsc' ? 'GSC' : 'Ahrefs' flash[:success] = "Synced #{result[:keywords_synced]} keywords via #{source}" end redirect_back_or_to site_map_path(@site_map) end |
#sync_visits ⇒ void
This method returns an undefined value.
Syncs the page's 30-day visit counts.
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'app/controllers/crm/site_maps_controller.rb', line 258 def sync_visits :manage, SiteMap @site_map = SiteMap.find(params[:id]) result = Seo::VisitsSyncService.new(site_map_ids: [@site_map.id]).call if result[:deferred] flash[:warning] = 'Visit counts were not updated — the daily aggregate backfill is awaiting rollout approval.' elsif result[:errors].present? flash[:error] = "Visit sync failed: #{Array(result[:errors]).join('; ')}" else @site_map.reload flash[:success] = "Visit counts synced — 30d: #{@site_map.visit_count_30d || 0}" end redirect_back_or_to site_map_path(@site_map) rescue StandardError => e flash[:error] = "Visit sync failed: #{e.}" redirect_back_or_to site_map_path(@site_map) end |
#update_keyword_target ⇒ Object
PATCH /site_maps/:id/update_keyword_target?seo_page_keyword_id=123&keyword_target=desired
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'app/controllers/crm/site_maps_controller.rb', line 195 def update_keyword_target :manage, SiteMap @site_map = SiteMap.find(params[:id]) pk = @site_map.seo_page_keywords.find(params.require(:seo_page_keyword_id)) value = params[:keyword_target].to_s.strip.presence value = nil if value.present? && !SeoPageKeyword.keyword_targets.key?(value) if pk.update(keyword_target: value) flash[:success] = 'Keyword target updated.' else flash[:error] = pk.errors..to_sentence end redirect_to site_map_path(@site_map, anchor: 'sm-keywords') end |
#update_seo_targeting ⇒ void
This method returns an undefined value.
Updates the page's target SEO query.
181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'app/controllers/crm/site_maps_controller.rb', line 181 def update_seo_targeting :manage, SiteMap @site_map = SiteMap.find(params[:id]) if @site_map.update(seo_targeting_params) flash[:success] = 'Target keyword updated.' else flash[:error] = @site_map.errors..to_sentence end redirect_back_or_to site_map_path(@site_map) end |