jskelet 0.6.2 → 0.6.3

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 (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -1,486 +1,486 @@
1
- # 05 — Island'lar
2
-
3
- Bu belge etkileşimin nasıl eklendiğini anlatır: `data-island` sözleşmesi, props
4
- geçişi, üç hidrasyon stratejisi ve IntersectionObserver mantığı,
5
- `client/entries/*` yapısı ve sayfa başına ek entry yükleme, runtime API'si
6
- (`register`, `registerAll`, `hydrate`, `observeDocument`, `start`), island'lar
7
- arası durum paylaşımı için `createStore`, DOM yardımcıları, `startSafeImages` ve
8
- ertelenmiş panel (fragment) deseni. Modelin *neden* böyle olduğu
9
- [02-mimari.md](./02-mimari.md)'de, bundle'ın nasıl üretildiği
10
- [08-build.md](./08-build.md)'de.
11
-
12
- ## Sözleşme
13
-
14
- Sunucu HTML'i tamdır; island yalnızca davranış ekler. Üç parça var.
15
-
16
- **1. Şablonda işaret.**
17
-
18
- ```ejs
19
- <div data-island="counter" data-island-props='{"start":5}'></div>
20
- ```
21
-
22
- **2. Island modülü — `mount` adlı named export.**
23
-
24
- ```js
25
- // client/islands/counter.js
26
- /**
27
- * @param {HTMLElement} element
28
- * @param {{ start?: number }} props
29
- * @returns {void | (() => void)} temizlik fonksiyonu (opsiyonel)
30
- */
31
- export function mount(element, props) {
32
- let value = props.start ?? 0;
33
- // …
34
- }
35
- ```
36
-
37
- **3. Entry'de kayıt.**
38
-
39
- ```js
40
- // client/entries/main.js
41
- import { registerAll, start } from "jskelet/client";
42
-
43
- registerAll({
44
- counter: () => import("../islands/counter.js"),
45
- });
46
-
47
- start();
48
- ```
49
-
50
- Loader'ın dinamik import olması modelin özü: modül yalnızca sayfada o island
51
- gerçekten varsa **ve** bağlanma koşulu sağlandığında indirilir. Bu haritayı
52
- büyütmek ilk yükü büyütmez.
53
-
54
- ## HTML attribute'ları
55
-
56
- | Attribute | Anlamı |
57
- | --- | --- |
58
- | `data-island="ad"` | Bağlanacak island'ın kayıtlı adı. Zorunlu. |
59
- | `data-island-props='{"…":…}'` | JSON props. Ayrıştırılamazsa konsola hata basılır ve `{}` geçilir. |
60
- | `data-island-eager` | Görünürlükten bağımsız, hemen bağla. |
61
- | `data-island-idle` | Görünür olsa bile `load` + boş zamana kadar bekle. |
62
- | `data-island-ready="true"` | **Framework yazar.** `mount()` başarıyla döndükten sonra eklenir; CSS ve testler bunu okuyabilir. |
63
-
64
- `data-island-props` içeriği HTML attribute'u olduğu için tek tırnakla sarmak en
65
- kolay yoldur. Değerleri sunucuda üretiyorsanız `jsonScript()` ya da `attrs()`
66
- kullanmak kaçış hatalarını önler:
67
-
68
- ```ejs
69
- <div <%- attrs({ "data-island": "chart", "data-island-props": JSON.stringify({ symbol }) }) %>></div>
70
- ```
71
-
72
- ## Hidrasyon stratejileri
73
-
74
- ### Varsayılan: görünürlüğe bağlı
75
-
76
- Her island bir `IntersectionObserver`'a verilir (`rootMargin: "200px 0px"`).
77
- Ekranda olanlar zaten ilk gözlemde tetiklenir; ekran dışındakiler kaydırılana
78
- kadar hiç indirilmez. Element bir kez görününce gözlemden çıkarılır.
79
-
80
- Bağlama işi ayrıca boş zamana kaydırılır (`requestIdleCallback`,
81
- `timeout: 500`; desteklenmiyorsa `setTimeout(fn, 0)`): aynı anda görünen çok
82
- sayıda island tek bir uzun task'a dönüşürse TBT ve INP bozulur.
83
-
84
- ### `data-island-eager`
85
-
86
- Görünürlük beklenmez, doğrudan bağlanır. Header davranışı, çerez bandı, tema
87
- değiştirici gibi sayfa genelinde geçerli island'lar için.
88
-
89
- ```ejs
90
- <header data-island="header" data-island-eager></header>
91
- ```
92
-
93
- ### `data-island-idle`
94
-
95
- Görünür olsa bile `load` olayı tamamlanıp ana iş parçacığı boşalana kadar
96
- bekletilir. İlk ekranda görünen ama kritik olmayan ağır modüller — örneğin
97
- grafik kütüphanesi çeken bir mini grafik — LCP ile yarışmasın diye.
98
-
99
- ```ejs
100
- <div data-island="sparkline" data-island-idle></div>
101
- ```
102
-
103
- Sayfa yüklendiğinde `document.readyState` zaten `complete` ise bekleme atlanır
104
- ve doğrudan boş zamana kaydırılır.
105
-
106
- ### Gizli elementler
107
-
108
- `hidden` bir drawer ya da dialog'un düzen kutusu yoktur ve
109
- `IntersectionObserver` onu **asla** bildirmez. Bu yüzden `hydrate()` ölçümleri
110
- tek seferde okur (`getClientRects().length > 0`) ve kutusu olmayan elementleri
111
- gözlemciye vermek yerine doğrudan bağlar. Ölçümlerin tek seferde okunması da
112
- bilinçli: araya yazma girmediği için tek reflow olur.
113
-
114
- Pratik sonucu: bir modal'ı `hidden` başlatabilirsiniz, island'ı yine bağlanır.
115
-
116
- ## `client/` dizini
117
-
118
- ```
119
- client/
120
- ├── entries/
121
- │ ├── main.js her sayfada yüklenen ortak bootstrap (veya main.ts)
122
- │ └── chart.js yalnızca isteyen sayfalarda
123
- └── islands/
124
- ├── counter.ts .js veya .ts
125
- └── chart.js
126
- ```
127
-
128
- `client/entries/*.{js,ts,mts}` içindeki **her dosya bir esbuild entry'sidir**.
129
- `main.js` (veya `main.ts`) layout tarafından her sayfada yüklenir (manifest'te
130
- varsa). Ek entry'ler yalnızca onları isteyen sayfalarda yüklenir. Aynı stem için
131
- iki uzantı (`main.js` + `main.ts`) build hatasıdır.
132
-
133
- ```js
134
- // controller — manifest anahtarı her zaman *.js kalır
135
- return { view: "pages/markets", entries: ["chart.js"] };
136
- ```
137
-
138
- Layout `entries` dizisindeki her adı `asset(entry)` ile çözüp bir
139
- `<script type="module">` basar. Ad manifest anahtarıdır (`chart.js`), kaynak
140
- dosya `chart.ts` olsa bile hash'siz anahtar `.js` kalır.
141
-
142
- Paylaşılan `@/lib` modülleri sunucuda da import ediliyorsa **`.js` kalsın** —
143
- Node runtime `.ts` çözmez; `.ts` yalnızca esbuild client hattında derlenir.
144
-
145
- Kod bölme (`splitting: true`) açık: iki entry'nin paylaştığı modüller ortak bir
146
- chunk'a çıkar ve iki kez indirilmez.
147
-
148
- `client/islands/` bir zorunluluk değil, yalnızca yaygın düzen; island modülleri
149
- entry'den erişilebilen herhangi bir yerde olabilir. `@/` alias'ı hem sunucuda
150
- hem bundle'da çalışır, böylece `lib/` altındaki paylaşılan modüller aynı import
151
- stilini kullanabilir.
152
-
153
- ## Runtime API — `jskelet/client`
154
-
155
- ### `register(name, loader)`
156
-
157
- Tek bir island kaydeder. `loader` `Promise<{ mount }>` döndüren bir fonksiyon
158
- olmalıdır.
159
-
160
- ```js
161
- import { register } from "jskelet/client";
162
-
163
- register("counter", () => import("../islands/counter.js"));
164
- ```
165
-
166
- ### `registerAll(entries)`
167
-
168
- Nesne biçiminde toplu kayıt. Pratikte tercih edilen biçim.
169
-
170
- ```js
171
- registerAll({
172
- counter: () => import("../islands/counter.js"),
173
- drawer: () => import("../islands/drawer.js"),
174
- });
175
- ```
176
-
177
- ### `hydrate(root?)`
178
-
179
- `root` (varsayılan `document`) altındaki tüm `[data-island]` elementlerini tarar
180
- ve bağlanma stratejisine göre işler. Zaten bağlanmış elementler atlanır.
181
-
182
- Bir island'ı elle yeniden taramak gerektiğinde (ör. kendi kodunuzla DOM
183
- eklediyseniz) doğrudan çağırabilirsiniz:
184
-
185
- ```js
186
- container.innerHTML = html;
187
- hydrate(container);
188
- ```
189
-
190
- ### `observeDocument()`
191
-
192
- `document.body` üzerine bir `MutationObserver` kurar ve sonradan DOM'a eklenen
193
- island'ları da yakalar (infinite scroll, portal, fragment yükleme).
194
- `MutationObserver` örneğini döndürür, böylece gerekirse `disconnect()`
195
- edilebilir.
196
-
197
- ### `start()`
198
-
199
- Tipik bootstrap: `DOMContentLoaded` beklenir (gerekiyorsa), sonra `hydrate()` ve
200
- `observeDocument()` çağrılır.
201
-
202
- ```js
203
- registerAll({ /* … */ });
204
- start();
205
- ```
206
-
207
- ### Bağlanma davranışı ve hatalar
208
-
209
- - Bir element aynı island adıyla **iki kez bağlanmaz**; kayıt element bazında
210
- `WeakMap` içinde tutulur.
211
- - Kayıtlı olmayan bir ad için konsola uyarı basılır:
212
- `[island] not registered: <name>`.
213
- - Modül import'u ya da `mount()` hata verirse konsola hata basılır
214
- (`[island] <name> failed to load`) ve **sayfanın kalanı etkilenmez**.
215
- - `mount()` başarıyla dönerse elemente `data-island-ready="true"` yazılır.
216
- - `mount()` bir temizlik fonksiyonu döndürebilir; framework onu saklar ve
217
- `unmount()` çağrıldığında işletir (aşağıya bakın).
218
-
219
- ### `unmount(root?)`
220
-
221
- `root` altındaki island'ları söker: saklanan temizlik fonksiyonlarını çağırır,
222
- `data-island-ready` işaretini kaldırır ve kaydı siler, böylece aynı düğüm
223
- tekrar DOM'a girerse yeniden bağlanabilir. `root`'un kendisi de island olabilir.
224
-
225
- DOM'un bir bölgesini değiştirirken çağrılması **zorunlu**:
226
-
227
- ```js
228
- import { hydrate, unmount } from "jskelet/client";
229
-
230
- unmount(container);
231
- container.innerHTML = html;
232
- hydrate(container);
233
- ```
234
-
235
- Atlanması en kolay gözden kaçan sızıntı biçimini üretiyor. `innerHTML` ile
236
- değiştirilen bir bölgenin island'ları DOM'dan çıkar, ama `document`/`window`
237
- üzerine kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder;
238
- birkaç takastan sonra aynı iş onlarca kez çalışır.
239
-
240
- ```js
241
- export function mount(element) {
242
- const timer = setInterval(() => tick(element), 1000);
243
- const onResize = () => layout(element);
244
- window.addEventListener("resize", onResize);
245
-
246
- return () => {
247
- clearInterval(timer);
248
- window.removeEventListener("resize", onResize);
249
- };
250
- }
251
- ```
252
-
253
- `swap()` ve form yardımcıları `unmount()`u kendileri çağırıyor; elle DOM
254
- değiştirdiğiniz yerlerde siz çağırıyorsunuz.
255
-
256
- ### `swap(target, url, options?)` ve `startSwapLinks(root?)`
257
-
258
- Bir bölgeyi sunucudan gelen parçayla değiştirir: eski alt ağacı söker, içeriği
259
- yazar, yeniden hidre eder ve odağı kaybolmuşsa geri getirir.
260
-
261
- ```html
262
- <a href="/_fragment/satirlar?sayfa=2" data-swap="#satirlar">Sonraki</a>
263
- ```
264
-
265
- Sunucu tarafı ve tüm seçenekler
266
- [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
267
-
268
- ### `enhanceForm(form)` ve `startForms(root?)`
269
-
270
- `data-enhance` taşıyan formları sayfa yenilemeden gönderir; JS kapalıyken
271
- normal POST + yönlendirme akışı çalışmaya devam eder. Sözleşmenin tamamı
272
- [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
273
-
274
- ## Durum paylaşımı: `createStore`
275
-
276
- React Context'in yerine kullanılan minimal pub/sub. `useSyncExternalStore`
277
- köprüsünün yerini alır: doğrudan `subscribe`.
278
-
279
- ```js
280
- // client/stores/theme.js
281
- import { createStore } from "jskelet/client";
282
-
283
- export const theme = createStore("light");
284
- ```
285
-
286
- ```js
287
- // client/islands/theme-toggle.js
288
- import { theme } from "../stores/theme.js";
289
-
290
- export function mount(element) {
291
- const paint = (value) => {
292
- element.textContent = value === "light" ? "Koyu tema" : "Açık tema";
293
- };
294
-
295
- const unsubscribe = theme.subscribe(paint);
296
- paint(theme.get());
297
-
298
- element.addEventListener("click", () => {
299
- theme.set((prev) => (prev === "light" ? "dark" : "light"));
300
- });
301
-
302
- return unsubscribe;
303
- }
304
- ```
305
-
306
- API:
307
-
308
- | Üye | Davranış |
309
- | --- | --- |
310
- | `get()` | Anlık değer |
311
- | `set(next)` | Değer ya da `(prev) => next` fonksiyonu. Değer **aynıysa** (`===`) dinleyiciler tetiklenmez. |
312
- | `subscribe(listener)` | Dinleyici ekler, kaldıran fonksiyonu döndürür. Abone olurken mevcut değerle çağrılmaz — ilk boyamayı kendiniz yapın. |
313
-
314
- ## DOM yardımcıları
315
-
316
- `jskelet/client` island'ların paylaştığı küçük bir yardımcı seti verir.
317
-
318
- | Fonksiyon | İmza | Davranış |
319
- | --- | --- | --- |
320
- | `qs` | `(root, selector) => HTMLElement \| null` | `querySelector` |
321
- | `qsa` | `(root, selector) => HTMLElement[]` | `querySelectorAll`, gerçek dizi olarak |
322
- | `on` | `(target, type, handler, options?) => () => void` | Dinleyici ekler ve **kaldıran fonksiyonu döndürür** |
323
- | `onClick` | `(root, selector, handler) => () => void` | Delege edilmiş click; `handler(event, target)` |
324
- | `debounce` | `(ms, fn) => fn` | Son çağrıdan `ms` sonra çalışır |
325
- | `raf` | `(fn) => fn` | Çağrıları tek bir `requestAnimationFrame`'de birleştirir |
326
- | `toggleClass` | `(element, name, active) => void` | `classList.toggle` |
327
- | `getOverlayRoot` | `() => HTMLElement` | `#jskelet-overlays` ya da `body` |
328
-
329
- `on()` ve `onClick()`'in kaldırıcı döndürmesi, `mount()`'un temizlik
330
- fonksiyonuyla doğal olarak eşleşir:
331
-
332
- ```js
333
- import { on, onClick, raf } from "jskelet/client";
334
-
335
- export function mount(element) {
336
- const offClick = onClick(element, "[data-tab]", (event, target) => {
337
- selectTab(target.dataset.tab);
338
- });
339
-
340
- const offScroll = on(window, "scroll", raf(() => updateShadow(element)), {
341
- passive: true,
342
- });
343
-
344
- return () => {
345
- offClick();
346
- offScroll();
347
- };
348
- }
349
- ```
350
-
351
- `getOverlayRoot()` modal/drawer içeriğini taşımak için: layout'ta
352
- `<div id="jskelet-overlays"></div>` varsa oraya, yoksa `body`ye. Portal,
353
- `overflow` ya da `transform` taşıyan bir ata elementin `position: fixed`
354
- overlay'i kırpmasını engeller.
355
-
356
- ## `startSafeImages()`
357
-
358
- Yüklenemeyen görseller için tek bir belge dinleyicisi. **Bilinçli olarak island
359
- değildir:** görsel ağırlıklı bir sayfada 80+ `<img>` olabiliyor ve her birine
360
- ayrı island bağlamak (gözlemci + dinamik import + mount) sırf hata ihtimali için
361
- ciddi bir hidrasyon yükü.
362
-
363
- ```js
364
- // client/entries/main.js
365
- import { registerAll, start, startSafeImages } from "jskelet/client";
366
-
367
- registerAll({ /* … */ });
368
- startSafeImages();
369
- start();
370
- ```
371
-
372
- Kullanım, şablon tarafında:
373
-
374
- ```ejs
375
- <%# 1. Minimal: framework ölçüleri koruyan bir blokla değiştirir %>
376
- <img src="/kapak.png" alt="Kapak" width="640" height="360" data-safe-image>
377
-
378
- <%# 2. Kendi hata görünümü %>
379
- <div data-safe-image-host>
380
- <img src="/kapak.png" alt="Kapak" data-safe-image>
381
- <template data-safe-image-fallback>
382
- <div class="flex h-40 items-center justify-center bg-slate-100">Görsel yok</div>
383
- </template>
384
- </div>
385
- ```
386
-
387
- Nasıl çalışır:
388
-
389
- - Belgeye **yakalama fazında** tek bir `error` dinleyicisi kurulur. `error`
390
- olayı kabarmaz ama yakalama fazında görülebilir; bu yüzden tek dinleyici tüm
391
- görselleri karşılar ve sonradan DOM'a eklenenler de kendiliğinden kapsanır.
392
- - `data-safe-image-host` sarmalayıcısı **ve** içinde
393
- `<template data-safe-image-fallback>` varsa sarmalayıcının tamamı template
394
- içeriğiyle değiştirilir. Framework hiçbir stil dayatmaz.
395
- - Yoksa görselin yerine minimal bir blok konur: `role="img"`, `alt` (ya da
396
- `data-fallback-label`) değeri `aria-label` olarak, görselin `className`i artı
397
- `data-fallback-class`, ve `width`/`height` varsa aynı ölçüler inline style
398
- olarak. Ölçülerin korunması değiştirme sırasında düzen kaymasını (CLS)
399
- önler.
400
- - JS çalışmadan önce başarısız olmuş görseller olay üretmez; bu yüzden bir kez
401
- tarama yapılır (`requestIdleCallback`, `timeout: 2000`): `complete` olup
402
- `naturalWidth === 0` olanlar değiştirilir.
403
-
404
- ## Ertelenmiş panel (fragment) deseni
405
-
406
- Ağır ve ikincil bir bölümü (yorumlar, ilgili haberler, uzun bir tablo) ilk HTML
407
- yanıtından tamamen çıkarmak istediğinizde island + layout'suz render birleşimi
408
- kullanılır. Framework'te bunun için özel bir API yok; iki hazır parçanın
409
- kombinasyonu:
410
-
411
- **1. Sunucuda layout'suz bir fragment ucu** (`renderView`, bkz.
412
- [03-routing.md](./03-routing.md)):
413
-
414
- ```js
415
- // routes/80-fragments.mjs
416
- export default function register(app, { renderView }) {
417
- app.get("/_fragment/yorumlar/:id", async (req, res) => {
418
- const comments = await getComments(req.params.id);
419
- res.type("html").send(await renderView("fragments/comments", { comments }));
420
- });
421
- }
422
- ```
423
-
424
- **2. Sayfada bir yer tutucu island.** Görünürlüğe bağlı bağlandığı için,
425
- ziyaretçi o bölüme kaydırmazsa ne modül ne de fragment indirilir:
426
-
427
- ```ejs
428
- <div data-island="deferred" data-island-props='{"src":"/_fragment/yorumlar/42"}'></div>
429
- ```
430
-
431
- **3. Island fragment'ı çekip yerleştirir ve içindeki island'ları hidre eder:**
432
-
433
- ```js
434
- // client/islands/deferred.js
435
- import { hydrate } from "jskelet/client";
436
-
437
- export async function mount(element, { src }) {
438
- try {
439
- const response = await fetch(src, { headers: { accept: "text/html" } });
440
- if (!response.ok) return;
441
-
442
- element.innerHTML = await response.text();
443
- hydrate(element);
444
- } catch {
445
- // İkincil içerik: sessizce vazgeç, sayfanın kalanı etkilenmesin.
446
- }
447
- }
448
- ```
449
-
450
- `observeDocument()` zaten çalışıyorsa son satırdaki `hydrate()` çağrısı
451
- gereksizdir; yine de açıkça çağırmak, `start()` kullanmayan bir kurulumda da
452
- doğru davranmasını sağlar.
453
-
454
- Fragment yolları için `/_fragment/` öneki önerilir: varsayılan `prewarmSkip`
455
- listesinde olduğu için ısıtma turu bu uçları taramaz
456
- ([06-cache.md](./06-cache.md)).
457
-
458
- ## Ortam değişkenleri ve `clientEnv`
459
-
460
- Tarayıcıda `process` yoktur, ama sunucuyla paylaşılan modüller yine de
461
- `process.env` okuyabilir. `jskelet.config.mjs` → `clientEnv` ile bildirilen
462
- anahtarlar build zamanında bundle'a gömülür:
463
-
464
- ```js
465
- export default {
466
- clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN"],
467
- };
468
- ```
469
-
470
- Next'teki `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın herkese açık
471
- olduğu isimden değil config'ten belli. `process.env`in tamamı tek nesne olarak
472
- define edilir, yani listede olmayan bir anahtar okunduğunda çökme yerine
473
- `undefined` döner. `NODE_ENV` her zaman gömülür.
474
-
475
- ## Tarayıcı desteği
476
-
477
- Bundle hedefi sabittir: `chrome111`, `edge111`, `firefox111`, `safari16.4`. ESM
478
- + dinamik import + `IntersectionObserver` island modelinin zaten alt sınırı;
479
- daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor.
480
- JS hiç çalışmasa da sunucu HTML'i tam olduğu için sayfa okunur kalır.
481
-
482
- ## Sırada ne var
483
-
484
- - Bundle, hash'ler ve `entries` manifest'i: [08-build.md](./08-build.md)
485
- - `entries` alanının controller tarafı: [03-routing.md](./03-routing.md)
486
- - Island durumunu dev panelinden izlemek: [09-dev-araclari.md](./09-dev-araclari.md)
1
+ # 05 — Island'lar
2
+
3
+ Bu belge etkileşimin nasıl eklendiğini anlatır: `data-island` sözleşmesi, props
4
+ geçişi, üç hidrasyon stratejisi ve IntersectionObserver mantığı,
5
+ `client/entries/*` yapısı ve sayfa başına ek entry yükleme, runtime API'si
6
+ (`register`, `registerAll`, `hydrate`, `observeDocument`, `start`), island'lar
7
+ arası durum paylaşımı için `createStore`, DOM yardımcıları, `startSafeImages` ve
8
+ ertelenmiş panel (fragment) deseni. Modelin *neden* böyle olduğu
9
+ [02-mimari.md](./02-mimari.md)'de, bundle'ın nasıl üretildiği
10
+ [08-build.md](./08-build.md)'de.
11
+
12
+ ## Sözleşme
13
+
14
+ Sunucu HTML'i tamdır; island yalnızca davranış ekler. Üç parça var.
15
+
16
+ **1. Şablonda işaret.**
17
+
18
+ ```ejs
19
+ <div data-island="counter" data-island-props='{"start":5}'></div>
20
+ ```
21
+
22
+ **2. Island modülü — `mount` adlı named export.**
23
+
24
+ ```js
25
+ // client/islands/counter.js
26
+ /**
27
+ * @param {HTMLElement} element
28
+ * @param {{ start?: number }} props
29
+ * @returns {void | (() => void)} temizlik fonksiyonu (opsiyonel)
30
+ */
31
+ export function mount(element, props) {
32
+ let value = props.start ?? 0;
33
+ // …
34
+ }
35
+ ```
36
+
37
+ **3. Entry'de kayıt.**
38
+
39
+ ```js
40
+ // client/entries/main.js
41
+ import { registerAll, start } from "jskelet/client";
42
+
43
+ registerAll({
44
+ counter: () => import("../islands/counter.js"),
45
+ });
46
+
47
+ start();
48
+ ```
49
+
50
+ Loader'ın dinamik import olması modelin özü: modül yalnızca sayfada o island
51
+ gerçekten varsa **ve** bağlanma koşulu sağlandığında indirilir. Bu haritayı
52
+ büyütmek ilk yükü büyütmez.
53
+
54
+ ## HTML attribute'ları
55
+
56
+ | Attribute | Anlamı |
57
+ | --- | --- |
58
+ | `data-island="ad"` | Bağlanacak island'ın kayıtlı adı. Zorunlu. |
59
+ | `data-island-props='{"…":…}'` | JSON props. Ayrıştırılamazsa konsola hata basılır ve `{}` geçilir. |
60
+ | `data-island-eager` | Görünürlükten bağımsız, hemen bağla. |
61
+ | `data-island-idle` | Görünür olsa bile `load` + boş zamana kadar bekle. |
62
+ | `data-island-ready="true"` | **Framework yazar.** `mount()` başarıyla döndükten sonra eklenir; CSS ve testler bunu okuyabilir. |
63
+
64
+ `data-island-props` içeriği HTML attribute'u olduğu için tek tırnakla sarmak en
65
+ kolay yoldur. Değerleri sunucuda üretiyorsanız `jsonScript()` ya da `attrs()`
66
+ kullanmak kaçış hatalarını önler:
67
+
68
+ ```ejs
69
+ <div <%- attrs({ "data-island": "chart", "data-island-props": JSON.stringify({ symbol }) }) %>></div>
70
+ ```
71
+
72
+ ## Hidrasyon stratejileri
73
+
74
+ ### Varsayılan: görünürlüğe bağlı
75
+
76
+ Her island bir `IntersectionObserver`'a verilir (`rootMargin: "200px 0px"`).
77
+ Ekranda olanlar zaten ilk gözlemde tetiklenir; ekran dışındakiler kaydırılana
78
+ kadar hiç indirilmez. Element bir kez görününce gözlemden çıkarılır.
79
+
80
+ Bağlama işi ayrıca boş zamana kaydırılır (`requestIdleCallback`,
81
+ `timeout: 500`; desteklenmiyorsa `setTimeout(fn, 0)`): aynı anda görünen çok
82
+ sayıda island tek bir uzun task'a dönüşürse TBT ve INP bozulur.
83
+
84
+ ### `data-island-eager`
85
+
86
+ Görünürlük beklenmez, doğrudan bağlanır. Header davranışı, çerez bandı, tema
87
+ değiştirici gibi sayfa genelinde geçerli island'lar için.
88
+
89
+ ```ejs
90
+ <header data-island="header" data-island-eager></header>
91
+ ```
92
+
93
+ ### `data-island-idle`
94
+
95
+ Görünür olsa bile `load` olayı tamamlanıp ana iş parçacığı boşalana kadar
96
+ bekletilir. İlk ekranda görünen ama kritik olmayan ağır modüller — örneğin
97
+ grafik kütüphanesi çeken bir mini grafik — LCP ile yarışmasın diye.
98
+
99
+ ```ejs
100
+ <div data-island="sparkline" data-island-idle></div>
101
+ ```
102
+
103
+ Sayfa yüklendiğinde `document.readyState` zaten `complete` ise bekleme atlanır
104
+ ve doğrudan boş zamana kaydırılır.
105
+
106
+ ### Gizli elementler
107
+
108
+ `hidden` bir drawer ya da dialog'un düzen kutusu yoktur ve
109
+ `IntersectionObserver` onu **asla** bildirmez. Bu yüzden `hydrate()` ölçümleri
110
+ tek seferde okur (`getClientRects().length > 0`) ve kutusu olmayan elementleri
111
+ gözlemciye vermek yerine doğrudan bağlar. Ölçümlerin tek seferde okunması da
112
+ bilinçli: araya yazma girmediği için tek reflow olur.
113
+
114
+ Pratik sonucu: bir modal'ı `hidden` başlatabilirsiniz, island'ı yine bağlanır.
115
+
116
+ ## `client/` dizini
117
+
118
+ ```
119
+ client/
120
+ ├── entries/
121
+ │ ├── main.js her sayfada yüklenen ortak bootstrap (veya main.ts)
122
+ │ └── chart.js yalnızca isteyen sayfalarda
123
+ └── islands/
124
+ ├── counter.ts .js veya .ts
125
+ └── chart.js
126
+ ```
127
+
128
+ `client/entries/*.{js,ts,mts}` içindeki **her dosya bir esbuild entry'sidir**.
129
+ `main.js` (veya `main.ts`) layout tarafından her sayfada yüklenir (manifest'te
130
+ varsa). Ek entry'ler yalnızca onları isteyen sayfalarda yüklenir. Aynı stem için
131
+ iki uzantı (`main.js` + `main.ts`) build hatasıdır.
132
+
133
+ ```js
134
+ // controller — manifest anahtarı her zaman *.js kalır
135
+ return { view: "pages/markets", entries: ["chart.js"] };
136
+ ```
137
+
138
+ Layout `entries` dizisindeki her adı `asset(entry)` ile çözüp bir
139
+ `<script type="module">` basar. Ad manifest anahtarıdır (`chart.js`), kaynak
140
+ dosya `chart.ts` olsa bile hash'siz anahtar `.js` kalır.
141
+
142
+ Paylaşılan `@/lib` modülleri sunucuda da import ediliyorsa **`.js` kalsın** —
143
+ Node runtime `.ts` çözmez; `.ts` yalnızca esbuild client hattında derlenir.
144
+
145
+ Kod bölme (`splitting: true`) açık: iki entry'nin paylaştığı modüller ortak bir
146
+ chunk'a çıkar ve iki kez indirilmez.
147
+
148
+ `client/islands/` bir zorunluluk değil, yalnızca yaygın düzen; island modülleri
149
+ entry'den erişilebilen herhangi bir yerde olabilir. `@/` alias'ı hem sunucuda
150
+ hem bundle'da çalışır, böylece `lib/` altındaki paylaşılan modüller aynı import
151
+ stilini kullanabilir.
152
+
153
+ ## Runtime API — `jskelet/client`
154
+
155
+ ### `register(name, loader)`
156
+
157
+ Tek bir island kaydeder. `loader` `Promise<{ mount }>` döndüren bir fonksiyon
158
+ olmalıdır.
159
+
160
+ ```js
161
+ import { register } from "jskelet/client";
162
+
163
+ register("counter", () => import("../islands/counter.js"));
164
+ ```
165
+
166
+ ### `registerAll(entries)`
167
+
168
+ Nesne biçiminde toplu kayıt. Pratikte tercih edilen biçim.
169
+
170
+ ```js
171
+ registerAll({
172
+ counter: () => import("../islands/counter.js"),
173
+ drawer: () => import("../islands/drawer.js"),
174
+ });
175
+ ```
176
+
177
+ ### `hydrate(root?)`
178
+
179
+ `root` (varsayılan `document`) altındaki tüm `[data-island]` elementlerini tarar
180
+ ve bağlanma stratejisine göre işler. Zaten bağlanmış elementler atlanır.
181
+
182
+ Bir island'ı elle yeniden taramak gerektiğinde (ör. kendi kodunuzla DOM
183
+ eklediyseniz) doğrudan çağırabilirsiniz:
184
+
185
+ ```js
186
+ container.innerHTML = html;
187
+ hydrate(container);
188
+ ```
189
+
190
+ ### `observeDocument()`
191
+
192
+ `document.body` üzerine bir `MutationObserver` kurar ve sonradan DOM'a eklenen
193
+ island'ları da yakalar (infinite scroll, portal, fragment yükleme).
194
+ `MutationObserver` örneğini döndürür, böylece gerekirse `disconnect()`
195
+ edilebilir.
196
+
197
+ ### `start()`
198
+
199
+ Tipik bootstrap: `DOMContentLoaded` beklenir (gerekiyorsa), sonra `hydrate()` ve
200
+ `observeDocument()` çağrılır.
201
+
202
+ ```js
203
+ registerAll({ /* … */ });
204
+ start();
205
+ ```
206
+
207
+ ### Bağlanma davranışı ve hatalar
208
+
209
+ - Bir element aynı island adıyla **iki kez bağlanmaz**; kayıt element bazında
210
+ `WeakMap` içinde tutulur.
211
+ - Kayıtlı olmayan bir ad için konsola uyarı basılır:
212
+ `[island] not registered: <name>`.
213
+ - Modül import'u ya da `mount()` hata verirse konsola hata basılır
214
+ (`[island] <name> failed to load`) ve **sayfanın kalanı etkilenmez**.
215
+ - `mount()` başarıyla dönerse elemente `data-island-ready="true"` yazılır.
216
+ - `mount()` bir temizlik fonksiyonu döndürebilir; framework onu saklar ve
217
+ `unmount()` çağrıldığında işletir (aşağıya bakın).
218
+
219
+ ### `unmount(root?)`
220
+
221
+ `root` altındaki island'ları söker: saklanan temizlik fonksiyonlarını çağırır,
222
+ `data-island-ready` işaretini kaldırır ve kaydı siler, böylece aynı düğüm
223
+ tekrar DOM'a girerse yeniden bağlanabilir. `root`'un kendisi de island olabilir.
224
+
225
+ DOM'un bir bölgesini değiştirirken çağrılması **zorunlu**:
226
+
227
+ ```js
228
+ import { hydrate, unmount } from "jskelet/client";
229
+
230
+ unmount(container);
231
+ container.innerHTML = html;
232
+ hydrate(container);
233
+ ```
234
+
235
+ Atlanması en kolay gözden kaçan sızıntı biçimini üretiyor. `innerHTML` ile
236
+ değiştirilen bir bölgenin island'ları DOM'dan çıkar, ama `document`/`window`
237
+ üzerine kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder;
238
+ birkaç takastan sonra aynı iş onlarca kez çalışır.
239
+
240
+ ```js
241
+ export function mount(element) {
242
+ const timer = setInterval(() => tick(element), 1000);
243
+ const onResize = () => layout(element);
244
+ window.addEventListener("resize", onResize);
245
+
246
+ return () => {
247
+ clearInterval(timer);
248
+ window.removeEventListener("resize", onResize);
249
+ };
250
+ }
251
+ ```
252
+
253
+ `swap()` ve form yardımcıları `unmount()`u kendileri çağırıyor; elle DOM
254
+ değiştirdiğiniz yerlerde siz çağırıyorsunuz.
255
+
256
+ ### `swap(target, url, options?)` ve `startSwapLinks(root?)`
257
+
258
+ Bir bölgeyi sunucudan gelen parçayla değiştirir: eski alt ağacı söker, içeriği
259
+ yazar, yeniden hidre eder ve odağı kaybolmuşsa geri getirir.
260
+
261
+ ```html
262
+ <a href="/_fragment/satirlar?sayfa=2" data-swap="#satirlar">Sonraki</a>
263
+ ```
264
+
265
+ Sunucu tarafı ve tüm seçenekler
266
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
267
+
268
+ ### `enhanceForm(form)` ve `startForms(root?)`
269
+
270
+ `data-enhance` taşıyan formları sayfa yenilemeden gönderir; JS kapalıyken
271
+ normal POST + yönlendirme akışı çalışmaya devam eder. Sözleşmenin tamamı
272
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
273
+
274
+ ## Durum paylaşımı: `createStore`
275
+
276
+ React Context'in yerine kullanılan minimal pub/sub. `useSyncExternalStore`
277
+ köprüsünün yerini alır: doğrudan `subscribe`.
278
+
279
+ ```js
280
+ // client/stores/theme.js
281
+ import { createStore } from "jskelet/client";
282
+
283
+ export const theme = createStore("light");
284
+ ```
285
+
286
+ ```js
287
+ // client/islands/theme-toggle.js
288
+ import { theme } from "../stores/theme.js";
289
+
290
+ export function mount(element) {
291
+ const paint = (value) => {
292
+ element.textContent = value === "light" ? "Koyu tema" : "Açık tema";
293
+ };
294
+
295
+ const unsubscribe = theme.subscribe(paint);
296
+ paint(theme.get());
297
+
298
+ element.addEventListener("click", () => {
299
+ theme.set((prev) => (prev === "light" ? "dark" : "light"));
300
+ });
301
+
302
+ return unsubscribe;
303
+ }
304
+ ```
305
+
306
+ API:
307
+
308
+ | Üye | Davranış |
309
+ | --- | --- |
310
+ | `get()` | Anlık değer |
311
+ | `set(next)` | Değer ya da `(prev) => next` fonksiyonu. Değer **aynıysa** (`===`) dinleyiciler tetiklenmez. |
312
+ | `subscribe(listener)` | Dinleyici ekler, kaldıran fonksiyonu döndürür. Abone olurken mevcut değerle çağrılmaz — ilk boyamayı kendiniz yapın. |
313
+
314
+ ## DOM yardımcıları
315
+
316
+ `jskelet/client` island'ların paylaştığı küçük bir yardımcı seti verir.
317
+
318
+ | Fonksiyon | İmza | Davranış |
319
+ | --- | --- | --- |
320
+ | `qs` | `(root, selector) => HTMLElement \| null` | `querySelector` |
321
+ | `qsa` | `(root, selector) => HTMLElement[]` | `querySelectorAll`, gerçek dizi olarak |
322
+ | `on` | `(target, type, handler, options?) => () => void` | Dinleyici ekler ve **kaldıran fonksiyonu döndürür** |
323
+ | `onClick` | `(root, selector, handler) => () => void` | Delege edilmiş click; `handler(event, target)` |
324
+ | `debounce` | `(ms, fn) => fn` | Son çağrıdan `ms` sonra çalışır |
325
+ | `raf` | `(fn) => fn` | Çağrıları tek bir `requestAnimationFrame`'de birleştirir |
326
+ | `toggleClass` | `(element, name, active) => void` | `classList.toggle` |
327
+ | `getOverlayRoot` | `() => HTMLElement` | `#jskelet-overlays` ya da `body` |
328
+
329
+ `on()` ve `onClick()`'in kaldırıcı döndürmesi, `mount()`'un temizlik
330
+ fonksiyonuyla doğal olarak eşleşir:
331
+
332
+ ```js
333
+ import { on, onClick, raf } from "jskelet/client";
334
+
335
+ export function mount(element) {
336
+ const offClick = onClick(element, "[data-tab]", (event, target) => {
337
+ selectTab(target.dataset.tab);
338
+ });
339
+
340
+ const offScroll = on(window, "scroll", raf(() => updateShadow(element)), {
341
+ passive: true,
342
+ });
343
+
344
+ return () => {
345
+ offClick();
346
+ offScroll();
347
+ };
348
+ }
349
+ ```
350
+
351
+ `getOverlayRoot()` modal/drawer içeriğini taşımak için: layout'ta
352
+ `<div id="jskelet-overlays"></div>` varsa oraya, yoksa `body`ye. Portal,
353
+ `overflow` ya da `transform` taşıyan bir ata elementin `position: fixed`
354
+ overlay'i kırpmasını engeller.
355
+
356
+ ## `startSafeImages()`
357
+
358
+ Yüklenemeyen görseller için tek bir belge dinleyicisi. **Bilinçli olarak island
359
+ değildir:** görsel ağırlıklı bir sayfada 80+ `<img>` olabiliyor ve her birine
360
+ ayrı island bağlamak (gözlemci + dinamik import + mount) sırf hata ihtimali için
361
+ ciddi bir hidrasyon yükü.
362
+
363
+ ```js
364
+ // client/entries/main.js
365
+ import { registerAll, start, startSafeImages } from "jskelet/client";
366
+
367
+ registerAll({ /* … */ });
368
+ startSafeImages();
369
+ start();
370
+ ```
371
+
372
+ Kullanım, şablon tarafında:
373
+
374
+ ```ejs
375
+ <%# 1. Minimal: framework ölçüleri koruyan bir blokla değiştirir %>
376
+ <img src="/kapak.png" alt="Kapak" width="640" height="360" data-safe-image>
377
+
378
+ <%# 2. Kendi hata görünümü %>
379
+ <div data-safe-image-host>
380
+ <img src="/kapak.png" alt="Kapak" data-safe-image>
381
+ <template data-safe-image-fallback>
382
+ <div class="flex h-40 items-center justify-center bg-slate-100">Görsel yok</div>
383
+ </template>
384
+ </div>
385
+ ```
386
+
387
+ Nasıl çalışır:
388
+
389
+ - Belgeye **yakalama fazında** tek bir `error` dinleyicisi kurulur. `error`
390
+ olayı kabarmaz ama yakalama fazında görülebilir; bu yüzden tek dinleyici tüm
391
+ görselleri karşılar ve sonradan DOM'a eklenenler de kendiliğinden kapsanır.
392
+ - `data-safe-image-host` sarmalayıcısı **ve** içinde
393
+ `<template data-safe-image-fallback>` varsa sarmalayıcının tamamı template
394
+ içeriğiyle değiştirilir. Framework hiçbir stil dayatmaz.
395
+ - Yoksa görselin yerine minimal bir blok konur: `role="img"`, `alt` (ya da
396
+ `data-fallback-label`) değeri `aria-label` olarak, görselin `className`i artı
397
+ `data-fallback-class`, ve `width`/`height` varsa aynı ölçüler inline style
398
+ olarak. Ölçülerin korunması değiştirme sırasında düzen kaymasını (CLS)
399
+ önler.
400
+ - JS çalışmadan önce başarısız olmuş görseller olay üretmez; bu yüzden bir kez
401
+ tarama yapılır (`requestIdleCallback`, `timeout: 2000`): `complete` olup
402
+ `naturalWidth === 0` olanlar değiştirilir.
403
+
404
+ ## Ertelenmiş panel (fragment) deseni
405
+
406
+ Ağır ve ikincil bir bölümü (yorumlar, ilgili haberler, uzun bir tablo) ilk HTML
407
+ yanıtından tamamen çıkarmak istediğinizde island + layout'suz render birleşimi
408
+ kullanılır. Framework'te bunun için özel bir API yok; iki hazır parçanın
409
+ kombinasyonu:
410
+
411
+ **1. Sunucuda layout'suz bir fragment ucu** (`renderView`, bkz.
412
+ [03-routing.md](./03-routing.md)):
413
+
414
+ ```js
415
+ // routes/80-fragments.mjs
416
+ export default function register(app, { renderView }) {
417
+ app.get("/_fragment/yorumlar/:id", async (req, res) => {
418
+ const comments = await getComments(req.params.id);
419
+ res.type("html").send(await renderView("fragments/comments", { comments }));
420
+ });
421
+ }
422
+ ```
423
+
424
+ **2. Sayfada bir yer tutucu island.** Görünürlüğe bağlı bağlandığı için,
425
+ ziyaretçi o bölüme kaydırmazsa ne modül ne de fragment indirilir:
426
+
427
+ ```ejs
428
+ <div data-island="deferred" data-island-props='{"src":"/_fragment/yorumlar/42"}'></div>
429
+ ```
430
+
431
+ **3. Island fragment'ı çekip yerleştirir ve içindeki island'ları hidre eder:**
432
+
433
+ ```js
434
+ // client/islands/deferred.js
435
+ import { hydrate } from "jskelet/client";
436
+
437
+ export async function mount(element, { src }) {
438
+ try {
439
+ const response = await fetch(src, { headers: { accept: "text/html" } });
440
+ if (!response.ok) return;
441
+
442
+ element.innerHTML = await response.text();
443
+ hydrate(element);
444
+ } catch {
445
+ // İkincil içerik: sessizce vazgeç, sayfanın kalanı etkilenmesin.
446
+ }
447
+ }
448
+ ```
449
+
450
+ `observeDocument()` zaten çalışıyorsa son satırdaki `hydrate()` çağrısı
451
+ gereksizdir; yine de açıkça çağırmak, `start()` kullanmayan bir kurulumda da
452
+ doğru davranmasını sağlar.
453
+
454
+ Fragment yolları için `/_fragment/` öneki önerilir: varsayılan `prewarmSkip`
455
+ listesinde olduğu için ısıtma turu bu uçları taramaz
456
+ ([06-cache.md](./06-cache.md)).
457
+
458
+ ## Ortam değişkenleri ve `clientEnv`
459
+
460
+ Tarayıcıda `process` yoktur, ama sunucuyla paylaşılan modüller yine de
461
+ `process.env` okuyabilir. `jskelet.config.mjs` → `clientEnv` ile bildirilen
462
+ anahtarlar build zamanında bundle'a gömülür:
463
+
464
+ ```js
465
+ export default {
466
+ clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN"],
467
+ };
468
+ ```
469
+
470
+ Next'teki `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın herkese açık
471
+ olduğu isimden değil config'ten belli. `process.env`in tamamı tek nesne olarak
472
+ define edilir, yani listede olmayan bir anahtar okunduğunda çökme yerine
473
+ `undefined` döner. `NODE_ENV` her zaman gömülür.
474
+
475
+ ## Tarayıcı desteği
476
+
477
+ Bundle hedefi sabittir: `chrome111`, `edge111`, `firefox111`, `safari16.4`. ESM
478
+ + dinamik import + `IntersectionObserver` island modelinin zaten alt sınırı;
479
+ daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor.
480
+ JS hiç çalışmasa da sunucu HTML'i tam olduğu için sayfa okunur kalır.
481
+
482
+ ## Sırada ne var
483
+
484
+ - Bundle, hash'ler ve `entries` manifest'i: [08-build.md](./08-build.md)
485
+ - `entries` alanının controller tarafı: [03-routing.md](./03-routing.md)
486
+ - Island durumunu dev panelinden izlemek: [09-dev-araclari.md](./09-dev-araclari.md)