# Bundles/Internal/Webhub/Handlers — SEO tarama pipeline'ı (3 tarayıcı + 1 danışman) > **Doğrulanan kod tabanı:** `d49b4413` (dal `feat/fw-license-master`) · **Tarih:** 2026-10-05 · **Yayın:** 0.9.7 = bu commit + sonrası; belge yalnız doğrulama anındaki kodu anlatır > **Kaynak klasör:** `Bundles/Internal/Webhub/Handlers/` — 5 `*.php`. > **Envanter:** 5 dosyanın 5'i anlatıldı. ## 1. Ne işe yarar, kimler kullanır Webhub'ın SEO puanlama motoru. `SeoScannerService` üç "tarayıcı" handler'ı sırayla çalıştırır, puanlarını toplar ve bir **danışman** handler'ı ile hem kural tabanlı hem de yapay zekâ destekli tavsiye üretir. Handler'lar birbirinin yerine geçen bağımsız ölçücülerdir; ortak taban sınıfları `BaseSeoHandler`'dır. **Kimler çağırır:** yalnız `SeoScannerService` (`SeoScannerService.php:30-41`). Alias'lar `ModuleData.php:38-43`'te kayıtlıdır; dolayısıyla bu handler'lar **kayıt dışından çağrılamaz**. ## 2. Klasör/dosya envanteri | Dosya | Görev | Önemli yöntemler / puan | |---|---|---| | `BaseSeoHandler.php` | Soyut taban. İki korumalı puanlama yöntemi ve iki durum alanı (`$report`, `$totalScore`) sağlar. `execute()` **tanımlanmamıştır** — alt sınıf zorunlu. | `award(string $criteria,bool $condition,int $successPoints,int $failPoints=0): void` (:21 *protected*), `awardLength(string $criteria,string $text,int $min,int $max,int $successPoints,int $failPoints=0): void` (:37 *protected*) | | `SettingsScannerHandler.php` | Ayar tabanlı ölçüm (alias `seo.settings`). Site başlığı, meta description, keywords uzunluklarını puanlar. **Maksimum 25.** | `execute(array $params=[]): array` (:15) | | `CoreScannerHandler.php` | Ağ katmanı ölçümü (alias `seo.core`). HTTPS, `robots.txt`, `sitemap.xml` — **canlı HTTP GET**. **Maksimum 30.** | `execute(array $params=[]): array` (:15), `checkUrlExists(string $url): bool` (:44 *private*) | | `DomScannerHandler.php` | Canlı HTML ölçümü (alias `seo.dom`). TTFB, favicon, HTML etiketleri, H1, alt etiketleri, Open Graph, Twitter Cards, Schema. **Maksimum 45.** | `execute(array $params=[]): array` (:15) | | `AdviceHandler.php` | Puan + rapordan tavsiye üretir (alias `seo.advice`). `BaseSeoHandler`'ı **genişletmez** — `BaseComponent`'tir, çünkü puanlamaz. | `execute(array $params=[]): array` (:18) | **Kapsama:** 5/5. ## 3. Akış — puanlama tamamı ``` SeoScannerService::scan() SeoScannerService.php:18 ├─ executePipeline(['seo.settings','seo.core','seo.dom'], $params) :30-34 │ her handler için: result = handler->execute($params) │ totalScore += result['score'] │ fullReport = array_merge(fullReport, result['report']) :105-111 │ score = min(100, totalScore) :114 └─ handler('seo.advice')->execute([score, report]) :37-41 → ['advice' => …, 'gemini' => …] AdviceHandler.php:75-78 ``` ### 3.1 `SettingsScannerHandler` (25 puan) ``` settings = service('settings')->withProject(active_project_key())->all() :20-22 map[setting_key] = setting_value :23-28 awardLength('Site Başlığı (10-65 karakter)', meta-title ?? site-title, 10, 65, 10, 5) :32 awardLength('Meta Description (50-160 karakter)', meta-description ?? site-description, 50, 160, 10, 5) :36 award('Meta Keywords Tanımlaması', !empty(meta-keywords), 5) :39 ``` ### 3.2 `CoreScannerHandler` (30 puan) ``` award('SSL / HTTPS Güvenli Bağlantı', protocol === 'https://', 5) :24 award('Robots.txt … (Canlı HTTP Kontrolü)', checkUrlExists(base_url.'/robots.txt'), 10) :27 award('Sitemap.xml … (Canlı HTTP Kontrolü)', checkUrlExists(base_url.'/sitemap.xml'), 15) :31 checkUrlExists(): $this->remote->get($url, …, ['timeout'=>3,'connect_timeout'=>2, 'curl'=>[SSL_VERIFYPEER=>false, SSL_VERIFYHOST=>false]]) :46-53 başarı = 200 <= http_code < 400 :55 ``` ### 3.3 `DomScannerHandler` (45 puan) ``` $response = remote->get(base_url, …, ['timeout'=>5]); $ttfb = elapsed :22-24 award('Sunucu Yanıt Hızı (TTFB < 1sn)', $ttfb < 1.0, 5) :27 HTML yoksa: award('Canlı HTML DOM Taraması Yapılabildi mi?', false, 40) → ERKEN DÖNÜŞ :29-35 (kalan 40 puan iptal edilir; en yüksek olası skor 5'e düşer) favicon (5) :40 lang (2) :44 viewport (2) :45 canonical(3) :46 h1 (10) :49-50 tek h1 tam, birden çok h1 → 5 alt (5) :53-63 alt doluluğu ≥%80 → 5, ≥%50 → 2, imaj yoksa 5 OG (8) :66-71 og:title+description+image üçü → 8, biri var → 4 Twitter (5) :74-78 twitter:card+title → 5, yalnız card → 2 Schema (5) :81-85 ld+json | schema.org | itemscope | @schema ``` ### 3.4 `AdviceHandler` ``` başarısız kriterler toplanır → $failedText :25-30 score >= 80 → "mükemmel" · >= 50 → "iyi" · < 50 → "kritik" :32-38 Gemini istemi (tek seferlik HTTP çağrısı): $this->service('gemini')->ask($prompt, $geminiService->textModel, true) :64-65 başarısızsa veya exception → hata metni döner, akış BOZULMAZ :66-73 ``` ## 4. Puan tablosu (tek doğruluk kaynağı) | Kriter | Handler | Tam | Kısmi | Koşul | |---|---|---:|---:|---| | Site Başlığı uzunluğu | `seo.settings` | 10 | 5 | 10-65 karakter (`SettingsScannerHandler.php:32`) | | Meta Description uzunluğu | `seo.settings` | 10 | 5 | 50-160 karakter (:36) | | Meta Keywords | `seo.settings` | 5 | 0 | boş değilse (:39) | | SSL/HTTPS | `seo.core` | 5 | 0 | `$protocol === 'https://'` (`CoreScannerHandler.php:24`) | | robots.txt | `seo.core` | 10 | 0 | HTTP 200-399 (:27,55) | | sitemap.xml | `seo.core` | 15 | 0 | HTTP 200-399 (:31,55) | | TTFB | `seo.dom` | 5 | 0 | < 1.0 sn (`DomScannerHandler.php:27`) | | Favicon | `seo.dom` | 5 | 0 | `` (:40-41) | | `html lang` | `seo.dom` | 2 | 0 | (:44) | | Viewport | `seo.dom` | 2 | 0 | (:45) | | Canonical | `seo.dom` | 3 | 0 | (:46) | | H1 tekil | `seo.dom` | 10 | 5 | `h1Count === 1` / `>1` (:49-50) | | Alt etiketleri | `seo.dom` | 5 | 2 | oran ≥0.8 / ≥0.5; imaj yoksa **5 korunur** (:60,62) | | Open Graph | `seo.dom` | 8 | 4 | 3/3 etiket / en az 1 (:67-71) | | Twitter Cards | `seo.dom` | 5 | 2 | 2/2 / 1 (:75-78) | | Schema/JSON-LD | `seo.dom` | 5 | 0 | (:81-85) | | **Toplam** | | **100** | | `SeoScannerService.php:114` `min(100, Σ)` | ## 5. Tuzaklar ve kurallar 1. **`BaseSeoHandler::execute()` tanımsızdır.** Soyut sınıf (`BaseSeoHandler.php:13`) yalnız `award`/`awardLength` sunar; alt sınıflar `execute()` uygulamak zorundadır. Yanlışlıkla taban çağrılırsa `report`/`score` dönmeyen boş sonuç gelir. 2. **Handler durumu her `execute()` başında sıfırlanır.** Üçü de `$this->totalScore = 0; $this->report = [];` ile başlar (`SettingsScannerHandler.php:17-18`, `CoreScannerHandler.php:17-18`, `DomScannerHandler.php:17-18`). Aynı örnek yeniden çağrılırsa puan **birikmez** — bu, `BaseComponent`'in örnek başına yeniden üretilmesine da uygundur. 3. **HTML alınamazsa 45 puanlık dal 5'e düşer.** `DomScannerHandler` erken döner (:29-35): `award(…, false, 40)` rapora 0/40 yazar ve kalan kriterler hiç ölçülmez. Panelde "DOM taraması yapılabildi mi?" satırı bu yüzden **her zaman** kırmızı görünür ve toplam en yüksek 30+25+5 = 60 olabilir. 4. **Ölçüm kendi sunucusundan dışarı HTTP atar.** `base_url` = `$_SERVER['HTTP_HOST']` üzerinden kurulur (`SeoScannerService.php:21`). `Host` başlığı değiştirilmiş bir istek **başka bir siteyi** tarar ve sonucu kaydeder. 5. **SSL doğrulaması iki handler'da da kapalı.** `CURLOPT_SSL_VERIFYPEER => false` (`CoreScannerHandler.php:50-51`, `DomScannerHandler.php:23`). Yanlış `base_url` ayarı sessizce "başarılı" sayılabilir. 6. **HEAD yerine GET kullanılıyor — bilinçli.** `checkUrlExists()` yorumu bunu açıklar: framework dinamik dosyaları HEAD'de 500 dönebiliyor (`CoreScannerHandler.php:40-43`). Bedeli: `robots.txt`/`sitemap.xml` **tamamen indirilir**. 7. **Zaman aşımı sabitleri.** `checkUrlExists`: 3 sn / bağlantı 2 sn. `DomScannerHandler`: 5 sn. `CoreScannerHandler`'ın 30 puanı bu zaman aşımlarına bağlıdır — yavaş sunucuda sessizce 25 puan kaybedilir. 8. **`$failPoints` varsayılanı 0'dır.** Kısmi puan yazan üç kriter (`h1`, alt, OG, Twitter) kısmi değeri **açıkça geçirir**; `awardLength` ise kısmi puanı varsayılan 0'a düşürür (`BaseSeoHandler.php:45`) — bu yüzden başlık/description uzunluğu ya tam ya hiç puan alır. 9. **Boş metin kısmi puanı da alamaz.** `awardLength()` metin boşsa `$failPoints` yerine **0** yazar (`BaseSeoHandler.php:40`) — "kısa metin" ile "metin yok" ayrımı raporda görünmez. 10. **Gemini çağrısı `try/catch` içindedir ve akışı değiştirmez.** `catch (\Exception $e)` (`AdviceHandler.php:71`) — `Error`/`TypeError` yakalanmaz, ama `throw` edilmediği için sonuç yine de döner. `gemini` alanı view'da düz HTML olarak basılır (`WebhubMap.php:41` "AI SEO Raporu"), bu yüzden istem **HTML dönmesini zorunlu kılar** (`AdviceHandler.php:58-62`). 11. **Rapor JSON'u settings'e yazılır, tabloda değil.** `actionScan()` `seo-report` değerine `json_encode(..., JSON_UNESCAPED_UNICODE)` yazar (`WebhubProvider.php:113`); `getReport()` onu geri çözüp geçen/başarısız sayımı **kayıtta** yapar (:72-78). Yani raporun doğruluğu son taramaya bağlıdır, anlık ölçüm değildir. ## 6. Örnek (gerçek koddan) ```php // Bundles/Internal/Webhub/Handlers/BaseSeoHandler.php:21-32 protected function award(string $criteria, bool $condition, int $successPoints, int $failPoints = 0): void { $earned = $condition ? $successPoints : $failPoints; $this->totalScore += $earned; $this->report[] = [ 'criteria' => $criteria, 'passed' => $condition, 'earned' => $earned, 'max' => $successPoints, ]; } ``` ```php // Bundles/Internal/Webhub/Services/SeoScannerService.php:100-117 private function executePipeline(array $handlers, array $params): array { $totalScore = 0; $fullReport = []; foreach ($handlers as $alias) { $handler = $this->handler($alias); $result = $handler->execute($params); $totalScore += $result['score'] ?? 0; $fullReport = array_merge($fullReport, $result['report'] ?? []); } return [ 'score' => (int) min(100, $totalScore), 'report' => $fullReport, ]; } ``` ## 7. İlgili belgeler * [Webhub/README.md](README.md) — modül haritası, ayar anahtarları, `actionScan()` yazma zinciri * [Core/Support/README.md](../../../Core/Support/README.md) — `remote` bileşeni * [kavramlar/05-asset-sistemi.md](../../../kavramlar/05-asset-sistemi.md) * [acik-sorular.md](../../../acik-sorular.md)