feat(seo): Display Google Search Console data on SEO audit dashboard
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Add GSC columns to DB, persist OAuth data during audits, and render clicks/impressions/CTR/position with top queries table on the dashboard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
14bd4f600d
commit
bc2bc4f556
@ -193,6 +193,13 @@ def _collect_seo_data(db, company) -> dict:
|
||||
analysis.crux_lcp_good_pct = crux_data.get('crux_lcp_ms_good_pct')
|
||||
analysis.crux_inp_good_pct = crux_data.get('crux_inp_ms_good_pct')
|
||||
analysis.crux_period_end = crux_data.get('crux_period_end')
|
||||
if search_console_data:
|
||||
analysis.gsc_clicks = search_console_data.get('clicks')
|
||||
analysis.gsc_impressions = search_console_data.get('impressions')
|
||||
analysis.gsc_ctr = search_console_data.get('ctr')
|
||||
analysis.gsc_avg_position = search_console_data.get('position')
|
||||
analysis.gsc_top_queries = search_console_data.get('top_queries', [])
|
||||
analysis.gsc_period_days = search_console_data.get('period_days', 28)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to persist live metrics for {company.name}: {e}")
|
||||
|
||||
@ -166,6 +166,13 @@ def seo_audit_dashboard(slug):
|
||||
'modern_image_count': analysis.modern_image_count,
|
||||
'legacy_image_count': analysis.legacy_image_count,
|
||||
'modern_image_ratio': float(analysis.modern_image_ratio) if analysis.modern_image_ratio is not None else None,
|
||||
# Google Search Console (OAuth)
|
||||
'gsc_clicks': analysis.gsc_clicks,
|
||||
'gsc_impressions': analysis.gsc_impressions,
|
||||
'gsc_ctr': float(analysis.gsc_ctr) if analysis.gsc_ctr is not None else None,
|
||||
'gsc_avg_position': float(analysis.gsc_avg_position) if analysis.gsc_avg_position is not None else None,
|
||||
'gsc_top_queries': analysis.gsc_top_queries,
|
||||
'gsc_period_days': analysis.gsc_period_days,
|
||||
# Citations list
|
||||
'citations': [{'directory_name': c.directory_name, 'listing_url': c.listing_url, 'status': c.status, 'nap_accurate': c.nap_accurate} for c in citations],
|
||||
}
|
||||
|
||||
@ -1157,6 +1157,14 @@ class CompanyWebsiteAnalysis(Base):
|
||||
google_maps_links = Column(JSONB) # Places API: directionsUri, writeAReviewUri, etc.
|
||||
google_open_now = Column(Boolean) # Whether business is currently open (at audit time)
|
||||
|
||||
# === GOOGLE SEARCH CONSOLE (OAuth) ===
|
||||
gsc_clicks = Column(Integer) # Total clicks from Google Search in period
|
||||
gsc_impressions = Column(Integer) # Total impressions in Google Search in period
|
||||
gsc_ctr = Column(Numeric(5, 2)) # Click-through rate as percentage
|
||||
gsc_avg_position = Column(Numeric(5, 1)) # Average position in search results
|
||||
gsc_top_queries = Column(JSONB) # Top search queries with clicks/impressions
|
||||
gsc_period_days = Column(Integer, default=28) # Data collection period in days
|
||||
|
||||
# === SEO AUDIT METADATA ===
|
||||
seo_audit_version = Column(String(20)) # Version of SEO audit script used
|
||||
seo_audited_at = Column(DateTime) # Timestamp of last SEO audit
|
||||
|
||||
9
database/migrations/063_gsc_columns.sql
Normal file
9
database/migrations/063_gsc_columns.sql
Normal file
@ -0,0 +1,9 @@
|
||||
-- Migration 063: Add Google Search Console (OAuth) columns to company_website_analysis
|
||||
-- These columns persist GSC data collected via OAuth during SEO audits
|
||||
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS gsc_clicks INTEGER;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS gsc_impressions INTEGER;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS gsc_ctr NUMERIC(5,2);
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS gsc_avg_position NUMERIC(5,1);
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS gsc_top_queries JSONB;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS gsc_period_days INTEGER DEFAULT 28;
|
||||
@ -683,6 +683,80 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Google Search Console Section -->
|
||||
{% if seo_data.gsc_clicks is not none %}
|
||||
<h2 class="section-title">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
Google Search Console
|
||||
<span style="font-size: var(--font-size-xs); background: #4285f4; color: white; padding: 2px 8px; border-radius: 12px; font-weight: 500; margin-left: 8px;">OAuth</span>
|
||||
</h2>
|
||||
|
||||
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
|
||||
<p style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin: 0 0 var(--spacing-md) 0;">Dane z Google Search za ostatnie {{ seo_data.gsc_period_days or 28 }} dni</p>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-name">Klikniecia</div>
|
||||
<div class="metric-value">{{ '{:,}'.format(seo_data.gsc_clicks)|replace(',', ' ') }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-name">Wyswietlenia</div>
|
||||
<div class="metric-value">{{ '{:,}'.format(seo_data.gsc_impressions)|replace(',', ' ') }}</div>
|
||||
</div>
|
||||
{% if seo_data.gsc_ctr is not none %}
|
||||
<div class="metric-card">
|
||||
<div class="metric-name">CTR</div>
|
||||
<div class="metric-value">{{ '%.1f'|format(seo_data.gsc_ctr) }}%</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if seo_data.gsc_avg_position is not none %}
|
||||
<div class="metric-card">
|
||||
<div class="metric-name">Srednia pozycja</div>
|
||||
<div class="metric-value">{{ '%.1f'|format(seo_data.gsc_avg_position) }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if seo_data.gsc_top_queries %}
|
||||
<h3 style="font-size: var(--font-size-sm); font-weight: 600; margin: var(--spacing-lg) 0 var(--spacing-sm) 0;">Top zapytania w Google</h3>
|
||||
<div style="overflow-x: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-xs);">
|
||||
<thead>
|
||||
<tr style="border-bottom: 2px solid var(--border-color);">
|
||||
<th style="text-align: left; padding: 8px 12px; font-weight: 600;">Zapytanie</th>
|
||||
<th style="text-align: right; padding: 8px 12px; font-weight: 600;">Klikniecia</th>
|
||||
<th style="text-align: right; padding: 8px 12px; font-weight: 600;">Wyswietlenia</th>
|
||||
<th style="text-align: right; padding: 8px 12px; font-weight: 600;">CTR</th>
|
||||
<th style="text-align: right; padding: 8px 12px; font-weight: 600;">Pozycja</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in seo_data.gsc_top_queries[:5] %}
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 8px 12px; font-weight: 500;">{{ q.query }}</td>
|
||||
<td style="text-align: right; padding: 8px 12px;">{{ q.clicks }}</td>
|
||||
<td style="text-align: right; padding: 8px 12px;">{{ q.impressions }}</td>
|
||||
<td style="text-align: right; padding: 8px 12px;">{{ '%.1f'|format(q.ctr) }}%</td>
|
||||
<td style="text-align: right; padding: 8px 12px;">{{ '%.1f'|format(q.position) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- GSC CTA - no data available -->
|
||||
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl); text-align: center;">
|
||||
<svg width="32" height="32" fill="none" stroke="#9ca3af" viewBox="0 0 24 24" style="margin-bottom: var(--spacing-sm);">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<p style="color: var(--text-secondary); margin: 0 0 var(--spacing-sm) 0; font-size: var(--font-size-sm);">Polacz Google Search Console aby zobaczyc dane o widocznosci w wyszukiwarce</p>
|
||||
<a href="/konto/integracje" style="display: inline-block; padding: 8px 20px; background: #4285f4; color: white; border-radius: var(--radius-md); text-decoration: none; font-size: var(--font-size-xs); font-weight: 500;">Polacz Search Console</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.local_seo_score is not none %}
|
||||
<!-- Local SEO Section -->
|
||||
<h2 class="section-title">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user