# Embed'ler ve bileşenler

Embed, bir mesajın metninin altında duran karttır: başlık, açıklama, alanlar, görsel ve renkli bir şerit taşır. Bileşen ise mesaja eklenen buton ve menüdür. İkisini de yalnızca botlar gönderebilir.

## İlk embed

Çalışan en küçük istek şudur; `content` yazmak gerekmez:

```bash
curl -X POST "https://noluyo.net/api/v1/channels/$CHANNEL_ID/messages" \
  -H "Authorization: Bot $BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"embeds":[{"title":"Merhaba","description":"İlk embed","color":3368703}]}'
```

Yanıttaki mesajda `embeds` dizisi gelir ve `application_id` botun uygulamasını gösterir. Aynı mesaj gateway'de `MESSAGE_CREATE` olarak da akar.

## Embed alanları

Bir mesajda en çok **10** embed olur. Hepsinin metinleri toplam **6000** karakteri, hepsinin JSON'u **16384** baytı (UTF-8) geçemez. Bir embed'de başlık, açıklama, alan, yazar, alt bilgi, görsel veya küçük görselden en az biri bulunmalıdır. Sınır sütunundaki sayılar karakter ya da öğe sayısıdır.

| Alan | Tür | Sınır | Zorunlu | Açıklama |
|---|---|---|---|---|
| `title` | metin | 256 | hayır | Başlık |
| `description` | metin | 4096 | hayır | Ana metin |
| `url` | metin | 2048 | hayır | Başlığın bağlandığı `https` adresi |
| `color` | sayı | 16777215 | hayır | Şeridin rengi; ondalık sayı olarak, aşağıya bak |
| `timestamp` | metin | - | hayır | RFC 3339 zamanı |
| `fields` | liste | 25 | hayır | Ad ve değer çiftleri |
| `fields[].name` | metin | 256 | evet | Alanın adı |
| `fields[].value` | metin | 1024 | evet | Alanın değeri |
| `fields[].inline` | doğru/yanlış | - | hayır | `true` ise alanlar yan yana durur |
| `footer.text` | metin | 2048 | evet | Alt bilgi metni |
| `footer.icon_url` | metin | - | hayır | `attachment://` ile bir görsel dosyası |
| `author.name` | metin | 256 | evet | Yazarın adı |
| `author.url` | metin | 2048 | hayır | Yazarın bağlandığı `https` adresi |
| `author.icon_url` | metin | - | hayır | `attachment://` ile bir görsel dosyası |
| `image.url` | metin | - | hayır | `attachment://` ile bir görsel dosyası |
| `thumbnail.url` | metin | - | hayır | `attachment://` ile bir görsel dosyası |

Bilmediğin bir alan yazarsan görmezden gelinir ve kaydedilmez. Metinlerdeki kontrol karakterleri (satır sonu ve sekme dışında) hata verir. Başında ve sonundaki boşluklar silinir.

## Görsel göndermek

Embed'in görseli, aynı mesajla yüklediğin bir dosyadır. Başka bir sitedeki adres kabul edilmez: görseli okuyanın tarayıcısı dışarıdaki bir sunucuya gitmesin, okuyanın IP adresi üçüncü bir tarafa ulaşmasın diye N'oluyo yalnızca kendi sakladığı dosyaları gösterir. `https://...` ile başlayan bir görsel adresi `invalid` ile reddedilir.

Dosyalı mesaj `multipart/form-data` ile gider. JSON `payload_json` parçasına, dosyalar `files[0]`, `files[1]` gibi parçalara konur:

```bash
curl -X POST "https://noluyo.net/api/v1/channels/$CHANNEL_ID/messages" \
  -H "Authorization: Bot $BOT_TOKEN" \
  -F 'payload_json={"embeds":[{"title":"Haftalık grafik","image":{"url":"attachment://grafik.png"}}]}' \
  -F 'files[0]=@grafik.png'
```

