auto-claude: 5.1 - Add GET /api/seo/audit endpoint for SEO results
Added two API endpoints for retrieving SEO audit data: - GET /api/seo/audit?company_id=X or ?slug=Y - GET /api/seo/audit/<slug> Features: - Returns pagespeed scores (SEO, performance, accessibility, best practices) - Returns on-page metrics (meta tags, headings, images, links, structured data) - Returns technical SEO checks (SSL, sitemap, robots.txt, mobile-friendly) - Returns Core Web Vitals (LCP, FID, CLS) - Automatically generates issues list from audit data - Handles companies without SEO audit gracefully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c24c545cfe
commit
db28aa6419
277
app.py
277
app.py
@ -2203,6 +2203,283 @@ def api_companies():
|
||||
db.close()
|
||||
|
||||
|
||||
def _build_seo_audit_response(company, analysis):
|
||||
"""
|
||||
Helper function to build SEO audit response JSON.
|
||||
Used by both /api/seo/audit and /api/seo/audit/<slug> endpoints.
|
||||
"""
|
||||
# Build issues list from various checks
|
||||
issues = []
|
||||
|
||||
# Check for images without alt
|
||||
if analysis.images_without_alt and analysis.images_without_alt > 0:
|
||||
issues.append({
|
||||
'severity': 'warning',
|
||||
'message': f'{analysis.images_without_alt} obrazów nie ma atrybutu alt',
|
||||
'category': 'accessibility'
|
||||
})
|
||||
|
||||
# Check for missing meta description
|
||||
if not analysis.meta_description:
|
||||
issues.append({
|
||||
'severity': 'warning',
|
||||
'message': 'Brak meta description',
|
||||
'category': 'on_page'
|
||||
})
|
||||
|
||||
# Check H1 count (should be exactly 1)
|
||||
if analysis.h1_count is not None:
|
||||
if analysis.h1_count == 0:
|
||||
issues.append({
|
||||
'severity': 'error',
|
||||
'message': 'Brak nagłówka H1 na stronie',
|
||||
'category': 'on_page'
|
||||
})
|
||||
elif analysis.h1_count > 1:
|
||||
issues.append({
|
||||
'severity': 'warning',
|
||||
'message': f'Strona zawiera {analysis.h1_count} nagłówków H1 (zalecany: 1)',
|
||||
'category': 'on_page'
|
||||
})
|
||||
|
||||
# Check SSL
|
||||
if analysis.has_ssl is False:
|
||||
issues.append({
|
||||
'severity': 'error',
|
||||
'message': 'Strona nie używa HTTPS (brak certyfikatu SSL)',
|
||||
'category': 'security'
|
||||
})
|
||||
|
||||
# Check robots.txt
|
||||
if analysis.has_robots_txt is False:
|
||||
issues.append({
|
||||
'severity': 'info',
|
||||
'message': 'Brak pliku robots.txt',
|
||||
'category': 'technical'
|
||||
})
|
||||
|
||||
# Check sitemap
|
||||
if analysis.has_sitemap is False:
|
||||
issues.append({
|
||||
'severity': 'info',
|
||||
'message': 'Brak pliku sitemap.xml',
|
||||
'category': 'technical'
|
||||
})
|
||||
|
||||
# Check indexability
|
||||
if analysis.is_indexable is False:
|
||||
issues.append({
|
||||
'severity': 'error',
|
||||
'message': f'Strona nie jest indeksowalna: {analysis.noindex_reason or "nieznana przyczyna"}',
|
||||
'category': 'technical'
|
||||
})
|
||||
|
||||
# Check structured data
|
||||
if analysis.has_structured_data is False:
|
||||
issues.append({
|
||||
'severity': 'info',
|
||||
'message': 'Brak danych strukturalnych (Schema.org)',
|
||||
'category': 'on_page'
|
||||
})
|
||||
|
||||
# Check Open Graph tags
|
||||
if analysis.has_og_tags is False:
|
||||
issues.append({
|
||||
'severity': 'info',
|
||||
'message': 'Brak tagów Open Graph (ważne dla udostępniania w social media)',
|
||||
'category': 'social'
|
||||
})
|
||||
|
||||
# Check mobile-friendliness
|
||||
if analysis.is_mobile_friendly is False:
|
||||
issues.append({
|
||||
'severity': 'warning',
|
||||
'message': 'Strona nie jest przyjazna dla urządzeń mobilnych',
|
||||
'category': 'technical'
|
||||
})
|
||||
|
||||
# Add issues from seo_issues JSONB field if available
|
||||
if analysis.seo_issues:
|
||||
stored_issues = analysis.seo_issues if isinstance(analysis.seo_issues, list) else []
|
||||
for issue in stored_issues:
|
||||
if isinstance(issue, dict):
|
||||
issues.append(issue)
|
||||
|
||||
# Build response
|
||||
return {
|
||||
'success': True,
|
||||
'company_id': company.id,
|
||||
'company_name': company.name,
|
||||
'website': company.website,
|
||||
'seo_audit': {
|
||||
'audited_at': analysis.seo_audited_at.isoformat() if analysis.seo_audited_at else None,
|
||||
'audit_version': analysis.seo_audit_version,
|
||||
'overall_score': analysis.seo_overall_score,
|
||||
'pagespeed': {
|
||||
'seo_score': analysis.pagespeed_seo_score,
|
||||
'performance_score': analysis.pagespeed_performance_score,
|
||||
'accessibility_score': analysis.pagespeed_accessibility_score,
|
||||
'best_practices_score': analysis.pagespeed_best_practices_score
|
||||
},
|
||||
'on_page': {
|
||||
'meta_title': analysis.meta_title,
|
||||
'meta_description': analysis.meta_description,
|
||||
'h1_count': analysis.h1_count,
|
||||
'h1_text': analysis.h1_text,
|
||||
'h2_count': analysis.h2_count,
|
||||
'h3_count': analysis.h3_count,
|
||||
'total_images': analysis.total_images,
|
||||
'images_without_alt': analysis.images_without_alt,
|
||||
'images_with_alt': analysis.images_with_alt,
|
||||
'internal_links_count': analysis.internal_links_count,
|
||||
'external_links_count': analysis.external_links_count,
|
||||
'has_structured_data': analysis.has_structured_data,
|
||||
'structured_data_types': analysis.structured_data_types
|
||||
},
|
||||
'technical': {
|
||||
'has_ssl': analysis.has_ssl,
|
||||
'ssl_issuer': analysis.ssl_issuer,
|
||||
'ssl_expires_at': analysis.ssl_expires_at.isoformat() if analysis.ssl_expires_at else None,
|
||||
'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,
|
||||
'load_time_ms': analysis.load_time_ms,
|
||||
'http_status_code': analysis.http_status_code
|
||||
},
|
||||
'core_web_vitals': {
|
||||
'largest_contentful_paint_ms': analysis.largest_contentful_paint_ms,
|
||||
'first_input_delay_ms': analysis.first_input_delay_ms,
|
||||
'cumulative_layout_shift': float(analysis.cumulative_layout_shift) if analysis.cumulative_layout_shift else None
|
||||
},
|
||||
'social': {
|
||||
'has_og_tags': analysis.has_og_tags,
|
||||
'og_title': analysis.og_title,
|
||||
'og_description': analysis.og_description,
|
||||
'og_image': analysis.og_image,
|
||||
'has_twitter_cards': analysis.has_twitter_cards
|
||||
},
|
||||
'language': {
|
||||
'html_lang': analysis.html_lang,
|
||||
'has_hreflang': analysis.has_hreflang
|
||||
},
|
||||
'issues': issues
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _get_seo_audit_for_company(db, company):
|
||||
"""
|
||||
Helper function to get SEO audit data for a company.
|
||||
Returns tuple of (response_dict, status_code) or (None, None) if audit exists.
|
||||
"""
|
||||
# Get latest SEO audit for this company
|
||||
analysis = db.query(CompanyWebsiteAnalysis).filter_by(
|
||||
company_id=company.id
|
||||
).order_by(CompanyWebsiteAnalysis.analyzed_at.desc()).first()
|
||||
|
||||
if not analysis:
|
||||
return {
|
||||
'success': True,
|
||||
'company_id': company.id,
|
||||
'company_name': company.name,
|
||||
'website': company.website,
|
||||
'seo_audit': None,
|
||||
'message': 'Brak danych SEO dla tej firmy. Audyt nie został jeszcze przeprowadzony.'
|
||||
}, 200
|
||||
|
||||
# Check if SEO audit was performed (seo_audited_at is set)
|
||||
if not analysis.seo_audited_at:
|
||||
return {
|
||||
'success': True,
|
||||
'company_id': company.id,
|
||||
'company_name': company.name,
|
||||
'website': company.website,
|
||||
'seo_audit': None,
|
||||
'message': 'Audyt SEO nie został jeszcze przeprowadzony dla tej firmy.'
|
||||
}, 200
|
||||
|
||||
# Build full response
|
||||
return _build_seo_audit_response(company, analysis), 200
|
||||
|
||||
|
||||
@app.route('/api/seo/audit')
|
||||
def api_seo_audit():
|
||||
"""
|
||||
API: Get SEO audit results for a company.
|
||||
|
||||
Query parameters:
|
||||
- company_id: Company ID (integer)
|
||||
- slug: Company slug (string)
|
||||
|
||||
At least one of company_id or slug must be provided.
|
||||
|
||||
Returns JSON with:
|
||||
- pagespeed scores (seo, performance, accessibility, best_practices)
|
||||
- on_page metrics (meta tags, headings, images, links, structured data)
|
||||
- technical checks (ssl, sitemap, robots.txt, mobile-friendly)
|
||||
- issues list with severity levels
|
||||
"""
|
||||
company_id = request.args.get('company_id', type=int)
|
||||
slug = request.args.get('slug', type=str)
|
||||
|
||||
if not company_id and not slug:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Podaj company_id lub slug firmy'
|
||||
}), 400
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Find company by ID or slug
|
||||
if company_id:
|
||||
company = db.query(Company).filter_by(id=company_id, status='active').first()
|
||||
else:
|
||||
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
||||
|
||||
if not company:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Firma nie znaleziona'
|
||||
}), 404
|
||||
|
||||
response, status_code = _get_seo_audit_for_company(db, company)
|
||||
return jsonify(response), status_code
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/seo/audit/<slug>')
|
||||
def api_seo_audit_by_slug(slug):
|
||||
"""
|
||||
API: Get SEO audit results for a company by slug.
|
||||
Convenience endpoint that uses slug from URL path.
|
||||
|
||||
Example: GET /api/seo/audit/pixlab-sp-z-o-o
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Find company by slug
|
||||
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
||||
|
||||
if not company:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Firma nie znaleziona'
|
||||
}), 404
|
||||
|
||||
response, status_code = _get_seo_audit_for_company(db, company)
|
||||
return jsonify(response), status_code
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/check-email', methods=['POST'])
|
||||
def api_check_email():
|
||||
"""API: Check if email is available"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user