jskelet 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +127 -0
  2. package/CHANGELOG.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +342 -0
  5. package/bin/jskelet.mjs +104 -0
  6. package/docs/01-baslangic.md +285 -0
  7. package/docs/02-mimari.md +287 -0
  8. package/docs/03-routing.md +437 -0
  9. package/docs/04-render-ve-sablonlar.md +490 -0
  10. package/docs/05-islands.md +429 -0
  11. package/docs/06-cache.md +409 -0
  12. package/docs/07-yapilandirma.md +673 -0
  13. package/docs/08-build.md +366 -0
  14. package/docs/09-dev-araclari.md +302 -0
  15. package/docs/10-dagitim.md +329 -0
  16. package/docs/11-tasima.md +352 -0
  17. package/docs/README.md +82 -0
  18. package/package.json +97 -0
  19. package/src/build/build.mjs +138 -0
  20. package/src/build/ensure-build.mjs +15 -0
  21. package/src/build/paths.mjs +118 -0
  22. package/src/build/resolve-peer.mjs +36 -0
  23. package/src/build/tasks/client.mjs +268 -0
  24. package/src/build/tasks/css.mjs +124 -0
  25. package/src/build/tasks/fonts.mjs +146 -0
  26. package/src/build/tasks/icons.mjs +224 -0
  27. package/src/build/tasks/images.mjs +244 -0
  28. package/src/build/tasks/precompress.mjs +78 -0
  29. package/src/client/devtools/overlay.js +1763 -0
  30. package/src/client/devtools/report.html +185 -0
  31. package/src/client/devtools/report.js +712 -0
  32. package/src/client/dom.js +95 -0
  33. package/src/client/index.js +26 -0
  34. package/src/client/registry.js +223 -0
  35. package/src/client/safe-image.js +91 -0
  36. package/src/client/store.js +36 -0
  37. package/src/config/defaults.js +102 -0
  38. package/src/config/index.js +433 -0
  39. package/src/config/pattern.js +107 -0
  40. package/src/dev-server.mjs +383 -0
  41. package/src/http/control-flow.js +56 -0
  42. package/src/http/request-cache.js +46 -0
  43. package/src/index.js +35 -0
  44. package/src/init.mjs +220 -0
  45. package/src/log.mjs +332 -0
  46. package/src/logo.png +0 -0
  47. package/src/runtime/alias-hooks.mjs +119 -0
  48. package/src/runtime/register.mjs +4 -0
  49. package/src/server/assets.js +119 -0
  50. package/src/server/create-app.js +167 -0
  51. package/src/server/dev/devtools.js +383 -0
  52. package/src/server/dev/report.js +351 -0
  53. package/src/server/head-hints.js +132 -0
  54. package/src/server/html-cache.js +166 -0
  55. package/src/server/metadata.js +102 -0
  56. package/src/server/middleware/compression.js +205 -0
  57. package/src/server/middleware/dev-gate.js +62 -0
  58. package/src/server/middleware/headers.js +37 -0
  59. package/src/server/middleware/redirects.js +32 -0
  60. package/src/server/middleware/static-precompressed.js +100 -0
  61. package/src/server/middleware/upstream-proxy.js +141 -0
  62. package/src/server/prewarm.js +283 -0
  63. package/src/server/render.js +356 -0
  64. package/src/server/router.js +121 -0
  65. package/src/server/status-page.js +164 -0
  66. package/src/server/upstream-tracking.js +51 -0
  67. package/src/start.mjs +7 -0
  68. package/src/templates/layout.ejs +44 -0
  69. package/src/version.mjs +17 -0
  70. package/src/views/components/loader.js +85 -0
  71. package/src/views/helpers/html.js +102 -0
  72. package/src/views/helpers/tags.js +193 -0
