feat(seo): Display all collected SEO data in 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
Route passes 16 new fields from DB (technical SEO, meta tags, structured data, performance) plus CrUX/security/image metrics. Template shows new sections: Meta Tags & Content, CrUX Field Data, Security Headers (score X/4), Image Optimization (% modern formats), and 9 new Technical SEO checklist items. Migration 059 adds 16 columns for persisting live data. AI service now saves CrUX/security/image data to DB during analysis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4c104744ea
commit
ea3d26282f
@ -156,6 +156,29 @@ def _collect_seo_data(db, company) -> dict:
|
||||
except Exception as e:
|
||||
logger.warning(f"CrUX error for {company.website}: {e}")
|
||||
|
||||
# Persist live-collected data to DB for dashboard display
|
||||
try:
|
||||
if security_headers:
|
||||
for key, val in security_headers.items():
|
||||
setattr(analysis, key, val)
|
||||
if image_formats:
|
||||
analysis.modern_image_count = image_formats.get('modern_format_count')
|
||||
analysis.legacy_image_count = image_formats.get('legacy_format_count')
|
||||
analysis.modern_image_ratio = image_formats.get('modern_format_ratio')
|
||||
if crux_data:
|
||||
analysis.crux_lcp_ms = crux_data.get('crux_lcp_ms')
|
||||
analysis.crux_inp_ms = crux_data.get('crux_inp_ms')
|
||||
analysis.crux_cls = crux_data.get('crux_cls')
|
||||
analysis.crux_fcp_ms = crux_data.get('crux_fcp_ms')
|
||||
analysis.crux_ttfb_ms = crux_data.get('crux_ttfb_ms')
|
||||
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')
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to persist live metrics for {company.name}: {e}")
|
||||
db.rollback()
|
||||
|
||||
return {
|
||||
'company_name': company.name,
|
||||
'company_category': company.category.name if company.category else None,
|
||||
|
||||
@ -128,6 +128,44 @@ def seo_audit_dashboard(slug):
|
||||
# Social sharing
|
||||
'has_og_tags': analysis.has_og_tags,
|
||||
'has_twitter_cards': analysis.has_twitter_cards,
|
||||
# Technical SEO details
|
||||
'has_sitemap': analysis.has_sitemap,
|
||||
'has_robots_txt': analysis.has_robots_txt,
|
||||
'has_canonical': analysis.has_canonical,
|
||||
'canonical_url': analysis.canonical_url,
|
||||
'is_indexable': analysis.is_indexable,
|
||||
'noindex_reason': analysis.noindex_reason,
|
||||
'is_mobile_friendly': analysis.is_mobile_friendly,
|
||||
'viewport_configured': analysis.viewport_configured,
|
||||
'html_lang': analysis.html_lang,
|
||||
'has_hreflang': analysis.has_hreflang,
|
||||
# Meta tags
|
||||
'meta_title': analysis.meta_title,
|
||||
'meta_description': analysis.meta_description,
|
||||
# Structured data
|
||||
'has_structured_data': analysis.has_structured_data,
|
||||
'structured_data_types': analysis.structured_data_types,
|
||||
# Performance
|
||||
'load_time_ms': analysis.load_time_ms,
|
||||
'word_count_homepage': analysis.word_count_homepage,
|
||||
# CrUX field data (real user metrics)
|
||||
'crux_lcp_ms': analysis.crux_lcp_ms,
|
||||
'crux_inp_ms': analysis.crux_inp_ms,
|
||||
'crux_cls': float(analysis.crux_cls) if analysis.crux_cls is not None else None,
|
||||
'crux_fcp_ms': analysis.crux_fcp_ms,
|
||||
'crux_ttfb_ms': analysis.crux_ttfb_ms,
|
||||
'crux_lcp_good_pct': float(analysis.crux_lcp_good_pct) if analysis.crux_lcp_good_pct is not None else None,
|
||||
'crux_inp_good_pct': float(analysis.crux_inp_good_pct) if analysis.crux_inp_good_pct is not None else None,
|
||||
# Security headers
|
||||
'has_hsts': analysis.has_hsts,
|
||||
'has_csp': analysis.has_csp,
|
||||
'has_x_frame_options': analysis.has_x_frame_options,
|
||||
'has_x_content_type_options': analysis.has_x_content_type_options,
|
||||
'security_headers_count': analysis.security_headers_count,
|
||||
# Image formats
|
||||
'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,
|
||||
# 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],
|
||||
}
|
||||
|
||||
24
database.py
24
database.py
@ -28,7 +28,7 @@ import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from sqlalchemy import create_engine, Column, Integer, String, Text, Boolean, DateTime, ForeignKey, Table, Numeric, Date, Time, TypeDecorator, UniqueConstraint, Enum
|
||||
from sqlalchemy import create_engine, Column, Integer, SmallInteger, String, Text, Boolean, DateTime, ForeignKey, Table, Numeric, Date, Time, TypeDecorator, UniqueConstraint, Enum
|
||||
from sqlalchemy.dialects.postgresql import ARRAY as PG_ARRAY, JSONB as PG_JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, relationship
|
||||
@ -1125,6 +1125,28 @@ class CompanyWebsiteAnalysis(Base):
|
||||
html_lang = Column(String(10)) # Language attribute from <html lang="...">
|
||||
has_hreflang = Column(Boolean, default=False) # Whether page has hreflang tags
|
||||
|
||||
# === CrUX FIELD DATA (Chrome User Experience Report) ===
|
||||
crux_lcp_ms = Column(Integer) # Real user LCP p75 in milliseconds
|
||||
crux_inp_ms = Column(Integer) # Real user INP p75 in milliseconds
|
||||
crux_cls = Column(Numeric(6, 4)) # Real user CLS p75
|
||||
crux_fcp_ms = Column(Integer) # Real user FCP p75 in milliseconds
|
||||
crux_ttfb_ms = Column(Integer) # Real user TTFB p75 in milliseconds
|
||||
crux_lcp_good_pct = Column(Numeric(5, 2)) # % of users with good LCP
|
||||
crux_inp_good_pct = Column(Numeric(5, 2)) # % of users with good INP
|
||||
crux_period_end = Column(Date) # End date of CrUX data collection period
|
||||
|
||||
# === SECURITY HEADERS ===
|
||||
has_hsts = Column(Boolean) # Strict-Transport-Security header present
|
||||
has_csp = Column(Boolean) # Content-Security-Policy header present
|
||||
has_x_frame_options = Column(Boolean) # X-Frame-Options header present
|
||||
has_x_content_type_options = Column(Boolean) # X-Content-Type-Options header present
|
||||
security_headers_count = Column(SmallInteger) # Count of security headers (0-4)
|
||||
|
||||
# === IMAGE FORMAT ANALYSIS ===
|
||||
modern_image_count = Column(Integer) # WebP + AVIF + SVG images count
|
||||
legacy_image_count = Column(Integer) # JPG + PNG + GIF images count
|
||||
modern_image_ratio = Column(Numeric(5, 2)) # % of images in modern formats
|
||||
|
||||
# === 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
|
||||
|
||||
28
database/migrations/059_seo_extended_metrics.sql
Normal file
28
database/migrations/059_seo_extended_metrics.sql
Normal file
@ -0,0 +1,28 @@
|
||||
-- Migration 059: Add CrUX field data, security headers, and image format columns
|
||||
-- These metrics are currently collected live during AI analysis but not persisted
|
||||
-- Adding DB columns allows displaying them in the SEO audit dashboard
|
||||
|
||||
-- CrUX field data (Chrome User Experience Report - real user metrics)
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS crux_lcp_ms INTEGER;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS crux_inp_ms INTEGER;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS crux_cls NUMERIC(6,4);
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS crux_fcp_ms INTEGER;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS crux_ttfb_ms INTEGER;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS crux_lcp_good_pct NUMERIC(5,2);
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS crux_inp_good_pct NUMERIC(5,2);
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS crux_period_end DATE;
|
||||
|
||||
-- Security headers
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS has_hsts BOOLEAN;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS has_csp BOOLEAN;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS has_x_frame_options BOOLEAN;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS has_x_content_type_options BOOLEAN;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS security_headers_count SMALLINT;
|
||||
|
||||
-- Image format analysis
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS modern_image_count INTEGER;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS legacy_image_count INTEGER;
|
||||
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS modern_image_ratio NUMERIC(5,2);
|
||||
|
||||
-- Grant permissions to application user
|
||||
GRANT ALL ON TABLE company_website_analysis TO nordabiz_app;
|
||||
@ -619,6 +619,70 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.crux_lcp_ms is not none %}
|
||||
<!-- CrUX Field Data Section -->
|
||||
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
Dane z Chrome UX Report (realni uzytkownicy)
|
||||
</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;">Metryki p75 z realnych sesji uzytkownikow Chrome (ostatnie 28 dni)</p>
|
||||
<div class="metrics-grid">
|
||||
{% set crux_lcp = seo_data.crux_lcp_ms %}
|
||||
{% set crux_lcp_class = 'good' if crux_lcp <= 2500 else ('medium' if crux_lcp <= 4000 else 'poor') %}
|
||||
<div class="metric-card {{ crux_lcp_class }}">
|
||||
<div class="metric-name">LCP (Field)</div>
|
||||
<div class="metric-value {{ crux_lcp_class }}">{{ '%.1f'|format(crux_lcp / 1000) }}s</div>
|
||||
{% if seo_data.crux_lcp_good_pct is not none %}
|
||||
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: 4px;">{{ '%.0f'|format(seo_data.crux_lcp_good_pct) }}% dobrych</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if seo_data.crux_inp_ms is not none %}
|
||||
{% set crux_inp = seo_data.crux_inp_ms %}
|
||||
{% set crux_inp_class = 'good' if crux_inp <= 200 else ('medium' if crux_inp <= 500 else 'poor') %}
|
||||
<div class="metric-card {{ crux_inp_class }}">
|
||||
<div class="metric-name">INP (Field)</div>
|
||||
<div class="metric-value {{ crux_inp_class }}">{{ crux_inp }}ms</div>
|
||||
{% if seo_data.crux_inp_good_pct is not none %}
|
||||
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: 4px;">{{ '%.0f'|format(seo_data.crux_inp_good_pct) }}% dobrych</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.crux_cls is not none %}
|
||||
{% set crux_cls = seo_data.crux_cls %}
|
||||
{% set crux_cls_class = 'good' if crux_cls < 0.1 else ('medium' if crux_cls < 0.25 else 'poor') %}
|
||||
<div class="metric-card {{ crux_cls_class }}">
|
||||
<div class="metric-name">CLS (Field)</div>
|
||||
<div class="metric-value {{ crux_cls_class }}">{{ '%.3f'|format(crux_cls) }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.crux_fcp_ms is not none %}
|
||||
{% set crux_fcp = seo_data.crux_fcp_ms %}
|
||||
{% set crux_fcp_class = 'good' if crux_fcp <= 1800 else ('medium' if crux_fcp <= 3000 else 'poor') %}
|
||||
<div class="metric-card {{ crux_fcp_class }}">
|
||||
<div class="metric-name">FCP (Field)</div>
|
||||
<div class="metric-value {{ crux_fcp_class }}">{{ '%.1f'|format(crux_fcp / 1000) }}s</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.crux_ttfb_ms is not none %}
|
||||
{% set crux_ttfb = seo_data.crux_ttfb_ms %}
|
||||
{% set crux_ttfb_class = 'good' if crux_ttfb <= 800 else ('medium' if crux_ttfb <= 1800 else 'poor') %}
|
||||
<div class="metric-card {{ crux_ttfb_class }}">
|
||||
<div class="metric-name">TTFB (Field)</div>
|
||||
<div class="metric-value {{ crux_ttfb_class }}">{{ crux_ttfb }}ms</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.local_seo_score is not none %}
|
||||
<!-- Local SEO Section -->
|
||||
<h2 class="section-title">
|
||||
@ -745,6 +809,64 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.meta_title or seo_data.meta_description or seo_data.load_time_ms is not none or seo_data.word_count_homepage is not none %}
|
||||
<!-- Meta Tags & Content -->
|
||||
<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="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/>
|
||||
</svg>
|
||||
Meta Tagi i Tresc
|
||||
</h2>
|
||||
|
||||
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
|
||||
{% if seo_data.meta_title %}
|
||||
{% set title_len = seo_data.meta_title|length %}
|
||||
{% set title_status = 'good' if title_len >= 50 and title_len <= 60 else ('medium' if title_len >= 30 and title_len <= 70 else 'poor') %}
|
||||
<div style="margin-bottom: var(--spacing-md);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
|
||||
<span style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary);">Meta Title</span>
|
||||
<span style="font-size: var(--font-size-xs); padding: 2px 8px; border-radius: var(--radius-sm); background: {{ '#dcfce7' if title_status == 'good' else ('#fef3c7' if title_status == 'medium' else '#fee2e2') }}; color: {{ '#10b981' if title_status == 'good' else ('#f59e0b' if title_status == 'medium' else '#ef4444') }};">{{ title_len }} znakow {% if title_status == 'good' %}(idealnie){% elif title_len < 50 %}(za krotki){% else %}(za dlugi){% endif %}</span>
|
||||
</div>
|
||||
<div style="padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); font-size: var(--font-size-sm); color: var(--text-secondary); word-break: break-word;">{{ seo_data.meta_title }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.meta_description %}
|
||||
{% set desc_len = seo_data.meta_description|length %}
|
||||
{% set desc_status = 'good' if desc_len >= 150 and desc_len <= 160 else ('medium' if desc_len >= 120 and desc_len <= 180 else 'poor') %}
|
||||
<div style="margin-bottom: var(--spacing-md);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
|
||||
<span style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary);">Meta Description</span>
|
||||
<span style="font-size: var(--font-size-xs); padding: 2px 8px; border-radius: var(--radius-sm); background: {{ '#dcfce7' if desc_status == 'good' else ('#fef3c7' if desc_status == 'medium' else '#fee2e2') }}; color: {{ '#10b981' if desc_status == 'good' else ('#f59e0b' if desc_status == 'medium' else '#ef4444') }};">{{ desc_len }} znakow {% if desc_status == 'good' %}(idealnie){% elif desc_len < 150 %}(za krotki){% else %}(za dlugi){% endif %}</span>
|
||||
</div>
|
||||
<div style="padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); font-size: var(--font-size-sm); color: var(--text-secondary); word-break: break-word;">{{ seo_data.meta_description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.load_time_ms is not none or seo_data.word_count_homepage is not none %}
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--spacing-sm);">
|
||||
{% if seo_data.load_time_ms is not none %}
|
||||
{% set lt = seo_data.load_time_ms %}
|
||||
{% set lt_status = 'good' if lt < 1000 else ('medium' if lt < 3000 else 'poor') %}
|
||||
<div style="padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); text-align: center;">
|
||||
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary);">Czas ladowania</div>
|
||||
<div style="font-size: var(--font-size-lg); font-weight: 700; color: {{ '#10b981' if lt_status == 'good' else ('#f59e0b' if lt_status == 'medium' else '#ef4444') }};">{{ '%.1f'|format(lt / 1000) }}s</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.word_count_homepage is not none %}
|
||||
{% set wc = seo_data.word_count_homepage %}
|
||||
{% set wc_status = 'good' if wc >= 300 else ('medium' if wc >= 100 else 'poor') %}
|
||||
<div style="padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); text-align: center;">
|
||||
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary);">Liczba slow</div>
|
||||
<div style="font-size: var(--font-size-lg); font-weight: 700; color: {{ '#10b981' if wc_status == 'good' else ('#f59e0b' if wc_status == 'medium' else '#ef4444') }};">{{ wc }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.has_ssl is not none or seo_data.has_google_analytics is not none or seo_data.h1_count is not none or seo_data.total_images is not none %}
|
||||
<!-- Technical SEO Checklist -->
|
||||
<h2 class="section-title">
|
||||
@ -814,6 +936,69 @@
|
||||
<span style="font-size: var(--font-size-sm);">Linki: {{ seo_data.internal_links_count or 0 }} wew., {{ seo_data.external_links_count or 0 }} zew.{% if broken > 0 %}, {{ broken }} uszkodzonych{% endif %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.has_sitemap is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_sitemap else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_sitemap else '#ef4444' }};">{{ '✓' if seo_data.has_sitemap else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Sitemap XML</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.has_robots_txt is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_robots_txt else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_robots_txt else '#ef4444' }};">{{ '✓' if seo_data.has_robots_txt else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Robots.txt</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.has_canonical is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_canonical else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_canonical else '#ef4444' }};">{{ '✓' if seo_data.has_canonical else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Canonical URL{% if seo_data.has_canonical and seo_data.canonical_url %}: <span style="color: var(--text-secondary); word-break: break-all;">{{ seo_data.canonical_url[:60] }}{% if seo_data.canonical_url|length > 60 %}...{% endif %}</span>{% endif %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.is_indexable is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.is_indexable else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.is_indexable else '#ef4444' }};">{{ '✓' if seo_data.is_indexable else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Indeksowalnosc{% if not seo_data.is_indexable and seo_data.noindex_reason %} ({{ seo_data.noindex_reason }}){% endif %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.is_mobile_friendly is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.is_mobile_friendly else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.is_mobile_friendly else '#ef4444' }};">{{ '✓' if seo_data.is_mobile_friendly else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Mobile Friendly</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.viewport_configured is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.viewport_configured else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.viewport_configured else '#ef4444' }};">{{ '✓' if seo_data.viewport_configured else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Viewport Meta Tag</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.html_lang %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: #dcfce7;">
|
||||
<span style="color: #10b981;">✓</span>
|
||||
<span style="font-size: var(--font-size-sm);">HTML Lang: <strong>{{ seo_data.html_lang }}</strong></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.has_hreflang is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_hreflang else '#f3f4f6' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_hreflang else '#9ca3af' }};">{{ '✓' if seo_data.has_hreflang else '—' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Hreflang{% if not seo_data.has_hreflang %} (opcjonalne){% endif %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.has_structured_data is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_structured_data else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_structured_data else '#ef4444' }};">{{ '✓' if seo_data.has_structured_data else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Dane strukturalne{% if seo_data.has_structured_data and seo_data.structured_data_types %}: {{ seo_data.structured_data_types|join(', ') }}{% endif %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if seo_data.h1_text %}
|
||||
@ -825,6 +1010,78 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.has_hsts is not none %}
|
||||
<!-- Security Headers Section -->
|
||||
<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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
Naglowki bezpieczenstwa
|
||||
</h2>
|
||||
|
||||
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
|
||||
{% set sec_count = seo_data.security_headers_count or 0 %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-md); margin-bottom: var(--spacing-md);">
|
||||
<div style="font-size: var(--font-size-xl); font-weight: 700; color: {{ '#10b981' if sec_count == 4 else ('#f59e0b' if sec_count >= 2 else '#ef4444') }};">{{ sec_count }}/4</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">
|
||||
{% if sec_count == 4 %}Wszystkie naglowki bezpieczenstwa skonfigurowane{% elif sec_count >= 2 %}Czesciowa ochrona — brakuje {{ 4 - sec_count }} naglowkow{% else %}Slaba ochrona — wymagana konfiguracja{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--spacing-sm);">
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_hsts else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_hsts else '#ef4444' }};">{{ '✓' if seo_data.has_hsts else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Strict-Transport-Security (HSTS)</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_csp else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_csp else '#ef4444' }};">{{ '✓' if seo_data.has_csp else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Content-Security-Policy (CSP)</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_x_frame_options else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_x_frame_options else '#ef4444' }};">{{ '✓' if seo_data.has_x_frame_options else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">X-Frame-Options</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_x_content_type_options else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if seo_data.has_x_content_type_options else '#ef4444' }};">{{ '✓' if seo_data.has_x_content_type_options else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">X-Content-Type-Options</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if seo_data.modern_image_ratio is not none %}
|
||||
<!-- Image Optimization Section -->
|
||||
<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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Optymalizacja obrazow
|
||||
</h2>
|
||||
|
||||
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
|
||||
{% set ratio = seo_data.modern_image_ratio %}
|
||||
{% set ratio_class = 'good' if ratio >= 80 else ('medium' if ratio >= 40 else 'poor') %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-lg); margin-bottom: var(--spacing-md);">
|
||||
<div>
|
||||
<div style="font-size: var(--font-size-xl); font-weight: 700; color: {{ '#10b981' if ratio_class == 'good' else ('#f59e0b' if ratio_class == 'medium' else '#ef4444') }};">{{ '%.0f'|format(ratio) }}%</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">nowoczesnych formatow</div>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="height: 12px; background: #e5e7eb; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 100%; width: {{ ratio }}%; background: {{ '#10b981' if ratio_class == 'good' else ('#f59e0b' if ratio_class == 'medium' else '#ef4444') }}; border-radius: 6px; transition: width 0.3s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: var(--spacing-lg); font-size: var(--font-size-sm); color: var(--text-secondary);">
|
||||
{% if seo_data.modern_image_count is not none %}
|
||||
<span>WebP/AVIF/SVG: <strong style="color: #10b981;">{{ seo_data.modern_image_count }}</strong></span>
|
||||
{% endif %}
|
||||
{% if seo_data.legacy_image_count is not none %}
|
||||
<span>JPG/PNG/GIF: <strong style="color: {{ '#ef4444' if seo_data.legacy_image_count > 0 else '#10b981' }};">{{ seo_data.legacy_image_count }}</strong></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- No Audit State -->
|
||||
<div class="no-audit-state">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user