Files
citrus/docs/DYNAMIC_TEMPLATE_SYSTEM.md
T

26 KiB
Raw Blame History

Dynamic Template System (Dinamik Şablon Sistemi)

Genel Bakış

Bu sistem, sayfalara dinamik header, section ve footer şablonları ekleyerek içerik yönetimini tamamen özelleştirilebilir hale getirir.

Mimari

1. Template Modülleri

1.1 Header Template

  • Tablo: header_templates
  • Alanlar:
    • id (bigint, PK)
    • title (string, 255) - Şablon başlığı
    • html_content (longtext) - HTML içerik + placeholder'lar 4GB kapasiteli
    • is_active (boolean) - Aktif/Pasif
    • created_at, updated_at, deleted_at

1.2 Section Template

  • Tablo: section_templates
  • Alanlar:
    • id (bigint, PK)
    • title (string, 255) - Şablon başlığı
    • html_content (longtext) - HTML içerik + placeholder'lar 4GB kapasiteli
    • is_active (boolean) - Aktif/Pasif
    • created_at, updated_at, deleted_at
  • Tablo: footer_templates
  • Alanlar:
    • id (bigint, PK)
    • title (string, 255) - Şablon başlığı
    • html_content (longtext) - HTML içerik + placeholder'lar 4GB kapasiteli
    • is_active (boolean) - Aktif/Pasif
    • created_at, updated_at, deleted_at

1.4 Menu Template

  • Tablo: menu_templates
  • Alanlar:
    • id (bigint, PK)
    • title (string, 255) - Şablon başlığı
    • html_content (longtext) - HTML içerik + {menu} placeholder 4GB kapasiteli
    • is_active (boolean) - Aktif/Pasif
    • created_at, updated_at, deleted_at

Not: longtext veri tipi MySQL'de 4,294,967,295 karakter (4GB) kapasiteye sahiptir. Bu, en karmaşık HTML template'ler ve zengin içerikler için fazlasıyla yeterlidir.

2. Placeholder Sistemi

2.1 Placeholder Formatı

{form_type.field_name}

Özel Placeholder'lar:

  • {menu} → Menü yapısını render eder (MenuTemplate için kullanılır)
  • Diğer tüm template tiplerinde (header, footer, section) normal placeholder'lar kullanılır

Desteklenen Form Tipleri:

Text Input Variants:

  • text → TextInput (basic text)
  • email → TextInput (email validation)
  • url → TextInput (URL validation)
  • tel → TextInput (telephone)
  • number → TextInput (numeric)
  • password → TextInput (password)

Text Area & Editors:

  • textarea → Textarea (multi-line text)
  • richtext → RichEditor (WYSIWYG HTML editor)
  • markdown → MarkdownEditor (Markdown editor with preview)
  • code → CodeEditor (syntax highlighted code)

Date & Time:

  • date → DatePicker (date only)
  • datetime → DateTimePicker (date + time)
  • time → TimePicker (time only)

File Uploads:

  • image → FileUpload (image only, with preview)
  • file → FileUpload (any file type)
  • files → FileUpload (multiple files)
  • images → FileUpload (multiple images)

Selection & Options:

  • select → Select (dropdown selection)
  • multiselect → Select (multiple selection)
  • checkbox → Checkbox (single checkbox)
  • checkboxlist → CheckboxList (multiple checkboxes)
  • radio → Radio (radio buttons)
  • toggle → Toggle (switch button)
  • togglebuttons → ToggleButtons (button group)

Color & Visual:

  • color → ColorPicker (color picker)

Structured Data:

  • tags → TagsInput (tag input)
  • keyvalue → KeyValue (key-value pairs)
  • repeater → Repeater (repeatable fields - not recommended in template)
  • builder → Builder (block builder - not recommended in template)

Advanced:

  • slider → Slider (range slider)
  • hidden → Hidden (hidden field for calculations)

Toplam: 30+ Form Tipi Desteği

