# Slotlar ve Şablonlar

> Zone modeli, dikey ve yatay slotlar, repeat zone (öğe şablonu) ve veri kaynakları, slider deseni, bölüm ve sayfa şablonları, id güvenliği — canlı repeat zone önizlemesiyle.
>
> Kaynak: https://tecof-app-dev.vercel.app/editor-slots · Bölüm: Tema geliştirme · Son güncelleme: 2018-10-20

Slotlar bir bileşenin içine başka bileşenlerin bırakılabildiği bölgelerdir. Bu sayfa slotların saklanma biçimini, yatay/dikey yerleşimi, verinin her satırı için tekrarlanan **repeat zone**’ları ve tek tıkla eklenen şablonları anlatır.

## Zone modeli

Kök düğümler dokümanın `content` dizisinde, iç içe düğümler `zones` nesnesinde tutulur. Zone anahtarı `parentId:slotAdı` biçimindedir:

```json
{
  "content": [{ "type": "Hero", "props": { "id": "hero-1", "title": "Yeni sezon" } }],
  "zones": {
    "hero-1:actions": [
      { "type": "Button", "props": { "id": "btn-1", "text": "Keşfet" } },
      { "type": "Button", "props": { "id": "btn-2", "text": "İncele" } }
    ]
  },
  "root": { "props": {} }
}
```

Üst düğüm silindiğinde alt zone ağacı da temizlenir. Zone anahtarı düğüm **id**’sine bağlı olduğu için bir bileşen tipinin adını değiştirmek zone anahtarlarını etkilemez.

## Dikey ve yatay slotlar

Slot alanları varsayılan olarak çocukları alt alta dizer. `orientation: "horizontal"` verildiğinde editör bırakma eksenini sol/sağ olarak hesaplar ve yayın aynı düzeni korur.

```tsx
fields: {
  columns: { type: "slot", label: "Kolonlar", orientation: "horizontal" },
  blocks: { type: "slot", label: "Bloklar" }, // dikey
}
```

Slot’u render’da çizmenin üç yolu vardır; yeni kodda ilk ikisini kullanın:

```tsx
// 1) defineSection: hazır <Slot> elemanı
render: (p, slots) => <section className="py-16">{slots.columns}</section>

// 2) <Slot> bileşeni: yerleşim tek kaynaktan
render: (p) => (
  <section className={cn("py-16", p.className)}>
    <Slot value={p.columns} layout="row-wrap" gap="lg" />
  </section>
)

// 3) Düşük seviye: puck.renderDropZone
render: ({ puck, className }) => (
  <section className={className}>{puck.renderDropZone({ zone: "columns", orientation: "horizontal" })}</section>
)
```

