# Core/Render/Builders — durum biriktiriciler (varlık, SEO, schema, breadcrumb, sitemap, feed, llms) > **Doğrulanan kod tabanı:** `c23b431f` · **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:** `Core/Render/Builders/` — **7 `*.php`**. > **Envanter:** 7 dosyanın **7'si** anlatıldı. > **Doğrulama platformu:** Windows + PHP 8.3, `AssetBuilder` gerçekten çalıştırılarak. ## 1. Ne işe yarar, kim kullanır Yedi sınıfın ortak işi **veri toplamak ve bir çıktı dizisine/stringsine dönüştürmek**; hiçbiri HTTP yanıtı yazmaz, hiçbiri `ViewEngine`'i çağırmaz. Hepsi `BaseComponent`/`BaseRender`'dan türer ve **kayıt altında `handlers`** olarak durur (`SystemRenderMapTrait.php:37-43`) — yani `$this->handler('seoBuilder')` ile erişilir. Çıktıyı asıl basan sınıflar [Providers](Providers.md)'tadır. **Tuzak:** `handler('seoBuilder')` bir **Builder**'ı döndürür, `resolver('seo')` ise **Resolver**. İkisi karıştırılmamalı; `SeoBuilder` override'ları biriktirir, `SeoResolver` bunları `SettingsService` verisiyle birleştirir. ## 2. Klasör/dosya envanteri (7/7) | Dosya | Görev | Önemli public yöntemler | |---|---|---| | `AssetBuilder.php` (378) | Varlık kuyruğu + derleyici + sıralayıcı. `prepare()` çekirdek yığını ve özel kaynakları toplar, `build()` sıralayıp `styles`/`scripts` dizisi döndürür. | `reset(): self`, `addStyle(string $path,int $priority=10): self`, `addScript(string $path,array $attrs=[],bool $inHead=false,int $priority=10): self`, `addFont(string $name,int $priority=15): self`, `prepare(string $appContext,$source=null): self`, `build(): array`, `compile($item,string $appContext='frontend'): ?array` · korumalı: `collect()`, `injectCoreStack()`, `generateWebUrl()`, `resolveToken()`, `detectType()` | | `SeoBuilder.php` (195) | Sayfa bazlı SEO **override** biriktirici. `prepare()` resolver çıktısıyla birleştirip `payload()` üretir. | `reset(): self`, `setTitle(?string)`, `setDescription(?string)`, `setKeywords(?string)`, `setCanonical(?string)`, `noIndex(string $directives='noindex, nofollow')`, `setImage(string)`, `getOverrides(): array`, `payload(): array`, `hasPayload(): bool`, `prepare(string $context='project',array $overrides=[]): self`, `build(array $resolved): array` | | `SchemaBuilder.php` | JSON-LD düğüm üreticisi, tür başına bir metot: `organization()`, `webSite()`, `page()`, `itemList()`, `breadcrumbs()`, `blogPosting()`, `newsArticle()`, `tvSeries()`, `movie()`, `person()`, `product()`, `serviceNode()` (+ `schema()`, `addRaw()`). Değer yoksa alan düşer; `aggregateRating`/`review`/`FAQPage`/`SearchAction` üretilmez. Ayrıntı: [Yapisal-Veri-Head-Izin.md](Yapisal-Veri-Head-Izin.md). | `schema(string\|array $type,array $data): self`, `organization(array)`, `webSite(array)`, `page(string,array)`, `itemList(array)`, `breadcrumbs(array)`, `blogPosting(array)`, `newsArticle(array)`, `tvSeries(array)`, `movie(array)`, `person(array)`, `product(array)`, `serviceNode(array)`, `geo(?string): ?array`, `phone(string,string): ?string` | | `BreadcrumbBuilder.php` (130) | Kırıntı (breadcrumb) adımlarını biriktirir; ana adımı korpustan, alt adımları `BreadcrumbResolver`'dan alır. | `reset(): self`, `build(…)`, `add(string $title,string $url,string $icon='',bool $active=false): self` | | `SitemapBuilder.php` | Sitemap girdilerini biriktirir ve ``/`` XML'i üretir (`google-arama/sitemap.md`): UTF-8, mutlak `` (2.048 karakterden kısa; geçersiz adres `addUrl()` ile reddedilir, çözücüler `isValidLoc()` ile eler), `& ' " > <` varlık kaçışı (`'`), dosya başına en çok 50.000 girdi ve 50 MB (aşılırsa `LengthException`; çözücü dosyaları böler ve indeks yazar). `lastmod` yalnız verilen GERÇEK değişiklik tarihidir (`FormatHelper::toIso8601()`); tarihsiz girdide etiket yoktur. `` ve `` YAZILMAZ (Google yok sayar). | `MAX_ENTRIES=50000`, `MAX_BYTES=52428800`, `MAX_LOC_LENGTH=2048`, `reset(): self`, `addUrl(string $loc,?string $lastmod=null): self`, `addUrls(array $urls): self`, `getEntries(): array`, `hasEntries(): bool`, `buildXml(bool $isIndex=false): string` · statik: `isValidLoc(string $loc): bool` | | `FeedBuilder.php` | RSS 2.0 feed XML'i üretir (`google-arama/rss-besleme.md`). Öğe: `link` ZORUNLU, `title` ya da `description`'dan en az biri (ikisi de boşsa öğe atlanır), aynı `link` tek öğe, `guid` = `link`, `pubDate` yalnız gerçek yayın tarihi varsa (RFC 822). `lastBuildDate` = en yeni `pubDate`. `` YAZILMAZ (RSS 2.0 `length` ister, bilinmez). Açıklama CDATA içindedir, `]]>` iki bölüme ayrılır. | `setChannel(array $channel): self`, `addPosts(array $posts): self`, `buildXml(): string` · korumalı: `clean(string $string,bool $stripNewlines=false): string` | | `LlmsBuilder.php` | `llms.txt` (Markdown) üretir: tek H1 (site adı), gerçek site özeti varsa blockquote (dolgu metin yok), `## Başlık` altında `- [ad](url)` maddeleri, ikincil bağlantı (sitemap) `## Optional` altında. Biçim llmstxt.org önerisidir. | `reset(): self`, `setSiteInfo(string $siteName,string $siteUrl,string $summary=''): self`, `addSection(string $title,array $items): self`, `buildMarkdown(): string` · statik: `sanitizeText(string $text): string`, `link(string $title,string $url): string` (içerik kaynaklı metni tek satıra / güvenli bağlantı biçimine indirir) | ## 3. Akış — `AssetBuilder` (en karmaşık zincir) `FrontendProvider::render()` → `AssetService::prepareContext($context)` (`Services/AssetService.php:28-31`) → `AssetBuilder::prepare($appContext, $source)`. ``` AssetService::prepareContext('frontend') Services/AssetService.php:28 └─ AssetBuilder::prepare('frontend', 'frontend') Builders/AssetBuilder.php:79 1. injectCoreStack($payload, $appContext) :87 → :245-283 ├─ route config 'css_engine' okunur (bootstrap | rbn) :250-252 ├─ AssetBundles::STACK_MAP['universal'] paketleri :257-263 │ · auth/panel ya da rbn motoru ise bootstrap, │ bootstrap_icons, font_awesome ATLANIR (0 KB bypass) :259-261 ├─ auth/panel ya da (rbn motoru + frontend) → 'rbn_master' :266-268 ├─ STACK_MAP[$appContext] paketleri :270-274 └─ auth/panel/rbn → common.css + rbnAlert.css filtrelenir :277-282 2. collect($payload, $source, $appContext) :90 → :207-240 · AssetBundles::BUNDLES ile ad→paket açılımı :215-219 · Özyineleme derinliği 8 ile sınırlı :209-213 · '.css' veya '@font/' ile başlayan → styles, diğer → scripts :232-237 3. frontend özel font varsa varsayılan Inter temizlenir :93-113 4. Her girdi compile() edilip kuyruğa eklenir :116-134 5. return $this AssetService::render('header') → AssetBuilder::build() Services/AssetService.php:51-66 ├─ frontend & hiç font yoksa '@font/Inter' + priority 5 :154-162 ├─ uasort() × $sortFn (proje son, çekirdek motor ilk, sonra prio) :164-196 └─ ['styles'=>…, 'scripts'=>…] ``` ### 3.1 Sıralama kuralı — ölçüldü `build()` içindeki `$sortFn` üç kademeli (`AssetBuilder.php:164-192`): 1. `/project-assets/` ile başlayan **daima en son**; 2. `bootstrap` / `rbn-master.css` / `rbn-core.css` içeren **daima ilk**; 3. sonra `priority` artan. Ölçülen çıktı (`DOC-AGAC-3-dogrula3.php` V4): ``` /framework-assets/css/rbn-core.css prio=1 /framework-assets/css/bootstrap.min.css prio=5 http://localhost/framework-assets/fonts/inter prio=5 ← otomatik Inter /project-assets/css/site.css prio=99 ``` ### 3.2 Token çözümleme ve web URL'i (AssetBuilder.php:289-377) `compile($item)`: 1. `resolveToken($path)` → `@project/` veya `@fw/` öneki soyulur; kalan kaynaksızsa `null` hint döner; `@font/` → hint `font` (`:350-369`). 2. `DiscoveryEngine::instance()->assets()->resolve($cleanPath, $sourceHint)` fiziksel dosyayı bulur; bulunamazsa ve `http` içermiyorsa **`null` döner** (kuyruğa eklenmez — `:297-299`). 3. Sürüm: `AssetConfig::VERSION` (`1.0`), fiziksel dosya varsa `filemtime()` (`:301-304`). 4. `generateWebUrl()`: * hint `font` → `/framework-assets/fonts/` (`:320-322`); * `http` içeriyorsa olduğu gibi döner (`:324-326`); * framework kaynağı ve çok parçalı yol → `/` (küme = ilk parça, `images` hariç tam yol küçük harfle) (`:335-342`); * sonuç: `url('//=v')` (`:347`). 5. `detectType()` → uzantı → `AssetConfig::EXTENSION_MAP`, varsayılan `js` (`:373-377`). ## 4. Yapılandırma ve varsayılanlar | Sabit | Değer | Kaynak | |---|---|---| | `AssetConfig::VERSION` | `1.0` | `Configs/AssetConfig.php:18` | | `AssetConfig::PROXY_SETUP['project']` | `{"path":"project-assets","token":"@project/"}` | `AssetConfig.php:26-29` (ölçüldü) | | `AssetConfig::PROXY_SETUP['framework']` | `{"path":"framework-assets","token":"@fw/"}` | `AssetConfig.php:30-33` (ölçüldü) | | `AssetConfig::EXTENSION_MAP` | `css→css, js→js, mjs→js, less→css, scss→css, png/jpg/svg→image, json→data` | `AssetConfig.php:80-90` | | `SeoConfig::VERSION` | `1.3` | `Configs/SeoConfig.php:19` (ölçüldü) | | Varsayılan font önceliği | `5` (en üstte yüklensin) | `AssetBuilder.php:159` | | Varsayılan font | `@font/Inter` (yalnız `frontend` bağlamında) | `AssetBuilder.php:154-155` | | `addFont` önceliği | `15` | `AssetBuilder.php:70` | | Özyineleme sınırı | `8` | `AssetBuilder.php:210` | | CSS motoru tercihi | route config `css_engine`: `bootstrap` (varsayılan) \| `rbn` | `AssetBuilder.php:250-252` | | `BreadcrumbConfig::TERMINAL_ACTIONS` | `["view","show","preview","download"]` | `Configs/BreadcrumbConfig.php:77` (ölçüldü) | ## 5. Tuzaklar ve kurallar (ölçülmüş) 1. **`addStyle()` ve `addScript()` ikisi de yinelenen yolu eler — koruma çalışıyor.** `addStyle()` `!isset($this->styles[$path])` kullanır (`AssetBuilder.php:41`), `addScript()` dizi taraması yapar (`:52-56`). Ölçüldü: `addStyle('x.css',1)`, `addStyle('y.css',2)`, `addStyle('x.css',9)` → `$styles` **anahtarları `["x.css","y.css"]`** (2 eleman) ve `x.css` önceliği **1'de kalır** (ikinci çağrının `9` değeri yok sayılır). `addScript('/a.js')` iki kez → `build()['scripts']` **1 eleman**. *(Not: `build()['styles']` bu durumda **3** eleman döndürür — fark, otomatik Inter fontundur (`§5.2`), çoğalma değildir. İlk ölçüm yanlış yorumlanmıştı; `acik-sorular.md` §1.8 bu düzeltmeyle güncellendi.)* 2. **⚠️ `addFont()` gerçek bir çift-font riski taşıyor (ölçüldü).** `build()`'in "hiç font yoksa Inter ekle" kontrolü **derlenmiş** yola bakar (`str_contains($path, '/fonts/')`, `AssetBuilder.php:148`), ama `addFont()` kuyruğa **derlenmemiş** `@font/` yazar (`:72`). Ölçülen sonuç: | Çağrı | `$styles` anahtarları | `build()['styles']` | |---|---|---| | `addFont('Inter')` | `["@font/Inter"]` | **1** — `…/fonts/inter` (prio 5) | | `addFont('Roboto')` | `["@font/Roboto"]` | **2** — `…/fonts/inter` (prio 5) **+** `@font/Roboto` (prio 15) | Yani **`addFont('Roboto')` çağrısında varsayılan Inter de eklenir**: kontrol `@font/Roboto` yolunda `/fonts/` **bulamaz** (yol henüz derlenmemiş), font yok sayar ve Inter'i ekler. `prepare()` akışında bu **düzelir** (`AssetBuilder.php:93-113` temizliği `payload` üzerinde çalışır ve `@font/` ile başlayan ham yolları görür); ama **`addFont()` doğrudan `AssetService`/controller üzerinden** çağrılırsa çift font basılır (`FrontendBaseController::addAsset()` → `AssetService::addStyle()` yolu). *(Beklenen: `build()` içindeki font kontrolü `str_contains($path, '/fonts/')` **veya** `str_starts_with($path, '@font/')` ile iki biçimi de kabul etsin. Kod değiştirilmedi — belge salt okunurdur.)* 3. **`prepare()` font temizliği yalnız `frontend` bağlamında çalışır** (`:93`). `panel`/`auth` bağlamında özel font eklenirse temizlenmez. 4. **`compile()` çözülemeyen yolu `NULL` döndürür** → kuyruğa **eklenmez** (`:297-299`). Ölçüldü: | Girdi | Sonuç | |---|---| | `/framework-assets/css/yok.css` | `NULL` | | `@project/css/a.css` | `NULL` (proje diskinde yok) | | `@font/Inter` | `{"path":"…/framework-assets/fonts/inter","type":"js", …}` | | `https://cdn.example.com/x.js` | `{"path":"https://cdn.example.com/x.js","type":"js", …}` (olduğu gibi) | Yani **bulunamayan varlık sessizce düşer** (404 değil, hiç ``/`