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
{{-- TUM sayfalarda ayni padding kullan --}} <div class="container mx-auto px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 2xl:px-20"> ... </div> // Bu pattern: header, footer, subheader, content tum bolumler icin gecerli!
{{-- Service/Page view'lari bu layout'u kullanir --}} @extends('themes.t-3.layouts.app') @section('module_content') <!-- Sayfa icerigi --> @endsection
Service/Page sayfalari minimal subheader + icerik tasarimi kullanir. Sol taraf icerik, sag taraf gorsel ve iletisim. Kartlar kare gorsel + gradient overlay + beyaz yazi.
{{-- MINIMAL SUBHEADER - Sadece breadcrumb + title --}} <section class="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> <div class="container mx-auto px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 2xl:px-20 py-4"> <!-- Breadcrumb --> <nav class="text-sm text-gray-500 dark:text-gray-400 mb-2"> @foreach($breadcrumbsArray as $index => $crumb) ... @endforeach </nav> <!-- Title --> <h1 class="text-2xl md:text-3xl font-bold font-heading text-gray-900 dark:text-white">{{ $title }}</h1> </div> </section>
{{-- 5 kolon grid: 3 icerik + 2 sidebar --}} <div class="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-12"> <!-- SOL: Ana Icerik (3 kolon) --> <article class="lg:col-span-3"> <div class="prose prose-base max-w-none dark:prose-invert font-body"> @parsewidgets($body ?? '') </div> </article> <!-- SAG: Gorsel + Iletisim (2 kolon) --> <aside class="lg:col-span-2"> <div class="sticky top-24 space-y-6"> <!-- Kare Gorsel --> <figure class="rounded-2xl overflow-hidden shadow-2xl"> <a href="{{ $image->getUrl() }}" class="glightbox"> <img class="w-full aspect-square object-cover"> </a> </figure> <!-- Iletisim Karti --> ... </div> </aside> </div>
{{-- 2 kolonlu minimal tasarim --}} <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-4"> <div class="grid grid-cols-2 gap-2"> @if($sitePhone) <a href="tel:..." class="flex items-center gap-3 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg p-3 transition-all"> <i class="fa-solid fa-phone text-primary-600"></i> <div> <div class="text-xs text-gray-500">Telefon</div> <div class="text-sm font-semibold text-gray-900 dark:text-white">{{ $sitePhone }}</div> </div> </a> @endif @if($whatsappUrl) <a href="{{ $whatsappUrl }}" class="flex items-center gap-3 ..."> <i class="fa-brands fa-whatsapp text-green-600"></i> <div> <div class="text-xs">WhatsApp</div> <div class="text-sm font-semibold">Hemen Yazin</div> </div> </a> @endif </div> </div>
{{-- Kare gorsel + gradient + beyaz yazi --}} <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6"> @foreach ($items as $item) <a href="..." class="group relative aspect-square rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-300"> <!-- Gorsel --> <img class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"> <!-- Gradient Overlay --> <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent"></div> <!-- Baslik (Beyaz) --> <div class="absolute bottom-0 left-0 right-0 p-4"> <h2 class="text-base md:text-lg font-semibold text-white font-heading drop-shadow-lg">{{ $title }}</h2> </div> </a> @endforeach </div> // DIKKAT: Kart hover'da ziplamasin! // hover:-translate-y-2 KULLANMA! // Sadece: hover:shadow-xl ve group-hover:scale-105 (gorsel icin)
font-heading
Basliklar icin (h1, h2, h3, kart basliklari)
font-body
Icerik metni icin (paragraf, prose)
// YANLIS - YAPMA!
$siteName = setting('site_name') ?: 'Varsayilan Ad';
$sitePhone = setting('site_phone') ?: '0212 123 45 67';
// Fallback yok, bos ise gizle
@if($sitePhone)
<a href="tel:{{ $sitePhone }}">{{ $sitePhone }}</a>
@endif
<!-- head icine ekle -->
<x-seo-meta />
// Dosya: resources/views/components/seo-meta.blade.php
Sayfa yenilendiginde dark mode kayboluyordu. Alpine.js'in baslangic degeri sistem tercihini kontrol etmiyordu.
<script>
if (localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
<body x-data="{
darkMode: localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') &&
window.matchMedia('(prefers-color-scheme: dark)').matches),
mobileMenu: false
}"
x-init="
document.documentElement.classList.toggle('dark', darkMode);
$watch('darkMode', val => {
localStorage.setItem('darkMode', val);
document.documentElement.classList.toggle('dark', val);
});
"
:class="{ 'dark': darkMode }"
class="bg-white dark:bg-slate-900 ...">
Kritik: x-init icinde document.documentElement.classList.toggle('dark', darkMode) olmali!
// BU YANLIS! Sistem tercihini kontrol etmiyor x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' }" x-init="$watch('darkMode', val => { ... })" // Sorun: localStorage bos ise darkMode = false olur // Sistem dark mode'da olsa bile light gosterir
tailwind.config = {
darkMode: 'class', // class bazli!
}
{{-- GLightbox CSS --}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox@3.2.0/dist/css/glightbox.min.css">
<script src="https://cdn.jsdelivr.net/npm/glightbox@3.2.0/dist/js/glightbox.min.js"></script>
<script>
const lightbox = GLightbox({
selector: '.glightbox',
touchNavigation: true,
loop: true,
closeButton: true
});
</script>
{{-- Tek gorsel --}} <a href="{{ $image->getUrl() }}" class="glightbox"> <img src="{{ $image->getUrl('medium') }}"> </a> {{-- Galeri (ayni data-gallery ile grupla) --}} @foreach($galleryImages as $image) <a href="{{ $image->getUrl() }}" class="glightbox" data-gallery="service-gallery"> <img src="{{ $image->getUrl('thumb') }}"> </a> @endforeach
// Queue'ya gonder generate_ai_cover($service, $service->title, 'service'); // Dosya: Modules/MediaManagement/helpers.php
$leonardo = app(\App\Services\Media\LeonardoAIService::class);
$imageData = $leonardo->generateFromPrompt($prompt, [
'width' => 1472, 'height' => 832, 'style' => 'stock_photo'
]);
if ($imageData) {
$model->addMediaFromUrl($imageData['url'])
->toMediaCollection('hero');
}
Modules/Page/resources/views/themes/ +-- simple/homepage.blade.php ← Fallback +-- t-3/homepage.blade.php ← Panjur ozel Modules/Service/resources/views/themes/ +-- simple/index.blade.php ← Fallback (tum tenantlar) +-- simple/show.blade.php Modules/Blog/resources/views/themes/ +-- simple/blog/index.blade.php +-- simple/blog/show.blade.php
Service fallback: Modules/Service/resources/views/themes/simple/ klasorundeki dosyalar TUM tenantlar icin gecerli. t-3 icin ozel view yok, simple kullanilir.