v6 - Layout & Settings Kurallari

t-{id} Tema Yapisi

Multi-Tenant Tema Sistemi - Layout, Settings, SEO, Dark Mode, Leonardo AI

1. Felsefe

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

2. Klasor Yapisi

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.

3. Layout Sistemi (Her Temada Olmali!)

Basit Anlatim

Her temanin kendi header ve footer'i olmali. Fallback degil, temaya ozel. Bu sayede her site kendi gorunumune sahip olur.

KURAL: Fallback Header/Footer YOK!

Her tema kendi layouts/header.blade.php ve layouts/footer.blade.php dosyasina sahip olmali. Baska temadan cekme!

Klasor Yapisi

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

app.blade.php (Ana Layout)

<!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>

header.blade.php Icerigi

  • Top bar (telefon, calisma saatleri)
  • Logo + Site adi
  • Desktop navigation
  • CTA butonlari (Ara, WhatsApp)
  • Dark mode toggle
  • Mobile menu (Alpine.js)

footer.blade.php Icerigi

  • About bolumu (logo, aciklama)
  • Hizli erisim linkleri
  • Hizmet linkleri
  • Iletisim bilgileri
  • Copyright
  • Floating WhatsApp butonu

Kullanim (Service/Page view'larinda)

{{-- Service/Page view'lari bu layout'u kullanir --}}
@extends('themes.t-3.layouts.app')

@section('content')
    <!-- Sayfa icerigi -->
@endsection

4. Settings Kullanim Kurallari

Basit Anlatim

Setting degerleri kullanimda FALLBACK olmamali. Bos ise alan gizlenir, varsayilan deger gosterilmez.

YASAK: Fallback Degerler

// YANLIS - YAPMA!
$siteName = setting('site_name') ?: 'Varsayilan Ad';
$sitePhone = setting('site_phone') ?: '0212 123 45 67';
$siteWhatsapp = setting('site_whatsapp') ?: '905001234567';

DOGRU: Kosullu Gosterim

// 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

Uygulanacak Alanlar

site_name
site_slogan
site_description
site_logo
site_phone
site_gsm
site_email
site_whatsapp
site_address

Bolum Bazli Gizleme

{{-- 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

5. SEO Meta Tag Implementasyonu

Basit Anlatim

SEO etiketleri, Google'in sitenizi anlamasini saglar. Title, description, Open Graph, Twitter Cards ve Schema.org otomatik olusturur.

Kullanim

<!-- 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>

Dosya Konumu

resources/views/components/seo-meta.blade.php   ← Component
app/View/Components/SeoMeta.php                 ← Class

6. Dark/Light Mode

1. FOUC Onleme (head icinde)

<script>
    if (localStorage.getItem('darkMode') === 'true' ||
        (!localStorage.getItem('darkMode') &&
         window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.classList.add('dark');
    }
</script>

2. Tailwind Config

tailwind.config = {
    darkMode: 'class',  // class bazli!
}

3. Alpine.js Toggle

<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>

7. Media Olusturma & Leonardo AI

Basit Anlatim

Leonardo AI API ile otomatik gorsel uretimi. Service, Page, Blog gibi modeller icin hero gorseli olusturur ve Spatie Media Library'ye ekler.

Helper Fonksiyon (Queue)

// 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

Senkron Uretim (Direkt)

// 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

Toplu Gorsel Uretimi (Script)

// /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

Leonardo Style Secenekleri

cinematic - Sinematik
stock_photo - Stok Foto
dynamic - Dinamik
hdr - HDR
vibrant - Canli
moody - Atmosferik

Prompt Ipuclari

  • Tutarli Renk: Her prompt'a ayni renk paleti ekle
  • Kalite: "professional photography, 4k quality, photorealistic"
  • Boyut: 1472x832 (16:9) veya 832x832 (1:1)
  • Rate Limit: Her istek arasinda 3-5 saniye bekle

8. Modul View Path'leri

Her 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

9. Tespit Edilen Sorunlar

1. Tenant Model Type (Tinker'da)

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);

2. Modul 404 Hatasi - Tenant Atamasi

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

3. Tinker'da Heredoc Parse Error

Cok satirli string'lerde parse error olursa:

// PHP dosyasina yaz:
/tmp/script.php

// Tinker ile calistir:
php artisan tinker /tmp/script.php

10. Yeni Tema Checklist