Form Tipi Referans Tablosu

Placeholder Type Filament Component Örnek Kullanım Veri Tipi Max Boyut
{text.name} TextInput Kısa metinler string 255 char
{email.address} TextInput (email) E-posta adresleri string 255 char
{url.link} TextInput (url) Web adresleri string 500 char
{tel.phone} TextInput (tel) Telefon numaraları string 20 char
{number.count} TextInput (numeric) Sayısal değerler integer/float -
{password.pass} TextInput (password) Şifreler string (hashed) 255 char
{textarea.desc} Textarea Çok satırlı metin string 5000 char
{richtext.content} RichEditor HTML içerik longtext 50000 char
{markdown.post} MarkdownEditor Markdown içerik longtext 50000 char
{code.snippet} CodeEditor Kod blokları longtext 50000 char
{date.birthday} DatePicker Tarih seçimi date -
{datetime.event} DateTimePicker Tarih + saat datetime -
{time.opening} TimePicker Saat seçimi time -
{image.banner} FileUpload Tek resim string (path) 5MB
{images.gallery} FileUpload (multiple) Çoklu resim json (paths) 10x5MB
{file.document} FileUpload Tek dosya string (path) 10MB
{files.attachments} FileUpload (multiple) Çoklu dosya json (paths) 10x10MB
{select.category} Select Tekli seçim string/int -
{multiselect.tags} Select (multiple) Çoklu seçim json (array) -
{checkbox.agree} Checkbox Tekli checkbox boolean -
{checkboxlist.options} CheckboxList Checkbox listesi json (array) -
{radio.gender} Radio Radio button string/int -
{toggle.active} Toggle Açık/Kapalı boolean -
{togglebuttons.size} ToggleButtons Buton grubu string -
{color.theme} ColorPicker Renk seçici string (hex) 7 char
{tags.keywords} TagsInput Etiket girişi json (array) -
{keyvalue.meta} KeyValue Anahtar-değer json (object) -
{slider.volume} Slider Kaydırıcı integer -
{hidden.calc} Hidden Gizli alan mixed -

2.2 Örnek Template (Comprehensive)

<header class="site-header" style="background-color: {color.header_bg};">
    <div class="container">
        <!-- Logo Section -->
        <div class="logo">
            <img src="{image.logo}" alt="{text.company_name}">
            <span class="tagline">{textarea.tagline}</span>
        </div>
        
        <!-- Navigation -->
        <nav class="main-nav">
            {richtext.menu_html}
        </nav>
        
        <!-- Contact Info -->
        <div class="contact-info">
            <a href="mailto:{email.contact_email}">{email.contact_email}</a>
            <a href="tel:{tel.phone}">{tel.phone}</a>
        </div>
        
        <!-- Social Links -->
        <div class="social-links">
            <a href="{url.facebook_link}" target="_blank">Facebook</a>
            <a href="{url.twitter_link}" target="_blank">Twitter</a>
            <a href="{url.linkedin_link}" target="_blank">LinkedIn</a>
        </div>
        
        <!-- Toggle Features -->
        <div class="features">
            {toggle.show_search}
            {toggle.show_cart}
        </div>
    </div>
</header>

<!-- CSS Snippet -->
<style>
    .site-header {
        padding: {number.padding}px;
        opacity: {slider.opacity}%;
    }
</style>

Bu template şu form alanlarını oluşturur:

  • Logo (FileUpload - image)
  • Company Name (TextInput)
  • Tagline (Textarea)
  • Menu HTML (RichEditor)
  • Contact Email (TextInput - email)
  • Phone (TextInput - tel)
  • Facebook Link (TextInput - url)
  • Twitter Link (TextInput - url)
  • LinkedIn Link (TextInput - url)
  • Show Search (Toggle)
  • Show Cart (Toggle)
  • Header BG (ColorPicker)
  • Padding (TextInput - number)
  • Opacity (Slider)

