---
title: 'Sunucuda Şablon Oluşturma'
source: 'https://academia.sh/tr/kurslar/sunucu-temelleri/sunucuda-sablon-olusturma'
course: 'Sunucu Tarafı Temelleri'
language: tr
updated: '2026-08-17T18:06:53+00:00'
license: 'CC BY-SA 4.0'
---

# Sunucuda Şablon Oluşturma

Görünümün sunucuda veriden üretilmesi: şablon değerlendiricisinin çözümleme ve üretim adımları, kaçışlamanın öntanımlı olması, ham çıktının açık işaretle istenmesi, öznitelik bağlamının farkı ve düzen ile sayfa şablonunun ayrılması.

Önceki ders diskteki dosyaları sundu. Katalog sayfası da o dosyalardan biriydi ama
içeriğini kendisi taşımıyordu: belge boş bir kap olarak gidiyor, kitap listesi tarayıcıda
betikle kuruluyordu. Sunucu kitap kayıtlarını zaten okuyabildiğine göre listeyi belgenin
içine kendisi yazabilir.

Bu birleştirmeyi yapan mekanizmaya **şablon** (template) denir: içine değer konacak yerleri
işaretlenmiş bir metin. Şablonu veriyle birleştiren programa **şablon değerlendiricisi**
(template engine) denir. Bu dersin merkezinde tek bir soru vardır: veriden gelen bir metin
işaretlemenin içine konurken ne olur?

## Şablonun İki Adımı

Değerlendirici işini iki adımda yapar. Önce şablon metni **çözümlenir**: düz metin
parçaları ile yer tutucular birbirinden ayrılır ve blok etiketleri iç içe bir yapı kurar.
Sonra bu yapı veriyle gezilerek **üretilir**.

İki adımın ayrı olması bir gerekliliktir. Çözümleme şablona bakar ve veriye bakmaz;
üretim veriye bakar ve şablon metnini bir daha okumaz. Böylece aynı şablon bir kez
çözümlenip yüz bin kez üretilebilir, ve daha önemlisi, **verinin içeriği hiçbir koşulda
şablon yapısını değiştiremez**. Veriden gelen bir metin bir blok etiketine benzese bile
üretim adımında etiket olarak yorumlanmaz, çünkü çözümleme çoktan bitmiştir.

Aşağıdaki değerlendirici dört etiket tanır: `{{ ad }}` değeri kaçışlayarak yazar,
`{{! ad }}` ham yazar, `{{# ad }}` ile `{{/ ad }}` arası bir dizi üzerinde yinelenir,
`{{^ ad }}` ile `{{/ ad }}` arası ise değer boşken çalışır.

