From db28aa6419a4d445d89af903154a1f32161a43a8 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Thu, 8 Jan 2026 08:03:49 +0100 Subject: [PATCH] auto-claude: 5.1 - Add GET /api/seo/audit endpoint for SEO results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added two API endpoints for retrieving SEO audit data: - GET /api/seo/audit?company_id=X or ?slug=Y - GET /api/seo/audit/ 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 --- app.py | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) diff --git a/app.py b/app.py index ed0e773..12dfd5d 100644 --- a/app.py +++ b/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/ 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/') +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"""