- `-F 'payload_json=...'`: mesajın JSON'u. Embed'deki `attachment://grafik.png`, aynı istekteki `grafik.png` dosyasına işaret eder.
- `-F 'files[0]=@grafik.png'`: yüklenen dosya. Kanalda `AttachFiles` izni gerekir.
- Yanıtta `image.attachment_id`, dosyanın mesajın `attachments` listesindeki kimliğidir. `image.url` yanıtta da `attachment://` olarak kalır; görseli bulmak için kimliği kullan.

Ad şu kurallarla eşleştirilir:

- Dosya adı yüklenirken temizlenir: harf, rakam, `-`, `_` ve bir `.` kalır, her diğer karakter `_` olur. `haftalık grafik.png` böylece `haftalık_grafik.png` olur. `attachment://` adresindeki ad da aynı biçimde temizlenir, yani iki yazım da aynı dosyayı bulur.
- Yalnızca PNG, JPEG, GIF ve WebP görselleri gösterilebilir. PDF veya başka bir dosyaya başvurmak `invalid` verir.
- Mesaja eklenmemiş bir ada başvurmak `invalid` verir.
- Aynı adı taşıyan iki dosya varsa o ada başvurmak `invalid` verir; dosyaları farklı adlarla yükle.

## Renk ve zaman

`color` ondalık bir sayıdır, `#RRGGBB` yazımının sayıya çevrilmiş halidir. Onaltılık renkten çevirmek için:

```js
const color = parseInt("3366FF", 16); // 3368703
```

`timestamp` RFC 3339 biçimindedir, örneğin `2026-11-07T18:30:00Z`. JavaScript'te `new Date().toISOString()` doğru biçimi verir. Zaman UTC olarak saklanır ve okuyanın saatine çevrilerek gösterilir.

## Metin, markdown ve etiketler

Metinler markdown biçimlendirmesiyle yazılabilir; sunucu metni olduğu gibi saklar, çizimi istemci yapar. Embed içinde yazdığın `<@kullanıcı_id>`, `<@&rol_id>` ve `@everyone` kimseyi etiketlemez; bildirim göndermez, okunmamış sayacını artırmaz.

> **Not:** Birine bildirim göndermek istiyorsan etiketi mesajın `content` alanına yaz.

### Kimi etiketlersin

`allowed_mentions` yalnızca daraltır: botun `@everyone` etiketleme izni yoksa `parse` içinde `everyone` yazmak bir şey değiştirmez. Etiket metinde kalır, yalnızca bildirim gitmez.

| Gönderdiğin | Sonuç |
| --- | --- |
| hiçbiri | Bir kişinin yazdığı mesajla aynı: metindeki her etiket bildirim gönderir. |
| `"parse": []` | Hiçbir etiket bildirim göndermez. |
| `"parse": ["users"]` | Yalnızca kullanıcı etiketleri. |
| `"parse": ["roles"]`, `["everyone"]` | Yalnızca rol etiketleri, yalnızca `@everyone` ve `@here`. |
| `"users": ["id"]` | Yalnızca listedeki kullanıcılar. `roles` için aynısı. Listeler en çok 100 kimlik taşır. |
| `"replied_user": false` | Yanıt, yanıtlanan kişiye bildirim göndermez. |

`parse` içinde `users` varken `users` listesi (rol için de aynısı) yazılamaz; istek `validation_failed` ile döner, `parse` içinde bilinmeyen bir değer de öyle. Aşağıdaki mesaj metinde kullanıcıyı etiketler ama kimseye bildirim göndermez, okunmamış sayacını da artırmaz:

```json
{ "content": "<@111500000000000001> toplantı 15:00'te", "allowed_mentions": { "parse": [] } }
```

Mesajı `PATCH` ile düzenlerken `content` değişiyorsa `allowed_mentions` o istekte de gönder; düzenleme kimseye yeni bildirim göndermez ama kimin etiketli sayıldığı güncellenir. `allowed_mentions` göndermezsen düzenlenen metin, bir kişinin yazdığı metin gibi çözülür.