```js
// sablon.mjs — kacislamayi ontanimli yapan kucuk bir sablon degerlendiricisi
const KACIS = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
export const kacisla = (deger) => String(deger).replace(/[&<>"']/g, (k) => KACIS[k]);

// {{ ad }} kacislar, {{! ad }} kacislamaz, {{# ad }}..{{/ ad }} yineler,
// {{^ ad }}..{{/ ad }} bos oldugunda calisir.
const ETIKET = /\{\{\s*([#^/!]?)\s*([\w.]+)\s*\}\}/g;

const coz = (yigit, ad) => {
  if (ad === ".") return yigit[yigit.length - 1];
  const parcalar = ad.split(".");
  for (let i = yigit.length - 1; i >= 0; i--) {
    let deger = yigit[i];
    if (deger === null || typeof deger !== "object") continue;
    if (!(parcalar[0] in deger)) continue;
    for (const p of parcalar) deger = deger?.[p];
    return deger;
  }
  return undefined;
};

// Sablonu tek geciste agaca cevirir: dizgi parcalari ve blok dugumleri.
const ayristir = (metin) => {
  const kok = { cocuk: [] };
  const yigit = [kok];
  let son = 0;
  for (const e of metin.matchAll(ETIKET)) {
    const [tam, tur, ad] = e;
    const ust = yigit[yigit.length - 1];
    if (e.index > son) ust.cocuk.push(metin.slice(son, e.index));
    son = e.index + tam.length;
    if (tur === "#" || tur === "^") {
      const dugum = { tur, ad, cocuk: [] };
      ust.cocuk.push(dugum);
      yigit.push(dugum);
    } else if (tur === "/") {
      if (yigit.length === 1 || yigit[yigit.length - 1].ad !== ad)
        throw new Error(`kapanmayan blok: ${ad}`);
      yigit.pop();
    } else {
      ust.cocuk.push({ tur, ad });
    }
  }
  if (yigit.length !== 1) throw new Error(`kapanmayan blok: ${yigit[yigit.length - 1].ad}`);
  kok.cocuk.push(metin.slice(son));
  return kok;
};

const uret = (dugum, yigit) =>
  dugum.cocuk.map((c) => {
    if (typeof c === "string") return c;
    const deger = coz(yigit, c.ad);
    if (c.tur === "#") {
      const liste = Array.isArray(deger) ? deger : deger ? [deger] : [];
      return liste.map((oge) => uret(c, [...yigit, oge])).join("");
    }
    if (c.tur === "^") {
      const bos = Array.isArray(deger) ? deger.length === 0 : !deger;
      return bos ? uret(c, yigit) : "";
    }
    if (deger === undefined || deger === null) return "";
    return c.tur === "!" ? String(deger) : kacisla(deger);
  }).join("");

export const olustur = (metin, veri) => uret(ayristir(metin), [veri]);
```

Ad çözümü bir kapsam yığıtı üzerinden yapılır: blok içine girildiğinde o turun ögesi
yığıta eklenir, ad önce en içteki kapsamda aranır, bulunamazsa dışarı doğru gidilir. Bu
sayede yineleme bloğunun içinden dıştaki alanlara da erişilebilir.

## Kaçışlamanın Öntanımlı Olması

**Kaçışlama** (escaping), işaretleme dilinde yapısal anlamı olan karakterlerin
**karakter varlığı** (character entity) biçimine çevrilmesidir. Bu dönüşüm yapılırsa
veriden gelen metin çıktıda metin olarak kalır; yapılmazsa işaretlemenin parçası olur.

Değerlendiricide kaçışlama öntanımlıdır ve ham çıktı ancak `{{! ad }}` yazılarak, yani
şablonu yazan kişinin açık kararıyla elde edilir. Sıralamanın tersi de yazılabilirdi;
farkı, hangisinin unutulduğunda ne olduğudur. Kaçışlama öntanımlıysa unutmanın bedeli
görünürde bir tuhaflıktır. Ham çıktı öntanımlıysa unutmanın bedeli, veriden gelen metnin
belge yapısına karışmasıdır.

Aşağıdaki blok aynı veriyi iki yer tutucuya birden verir ve farkı gösterir. İşaret metni
zararsızdır: bir vurgulama ögesi.