3. Pages Modülü Entegrasyonu

3.1 Pages Tablo Güncellemesi

// Migration: add_template_fields_to_pages_table
Schema::table('pages', function (Blueprint $table) {
    $table->foreignId('header_template_id')->nullable()->constrained('header_templates')->nullOnDelete();
    $table->json('header_data')->nullable(); // Header placeholder değerleri
    
    $table->foreignId('footer_template_id')->nullable()->constrained('footer_templates')->nullOnDelete();
    $table->json('footer_data')->nullable(); // Footer placeholder değerleri
    
    // sections_data -> Repeater ile sections (eski page_sections yerine)
    // Format: [
    //   {
    //     'section_template_id': 1,
    //     'section_data': { 'text.title': 'Başlık', 'image.banner': 'path/to/image.jpg' }
    //   },
    //   ...
    // ]
    $table->json('sections_data')->nullable();
});

Veri Tipi Notları:

  • json column tipi: MySQL 5.7.8+ ve PostgreSQL 9.4+ destekli
  • JSON alanlar Laravel tarafından otomatik encode/decode edilir
  • longtext alternatifi: Tüm veritabanlarıyla uyumlu (Laravel JSON cast ile)
  • Her iki durumda da Laravel'in $casts = ['header_data' => 'array'] özelliği kullanılır

3.2 Page Model İlişkileri

class Page extends Model
{
    public function headerTemplate()
    {
        return $this->belongsTo(HeaderTemplate::class);
    }
    
    public function footerTemplate()
    {
        return $this->belongsTo(FooterTemplate::class);
    }
    
    // sections_data JSON içinde section_template_id'ler var
    public function getSectionsAttribute()
    {
        $sectionsData = $this->sections_data ?? [];
        return collect($sectionsData)->map(function ($section) {
            return [
                'template' => SectionTemplate::find($section['section_template_id']),
                'data' => $section['section_data'] ?? [],
            ];
        });
    }
}

4. Filament Form Yapısı

4.1 Page Form (PageForm.php)

use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Repeater;

Tabs::make('PageTabs')
    ->tabs([
        Tab::make('Genel Bilgiler')
            ->schema([
                TextInput::make('title')->required(),
                TextInput::make('slug')->required(),
                // ... diğer genel alanlar
            ]),
        
        Tab::make('Header Template')
            ->schema([
                Select::make('header_template_id')
                    ->label('Header Template')
                    ->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
                    ->live()
                    ->afterStateUpdated(fn ($state, Set $set) => $set('header_data', [])),
                
                // Dinamik header alanları
                Group::make()
                    ->schema(fn (Get $get) => self::generateDynamicFields(
                        HeaderTemplate::find($get('header_template_id')),
                        'header_data'
                    ))
                    ->visible(fn (Get $get) => filled($get('header_template_id'))),
            ]),
        
        Tab::make('Sections')
            ->schema([
                Repeater::make('sections_data')
                    ->schema([
                        Select::make('section_template_id')
                            ->label('Section Template')
                            ->options(SectionTemplate::where('is_active', true)->pluck('title', 'id'))
                            ->live()
                            ->required(),
                        
                        // Dinamik section alanları
                        Group::make()
                            ->schema(fn (Get $get, $record) => self::generateDynamicFields(
                                SectionTemplate::find($get('section_template_id')),
                                'section_data'
                            ))
                            ->visible(fn (Get $get) => filled($get('section_template_id'))),
                    ])
                    ->itemLabel(fn (array $state): ?string => 
                        SectionTemplate::find($state['section_template_id'] ?? null)?->title ?? 'Section'
                    )
                    ->collapsible()
                    ->reorderable()
                    ->addActionLabel(__('pages.add_section')),
            ]),
        
        Tab::make('Footer Template')
            ->schema([
                Select::make('footer_template_id')
                    ->label('Footer Template')
                    ->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
                    ->live()
                    ->afterStateUpdated(fn ($state, Set $set) => $set('footer_data', [])),
                
                // Dinamik footer alanları
                Group::make()
                    ->schema(fn (Get $get) => self::generateDynamicFields(
                        FooterTemplate::find($get('footer_template_id')),
                        'footer_data'
                    ))
                    ->visible(fn (Get $get) => filled($get('footer_template_id'))),
            ]),
    ]);