Özel emojiler (`<:ad:id>`) `content` ve embed metinlerinde botun kullanabildiği emojilerle sınırlıdır; kullanamadığı emoji `:ad:` düz metnine döner. Embed'ler ve bileşenler birlikte en çok **50** özel emoji taşır, fazlası `too_long` döner. Emoji sunucudaki güncel adıyla yazılır; ad senin yazdığından uzunsa 16384 bayt sınırı bu haliyle denetlenir.

## Webhook'tan embed

Webhook bağlantısına (`POST /api/v1/webhooks/{id}/{token}`) gönderdiğin `embeds` de aynı kurallarla gösterilir; `content` yazmak gerekmez. İki fark var:

- Görseller düşer. `image`, `thumbnail`, `author.icon_url` ve `footer.icon_url` hata vermez ama saklanmaz; dış adres okuyanlara hiç ulaşmaz. Başlık, açıklama ve alanlar durur. Yalnızca görselden oluşan bir embed görselle birlikte kaybolur.
- Bileşen yoktur ve mesaj bir uygulamaya bağlanmaz; `application_id` gelmez. `flags` ve `allowed_mentions` eskisi gibi alınıp yok sayılır.

Bugün çalışan bir isteğin kırılmaması için embed'ler hoşgörüyle okunur. Bilmediğimiz alanlar (`type`, `video` gibi) yok sayılır. Kurallara uymayan bir embed varsa (`title` 257 karakter, `http://` adres, yalnızca `color` taşıyan embed ya da liste olmayan `embeds` gibi) mesajın **bütün** embed'leri düşer ve `content` yine gönderilir. `400` yalnızca geriye hiçbir şey kalmayınca gelir: `content` boşsa hata embed'lerin neyinin yanlış olduğunu söyler. Embed'ler botlarınkiyle aynı [yayılma bütçesine](https://noluyo.net/gelistirici/rate-limit.md#embed-ve-bilesenlerin-yayilma-butcesi) sayılır. Sunucunun otomatik moderasyonu embed metnine de bakar.

## Kişi ne görür

> **Not:** Bu bölüm webde gözlenen davranışı anlatır. Masaüstü uygulaması aynı web kodunu kullanır ama ayrıca çalıştırılmadı. Android ve iOS uygulamalarında henüz gözlenmedi.

Embed, mesajın metninin altında bir kart olarak çizilir. Kartın sol kenarında `color` renginde ince bir şerit durur; `color` yoksa şerit kişinin seçtiği temanın rengini alır. Yukarıdan aşağıya sıra şudur: yazar, başlık, açıklama, alanlar, görsel, altbilgi ve zaman. `thumbnail` kartın sağ üstünde küçük bir kare olur, dar ekranda biraz daha küçülür.

`inline: true` alanlar yan yana dizilir: geniş kartta üç sütun, telefonda tek sütun. Satır içi olmayan alan tüm genişliği kaplar. `url` verdiysen `title` ve `author.name` yeni sekmede açılan bağlantı olur. Açıklamada ve alan değerlerinde markdown çalışır, HTML etiketleri düz metin olarak görünür. Uzun bir kelime kartın dışına taşmaz, satıra bölünür.

Embed'in gösterdiği dosya (`image`, `thumbnail`, `author` ve `footer` simgesi) mesajın dosya listesinde ikinci kez görünmez. Embed'e bağlanmayan dosyalar kartın altında her zamanki gibi durur. Görsele basınca büyür. Bir mesajdaki 10 embed alt alta dizilir.

Metni olmayan bir mesajda sabitlenmiş mesajlar, arama sonuçları, yanıt satırı ve bildirim ilk embed'in başlığını, başlık yoksa açıklamasını gösterir.

Eski bir uygulama sürümü embed'i çizmez: dosyaları gösterir, yalnızca embed'den oluşan mesaj boş görünür. Kaçırılmaması gereken bir bilgiyi `content`'e de yaz.

## Bir mesajda neler birlikte olur

| Birleşim | Olur mu | Not |
|---|---|---|
| `content` ve embed | Evet | Metin üstte, embed altta durur |
| Yalnız embed | Evet | `content` gerekmez |
| Embed ve dosya | Evet | Embed, dosyayı `attachment://` ile gösterir; dosya `attachments` listesinde de kalır |
| Yalnız bileşen | Hayır | `content`, embed veya dosya gerekir; `content` için `required` döner |
| Embed ve anket, iletme ya da çıkartma | Hayır | `embeds` için `invalid` döner |
| Bileşen ve anket, iletme ya da çıkartma | Hayır | `embeds` için `invalid` döner |

Bir kişi `embeds`, `components`, `flags` veya genişletilmiş `allowed_mentions` gönderirse istek `validation_failed` ile reddedilir. Bir mesaj iletilirse metni ve dosyaları gider, embed ve bileşenleri gitmez; yalnızca embed'den oluşan bir mesaj iletilemez.

## Bileşenler

Bileşenler eylem satırlarına konur. Bir mesajda en çok **5** satır olur; satır 1-5 buton ya da tam bir menü taşır:

| Alan | Tür | Sınır | Zorunlu | Açıklama |
|---|---|---|---|---|
| `components` | liste | 5 | hayır | Eylem satırları; her satırın `type` değeri `1` |
| `components[].components` | liste | 5 | evet | Satırdaki butonlar ya da tek bir menü |
| `type` | sayı | - | evet | `2` buton, `3` metin menüsü, `5` kullanıcı, `6` rol, `8` kanal menüsü |
| `style` | sayı | - | buton için | `1` birincil, `2` ikincil, `3` başarılı, `4` tehlike, `5` bağlantı |
| `label` | metin | 80 | buton için | Buton yazısı |
| `custom_id` | metin | 100 | bağlantı dışında | Mesajda tekil olmalı |
| `url` | metin | 2048 | bağlantı için | `https` adresi |
| `emoji` | nesne | - | hayır | `{"name":"👍"}` ya da botun kullanabildiği `{"id":"..."}` |
| `placeholder` | metin | 150 | hayır | Menüde seçim yokken görünen yazı |
| `options` | liste | 25 | metin menüsü için | Seçenekler |
| `options[].label` | metin | 100 | evet | Seçenek yazısı |
| `options[].value` | metin | 100 | evet | Seçenek değeri; menüde tekil |
| `min_values`, `max_values` | sayı | 25 | hayır | Seçilebilecek en az ve en çok değer; varsayılan 1 |

Tüm bileşenlerin JSON'u **16384** baytı geçemez. Bileşenler `content`, embed veya dosyayla birlikte gönderilir.

Bir kişi butona bastığında bot `INTERACTION_CREATE` alır. Akış, menü türleri ve botun alacakları [Bileşenler](https://noluyo.net/gelistirici/bilesenler.md) sayfasında.

## Düzenleme

`PATCH /channels/{channel_id}/messages/{message_id}` ile botun kendi mesajı düzenlenir. `content`, `embeds` ve `components` ayrı ayrı gönderilebilir:

```bash
curl -X PATCH "https://noluyo.net/api/v1/channels/$CHANNEL_ID/messages/$MESSAGE_ID" \
  -H "Authorization: Bot $BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"components":[]}'
```

- Göndermediğin alan olduğu gibi kalır. `components` yazıp başka bir şey yazmazsan metin ve embed'ler aynı kalır.
- Boş liste (`"embeds":[]` ya da `"components":[]`) o alanı kaldırır.
- Yalnızca `components` değişirse mesaj düzenlendi sayılmaz; `content` veya `embeds` değişirse `edited_timestamp` dolar.
- Embed'deki `attachment://` yalnızca mesajın var olan dosyalarına bakar. Yeni dosya yüklemek isteyen `multipart` istek `invalid_body` ile reddedilir; yeni görsel yeni bir mesajla gönderilir.
- Mesajda metin, embed ve dosya kalmıyorsa `content` için `required` döner.

## Mesaj içeriği intent'i

`MESSAGE_CONTENT` intent'i olmayan bir bot, başka kişilerin ve botların mesajlarındaki `embeds` ve `components` alanlarını görmez; gateway olaylarında da REST yanıtlarında da bu alanlar gelmez. Kendi mesajları ve kendisini etiketleyen mesajlar eksiksiz gelir. `flags` ve `application_id` her zaman gelir. Ayrıntı için [Intent'ler](https://noluyo.net/gelistirici/intentler.md) sayfasına bak.

## Limit aşımı

Sınırı aşan alan, adıyla birlikte döner:

```json
{
  "code": "validation_failed",
  "message": "One or more fields are invalid.",
  "errors": { "embeds[0].title": "too_long" }
}
```

Hata anahtarı sorunlu alanın yoludur. Yapılacak şey metni kısaltmak ya da içeriği birkaç mesaja bölmektir. Toplam sınırlar aşılırsa anahtar `embeds` veya `components` olur.

Embed'ler ayrıca ortak bir tavana sayılır: her mesajın embed ve bileşenleri sunucunun üye sayısıyla çarpılarak tüm botların ve webhook'ların birlikte kullandığı bir bütçeden düşer; bir sahibin botlarına bunun bir payı düşer. Payın dolunca `X-RateLimit-Scope: bot`, bütçenin tamamı dolunca `shared` başlığıyla `429` gelir; ikincisi botunun suçu değildir. `retry_after` kadar bekleyip yeniden dene. Bütçenin 30 saniyesinden fazla tutan mesaj `400 fanout_too_large` alır; `max_bytes` o kanalın alabileceği en büyük embed ve bileşen boyutunu söyler. Kalabalık bir sunucuya giden büyük bir mesaj o saniyede kalanı alır, bütçe onu ödeyene kadar tüm botların sayılan mesajlarını bekletir, sen de bir sonraki büyük mesajından önce bunun dört katı beklersin. Ayrıntı [Hız sınırları](https://noluyo.net/gelistirici/rate-limit.md) sayfasında.

## Node.js ile tam örnek

Node.js 22 veya üstü yeter, ek paket gerekmez. Token'ı koda yazma; ortam değişkeninden okunur. `grafik.png` aynı klasörde olmalı:

```js title=embed.mjs
import { readFile } from "node:fs/promises";

const API = "https://noluyo.net/api/v1";
const token = process.env.BOT_TOKEN;
const channelId = process.env.CHANNEL_ID;
if (!token || !channelId) throw new Error("BOT_TOKEN ve CHANNEL_ID tanımlı olmalı");

async function call(method, path, body) {
  for (;;) {
    const res = await fetch(API + path, {
      method,
      headers: { Authorization: `Bot ${token}`, ...(body instanceof FormData ? {} : { "Content-Type": "application/json" }) },
      body: body instanceof FormData ? body : JSON.stringify(body),
    });
    const data = await res.json();
    if (res.status === 429) {
      await new Promise((done) => setTimeout(done, data.retry_after * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${data.code} ${JSON.stringify(data.errors ?? {})}`);
    return data;
  }
}

