Multi-Tenant Tema Sistemi - Layout, Settings, SEO, Dark Mode, Leonardo AI
t-{id} = Tenant temasi. Sadece ozel dosyalar burada.
simple = Fallback (mevcut, zaten var). t-{id}'de yoksa buradan alir.
Ornek: t-3/homepage.blade.php var -> onu kullan, t-3/blog/show.blade.php yok -> simple'dan al
resources/views/themes/ | +-- simple/ ← FALLBACK (mevcut, dokunma) | +-- layouts/ | +-- homepage.blade.php | +-- t-1001/ ← Muzibu +-- t-2/ ← Ixtif +-- t-3/ ← Panjur +-- layouts/ | +-- app.blade.php (Ana layout) | +-- header.blade.php (Header) | +-- footer.blade.php (Footer) +-- homepage.blade.php (STANDALONE)
ONEMLI: Modul view'lari AYRI konumda! "Modul View Path'leri" bolumune bak.
Her temanin kendi header ve footer'i olmali. Fallback degil, temaya ozel. Bu sayede her site kendi gorunumune sahip olur.
Her tema kendi layouts/header.blade.php ve layouts/footer.blade.php dosyasina sahip olmali. Baska temadan cekme!
resources/views/themes/t-{id}/
+-- layouts/
| +-- app.blade.php ← Ana layout (head, body wrapper)
| +-- header.blade.php ← Top bar + Navigation
| +-- footer.blade.php ← Footer + WhatsApp button
+-- homepage.blade.php ← Standalone veya @extends
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<!-- FOUC Prevention -->
<script>
if (localStorage.getItem('darkMode') === 'true') {
document.documentElement.classList.add('dark');
}
</script>
<x-seo-meta />
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
@stack('styles')
</head>
<body x-data="{ darkMode: localStorage.getItem('darkMode') === 'true', mobileMenu: false }"
x-init="$watch('darkMode', val => { localStorage.setItem('darkMode', val); ... })">
@include('themes.t-{id}.layouts.header')
<main>
@yield('content')
</main>
@include('themes.t-{id}.layouts.footer')
@stack('scripts')
</body>
</html>
{{-- Service/Page view'lari bu layout'u kullanir --}} @extends('themes.t-3.layouts.app') @section('content') <!-- Sayfa icerigi --> @endsection
Setting degerleri kullanimda FALLBACK olmamali. Bos ise alan gizlenir, varsayilan deger gosterilmez.
// YANLIS - YAPMA!
$siteName = setting('site_name') ?: 'Varsayilan Ad';
$sitePhone = setting('site_phone') ?: '0212 123 45 67';
$siteWhatsapp = setting('site_whatsapp') ?: '905001234567';
// DOGRU - Fallback yok, bos ise null @php $siteName = setting('site_name'); $sitePhone = setting('site_phone'); $siteWhatsapp = setting('site_whatsapp') ? preg_replace('/[^0-9]/', '', setting('site_whatsapp')) : null; @endphp // Kosullu gosterim - Bos ise gizle @if($sitePhone) <a href="tel:{{ $sitePhone }}">{{ $sitePhone }}</a> @endif @if($siteWhatsapp) <a href="https://wa.me/{{ $siteWhatsapp }}">WhatsApp</a> @endif
site_namesite_slogansite_descriptionsite_logosite_phonesite_gsmsite_emailsite_whatsappsite_address{{-- Iletisim bolumu - Hic veri yoksa tum bolumu gizle --}} @if($sitePhone || $siteMobile || $siteEmail) <div class="contact-section"> @if($sitePhone) <div>Telefon: {{ $sitePhone }}</div> @endif @if($siteMobile) <div>Mobil: {{ $siteMobile }}</div> @endif @if($siteEmail) <div>Email: {{ $siteEmail }}</div> @endif </div> @endif {{-- Top bar - Telefon yoksa gizle --}} @if($sitePhone || $siteMobile) <div class="top-bar">...</div> @endif
SEO etiketleri, Google'in sitenizi anlamasini saglar. Title, description, Open Graph, Twitter Cards ve Schema.org otomatik olusturur.
<!-- head icine ekle -->
<x-seo-meta />
<!-- Otomatik uretir: -->
<title>Sayfa Basligi</title>
<meta name="description" content="...">
<meta property="og:title" content="...">
<script type="application/ld+json">{Schema.org}</script>
resources/views/components/seo-meta.blade.php ← Component app/View/Components/SeoMeta.php ← Class
<script>
if (localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
tailwind.config = {
darkMode: 'class', // class bazli!
}
<button x-data="{ dark: ... }"
@click="dark = !dark; localStorage.setItem('darkMode', dark);
document.documentElement.classList.toggle('dark', dark)">
<i :class="dark ? 'fa-sun' : 'fa-moon'"></i>
</button>
Leonardo AI API ile otomatik gorsel uretimi. Service, Page, Blog gibi modeller icin hero gorseli olusturur ve Spatie Media Library'ye ekler.
// Kullanim - Otomatik queue'ya gonder generate_ai_cover($model, 'Baslik', 'type'); // Ornekler: generate_ai_cover($service, $service->title, 'service'); generate_ai_cover($page, $page->title, 'page'); generate_ai_cover($blog, $blog->title, 'blog'); // Dosya: Modules/MediaManagement/helpers.php
// LeonardoAIService direkt kullanim $leonardo = app(\App\Services\Media\LeonardoAIService::class); $imageData = $leonardo->generateFromPrompt($prompt, [ 'width' => 1472, 'height' => 832, 'style' => 'stock_photo', // cinematic, dynamic, hdr, vibrant... ]); if ($imageData && !empty($imageData['url'])) { $model->addMediaFromUrl($imageData['url']) ->usingFileName('hero.jpg') ->withCustomProperties([ 'ai_generated' => true, 'prompt' => $prompt, ]) ->toMediaCollection('hero'); } // Dosya: app/Services/Media/LeonardoAIService.php
// /tmp/generate-images.php <?php $tenant = \App\Models\Tenant::find(3); tenancy()->initialize($tenant); $leonardo = app(\App\Services\Media\LeonardoAIService::class); // Tutarli renk paleti icin ortak stil $colorStyle = ", professional photography, warm orange and cool gray color palette, modern industrial aesthetic, 4k quality"; $prompts = [ 1 => 'Technician repairing shutter mechanism' . $colorStyle, 2 => 'Modern motorized roller shutter' . $colorStyle, // ... ]; foreach ($prompts as $id => $prompt) { $service = \Modules\Service\App\Models\Service::find($id); if ($service->hasMedia('hero')) continue; // Skip if exists $imageData = $leonardo->generateFromPrompt($prompt, [ 'width' => 1472, 'height' => 832, 'style' => 'stock_photo' ]); if ($imageData) { $service->addMediaFromUrl($imageData['url']) ->toMediaCollection('hero'); } sleep(5); // Rate limit } // Calistir: php artisan tinker /tmp/generate-images.php
cinematic - Sinematikstock_photo - Stok Fotodynamic - Dinamikhdr - HDRvibrant - Canlimoody - AtmosferikHer modul (Page, Service, Blog) kendi view dosyalarini kendi klasorunde tutar. Ana resources/views degil, Modules klasoru icinde!
Modules/Page/resources/views/themes/ +-- simple/homepage.blade.php ← Fallback +-- ixtif/homepage.blade.php +-- t-3/homepage.blade.php ← Panjur Modules/Service/resources/views/themes/ +-- simple/service/index.blade.php +-- simple/service/show.blade.php Modules/Blog/resources/views/themes/ +-- simple/blog/index.blade.php +-- simple/blog/show.blade.php
Tinker'da tenant context'i baslatirken dogru model kullan:
// YANLIS: Tenant::find(3); // YANLIS: \Stancl\Tenancy\Database\Models\Tenant::find(3); // DOGRU: $tenant = \App\Models\Tenant::find(3); tenancy()->initialize($tenant);
Service/Page modulu 404 donuyorsa module_tenants tablosunda atama eksik olabilir:
// Central DB'de calistir: INSERT INTO module_tenants (module_id, tenant_id, is_active, created_at, updated_at) VALUES (27, 3, true, NOW(), NOW()); // module_id: modules tablosundan Service = 27, Page = 12 // tenant_id: tenants tablosundan
Cok satirli string'lerde parse error olursa:
// PHP dosyasina yaz: /tmp/script.php // Tinker ile calistir: php artisan tinker /tmp/script.php