4.2 Dinamik Form Generator

use Filament\Forms\Components\{
    TextInput, Textarea, Select, Checkbox, CheckboxList,
    Radio, Toggle, ToggleButtons, DateTimePicker, DatePicker,
    TimePicker, FileUpload, RichEditor, MarkdownEditor, 
    ColorPicker, TagsInput, KeyValue, CodeEditor, Hidden, Slider
};

protected static function generateDynamicFields($template, string $dataKey): array
{
    if (!$template) return [];
    
    $placeholders = self::parsePlaceholders($template->html_content);
    $fields = [];
    
    foreach ($placeholders as $placeholder) {
        [$type, $name] = explode('.', $placeholder);
        
        $label = str($name)->title()->replace('_', ' ')->toString();
        
        $field = match($type) {
            // Text Input Variants
            'text' => TextInput::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->maxLength(255),
            
            'email' => TextInput::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->email()
                ->maxLength(255),
            
            'url' => TextInput::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->url()
                ->maxLength(500),
            
            'tel' => TextInput::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->tel()
                ->maxLength(20),
            
            'number' => TextInput::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->numeric(),
            
            'password' => TextInput::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->password()
                ->dehydrated(fn ($state) => filled($state))
                ->maxLength(255),
            
            // Text Area & Editors
            'textarea' => Textarea::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->rows(4)
                ->maxLength(5000),
            
            'richtext' => RichEditor::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->toolbarButtons([
                    'bold', 'italic', 'underline', 'link',
                    'bulletList', 'orderedList', 'h2', 'h3',
                ])
                ->maxLength(50000),
            
            'markdown' => MarkdownEditor::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->toolbarButtons([
                    'bold', 'italic', 'strike', 'link',
                    'heading', 'bulletList', 'orderedList', 'codeBlock',
                ])
                ->maxLength(50000),
            
            'code' => CodeEditor::make("{$dataKey}.{$placeholder}")
                ->label($label)
                //->lineNumbers()
                ->maxLength(50000),
            
            // Date & Time
            'date' => DatePicker::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->native(false),
            
            'datetime' => DateTimePicker::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->native(false)
                ->seconds(false),
            
            'time' => TimePicker::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->native(false)
                ->seconds(false),
            
            // File Uploads
            'image' => FileUpload::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->image()
                ->imageEditor()
                ->imageEditorAspectRatios([
                    null,
                    '16:9',
                    '4:3',
                    '1:1',
                ])
                ->directory('templates/images')
                ->maxSize(5120),
            
            'images' => FileUpload::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->image()
                ->multiple()
                ->imageEditor()
                ->directory('templates/images')
                ->maxSize(5120)
                ->maxFiles(10),
            
            'file' => FileUpload::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->directory('templates/files')
                ->maxSize(10240),
            
            'files' => FileUpload::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->multiple()
                ->directory('templates/files')
                ->maxSize(10240)
                ->maxFiles(10),
            
            // Selection & Options
            'select' => Select::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->options(self::getSelectOptions($name))
                ->searchable(),
            
            'multiselect' => Select::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->multiple()
                ->options(self::getSelectOptions($name))
                ->searchable(),
            
            'checkbox' => Checkbox::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->inline(false),
            
            'checkboxlist' => CheckboxList::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->options(self::getSelectOptions($name))
                ->columns(2),
            
            'radio' => Radio::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->options(self::getSelectOptions($name))
                ->inline()
                ->inlineLabel(false),
            
            'toggle' => Toggle::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->inline(false),
            
            'togglebuttons' => ToggleButtons::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->options(self::getSelectOptions($name))
                ->inline()
                ->grouped(),
            
            // Color & Visual
            'color' => ColorPicker::make("{$dataKey}.{$placeholder}")
                ->label($label),
            
            // Structured Data
            'tags' => TagsInput::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->separator(','),
            
            'keyvalue' => KeyValue::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->keyLabel('Key')
                ->valueLabel('Value')
                ->reorderable(),
            
            // Advanced
            'slider' => Slider::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->minValue(0)
                ->maxValue(100)
                ->step(1),
            
            'hidden' => Hidden::make("{$dataKey}.{$placeholder}"),
            
            // Default
            default => TextInput::make("{$dataKey}.{$placeholder}")
                ->label($label)
                ->maxLength(255)
                ->helperText("Unknown type: {$type}"),
        };
        
        $fields[] = $field;
    }
    
    return $fields;
}