const form = new FormData();
form.set(
  "payload_json",
  JSON.stringify({
    embeds: [
      {
        title: "Haftalık grafik",
        description: "Geçen haftanın özeti",
        color: parseInt("3366FF", 16),
        timestamp: new Date().toISOString(),
        fields: [{ name: "Toplam", value: "128", inline: true }],
        image: { url: "attachment://grafik.png" },
      },
    ],
    components: [
      { type: 1, components: [{ type: 2, style: 5, label: "Ayrıntılar", url: "https://noluyo.net" }] },
    ],
  }),
);
form.set("files[0]", new Blob([await readFile("grafik.png")], { type: "image/png" }), "grafik.png");

const message = await call("POST", `/channels/${channelId}/messages`, form);
console.log("gönderildi:", message.id);

// Başlığı değiştir; görsel ve bileşenler olduğu gibi kalır.
const embeds = message.embeds.map((e) => ({ ...e, title: "Güncel grafik" }));
await call("PATCH", `/channels/${channelId}/messages/${message.id}`, { embeds });
```

Düzenleme isteğinde `image.attachment_id` gibi yanıttan gelen alanlar gönderilince yok sayılır; görsel `attachment://` adresiyle aynı dosyaya yeniden bağlanır.

## Sık hatalar

| Hata | Neden | Ne yapmalı |
|---|---|---|
| `embeds[0].image.url`: `invalid` | Adres `attachment://` ile başlamıyor, dosya mesajla gönderilmemiş, görsel değil ya da aynı adlı iki dosya var | Dosyayı aynı istekte, görsel olarak ve tekil adla yükle |
| `embeds`: `too_long` | Toplam 6000 karakter ya da 16384 bayt aşıldı, ya da 50'den çok özel emoji var | Metni kısalt ya da birkaç mesaja böl |
| `embeds[0]`: `required` | Embed boş | En az bir alan doldur |
| `content`: `required` | Yalnız bileşen gönderildi ya da düzenleme sonrası mesaj boş kalıyor | Metin, embed veya dosya ekle |
| `components[0].components[0].emoji`: `invalid` | Botun kullanamadığı özel emoji | Botun kullanabildiği bir emoji ya da `{"name":"👍"}` yaz |
| `ephemeral_forbidden` | Kanala giden mesajda `flags: 64` | `flags` alanını çıkar |
| `automod_blocked` | Sunucunun otomatik moderasyonu embed metnini ya da buton yazısını engelledi | Metni değiştir |
| `invalid_body` | `embeds` liste olmalı ama başka tür gönderildi | Gövdeyi kontrol et |
| `429` ve `X-RateLimit-Scope: shared` | Ortak bütçe dolu | `retry_after` kadar bekle |

