kalimeromk / seo-report
Framework-agnostic PHP SEO analysis: analyze URL/sitemap, return API result (JSON). Works with Laravel, Yii, or plain PHP.
Installs: 5
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/kalimeromk/seo-report
Requires
- php: ^8.2
- ext-dom: *
- ext-json: *
- ext-libxml: *
- ext-openssl: *
- guzzlehttp/guzzle: ^7.2
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0|^12.0
- rector/rector: ^2.0
This package is auto-updated.
Last update: 2026-02-07 10:56:47 UTC
README
SEO analysis library: analyze a URL or sitemap, run SEO/performance/security checks, and return an API result (no database). Works with Laravel, Yii, or plain PHP.
Requirements
- PHP 8.2+
- ext-dom, ext-json
- guzzlehttp/guzzle ^7.2
Optional (for advanced features):
- Docker 20.10+ (for Core Web Vitals, JS rendering, screenshots)
- Docker Compose 2.0+
No framework required. Use the same package in Laravel, Yii, Symfony, or standalone.
Installation
composer require kalimeromk/seo-report
Quick Start (CLI)
Use directly from terminal:
# Analyze a URL php run_test.php ogledalo.mk # With full URL php run_test.php https://ogledalo.mk/ # Analyze sitemap php run_test.php https://example.com/sitemap.xml --sitemap # Save to file php run_test.php ogledalo.mk > report.json # Pretty format with jq php run_test.php ogledalo.mk | jq
Output: JSON API response with analysis results.
Checks Covered
The report is grouped by categories and includes checks like:
- SEO: title, meta description, headings (H1-H6), keyword consistency, image alts, canonical, hreflang, robots, noindex, in-page links, nofollow, language, favicon, friendly URLs
- Content Quality: duplicate content detection, internal linking analysis, content readability (Flesch score), keyword stuffing detection, thin content detection
- Performance: compression, load time, TTFB, page size, HTTP requests, cache headers, redirects, cookie-free domains, empty src/href, image optimization, defer JS, render blocking, minification, DOM size, doctype, resource hints (preconnect, preload, prefetch)
- Security: HTTPS, HTTP/2, mixed content, server signature, unsafe cross-origin links, HSTS, plaintext emails, security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy)
- Mobile: viewport configuration, touch target size, font size readability, flexible layout detection
- Misc: structured data (Open Graph, Twitter Cards, Schema.org validation), meta viewport, charset, sitemap, social links, content length, text/HTML ratio, inline CSS, deprecated HTML tags, llms.txt, flash, iframes, accessibility checks
- Technology: server IP, DNS, DMARC/SPF, SSL certificate, reverse DNS, analytics and tech detection
Advanced Features (with Docker)
With Docker installed, you get additional real-world analysis:
- Core Web Vitals: Real LCP, FID, CLS, TTFB using Google Lighthouse (no API key needed)
- JavaScript Rendering: Detect React/Vue/Angular, hydration issues, console errors
- Screenshots: Desktop, mobile, and tablet screenshots
- Advanced Technology Detection: Detailed tech stack using Wappalyzer
Docker Setup (Optional but Recommended)
For real Core Web Vitals and JavaScript SEO analysis, install Docker:
# 1. Build Docker images (one-time setup) cd vendor/kalimeromk/seo-report/docker # or wherever the package is docker-compose build # 2. Verify it works docker-compose run lighthouse https://example.com
That's it! No API keys, no registration, no rate limits.
Why Docker?
| Feature | Without Docker | With Docker |
|---|---|---|
| Core Web Vitals | ❌ Proxy estimates only | ✅ Real Chrome metrics (LCP, FID, CLS) |
| JavaScript SEO | ❌ Can't analyze JS | ✅ Detects React/Vue/Hydration issues |
| Screenshots | ❌ Not available | ✅ Desktop/Mobile/Tablet screenshots |
| Cost | Free | Free (just your server) |
| Rate Limits | None | None |
What It Returns
The package returns a JSON API response with this structure:
{
"url": "https://ogledalo.mk/",
"score": 83.33,
"generated_at": "2026-01-28T08:44:42+00:00",
"results": {
"title": {
"passed": true,
"importance": "high",
"value": "Ogledalo - Orthodox news portal & daily aggregator"
},
"meta_description": {
"passed": true,
"importance": "high",
"value": "Ogledalo is an Orthodox news portal and aggregator..."
},
"headings": {
"passed": false,
"importance": "high",
"value": {
"h1": ["Ogledalo - Orthodox news portal & daily aggregator"],
"h2": ["Article 1", "Article 2", ...],
"h3": [...]
},
"errors": {
"duplicate": null
}
},
"load_time": {
"passed": true,
"importance": "medium",
"value": 0.94
},
"page_size": {
"passed": true,
"importance": "medium",
"value": 14886
},
"https_encryption": {
"passed": true,
"importance": "high",
"value": "https://ogledalo.mk/"
},
"structured_data": {
"passed": true,
"importance": "medium",
"value": {
"Open Graph": { "og:title": "...", "og:description": "..." },
"Twitter": { "twitter:card": "..." },
"Schema.org": { "@context": "https://schema.org", ... }
}
}
},
"categories": {
"seo": ["title", "title_optimal_length", "meta_description", "meta_description_optimal_length", "headings", "h1_usage", "header_tag_usage", "content_keywords", "keyword_consistency", "image_keywords", "open_graph", "twitter_cards", "seo_friendly_url", "canonical_tag", "canonical_self_reference", "hreflang", "404_page", "robots", "noindex", "robots_directives", "noindex_header", "in_page_links", "nofollow_links", "link_url_readability", "language", "favicon", "duplicate_h1", "title_uniqueness", "thin_content", "content_uniqueness", "meta_description_quality", "canonical_effectiveness", "link_ratio", "link_distribution", "contextual_links", "outbound_links", "anchor_text_quality", "url_length", "url_depth", "url_parameters", "url_format", "trailing_slash", "url_case", "url_encoding", "international_seo", "pagination"],
"performance": ["text_compression", "brotli_compression", "load_time", "ttfb", "page_size", "http_requests", "static_cache_headers", "expires_headers", "avoid_redirects", "redirect_chains", "cookie_free_domains", "empty_src_or_href", "image_format", "image_dimensions", "image_lazy_loading", "image_size_optimization", "lcp_proxy", "cls_proxy", "defer_javascript", "render_blocking_resources", "minification", "dom_size", "doctype", "preconnect_hints", "dns_prefetch_hints", "preload_hints", "prefetch_hints", "resource_hints_coverage"],
"security": ["https_encryption", "http2", "mixed_content", "server_signature", "unsafe_cross_origin_links", "hsts", "plaintext_email", "content_security_policy", "x_frame_options", "x_content_type_options", "referrer_policy", "permissions_policy", "cross_origin_policies", "security_headers_score"],
"mobile": ["viewport_config", "touch_target_size", "font_size_readability", "flexible_layout", "mobile_friendly_patterns", "zoom_accessibility"],
"miscellaneous": ["structured_data", "structured_data_validation", "meta_viewport", "charset", "sitemap", "social", "content_length", "text_html_ratio", "inline_css", "deprecated_html_tags", "llms_txt", "flash_content", "iframes", "form_labels", "skip_navigation", "aria_usage", "heading_hierarchy", "link_text_quality", "table_accessibility"],
"technology": ["server_ip", "dns_servers", "dmarc_record", "spf_record", "ssl_certificate", "reverse_dns", "analytics", "technology_detection"]
}
}
Result Structure
Each check in results has:
passed(bool) - Whether the check passedimportance('high' | 'medium' | 'low') - Priority levelvalue(mixed) - The actual value found (string, number, array, etc.)errors(array, optional) - Error details ifpassedis false
Example checks:
title- Page title (string)meta_description- Meta description (string)headings- All headings h1-h6 (array)header_tag_usage- H2-H6 heading counts and usage (array)load_time- Page load time in seconds (float)page_size- HTML size in bytes (int)https_encryption- Whether HTTPS is used (bool via passed)structured_data- Open Graph, Twitter, Schema.org (array)http_requests- Count of JS/CSS/Images/etc. (array)- And 70+ more checks...
Usage in Code
Basic Usage
use KalimeroMK\SeoReport\Config\SeoReportConfig; use KalimeroMK\SeoReport\SeoAnalyzer; // Create config (empty = use defaults) $config = new SeoReportConfig([]); // Create analyzer $analyzer = new SeoAnalyzer($config); // Analyze URL $result = $analyzer->analyze('https://ogledalo.mk/'); // Get values echo $result->getUrl(); // "https://ogledalo.mk/" echo $result->getScore(); // 83.33 echo $result->getGeneratedAt(); // DateTimeImmutable // Get all results $results = $result->getResults(); echo $results['title']['value']; // "Ogledalo - Orthodox news portal..." echo $results['load_time']['value']; // 0.94 echo $results['page_size']['value']; // 14886 // Check if specific check passed if ($results['title']['passed']) { echo "Title is OK"; } // Get API response (JSON) $json = $result->toJson(); $array = $result->toArray();
Advanced Usage (with Docker)
Use SeoAnalyzerWithDocker instead of SeoAnalyzer to get real Core Web Vitals and JavaScript analysis:
use KalimeroMK\SeoReport\SeoAnalyzerWithDocker; use KalimeroMK\SeoReport\Config\SeoReportConfig; $config = new SeoReportConfig([]); // Create analyzer with Docker support $analyzer = new SeoAnalyzerWithDocker($config); // Analyze URL - automatically includes Docker checks if available $result = $analyzer->analyze('https://example.com'); // Check if Docker is available if ($analyzer->hasDockerSupport()) { echo "Docker features enabled!"; } // Get specific Docker-based results $cwv = $analyzer->getCoreWebVitals('https://example.com'); echo "LCP: " . $cwv['performance']['metrics']['lcp'] . "ms"; // Take screenshot $screenshot = $analyzer->takeScreenshot('https://example.com', 'mobile'); $base64Image = $screenshot['screenshot']['base64']; // JavaScript analysis $js = $analyzer->getJavaScriptAnalysis('https://example.com'); echo "Framework: " . $js['pageInfo']['framework']; // React, Vue, etc. echo "Console errors: " . $js['console']['errors'];
Without Docker? No problem! SeoAnalyzerWithDocker gracefully falls back to basic analysis.
Custom Configuration
use KalimeroMK\SeoReport\Config\SeoReportConfig; $config = new SeoReportConfig([ 'request_timeout' => 10, // HTTP timeout in seconds 'request_http_version' => '2', // HTTP version: '1.1' or '2' 'request_user_agent' => 'MyBot/1.0', // Custom user agent 'request_proxy' => "http://proxy1:8080\nhttp://proxy2:8080", // Proxy list 'sitemap_links' => 100, // Max URLs from sitemap (-1 = unlimited) 'report_limit_min_title' => 10, // Min title length 'report_limit_max_title' => 70, // Max title length 'report_limit_min_words' => 300, // Min words on page 'report_limit_max_links' => 200, // Max links on page 'report_limit_load_time' => 3, // Max load time (seconds) 'report_limit_page_size' => 500000, // Max page size (bytes) 'report_score_high' => 10, // Points for high importance checks 'report_score_medium' => 5, // Points for medium importance checks 'report_score_low' => 0, // Points for low importance checks ]); $analyzer = new SeoAnalyzer($config); $result = $analyzer->analyze('https://example.com');
Analyze Sitemap
// Analyze all URLs from sitemap $results = $analyzer->analyzeSitemap('https://example.com/sitemap.xml'); // Limit to first 50 URLs $results = $analyzer->analyzeSitemap('https://example.com/sitemap.xml', 50); // $results is array of AnalysisResult foreach ($results as $result) { echo $result->getUrl() . ' - Score: ' . $result->getScore() . "\n"; }
REST API Example (Laravel)
use KalimeroMK\SeoReport\Config\SeoReportConfig; use KalimeroMK\SeoReport\SeoAnalyzer; use KalimeroMK\SeoReport\SeoAnalyzerException; class SeoReportController extends Controller { public function analyze(Request $request) { try { $config = new SeoReportConfig(config('seo-report')); $analyzer = new SeoAnalyzer($config); $result = $analyzer->analyze($request->input('url')); return response()->json($result->toArray()); } catch (SeoAnalyzerException $e) { return response()->json([ 'error' => 'Could not analyze URL: ' . $e->getMessage() ], 400); } } }
REST API Example (Yii)
use KalimeroMK\SeoReport\Config\SeoReportConfig; use KalimeroMK\SeoReport\SeoAnalyzer; class SeoReportController extends \yii\web\Controller { public function actionAnalyze() { $url = \Yii::$app->request->get('url'); $config = new SeoReportConfig(\Yii::$app->params['seo-report'] ?? []); $analyzer = new SeoAnalyzer($config); $result = $analyzer->analyze($url); \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; return $result->toArray(); } }
Storing Results
The package does not save to database. Your app decides how to persist:
// Laravel example $result = $analyzer->analyze($url); Report::create([ 'user_id' => auth()->id(), 'url' => $result->getUrl(), 'results' => $result->getResults(), // JSON column 'score' => $result->getScore(), 'generated_at' => $result->getGeneratedAt(), ]); // Yii example $report = new Report(); $report->url = $result->getUrl(); $report->results = json_encode($result->getResults()); $report->score = $result->getScore(); $report->generated_at = $result->getGeneratedAt()->format('Y-m-d H:i:s'); $report->save();
All Available Checks
SEO (33+ checks)
title- Page title tagtitle_optimal_length- Title optimal length (50-60 chars)meta_description- Meta description tagmeta_description_optimal_length- Meta description optimal length (120-160 chars)headings- H1-H6 headings structureheader_tag_usage- H2-H6 heading tag usage countscontent_keywords- Keywords in title vs contentkeyword_consistency- Keyword distribution across title, meta description, headingsimage_keywords- Images with missing alt attributesseo_friendly_url- URL structure and keywordscanonical_tag- Canonical link taghreflang- Alternate hreflang links404_page- Custom 404 error pagerobots- robots.txt rulesnoindex- Noindex meta tagnoindex_header- X-Robots-Tag header (noindex)in_page_links- Internal/external links countnofollow_links- Nofollow links countlink_url_readability- URL structure of all links (unfriendly URLs)language- HTML lang attributefavicon- Favicon presence
Content Quality & Duplicate Content:
duplicate_h1- Multiple H1 tags detectiontitle_uniqueness- Title vs H1 duplication checkthin_content- Low word count detection (< 300 words)content_uniqueness- Boilerplate content ratio analysismeta_description_quality- Meta description quality checkcanonical_effectiveness- Canonical tag implementation checkreadability_score- Flesch Reading Ease scorekeyword_stuffing- Excessive keyword density detectiontitle_quality- Title format analysis (power words, numbers)content_structure- Paragraph length and heading hierarchy
Internal Linking:
link_ratio- Internal vs external link ratiolink_distribution- Duplicate links and excessive linkscontextual_links- Descriptive vs navigational linksoutbound_links- Self-referencing link detectionanchor_text_quality- Empty/short anchor text detection
URL Structure:
url_length- URL length validation (< 115 chars)url_depth- Path depth analysis (max 3 levels)url_parameters- Query parameter analysisurl_format- Underscores, case, special chars checktrailing_slash- Trailing slash consistency
International SEO:
hreflang_validation- Valid language codeshtml_lang- HTML lang attribute validationlanguage_consistency- HTML lang vs hreflang matchx_default_hreflang- X-default fallback check
Pagination:
pagination_detection- Pagination indicatorsrel_next_prev- Next/prev link detectioninfinite_scroll- Infinite scroll detection
Performance (14+ checks)
text_compression- Gzip/Brotli compressionload_time- Page load timepage_size- HTML file sizehttp_requests- Number of resources (JS/CSS/Images)image_format- Modern image formats (WebP, AVIF)defer_javascript- Defer attribute on scriptsminification- JS/CSS minification checkdom_size- DOM nodes countdoctype- DOCTYPE declarationstatic_cache_headers- Static asset cachingexpires_headers- Cache expiration headersredirect_chains- Redirect chain detection
Resource Hints:
preconnect_hints- Preconnect to external domainsdns_prefetch_hints- DNS prefetch hintspreload_hints- Preload critical resourcesprefetch_hints- Prefetch next-page resources
Security (14+ checks)
https_encryption- HTTPS usagehttp2- HTTP/2 protocolmixed_content- HTTP resources on HTTPS pageserver_signature- Server header exposureunsafe_cross_origin_links- Links without rel="noopener"hsts- HTTP Strict Transport Security headerplaintext_email- Plaintext emails in HTML
Security Headers:
content_security_policy- CSP header validationx_frame_options- Clickjacking protection (DENY/SAMEORIGIN)x_content_type_options- MIME sniffing protectionreferrer_policy- Referrer information controlpermissions_policy- Feature policy restrictionscross_origin_policies- COOP/COEP/CORP headerssecurity_headers_score- Overall security score (A-D rating)
Mobile (6 checks)
viewport_config- Viewport meta tag validationtouch_target_size- Minimum 44x44px touch targets (WCAG 2.1)font_size_readability- Minimum 12px font sizeflexible_layout- Fixed width detectionmobile_friendly_patterns- Flash, frames, deprecated pluginszoom_accessibility- User zoom prevention detection
Miscellaneous (18+ checks)
structured_data- Open Graph, Twitter Cards, Schema.orgmeta_viewport- Viewport meta tagcharset- Character encodingsitemap- Sitemap in robots.txtsocial- Social media linkscontent_length- Word counttext_html_ratio- Text to HTML ratioinline_css- Inline CSS usagedeprecated_html_tags- Deprecated HTML tagsllms_txt- llms.txt file presence (for AI crawlers)flash_content- Flash/Shockwave content detectioniframes- iFrames usage
Structured Data Validation:
json_ld_validation- JSON-LD syntax validationschema_requirements- Required properties for common schemasduplicate_ids- Duplicate @id detectionstructured_data_images- Image URL validationschema_context- @context validation
Accessibility:
form_labels- Form inputs with missing labelsskip_navigation- Skip to content linkaria_usage- ARIA roles and attributes validationheading_hierarchy- H1-H6 hierarchy validationlink_text_quality- Generic link text detectiontable_accessibility- Table headers and captions
Technology (8 checks, no external APIs)
server_ip- Resolved server IP (gethostbyname)dns_servers- NS recordsdmarc_record- DMARC TXT record (_dmarc.domain)spf_record- SPF TXT recordssl_certificate- SSL/TLS certificate (valid, valid_from, valid_to, issuer_cn, subject_cn) — pure PHP, inspired by phpRank Softwarereverse_dns- PTR record for server IP (gethostbyaddr)analytics- Detected analytics (e.g. Google Analytics)technology_detection- Detected tech (e.g. jQuery, Font Awesome, Facebook Pixel)
Docker-Based Checks (requires Docker)
These checks run only when Docker is available:
Core Web Vitals (Real Chrome Metrics):
core_web_vitals- Real LCP, FID, CLS, TTFB using Google Lighthouselcp- Largest Contentful Paint (ms) - should be < 2500msfid- First Input Delay (ms) - should be < 100mscls- Cumulative Layout Shift - should be < 0.1ttfb- Time to First Byte (ms)performance_score- Lighthouse performance score (0-100)
JavaScript Rendering Analysis:
javascript_rendering- Full browser JavaScript execution checkframework- Detected framework (React, Vue, Angular, Next.js, Nuxt.js)has_hydration- Whether client-side hydration is detectedtitle_rendered- Whether title is rendered by JSh1_rendered- Whether H1 is rendered by JSrender_time_ms- Total render timeconsole_errors- JavaScript console errors count
Advanced Technology Detection:
advanced_technology_detection- Detailed tech stack using Wappalyzer- CMS, frameworks, analytics, hosting, CDN detection
Screenshots:
screenshot- Available viatakeScreenshot()method- Desktop, mobile, tablet viewports
- Base64 encoded images
- Visual metrics (CLS during load)
Exceptions
use KalimeroMK\SeoReport\SeoAnalyzerException; try { $result = $analyzer->analyze('https://example.com'); } catch (SeoAnalyzerException $e) { // URL cannot be fetched (connection error, timeout, etc.) echo "Error: " . $e->getMessage(); }
Docker FAQ
Do I need Docker?
No. The package works great without Docker. Docker is only needed for:
- Real Core Web Vitals (LCP, FID, CLS)
- JavaScript rendering analysis
- Screenshots
Without Docker, you still get 70+ SEO checks.
How much does Docker cost?
Free. You run it on your own server. No API fees, no rate limits.
What are the system requirements for Docker?
- Docker 20.10+
- Docker Compose 2.0+
- 4GB+ RAM (for headless Chrome)
- 2GB disk space
Can I use this in production?
Yes. Docker containers are isolated and run fresh on every request. No state is preserved between runs.
Is caching used?
No. By default, all Docker-based checks return fresh results. This is intentional - you want to see if issues were fixed after making changes.
What if Docker is not available?
SeoAnalyzerWithDocker gracefully falls back to basic analysis. No errors, just fewer features.
$analyzer = new SeoAnalyzerWithDocker($config); if ($analyzer->hasDockerSupport()) { echo "Full analysis with Core Web Vitals"; } else { echo "Basic analysis (no Docker available)"; }
Example: Full Analysis with Docker
use KalimeroMK\SeoReport\SeoAnalyzerWithDocker; use KalimeroMK\SeoReport\Config\SeoReportConfig; $config = new SeoReportConfig([]); $analyzer = new SeoAnalyzerWithDocker($config); $result = $analyzer->analyze('https://example.com'); // Results include Docker-based checks if available $results = $result->getResults(); if (isset($results['core_web_vitals'])) { $cwv = $results['core_web_vitals']['value']; echo "LCP: {$cwv['lcp']}ms\n"; echo "CLS: {$cwv['cls']}\n"; echo "Performance Score: {$cwv['performance_score']}\n"; } if (isset($results['javascript_rendering'])) { $js = $results['javascript_rendering']['value']; echo "Framework: {$js['framework']}\n"; echo "Hydration: " . ($js['has_hydration'] ? 'Yes' : 'No') . "\n"; echo "Console Errors: {$js['console_errors']}\n"; }
Testing
# Run all tests composer test # Run integration test (scans real domain) composer test -- --filter=OgledaloMkApiResponseTest # Code quality composer analyse # PHPStan composer rector-dry # Rector dry-run
Reference
SSL certificate check (pure PHP, no Spatie dependency) and DNS/IP logic are inspired by phpRank Software (e.g. SSL Checker, DNS Lookup, Domain IP Lookup in ToolController). If you have that codebase at /path/to/phpRank Software, you can compare behaviour; seo-report uses only built-in PHP and Guzzle (no GeoIP, no WHOIS, no external APIs).
License
MIT.