/**
 * Select, MultiSelect, CheckboxList, Radio, ToggleButtons için
 * dinamik options yükler. Gerçek uygulamada config veya database'den
 * okunabilir.
 */
protected static function getSelectOptions(string $fieldName): array
{
    // Örnek: config/template-options.php dosyasından okuyabilirsiniz
    return config("template-options.{$fieldName}", [
        'option_1' => 'Option 1',
        'option_2' => 'Option 2',
        'option_3' => 'Option 3',
    ]);
}

/**
 * Template HTML içeriğindeki placeholder'ları parse eder
 * Desteklenen format: {type.field_name}
 */
protected static function parsePlaceholders(string $html): array
{
    // Regex: {word.word_with_underscores}
    preg_match_all('/\{([a-z]+\.[a-z_]+)\}/i', $html, $matches);
    return array_unique($matches[1] ?? []);
}

5. Frontend Rendering

5.1 Blade Component (PageRenderer)

// app/View/Components/PageRenderer.php
class PageRenderer extends Component
{
    public function __construct(public Page $page) {}
    
    public function render()
    {
        return view('components.page-renderer');
    }
    
    public function renderHeader()
    {
        if (!$this->page->headerTemplate) return '';
        
        return $this->replaceePlaceholders(
            $this->page->headerTemplate->html_content,
            $this->page->header_data ?? []
        );
    }
    
    public function renderSections()
    {
        return collect($this->page->sections)->map(function ($section) {
            return $this->replacePlaceholders(
                $section['template']->html_content,
                $section['data']
            );
        })->implode('');
    }
    
    public function renderFooter()
    {
        if (!$this->page->footerTemplate) return '';
        
        return $this->replacePlaceholders(
            $this->page->footerTemplate->html_content,
            $this->page->footer_data ?? []
        );
    }
    
    protected function replacePlaceholders(string $html, array $data): string
    {
        foreach ($data as $placeholder => $value) {
            // {text.name} → value
            $html = str_replace("{{$placeholder}}", $value, $html);
        }
        return $html;
    }
}

5.2 Blade View

<!-- resources/views/components/page-renderer.blade.php -->
<div class="page-wrapper">
    {!! $renderHeader() !!}
    
    <main class="page-content">
        {!! $renderSections() !!}
    </main>
    
    {!! $renderFooter() !!}
</div>

6. Navigasyon Yapısı

// Filament Panel Provider
->navigationGroups([
    'Template' => [
        'icon' => 'heroicon-o-document-duplicate',
        'order' => 3,
    ],
])

// Her Resource'da
public static function getNavigationGroup(): string
{
    return 'Template';
}

Dosya Yapısı