## Bu sayfayı yapay zekaya verirsen

> **İpucu:** Bu sayfayı bir yapay zeka asistanına verip kod yazdırıyorsan şu kuralları da ekle:
>
> - Token koda yazılmaz, `process.env.BOT_TOKEN`'dan okunur ve depoya girmeyen bir yerde tutulur.
> - Embed görselleri yalnızca `attachment://` ile, aynı mesajla yüklenen PNG, JPEG, GIF ya da WebP dosyalarından gelir. Dış adres kullanılmaz.
> - Renk `#RRGGBB` değil, ondalık sayıdır.
> - Embed sınırları: mesajda en çok 10 embed, hepsinde toplam 6000 karakter ve 16384 bayt; `title` 256, `description` 4096 karakter, en çok 25 alan, alan adı 256, alan değeri 1024 karakter.
> - Bileşen sınırları: en çok 5 satır, satırda 5 buton ya da tek menü, `custom_id` 100 karakter ve mesajda tekil, JSON'u en çok 16384 bayt. Yalnızca bileşenden oluşan mesaj gönderilmez.
> - Butona basılınca ilk yanıt 3 saniye içinde gider; uzun iş yanıttan sonra yapılır.
> - Bildirim gerekiyorsa etiket `content` içine yazılır, embed içine değil.
> - `429` gelince `retry_after` kadar beklenir; hemen yeniden denenmez.

## Sırada ne var

- [Bileşenler](https://noluyo.net/gelistirici/bilesenler.md): mesajın altına eklenen buton ve seçim menüleri
- [Etkileşimler](https://noluyo.net/gelistirici/etkilesimler.md): bileşen ve komut yanıtlarının yaşam döngüsü
- [Hız sınırları](https://noluyo.net/gelistirici/rate-limit.md): embed yayılma bütçesi ve `429` başlıkları