@@ -0,0 +1,285 @@
1
+ # 01 — Başlangıç
2
+
3
+ Bu belge JSkelet'i sıfırdan çalıştırmayı anlatır: paket kurulumu, `jskelet init`
4
+ ile iskeletin oluşturulması, ilk route ve ilk island'ın yazılması, oluşan dizin
5
+ yapısının ne anlama geldiği ve CLI'ın dört komutu. Sonunda tarayıcıda sunucuda
6
+ render edilmiş, önbelleğe alınmış ve island'ı görünürlükte hidre olan bir sayfa
7
+ olacak. Kararların *nedenleri* için [02-mimari.md](./02-mimari.md)'ye, buradaki
8
+ her config alanının tam referansı için
9
+ [07-yapilandirma.md](./07-yapilandirma.md)'ye bakın.
10
+
11
+ ## Gereksinimler
12
+
13
+ - **Node.js 22 veya üstü.** `package.json` → `engines` bunu zorunlu tutuyor.
14
+ Framework `node:async_hooks`, `fs.readdirSync(..., { recursive: true })`,
15
+ `--env-file-if-exists` ve `module.register()` gibi yeni Node yüzeylerini
16
+ doğrudan kullanıyor.
17
+ - Tailwind CSS kullanacaksanız `postcss`, `@tailwindcss/postcss` ve
18
+ `tailwindcss` paketleri. Bunlar framework'ün **opsiyonel peer
19
+ bağımlılıkları**dır; kurulu değilse CSS adımı atlanır ve site stilsiz ama
20
+ çalışır durumda kalır (ayrıntı: [08-build.md](./08-build.md)).
21
+
22
+ ## Kurulum
23
+
24
+ ```bash
25
+ mkdir benim-sitem && cd benim-sitem
26
+ npm init -y
27
+ npm pkg set type=module
28
+ npm install jskelet
29
+ npm install -D postcss @tailwindcss/postcss tailwindcss lightningcss
30
+ ```
31
+
32
+ `type: "module"` şart: route modülleri, bileşenler ve config dosyası ESM olarak
33
+ yüklenir.
34
+
35
+ Ardından `package.json` içine script'leri ekleyin:
36
+
37
+ ```json
38
+ {
39
+ "scripts": {
40
+ "dev": "jskelet dev",
41
+ "build": "jskelet build",
42
+ "start": "jskelet start"
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## `jskelet init`
48
+
49
+ ```bash
50
+ npx jskelet init
51
+ ```
52
+
53
+ Bu komut bulunduğunuz dizine çalışan bir minimum iskelet kurar. **Var olan
54
+ dosyaların üzerine yazmaz**: ikinci kez çalıştırmak yalnızca eksikleri
55
+ tamamlar, atlanan dosyaların sayısını uyarı olarak basar. Amaç, "kurulumu
56
+ yaptım ama hiçbir şey çalışmıyor" aşamasını tamamen atlamak — `jskelet dev`
57
+ hemen ardından çalışır.
58
+
59
+ Oluşturulan dosyalar:
60
+
61
+ ```
62
+ jskelet.config.mjs config: brand, preconnect, cache(), hooks
63
+ routes/10-pages.mjs "/" route'u
64
+ views/pages/home.ejs ana sayfa şablonu
65
+ views/pages/not-found.ejs 404 şablonu
66
+ views/components/button.js örnek bileşen (HTML string döndüren fonksiyon)
67
+ client/entries/main.js island bootstrap'ı
68
+ client/islands/counter.js örnek island
69
+ styles/globals.css Tailwind girişi + @source direktifleri
70
+ jsconfig.json checkJs + "@/*" alias'ı
71
+ .gitignore node_modules/, .jskelet/, public/assets/, .env
72
+ ```
73
+
74
+ Sonra:
75
+
76
+ ```bash
77
+ npm run dev
78
+ ```
79
+
80
+ Terminalde banner, hizalı build satırları ve bir `Ready` özeti görürsünüz;
81
+ `http://localhost:3000` sayfayı verir. Sağ altta dev overlay baloncuğu durur,
82
+ `Alt+D` ile açılır ([09-dev-araclari.md](./09-dev-araclari.md)).
83
+
84
+ ## Dizin yapısı
85
+
86
+ Dizin adlarının hiçbiri sabit değildir; hepsi `jskelet.config.mjs` → `paths`
87
+ ile ezilebilir. Aşağıdaki değerler varsayılanlardır (`src/config/defaults.js`).
88
+
89
+ | Dizin | Varsayılan | İçeriği |
90
+ | --- | --- | --- |
91
+ | `views` | `views` | EJS layout, sayfalar ve bileşenler |
92
+ | `public` | `public` | Statik dosyalar; build çıktısı da buraya yazılır |
93
+ | `client` | `client` | Island runtime kaynakları ve entry'ler |
94
+ | `routes` | `routes` | Route modülleri |
95
+ | `styles` | `styles/globals.css` | Tailwind/PostCSS giriş **dosyası** |
96
+ | `generated` | `.jskelet` | Build ara çıktıları: `manifest.json`, `metafile.json`, `images.json` |
97
+
98
+ Bunlara ek olarak framework iki yolu her zaman türetir ve ayrı ayar kabul
99
+ etmez: `public/assets` (hash'li build çıktısı) ve `public/fonts` (self-host
100
+ fontlar).
101
+
102
+ Tipik bir proje:
103
+
104
+ ```
105
+ benim-sitem/
106
+ ├── jskelet.config.mjs
107
+ ├── jsconfig.json
108
+ ├── routes/
109
+ │ ├── 10-pages.mjs
110
+ │ └── 90-catch-all.mjs
111
+ ├── views/
112
+ │ ├── layout.ejs
113
+ │ ├── pages/
114
+ │ │ ├── home.ejs
115
+ │ │ └── not-found.ejs
116
+ │ └── components/
117
+ │ └── card.js
118
+ ├── client/
119
+ │ ├── entries/
120
+ │ │ └── main.js
121
+ │ └── islands/
122
+ │ └── counter.js
123
+ ├── styles/
124
+ │ └── globals.css
125
+ ├── public/
126
+ │ └── (statik dosyalar; build → public/assets)
127
+ └── .jskelet/
128
+ └── manifest.json
129
+ ```
130
+
131
+ ## İlk route
132
+
133
+ Route modülleri **dosya sistemine dayalı otomatik URL türetmez**; her modül
134
+ kendi yollarını `app.get(...)` ile açıkça yazar. Modül sözleşmesi: default
135
+ export ya da `register` adlı named export, `(app, api)` imzasıyla.
136
+
137
+ ```js
138
+ // routes/10-pages.mjs
139
+ export default function register(app, { route }) {
140
+ app.get(
141
+ "/",
142
+ route(
143
+ async () => ({
144
+ view: "pages/home",
145
+ metadata: { title: "Ana sayfa" },
146
+ data: { heading: "JSkelet çalışıyor", items: ["Bir", "İki"] },
147
+ }),
148
+ { revalidate: 60 },
149
+ ),
150
+ );
151
+ }
152
+ ```
153
+
154
+ `api` nesnesi içinde `route`, `renderView`, `renderPage`, `notFound`, `redirect`
155
+ ve `permanentRedirect` hazır gelir; route dosyaları framework'ten tek tek import
156
+ yapmak zorunda kalmaz. `route()` controller'ı sarar: HTML cache'i,
157
+ notFound/redirect kontrol akışı, sıkıştırma ve `X-JSkelet-Cache` başlığı ondan
158
+ gelir. Controller'ın tek işi bir sayfa tanımı döndürmektir.
159
+
160
+ Dosya adındaki `10-` öneki yükleme sırasını belirler. `routes/` alfabetik
161
+ tarandığı için `/:slug` gibi yakalayıcı route'ları daha yüksek numaralı bir
162
+ dosyaya koymalısınız; aksi hâlde `/hakkinda` bir slug sanılır. Ayrıntı:
163
+ [03-routing.md](./03-routing.md).
164
+
165
+ Şablon tarafı düz EJS:
166
+
167
+ ```ejs
168
+ <%# views/pages/home.ejs %>
169
+ <section class="wrapper">
170
+ <h1 class="text-3xl font-bold"><%= heading %></h1>
171
+ <%- list({ items }) %>
172
+ <div data-island="counter" data-island-props='{"start":5}'></div>
173
+ </section>
174
+ ```
175
+
176
+ `list` burada `views/components/list.js` içinde tanımlı bir fonksiyondur ve
177
+ import edilmemiştir: `views/components/**` altındaki her named export otomatik
178
+ olarak şablon local'i olur ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
179
+
180
+ ## İlk island
181
+
182
+ Island, sunucunun ürettiği HTML'e davranış ekleyen küçük bir modüldür. Sözleşme
183
+ iki parçadan oluşur.
184
+
185
+ **1. Şablonda işaret:** bir elemente `data-island="ad"` verin. Props JSON olarak
186
+ `data-island-props` içinde taşınır.
187
+
188
+ ```ejs
189
+ <div data-island="counter" data-island-props='{"start":5}'></div>
190
+ ```
191
+
192
+ **2. Modülde `mount`:** island `mount(element, props)` adlı bir named export
193
+ verir.
194
+
195
+ ```js
196
+ // client/islands/counter.js
197
+ /**
198
+ * @param {HTMLElement} element
199
+ * @param {{ start?: number }} props
200
+ */
201
+ export function mount(element, props) {
202
+ let value = props.start ?? 0;
203
+
204
+ const button = document.createElement("button");
205
+ button.type = "button";
206
+
207
+ const paint = () => {
208
+ button.textContent = `Tıklama: ${value}`;
209
+ };
210
+
211
+ button.addEventListener("click", () => {
212
+ value += 1;
213
+ paint();
214
+ });
215
+
216
+ paint();
217
+ element.append(button);
218
+ }
219
+ ```
220
+
221
+ **3. Kayıt:** `client/entries/main.js` island adını dinamik import'a bağlar ve
222
+ runtime'ı başlatır.
223
+
224
+ ```js
225
+ import { registerAll, start } from "jskelet/client";
226
+
227
+ registerAll({
228
+ counter: () => import("../islands/counter.js"),
229
+ });
230
+
231
+ start();
232
+ ```
233
+
234
+ Değerlerin dinamik import olması kritik: modül yalnızca sayfada o island
235
+ gerçekten varsa **ve** element görünür hâle geldiğinde indirilir. Yani bu
236
+ haritayı büyütmek ilk yükü büyütmez. Hidrasyon stratejileri
237
+ (`data-island-eager`, `data-island-idle`) ve runtime API'sinin tamamı
238
+ [05-islands.md](./05-islands.md)'de.
239
+
240
+ ## CLI komutları
241
+
242
+ `bin/jskelet.mjs` dört alt komut sunar. Her biri ayrı bir Node sürecinde
243
+ çalışır; sebebi `dev`in iki uzun ömürlü süreci yönetmesi ve sunucunun ESM
244
+ resolve hook'larına (`--import`) süreç başlangıcında ihtiyaç duyması.
245
+
246
+ | Komut | Ne yapar |
247
+ | --- | --- |
248
+ | `jskelet dev` | Build watch + sunucu, tek terminalde. Canlı yenileme, CSS hot-swap, dev overlay. `NODE_ENV=development`. |
249
+ | `jskelet build` | Tek seferlik prod build: fontlar → ikon sprite → CSS → client JS → görseller → manifest → precompress. `NODE_ENV` verilmemişse `production`. |
250
+ | `jskelet start` | Prod sunucu. Build çıktısı yoksa önce üretir. `NODE_ENV` verilmemişse `production`. |
251
+ | `jskelet init` | Bulunduğun dizine minimal iskelet kurar; var olan dosyalara dokunmaz. |
252
+
253
+ Bilinmeyen bir komut ya da argümansız çağrı kullanım metnini basar.
254
+
255
+ Her komut iki Node bayrağıyla çalışır:
256
+
257
+ - `--env-file=.env` — yalnızca dosya gerçekten varsa geçilir; yoksa hiçbir
258
+ bayrak eklenmez ve uyarı basılmaz.
259
+ - `--import <register.mjs>` — `jsconfig.json` / `tsconfig.json` içindeki
260
+ `compilerOptions.paths` alias'larını (`@/lib/x`) ve uzantısız göreli
261
+ import'ları (`./cache` → `./cache.js`) çözen ESM hook'larını kurar.
262
+ (`jskelet dev` bu hook'ları kendi alt süreçlerinde kurar, dış süreçte kurmaz.)
263
+
264
+ ## İthal yolları
265
+
266
+ `package.json` → `exports` haritası kararlı yüzeyi tanımlar. Örneklerde
267
+ yalnızca bu belirteçleri kullanın:
268
+
269
+ | Belirteç | İçeriği |
270
+ | --- | --- |
271
+ | `jskelet` | Sunucu API'si: `route`, `renderPage`, `renderView`, `renderNotFound`, `createApp`, `startServer`, `notFound`, `redirect`, `permanentRedirect`, `cache`, `withRequestCache`, `reportUpstreamFailure`, `asset`, `hasAsset`, `optimizedImage`, `getSpriteIds`, `headHints`, `renderHeadMeta`, HTML cache fonksiyonları, `prewarm`, `createProxy`, `getConfig`, `loadConfig` ve html/tag yardımcıları |
272
+ | `jskelet/server` | `jskelet` ile aynı modül (okunurluk için takma ad) |
273
+ | `jskelet/client` | Tarayıcı runtime'ı: `register`, `registerAll`, `hydrate`, `observeDocument`, `start`, `createStore`, DOM yardımcıları, `startSafeImages` |
274
+ | `jskelet/html` | `esc`, `attrs`, `cx`, `cn`, `jsonScript` |
275
+ | `jskelet/tags` | `link`, `image`, `icon`, `preloadImage`, `toKebab` |
276
+ | `jskelet/log` | Konsol çıktısı yardımcıları (`banner`, `event`, `task`, `size`, `ms`, …) |
277
+ | `jskelet/register` | `node --import jskelet/register` ile alias + uzantı hook'ları |
278
+ | `jskelet/layout` | Framework'ün varsayılan `layout.ejs` dosyasının yolu |
279
+
280
+ ## Sırada ne var
281
+
282
+ - Neden bu şekilde çalışıyor: [02-mimari.md](./02-mimari.md)
283
+ - Daha fazla route ve yakalayıcı desenler: [03-routing.md](./03-routing.md)
284
+ - Layout'u devralmak ve metadata: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
285
+ - Önbelleği ayarlamak: [06-cache.md](./06-cache.md)
@@ -0,0 +1,287 @@
1
+ # 02 — Mimari ve kararların gerekçeleri
2
+
3
+ Bu belge JSkelet'in nasıl çalıştığını değil, **neden böyle çalıştığını**
4
+ anlatır. Bir isteğin sunucudan tarayıcıya kadar izlediği yol, island modelinin
5
+ neden görünürlüğe bağlı olduğu, HTML'in neden tam üretildiği, önbelleğin neden
6
+ süreç belleğinde durduğu ve middleware sırasının neden yer değiştirmemesi
7
+ gerektiği burada. Gerekçelerin çoğu kaynak dosyaların başlıklarındaki ölçüm
8
+ notlarından geliyor; API'lerin kendisi için [03](./03-routing.md),
9
+ [04](./04-render-ve-sablonlar.md), [05](./05-islands.md) ve
10
+ [06](./06-cache.md) numaralı belgelere bakın.
11
+
12
+ ## Temel önerme
13
+
14
+ Bir haber ya da içerik sitesinde ziyaretçinin gördüğü şeyin neredeyse tamamı
15
+ sunucuda hazırdır. Etkileşim ise nokta nokta dağılmıştır: bir arama kutusu, bir
16
+ drawer, bir grafik, bir yorum formu. Bu profilde tüm sayfayı istemcide yeniden
17
+ kurmak (hidrasyon) ödediğiniz en büyük maliyettir ve karşılığında ziyaretçi
18
+ hiçbir şey kazanmaz.
19
+
20
+ JSkelet bu gözlemi mimarinin merkezine alır:
21
+
22
+ 1. **Sunucu HTML'i tamdır.** JS çalışmasa bile sayfa okunur, gezilebilir ve
23
+ indekslenebilir.
24
+ 2. **JS yalnızca davranış ekler.** Her etkileşimli parça bağımsız bir "island"
25
+ olarak, kendi modülüyle, kendi zamanında bağlanır.
26
+ 3. **Sayfa üretimi önbelleklenir.** Aynı HTML'i her istekte yeniden üretmenin
27
+ anlamı yok; TTL'li bir bellek önbelleği ISR'nin yerini tutar.
28
+
29
+ ## Bir isteğin yolu
30
+
31
+ ```
32
+ İstek
33
+ ├─ rewrites(beforeFiles) config → proxy ya da req.url değişimi
34
+ ├─ compression brotli/gzip pazarlığı (kalite 5)
35
+ ├─ headers statik cache + config headers()
36
+ ├─ devGate DEV_TOKEN varsa token yoksa 404
37
+ ├─ redirects config redirects(), ilk eşleşen kazanır
38
+ ├─ staticPrecompressed build'de üretilmiş .br/.gz kopyalar (kalite 11)
39
+ ├─ express.static public/ altındaki dosyalar
40
+ ├─ (dev) devtools yalnızca NODE_ENV=development
41
+ ├─ body parser'lar urlencoded 64kb + json 256kb
42
+ ├─ rewrites(afterFiles) statik denendikten sonra
43
+ ├─ route'lar
44
+ │ └─ route(controller)
45
+ │ └─ withHtmlCache TTL + stale-while-revalidate
46
+ │ └─ withUpstreamTracking
47
+ │ └─ withRequestCache
48
+ │ └─ controller → renderPage → EJS
49
+ ├─ 404 → hooks.notFound()
50
+ └─ hata yönetimi redirect/notFound + 500 fallback
51
+ ```
52
+
53
+ ## Middleware sırası neden bu sıra
54
+
55
+ `src/server/create-app.js` dosyasının asıl değeri sıradır; her konumun bir
56
+ sebebi var ve yer değiştirmek sessiz bozulmalara yol açıyor.
57
+
58
+ - **`rewrites(beforeFiles)` statik dosyalardan da önce.** Aksi hâlde
59
+ `/assets/x.js` yolunu başka bir yere taşıyan bir kural hiç işlemez, çünkü
60
+ `express.static` isteği önce yanıtlar.
61
+ - **`compression`, static'ten önce.** Sonra gelirse statik dosyalar hiç
62
+ sıkışmaz.
63
+ - **`headers` → `devGate` → `redirects`.** Gate'in 404'ü redirect'ten önce
64
+ gelmeli: yayına açılmamış bir ortam, yönlendirme kurallarını bile dışarıya
65
+ sızdırmamalı.
66
+ - **`staticPrecompressed`, `express.static`ten önce.** Build'de üretilmiş
67
+ `.br`/`.gz` kopyalar varsa onlar servis edilir (brotli kalite 11); yoksa
68
+ istek altındaki `static`e düşer ve middleware anında sıkıştırır (kalite 5).
69
+ Hash'li ve `immutable` bir dosyayı her istekte yeniden sıkıştırmak boşa CPU.
70
+ - **Body parser'lar statikten sonra.** Görsel isteklerinde gövde ayrıştırma
71
+ maliyeti ödenmesin.
72
+ - **`rewrites(afterFiles)`, statik denendikten sonra ve sayfalardan önce.**
73
+ Next.js'teki iki fazlı rewrite semantiğinin karşılığı.
74
+ - **404 ve hata yönetimi en sonda.** Hata yöneticisi `notFound`/`redirect`
75
+ kontrol akışını da yakalar, çünkü bunlar bir controller dışında (ör. bir
76
+ middleware içinde) da fırlatılabilir.
77
+
78
+ Framework `x-powered-by`'ı kapatır ve yerine markalanabilir bir başlık yazar,
79
+ `etag`i `strong` yapar ve `trust proxy`yi açar. `trust proxy` ters proxy
80
+ arkasında doğru protokol ve istemci IP'si için gerekli
81
+ ([10-dagitim.md](./10-dagitim.md)).
82
+
83
+ ## Island modeli: neden görünürlüğe bağlı hidrasyon
84
+
85
+ `src/client/registry.js` her `[data-island]` elementini bir
86
+ `IntersectionObserver`'a verir (`rootMargin: "200px 0px"`). Ekranda olanlar
87
+ zaten ilk gözlemde tetiklenir; ekran dışındakiler kaydırılana kadar **hiç
88
+ indirilmez**. Ana sayfadaki grafik kütüphanesi gibi ağır modüller böylece ilk
89
+ yükten tamamen çıkar.
90
+
91
+ Üç davranış var, hepsi HTML'den kontrol edilir:
92
+
93
+ - **Varsayılan:** görünürlüğe bağlı.
94
+ - **`data-island-eager`:** görünürlükten bağımsız, hemen bağlanır. Header,
95
+ çerez bandı gibi global davranışlar için.
96
+ - **`data-island-idle`:** görünür olsa bile `load` tamamlanıp ana iş parçacığı
97
+ boşalana kadar bekler. İlk ekranda görünen ama kritik olmayan ağır modüller
98
+ (ör. grafik kütüphanesi çeken mini grafik) LCP ile yarışmasın diye.
99
+
100
+ İki ek ayrıntı ölçümden geldi:
101
+
102
+ - **Bağlama işi boş zamana kaydırılır** (`requestIdleCallback`, `timeout: 500`).
103
+ Aynı anda görünen çok sayıda island tek bir uzun task'a dönüşürse TBT ve INP
104
+ bozulur.
105
+ - **Düzen kutusu olmayan elementler doğrudan bağlanır.** `hidden` bir
106
+ drawer/dialog'un düzen kutusu yoktur ve `IntersectionObserver` onu asla
107
+ bildirmez; bu yüzden `hydrate()` ölçümleri tek seferde okur
108
+ (`getClientRects().length`) ve kutusu olmayanları gözlemciye vermek yerine
109
+ hemen bağlar.
110
+
111
+ Buradan çıkan bir sonuç: **görsel hata yönetimi island değildir.** Görsel
112
+ ağırlıklı bir sayfada 80+ `<img>` olabiliyor ve her birine ayrı island bağlamak
113
+ (gözlemci + dinamik import + mount) sırf hata ihtimali için ciddi bir hidrasyon
114
+ yükü. `startSafeImages()` bunun yerine belgeye tek bir yakalama fazı
115
+ dinleyicisi kurar ([05-islands.md](./05-islands.md)).
116
+
117
+ ## Sunucu HTML'i neden tam
118
+
119
+ Layout ve sayfa şablonu, ziyaretçinin göreceği içeriğin tamamını üretir.
120
+ İstemci tarafında "iskelet göster, sonra doldur" deseni yoktur. Bunun üç
121
+ karşılığı var:
122
+
123
+ 1. **SEO:** kazıyıcı JS beklemek zorunda kalmaz.
124
+ 2. **LCP:** en büyük içerik öğesi ilk HTML yanıtında gelir; JS'in indirilmesi,
125
+ ayrıştırılması ve çalıştırılması LCP yolunda değildir.
126
+ 3. **CLS:** içerik sonradan enjekte edilmediği için düzen kaymaz.
127
+
128
+ Aynı ilke `<head>` tarafında da uygulanır. Layout kaynak ipuçlarını
129
+ (`preconnect`, LCP `preload`) `<head>`in **en başına** basar; bunları
130
+ geciktirmek doğrudan LCP'ye yazılır.
131
+
132
+ ### Neden tek, render-blocking stylesheet
133
+
134
+ Ayrı bir "critical CSS" üretilmez. Ölçümde inline kritik CSS ilk ekranı tam
135
+ kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasında CLS
136
+ 0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış tek
137
+ sheet'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci ziyarette
138
+ zaten `immutable` önbellekten geliyor.
139
+
140
+ Aynı mantık ikonlarda da var: her ikon için ayrı istek yerine, build zamanında
141
+ yalnızca kaynakta kullanılan sembollerden bir SVG sprite üretilir. Tüm Phosphor
142
+ setini göndermek 1500+ ikon, yani birkaç megabayt; tarama sprite'ı tipik olarak
143
+ 10-30 sembolde tutuyor ([08-build.md](./08-build.md)).
144
+
145
+ ## Cache stratejisi: ISR yerine bellek içi TTL
146
+
147
+ `src/server/html-cache.js` route + query anahtarlı, TTL'li, LRU bir HTML
148
+ önbelleği tutar (en fazla 500 girdi). TTL dolduğunda girdi hemen atılmaz: `stale`
149
+ pencerede eski HTML anında döner ve tazeleme arkada çalışır
150
+ (stale-while-revalidate, `STALE_FACTOR = 1`, yani stale penceresi TTL kadar).
151
+
152
+ Kazanç: ilk ısıtmadan sonra hiçbir istek render'ı beklemez. Bedel: HTML'deki
153
+ veri en fazla `revalidate + bir tazeleme turu` kadar geride olabilir. Bu bedel
154
+ kabul edilebilir, çünkü fiyat gibi canlı alanlar istemcide WebSocket'ten
155
+ güncelleniyor ve gecikme ekranda görünmüyor.
156
+
157
+ Diske yazmama kararı bilinçli. Next'teki build-time prerender'ın karşılığı
158
+ prewarm'dır ama çıktı diske yazılmaz: önbellek süreç belleğinde yaşadığı için
159
+ ısıtma da süreç ayağa kalkınca yapılır. Kazanç aynı — ilk ziyaretçi soğuk
160
+ render'ı beklemez — fakat veri dondurulmaz; her girdi route'un `revalidate`
161
+ süresiyle yaşlanır ([06-cache.md](./06-cache.md)).
162
+
163
+ ### Sıkıştırılmış gövdenin önbellekte durması
164
+
165
+ Önbelleğe alınan her girdi, brotli/gzip çıktısını HTML ile birlikte saklar
166
+ (`encoded` haritası, HTML ile aynı ömrü paylaşır). Aynı sayfa her istekte
167
+ yeniden brotli'lenmez. `Content-Encoding` bu yolda `route()` içinde ayarlandığı
168
+ için sıkıştırma middleware'i devreye girmez.
169
+
170
+ ### Neden geçici ve kalıcı upstream hataları farklı ele alınır
171
+
172
+ Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir ve böyle
173
+ bir HTML önbelleğe **yazılmaz**: sonraki istek yeniden dener.
174
+
175
+ Ancak bu yalnızca *geçici* hatalar için geçerli (ağ hatası, 408, 425, 429 ve tüm
176
+ 5xx). 400/403/404 gibi deterministik cevaplar tekrar denemekle düzelmez; onlar
177
+ yüzünden önbelleği kapatmak sayfayı her ziyarette baştan render etmek olur —
178
+ içerik yine aynı eksik hâliyle döner, ziyaretçi sadece render süresini öder. Bu
179
+ yüzden kalıcı hatalar yalnızca loglanır, önbelleği engellemez.
180
+
181
+ Bu bilginin framework'e ulaşma yönü de bilinçli olarak terstir: framework veri
182
+ katmanını tanımaz, veri katmanı framework'e haber verir
183
+ (`reportUpstreamFailure()`). Hiç çağıran olmazsa maliyet boş bir dizidir.
184
+
185
+ ### Üç kapsamın iç içe sırası
186
+
187
+ `route()` şu sırayı kurar:
188
+
189
+ ```
190
+ withHtmlCache( withUpstreamTracking( withRequestCache( controller ) ) )
191
+ ```
192
+
193
+ Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki çağrı
194
+ tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
195
+ eksik veriyle üretilen çıktı önbelleğe yazılmasın.
196
+
197
+ ## Hata toleransı: hiçbir eksik siteyi indirmez
198
+
199
+ Framework boyunca tekrarlanan bir ilke var: eksik yapılandırma ya da eksik build
200
+ çıktısı, hata yerine bozulmuş ama çalışan bir sayfa üretir.
201
+
202
+ - **Config dosyası yoksa ya da okunamıyorsa** uyarı basılır ve sunucu
203
+ varsayılanlarla ayağa kalkar. Bozuk bir düzenleme siteyi açılamaz hâle
204
+ getirmemeli. Aynı şekilde `headers()`/`redirects()`/`rewrites()`/`cache()`
205
+ bölümlerinden biri hata verirse yalnızca o bölüm yok sayılır.
206
+ - **Hook'lar hata verirse** framework kendi varsayılanına döner ve uyarır.
207
+ - **Build çalışmadıysa** `asset()` `/assets/<isim>` döner, `hasAsset()` false
208
+ olur ve layout stylesheet/script etiketlerini hiç basmaz. `jskelet build`
209
+ unutulduğunda hata yerine stilsiz ama çalışan bir sayfa görürsünüz.
210
+ - **Dev'de bozuk bir route modülü** uyarı basıp atlanır; **üretimde fırlatır.**
211
+ Yarım route tablosuyla yayına çıkmak, sessizce 404 dönen sayfalar demek.
212
+ - **404 render'ı da patlarsa** şablonsuz, minimal bir HTML döner; ziyaretçi boş
213
+ yanıt görmesin.
214
+ - **Tek bir istek hatası süreci düşürmez:** `unhandledRejection` ve
215
+ `uncaughtException` loglanır ve süreç ayakta kalır. Bir haber sitesinde tek
216
+ sayfanın hatası tüm siteyi indirmemeli.
217
+
218
+ ## Neden dosya sistemi tabanlı routing yok
219
+
220
+ Sıra önemli. `/:slug` gibi tek segmentli bir yakalayıcı `/about` rotasından önce
221
+ kaydedilirse "about" bir slug sanılır. Sırayı dosya adına gizlemek yerine
222
+ görünür kılmak teşhisi kolaylaştırıyor: ya `jskelet.config.mjs` → `routes` ile
223
+ açık bir liste verirsiniz, ya da `routes/` dizinini alfabetik taratıp dosya
224
+ adlarına sayısal önek koyarsınız (`10-pages.js`, `50-blog.js`,
225
+ `99-catch-all.js`). Ayrıntı: [03-routing.md](./03-routing.md).
226
+
227
+ ## Neden tek bir config gerçek kaynağı
228
+
229
+ `src/config/index.js` proje kökünü, dizin yollarını, markalamayı, hook'ları ve
230
+ kuralları normalize eder. Diğer modüller yol hesaplamaz, `getConfig()` çağırır.
231
+ Sebebi somut: framework `node_modules/` içine girdiğinde `../..` sayarak kök
232
+ bulmaya çalışan her dosya bozulur. Aynı gerekçeyle build tarafında da tek bir
233
+ mutasyon noktası var (`initBuildPaths()`).
234
+
235
+ `getConfig()` `loadConfig()` çağrılmadan kullanılırsa boş bir proje kökü
236
+ varsaymak yerine hata verir: sessiz yanlış yol, "stylesheet neden yok" gibi
237
+ teşhisi zor sorunlara dönüşüyor.
238
+
239
+ ## Neden bu bağımlılık listesi
240
+
241
+ Çalışma zamanı bağımlılıkları dörttür: `express`, `ejs`, `esbuild`,
242
+ `tailwind-merge`. Geri kalan her şey (Tailwind, PostCSS, lightningcss, sharp,
243
+ Phosphor ikonları) **opsiyonel peer bağımlılığıdır** ve yoksa ilgili build adımı
244
+ atlanır.
245
+
246
+ İki karar ayrıca açıklanmayı hak ediyor:
247
+
248
+ - **`compression` paketi yerine `node:zlib`.** Paket brotli desteklemiyor ve
249
+ yedi geçişli bir bağımlılık ağacı getiriyor; brotli + gzip pazarlığını elle
250
+ yapmak yeterli. Brotli tercih edilir: ana sayfa HTML'inde gzip'e göre ~%35
251
+ daha küçük.
252
+ - **`tailwind-merge` çalışma zamanında kalır.** Sınıf hesabı yalnızca sunucuda
253
+ yapılır, client bundle'a hiç girmez, dolayısıyla sayfa ağırlığına etkisi
254
+ yoktur. Elle yazılmış bir grup tablosu ise `border-2` + `border-transparent`
255
+ gibi genişlik/renk çiftlerini birbirine karıştırıp sınıf düşürdüğü için
256
+ görsel regresyon üretiyordu.
257
+
258
+ Opsiyonel paketler **uygulamanın** `node_modules`'ünden çözülür, framework'ün
259
+ kendisinden değil. Framework `file:` ya da workspace bağlantısıyla kuruluysa düz
260
+ bir `import "postcss"` framework'ün ağacına bakar — uygulamanınkine değil.
261
+
262
+ ## Neden alias ve uzantı hook'ları
263
+
264
+ `node --import jskelet/register` iki iş yapar:
265
+
266
+ 1. `jsconfig.json` / `tsconfig.json` içindeki `compilerOptions.paths`
267
+ alias'larını çözer (`@/lib/x` → `<root>/lib/x`). Editör ve çalışma zamanı aynı
268
+ dosyadan beslendiği için ikisi birbirinden ayrışmaz.
269
+ 2. Uzantısız göreli import'lara uzantı ekler (`./cache` → `./cache.js`). Node ESM
270
+ bunu yapmaz ve bundler'dan taşınan kodda en sık karşılaşılan kırılma noktası
271
+ budur.
272
+
273
+ esbuild tarafındaki `@/` çözümü de aynı davranışı taklit eder, böylece `lib/`
274
+ altındaki modüller hem sunucuda hem tarayıcıda aynı import stilini kullanabilir.
275
+
276
+ `--import` bir modül **belirteci** bekler, dosya yolu değil. Windows'ta `H:\...`
277
+ mutlak yolu `h:` şemalı bir URL sanılıp reddediliyor; bu yüzden framework her
278
+ yerde `pathToFileURL(...).href` kullanır. Aynı sebeple config, route modülleri
279
+ ve bileşenler de `file://` URL'le import edilir.
280
+
281
+ ## Sırada ne var
282
+
283
+ - Route ve controller sözleşmesi: [03-routing.md](./03-routing.md)
284
+ - Şablon katmanı ve metadata: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
285
+ - Island runtime API'si: [05-islands.md](./05-islands.md)
286
+ - Önbelleğin ayarları ve prewarm: [06-cache.md](./06-cache.md)
287
+ - Dev akışının iç işleyişi: [09-dev-araclari.md](./09-dev-araclari.md)