app/
├── Models/
│   ├── HeaderTemplate.php
│   ├── SectionTemplate.php
│   └── FooterTemplate.php
├── Filament/Admin/Resources/
│   ├── HeaderTemplates/
│   │   ├── HeaderTemplateResource.php
│   │   ├── Pages/
│   │   ├── Schemas/
│   │   └── Tables/
│   ├── SectionTemplates/
│   │   ├── SectionTemplateResource.php
│   │   ├── Pages/
│   │   ├── Schemas/
│   │   └── Tables/
│   └── FooterTemplates/
│       ├── FooterTemplateResource.php
│       ├── Pages/
│       ├── Schemas/
│       └── Tables/
├── View/Components/
│   └── PageRenderer.php
└── Services/
    └── TemplateService.php (helper methods)

database/migrations/
├── create_header_templates_table.php
├── create_section_templates_table.php
├── create_footer_templates_table.php
└── add_template_fields_to_pages_table.php

lang/
├── tr/
│   ├── header-templates.php
│   ├── section-templates.php
│   └── footer-templates.php
└── en/
    ├── header-templates.php
    ├── section-templates.php
    └── footer-templates.php

resources/views/
└── components/
    └── page-renderer.blade.php

Örnek Kullanım Senaryosu

1. Template Tanımlama

Header Template:

<header>
    <img src="{image.logo}" alt="{text.site_name}">
    <nav>{richtext.menu_html}</nav>
    <a href="mailto:{email.contact}">{email.contact}</a>
</header>

2. Page'de Template Seçimi

  • Header Template: "Ana Header" seçildi
  • Dinamik alanlar açıldı:
    • Logo (FileUpload)
    • Site Name (TextInput)
    • Menu HTML (RichEditor)
    • Contact Email (TextInput)
  • Values dolduruldu ve kaydedildi

3. Frontend'de Görüntüleme

<x-page-renderer :page="$page" />

Output:

<header>
    <img src="/storage/uploads/logo.png" alt="My Company">
    <nav><ul><li><a href="/">Home</a></li></ul></nav>
    <a href="mailto:info@company.com">info@company.com</a>
</header>

Avantajlar

  1. Tamamen Dinamik: Kod değişikliği olmadan yeni template'ler eklenebilir
  2. Yeniden Kullanılabilir: Aynı template birden fazla sayfada kullanılabilir
  3. Kolay Yönetim: Admin panel üzerinden HTML düzenlenebilir
  4. Tip Güvenli: Placeholder'lar form tipini belirler
  5. Ölçeklenebilir: Yeni form tipleri kolayca eklenebilir
  6. SEO Dostu: Her sayfa kendi içeriğine sahip
  7. Performanslı: Sadece seçili template'ler yüklenir
  8. 30+ Form Tipi: Filament 4.x'in tüm form component'leri destekleniyor
  9. Zengin İçerik: HTML, Markdown, Code editor desteği
  10. Büyük Veri: longtext ile 4GB kapasiteli template'ler

Veri Tipi Özeti

HTML Content (Template'ler için)

html_content LONGTEXT
-- Kapasite: 4,294,967,295 karakter (4GB)
-- Kullanım: Template HTML + placeholder'lar
-- Avantaj: En karmaşık template'ler için bile yeterli

JSON Data (Page verisi için)

header_data JSON
footer_data JSON
sections_data JSON
-- MySQL 5.7.8+ ve PostgreSQL 9.4+ native JSON desteği
-- Alternatif: LONGTEXT + Laravel JSON cast
-- Laravel otomatik encode/decode yapar

Model Cast Kullanımı

protected $casts = [
    'header_data' => 'array',
    'footer_data' => 'array',
    'sections_data' => 'array',
];

Git Branch

feature/dynamic-template-system

İmplement Adımları

  1. Migrations oluştur (4 adet)
  2. Model'ler oluştur (3 adet)
  3. Filament Resources oluştur (3 adet)
  4. TemplateService helper oluştur
  5. PageForm'u güncelle (template tabs ekle)
  6. PageRenderer component oluştur
  7. Lang dosyaları oluştur (6 adet)
  8. Frontend route ve controller güncelle
  9. Test

Not: Bu sistem mevcut page_sections sisteminin yerini alacak ve çok daha güçlü bir alternatif sunacaktır.