```js
// dene.mjs — ayni veriyle kacislamali ve kacislamasiz uretimi karsilastirir
import { olustur } from "./sablon.mjs";

const SABLON = `<ul class="katalog">
{{# kitaplar }}  <li>{{ baslik }} — <span class="yazar">{{ yazar }}</span></li>
{{/ kitaplar }}{{^ kitaplar }}  <li class="bos">Sonuc yok</li>
{{/ kitaplar }}</ul>
<p class="not">{{ not }}</p>
<p class="not-ham">{{! not }}</p>`;

const VERI = {
  kitaplar: [
    { baslik: "Kayip Zamanin Izinde", yazar: "Marcel Proust" },
    { baslik: "Kar & Buz <arsiv>", yazar: "Sube 3" },
  ],
  not: "<mark>ayrilmis</mark>",
};

const cikti = olustur(SABLON, VERI);
console.log(cikti);
console.log("---");
console.log("cikti bayt:", Buffer.byteLength(cikti));
console.log("mark ogesi sayisi:", (cikti.match(/<mark>/g) ?? []).length);
console.log("--- bos liste ---");
console.log(olustur(SABLON, { kitaplar: [], not: "yok" }));
```

```
<ul class="katalog">
  <li>Kayip Zamanin Izinde — <span class="yazar">Marcel Proust</span></li>
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Sube 3</span></li>
</ul>
<p class="not">&lt;mark&gt;ayrilmis&lt;/mark&gt;</p>
<p class="not-ham"><mark>ayrilmis</mark></p>
---
cikti bayt: 278
mark ogesi sayisi: 1
--- bos liste ---
<ul class="katalog">
  <li class="bos">Sonuc yok</li>
</ul>
<p class="not">yok</p>
<p class="not-ham">yok</p>
```

Aynı veri iki farklı sonuç vermiştir. Kaçışlanan yer tutucuda işaret metni **ekranda
okunacak bir metindir**; ham yer tutucuda **belgeye eklenmiş bir ögedir**. Sayım bunu
doğrular: çıktıda bir tek vurgulama ögesi vardır ve o da ham yazılan yerden gelmiştir.

Kitap başlığındaki `&` ve açılı ayraçlar da kaçışlanmıştır. Bu, güvenlikten önce bir
doğruluk meselesidir: kaçışlanmasaydı başlığın bir bölümü ekranda hiç görünmezdi, çünkü
işaretleme çözümleyicisi onu etiket başlangıcı sayardı.

## Metin Bağlamı ile Öznitelik Bağlamı

Tek bir kaçışlama işlevi, değerin belgenin neresine konduğunu bilmez. Metin içine konan
bir değerde açılı ayraçlar önemlidir; öznitelik değerine konan bir değerde **tırnak**
önemlidir. Yukarıdaki işlev her ikisini de dönüştürdüğü için iki bağlamda da doğru sonuç
verir, ama bunun neden gerekli olduğu ayrıca gösterilmelidir.

```js
// oznitelik.mjs — kacislamanin oznitelik degeri uzerindeki etkisi
import { olustur } from "./sablon.mjs";

const KACISLAMALI = `<a class="sube" title="{{ ad }}" href="/sube/{{ kod }}">Sube</a>`;
const HAM = `<a class="sube" title="{{! ad }}" href="/sube/{{ kod }}">Sube</a>`;
const VERI = { ad: 'Merkez "Eski Kitaplar" Salonu', kod: "s3" };

// Cozumleyici oznitelik degerini ilk kapanis tirnaginda bitirir; asagidaki desen
// ayni kurali uygular ve degerin nerede kesildigini gosterir.
const baslikDegeri = (metin) => metin.match(/title="([^"]*)"/)[1];

for (const [ad, sablon] of [["kacislamali", KACISLAMALI], ["ham", HAM]]) {
  const cikti = olustur(sablon, VERI);
  console.log(ad);
  console.log("  cikti :", cikti);
  console.log("  title :", JSON.stringify(baslikDegeri(cikti)));
}
```

```
kacislamali
  cikti : <a class="sube" title="Merkez &quot;Eski Kitaplar&quot; Salonu" href="/sube/s3">Sube</a>
  title : "Merkez &quot;Eski Kitaplar&quot; Salonu"
ham
  cikti : <a class="sube" title="Merkez "Eski Kitaplar" Salonu" href="/sube/s3">Sube</a>
  title : "Merkez "
```

Ham çıktıda öznitelik değeri **"Merkez "** ile bitmiştir: şube adındaki tırnak,
özniteliği kapatmıştır. Geri kalan sözcükler artık değerin parçası değil, etiketin
gövdesindeki başka bir şeydir. Kaçışlanan çıktıda ise değer bütün olarak durur ve
çözümleyici karakter varlıklarını okurken şube adını eksiksiz geri verir.

Buradan çıkan kural şudur: öznitelik değerleri **her zaman tırnak içine alınır** ve
şablona konan değer bu tırnağı kapatabilecek karakterlerden arındırılır. Tırnaksız
öznitelik yazımı, kaçışlama işlevinin dönüştürmediği boşluk gibi karakterleri de
yapısal hâle getirdiği için bu düzende hiç kullanılmaz.

## Düzen ve Sayfa

Her sayfanın belge iskeleti aynıdır: aynı dil bildirimi, aynı başlık ögesi, aynı gövde
çerçevesi. Bu ortak kısmı her şablona kopyalamak yerine bir **düzen şablonu** yazılır ve
sayfanın kendi çıktısı ona yerleştirilir. Yerleştirme, düzenin içindeki tek ham yer
tutucuyla yapılır — çünkü oraya konan şey veri değil, zaten kaçışlanmış bir üretim
çıktısıdır.

Şablonun gördüğü veri yapısına **görünüm modeli** (view model) denir. Görünüm modeli, veri
kaynağıyla şablon arasındaki sözleşmedir: şablon kayıtların nereden geldiğini bilmez,
kayıtları okuyan kod da işaretlemeyi bilmez.

```js
// gorunum.mjs — katalog sayfasini sunucuda uretip sunar (sablon.mjs ile ayni dizinde)
import { createServer } from "node:http";
import { olustur } from "./sablon.mjs";

const KITAPLAR = [
  { baslik: "Kayip Zamanin Izinde", yazar: "Marcel Proust", sube: "Merkez" },
  { baslik: "Tutunamayanlar", yazar: "Oguz Atay", sube: "Sube 3" },
  { baslik: "Kar & Buz <arsiv>", yazar: "Derleme", sube: "Sube 3" },
];

const DUZEN = `<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>{{ baslik }}</title></head>
<body><h1>{{ baslik }}</h1>{{! icerik }}</body></html>
`;

const KATALOG = `<p class="ozet">{{ sayi }} kayit</p>
<ul class="katalog">
{{# kitaplar }}  <li>{{ baslik }} — <span class="yazar">{{ yazar }}</span> ({{ sube }})</li>
{{/ kitaplar }}{{^ kitaplar }}  <li class="bos">Sonuc yok</li>
{{/ kitaplar }}</ul>`;

// Gorunum modeli: sablonun gordugu tek veri yapisi. Sorgu ve kayitlar burada
// birlestirilir; sablonun kendisi veri kaynagini hic bilmez.
const gorunumModeli = (sorgu) => {
  const q = (sorgu ?? "").toLocaleLowerCase("tr");
  const kitaplar = q ? KITAPLAR.filter((k) => k.baslik.toLocaleLowerCase("tr").includes(q)) : KITAPLAR;
  return { kitaplar, sayi: kitaplar.length };
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  if (adres.pathname !== "/katalog") return yanit.writeHead(404).end();

  const icerik = olustur(KATALOG, gorunumModeli(adres.searchParams.get("q")));
  const belge = olustur(DUZEN, { baslik: "Kutuphane Katalogu", icerik });

  yanit.setHeader("Content-Type", "text/html; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(belge));
  yanit.writeHead(200).end(belge);
}).listen(8311, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8311"));
```

```bash
#!/usr/bin/env bash
# gorunum.mjs'yi baslatir, uretilen belgeyi ve suzulmus sonucu gosterir, durdurur.
node gorunum.mjs > /dev/null &
sunucu=$!
sleep 1
A=http://127.0.0.1:8311

echo "--- tum katalog ---"
curl -sS "$A/katalog"
echo "--- suzgec: q=kar ---"
curl -sS "$A/katalog?q=kar"
echo "--- eslesmeyen sorgu ---"
curl -sS "$A/katalog?q=zzz" | grep -c 'class="bos"'

kill "$sunucu"
```

```
--- tum katalog ---
<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane Katalogu</title></head>
<body><h1>Kutuphane Katalogu</h1><p class="ozet">3 kayit</p>
<ul class="katalog">
  <li>Kayip Zamanin Izinde — <span class="yazar">Marcel Proust</span> (Merkez)</li>
  <li>Tutunamayanlar — <span class="yazar">Oguz Atay</span> (Sube 3)</li>
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Derleme</span> (Sube 3)</li>
</ul></body></html>
--- suzgec: q=kar ---
<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane Katalogu</title></head>
<body><h1>Kutuphane Katalogu</h1><p class="ozet">1 kayit</p>
<ul class="katalog">
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Derleme</span> (Sube 3)</li>
</ul></body></html>
```

Boş sonuç dalı ayrıca sınanmıştır: eşleşmeyen sorguda `class="bos"` geçen satır sayısı
birdir. Bu, boş listenin şablonda ele alınmış olduğunu doğrular; ele alınmasaydı çıktıda
gövdesiz bir liste ögesi kalırdı.

Bağlantı noktası 8311 keyfîdir ve boş olmalıdır.

## Şablonun Taşıyamayacağı Yük

Şablon bir görünüm aracıdır ve yükü arttıkça bozulur. Üç sınır pratikte belirleyicidir.

**Hesap şablonda yapılmaz.** Gecikmiş ödünç işleminin gün sayısı, ceza tutarı ya da
sıralama ölçütü görünüm modelinde hesaplanır. Şablonda hesap yapılırsa aynı kural iki
sayfada iki farklı sonuç vermeye başlar ve sınanamaz hâle gelir.

**Veri erişimi şablonda yapılmaz.** Yineleme bloğunun içinden bir sorgu çalıştırılabilseydi
sayfadaki satır sayısı kadar sorgu doğardı. Görünüm modeli, üretim başlamadan önce ihtiyaç
duyulan her şeyi hazır getirir.

**Ham çıktı sayılabilir olmalıdır.** Ham yer tutucu, sistemde denetlenmesi gereken tek
noktadır. Sayısı azaldıkça ve yalnızca üretim çıktısı yerleştirilen yerlerde kaldıkça
şablon katmanının doğruluğu gözle sınanabilir kalır.

## Özet

- Şablon değerlendiricisi önce şablonu çözümler, sonra veriyle üretir; verinin içeriği
  çözümleme bittikten sonra geldiği için şablon yapısını değiştiremez.
- Kaçışlama, işaretlemede yapısal anlamı olan karakterleri karakter varlığına çevirir ve
  öntanımlı olmalıdır; ham çıktı şablonu yazanın açık kararıyla istenir.
- Ölçüm iki bağlamı ayırır: metin bağlamında kaçışlanmayan değer belgeye öge ekler,
  öznitelik bağlamında ise değeri ilk tırnakta keser.
- Düzen şablonu ortak belge iskeletini taşır; sayfa çıktısı oraya tek bir ham yer
  tutucuyla yerleşir, çünkü yerleşen şey veri değil zaten üretilmiş çıktıdır.
- Görünüm modeli şablon ile veri kaynağı arasındaki sözleşmedir; hesap ve veri erişimi
  şablonda değil, görünüm modelini kuran kodda yapılır.

## Sonraki Adım

Şimdiye kadar veri hep içeriden geldi: diskteki dosyalar, bellekteki kayıtlar. Kütüphane
servisinin gerçek işleyişinde dışarıdan da veri gelir — üye fotoğrafı, bağış yapılan
kitabın kapak görüntüsü, şubeden yüklenen sayım dosyası. Bu içerik sunucuya gelirken
boyutu bilinmez, türü bildirildiği gibi olmayabilir ve nereye yazılacağı bir karardır.
Sonraki ders yüklemeyi bu üç soruyla ele alır: sunucu sınırı aşan bir yükü ne zaman ve
nasıl reddeder, dosyanın türüne uzantısına bakmadan nasıl karar verir, ve yazılan dosya
sunulan dizinden neden ayrı tutulur?