Düz `renderDropZone` ile yatay slot yayında inline `display:flex; flex-wrap:wrap; gap:8px` alır; `<Slot>` ise bu varsayılanı kapatıp yerleşimi `layout` / `gap` / `className`’den kurar ([Slot bileşeni](/editor-components#slot-bileseni)).

## Repeat zone (öğe şablonu)

Bir slot alanına `repeatSource` verildiğinde slot bir **öğe şablonuna** dönüşür: kart bir kez tasarlanır ve veri listesindeki her satır için tekrarlanır (ürün ızgarası, koleksiyon listesi, SSS…). Şablondaki prop’lar satıra `{{ item.<alan> }}` token’larıyla bağlanır.

<!-- preview:slot-repeat -->

```tsx title="ProductGrid.tsx"
ProductGrid: {
  label: "Ürün ızgarası",
  fields: {
    items: createRepeaterField({
      label: "Ürünler",
      subFields: { name: { type: "text", label: "Ad" }, price: { type: "text", label: "Fiyat" }, image: createUploadField({ label: "Görsel", allowMultiple: false }) },
    }),
    card: { type: "slot", label: "Kart şablonu", orientation: "horizontal", repeatSource: "items" },
  },
  render: ({ card, className }) => <section className={className}>{card}</section>,
}
```

Şablondaki bir Text düğümünün `text` prop’u `"{{ item.name }}"`, fiyat satırı `"Fiyat: {{ item.price }} TL"` olabilir.

- **Tam token ham değeri geçirir:** prop’un değeri yalnız `"{{ item.image }}"` ise görsel/bağlantı nesnesi olduğu gibi iletilir; metne gömülü token metne çevrilir.
- **Özel yollar:** `{{ item._index }}` (0 tabanlı), `{{ item._position }}` (1 tabanlı).
- **Token’ları kütüphane çözer:** `{{ item.* }}` hem editör kanvasında hem `TecofRender`’da çözülür; host tarafında iş gerekmez. (`{{ data.* }}` ise CMS kaydından, `cmsData` ile çözülür.)
- **Editörde** şablon ilk satırın verisiyle bir kez düzenlenir; kalan satırlar etkileşimsiz, soluk kopyalar olarak aynı grid/flex akışında görünür.
- **Yayında** döngü gerçek veriyle döner; satır yoksa şablon **hiç** render edilmez — canlı sitede ham token görünmez.

> [!WARNING] Bağlı metni kanvasta düzenlemeyin
> Şablonda bağlı bir metne çift tıklayıp satır içi düzenlemek token’ı literal değerle ezer ve bağı koparır. Bağlı alanları Inspector’dan düzenleyin; Inspector her zaman ham token’ı gösterir.

### Veri kaynakları

`repeatSource`’un gösterdiği prop’un değerine göre satırlar otomatik çözülür:

| Kaynak | Satırlar nereden gelir |
| :--- | :--- |
| Dizi prop / `createRepeaterField` | Dokümanda kayıtlı satırlar |
| `createApiListField` | Host’un `fetchList` fonksiyonu; editörde ve yayında (istemcide) kütüphane çağırır |
| `createCmsCollectionField` | CMS koleksiyonu; ağaçta `TecofProvider` gerekir (yoksa satırlar boş kalır) |
| `renderDropZone({ zone, repeatItems })` | Bileşen satırları render anında kendisi verir (tam kontrol) |

```tsx
// Manuel yol: veriyi bileşen kendisi getirir
render: ({ puck }) => {
  const { rows } = useMyProducts();
  return puck.renderDropZone({ zone: "card", repeatItems: rows });
},
```

Bağlama penceresindeki **Öğe alanları** listesi sırasıyla slot’un `itemSchema`’sından, kaynak alanın `itemSchema`’sından ya da çözülen ilk satırdan çıkarılır. Yalnız manuel `repeatItems` yolunda `itemSchema` bildirmeniz gerekir:

```tsx
card: {
  type: "slot",
  itemSchema: [
    { key: "name", label: "Ürün adı", type: "text" },
    { key: "image", label: "Görsel", type: "image" },
  ],
}
```

| Dışa aktarım | Ne işe yarar |
| :--- | :--- |
| `useRepeatItem()` | Şablon bileşeni içinde `{ item, index, count }`; şablon dışında `null` |
| `resolveItemTokens(value, item, index)` | Renderer’ların kullandığı token çözücü (host’un kendi render’ı için) |
| `resolveRepeatRows(value, sourceFieldDef, apiClient?)` | Satırları React dışında çözer (ör. SSR ısıtma) |
| `useRepeatRows(value, sourceFieldDef)` | Aynı çözümleme, React kancası olarak |
| `clearRepeatRowsCache()` | API/CMS satır önbelleğini temizler |
| `findRepeatScope(doc, config, nodeId)` | Bir düğümün içinde bulunduğu repeat şablonunu bulur |

> [!NOTE] Repeat slotlarda yayın yerleşimi `orientation`’dan gelir
> 0.0.96 sürümünde `TecofRender`, `repeatSource`’lu bir slotu kendi iç bileşeniyle çizer ve `<Slot>`’un `layout` / `gap` / `className` değerlerini bu bileşene **iletmez**. Yayında yatay repeat slot `orientation: "horizontal"` varsayılanını (inline `flex-wrap`, `gap: 8px`) kullanır. Kart aralığını kartın kendi kenar boşluğuyla ya da kapsayıcı bölümün sınıflarıyla verin; yukarıdaki önizleme bu davranışı gösterir.

## Slider / carousel deseni

Slaytlar ayrı bir bileşen olarak slota bırakılır. Kritik nokta `editMode` ayrımıdır: editörde tüm slaytlar yan yana görünüp sürükle-bırak almalı, yayında aynı zone kaydırılabilir bir carousel olmalıdır.

```tsx title="Slider.tsx"
const Slider = {
  label: "Slider",
  acceptsChildren: { slides: ["Slide"] },
  maxItems: { slides: 8 },
  fields: { slides: { type: "slot", label: "Slaytlar", orientation: "horizontal" } },
  render: ({ puck, editMode, className }) => (
    <section className={className}>
      {puck.renderDropZone({
        zone: "slides",
        orientation: "horizontal",
        className: editMode ? "gap-4" : "snap-x snap-mandatory overflow-x-auto gap-4",
        style: editMode ? undefined : { flexWrap: "nowrap" },
      })}
    </section>
  ),
};

const Slide = {
  label: "Slayt",
  allowedParents: ["Slider"],
  fields: { image: createUploadField({ label: "Görsel", allowMultiple: false }), caption: { type: "text", label: "Başlık" } },
  render: ({ image, caption, className }) => (
    <figure className={cn("w-80 shrink-0 snap-center", className)}>
      <TecofPicture data={image?.[0]} alt={caption ?? ""} size="large" />
      {caption && <figcaption className="mt-2 text-sm">{caption}</figcaption>}
    </figure>
  ),
};
```

- `snap-*` sınıfları slaydın kendi kökündedir; yayında zone çocukları doğrudan render edildiği için scroll-snap birebir çalışır.
- Swiper/Embla gibi DOM’u klonlayan ya da ölçen kütüphaneleri yalnız `editMode === false` iken bağlayın; editörde zone düz düzende kalmalıdır ki seçim ve bırakma hedefleri doğru çalışsın. Çoğu senaryoda CSS scroll-snap yeterlidir.

## Bölüm şablonları

`config.templates`, **Bölüm Ekle** penceresinde ayrı sekmede gösterilen hazır bölümlerdir. Şablon bir alt ağaçtır: kök düğüm ve alt zone’ları.

```tsx
templates: [
  {
    id: "hero-cta",
    label: "Hero + iki buton",
    category: "Dönüşüm",
    thumbnail: "/templates/hero-cta.webp",
    payload: {
      node: { type: "Hero", props: { id: "t-hero", title: "Başlık" } },
      zones: {
        "t-hero:actions": [
          { type: "Button", props: { id: "t-b1", text: "Başla" } },
          { type: "Button", props: { id: "t-b2", text: "İncele" } },
        ],
      },
    },
  },
],
```

| Alan | Tip | Açıklama |
| :--- | :--- | :--- |
| `id` | `string` | Kararlı kimlik |
| `label` | `string` | Kartta görünen ad |
| `category` | `string?` | Kategori |
| `thumbnail` | `string?` | Önizleme görseli (yoksa ikon) |
| `payload.node` | `TecofNode` | Eklenecek kök düğüm |
| `payload.zones` | `Record<string, TecofNode[]>?` | Kök düğümün alt zone’ları (`nodeId:slotAdı`) |

## Sayfa şablonları

`config.pageTemplates` tüm sayfayı kuran bölüm dizileridir (Ana Sayfa, Hakkımızda, İletişim…). Sol paneldeki **Sayfa şablonları** bölümünde ve Bölüm Ekle penceresinde görünür.

```tsx
pageTemplates: [
  {
    id: "core-home",
    label: "Ana sayfa",
    description: "Hero, hizmetler, referanslar ve iletişim çağrısı",
    keywords: ["kurumsal", "landing"],
    previewUrl: "/preview-template/core-home",
    sections: [
      { node: { type: "Hero", props: { id: "t-hero", title: "Başlık" } } },
      { node: { type: "Services", props: { id: "t-svc" } }, zones: { "t-svc:items": [] } },
    ],
  },
],
```

Şablona tıklamak bölümleri doğrudan eklemez; önce bir onay penceresi açılır. `previewUrl` varsa sayfa 1280 px referans genişlikte bir iframe’de (tembel, sandbox’lı, tıklanamaz) gösterilir; göreli adres editörün çalıştığı origin’e göre çözülür. Yoksa bölümlerin canlı önizlemesi üst üste dizilir. Onaydan sonra mevcut içerik silinmez; bölümler eklenir ve tamamı **tek geri al** adımıdır.

## Id güvenliği

Şablondaki id’ler yalnız ilişki kurmak için örnek kimliklerdir. Studio her eklemede kök ve tüm alt düğümler için yeni id üretir ve zone anahtarlarını buna göre yeniden yazar; aynı şablonu güvenle birden çok kez ekleyebilirsiniz. Kopyala/yapıştır ve çoğaltma için de aynı kural geçerlidir.
