jskelet 0.6.3 → 0.6.4

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 (153) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +628 -620
  3. package/LICENSE +21 -21
  4. package/README.md +2 -0
  5. package/bin/jskelet.mjs +130 -130
  6. package/docs/01-baslangic.md +291 -291
  7. package/docs/02-mimari.md +310 -310
  8. package/docs/03-routing.md +515 -515
  9. package/docs/04-render-ve-sablonlar.md +667 -661
  10. package/docs/05-islands.md +486 -486
  11. package/docs/06-cache.md +1467 -1443
  12. package/docs/07-yapilandirma.md +1208 -1197
  13. package/docs/08-build.md +429 -429
  14. package/docs/09-dev-araclari.md +364 -364
  15. package/docs/10-dagitim.md +348 -338
  16. package/docs/12-panel-ve-oturum.md +479 -478
  17. package/docs/README.md +83 -83
  18. package/docs/en/01-getting-started.md +298 -298
  19. package/docs/en/02-architecture.md +329 -329
  20. package/docs/en/03-routing.md +531 -531
  21. package/docs/en/04-rendering.md +675 -669
  22. package/docs/en/05-islands.md +497 -497
  23. package/docs/en/06-caching.md +1476 -1453
  24. package/docs/en/07-configuration.md +1229 -1219
  25. package/docs/en/08-build.md +447 -447
  26. package/docs/en/09-dev-tools.md +373 -373
  27. package/docs/en/10-deployment.md +351 -340
  28. package/docs/en/11-migration.md +398 -398
  29. package/docs/en/12-dashboards-and-sessions.md +489 -488
  30. package/docs/en/README.md +87 -87
  31. package/package.json +137 -137
  32. package/src/build/ensure-build.mjs +19 -19
  33. package/src/build/paths.mjs +153 -153
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +349 -349
  36. package/src/build/tasks/css.mjs +235 -235
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +357 -357
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/build/tasks/templates.mjs +20 -20
  42. package/src/client/admin/i18n.js +764 -764
  43. package/src/client/admin/login.html +74 -74
  44. package/src/client/admin/panel.css +809 -809
  45. package/src/client/admin/panel.html +495 -495
  46. package/src/client/admin/panel.js +1251 -1251
  47. package/src/client/devtools/report.html +185 -185
  48. package/src/client/devtools/report.js +745 -745
  49. package/src/client/devtools/seo.js +628 -628
  50. package/src/client/dom.js +95 -95
  51. package/src/client/form.js +192 -192
  52. package/src/client/index.js +45 -45
  53. package/src/client/registry.js +305 -305
  54. package/src/client/safe-image.js +91 -91
  55. package/src/client/shared-cookie.js +225 -225
  56. package/src/client/store.js +36 -36
  57. package/src/client/swap.js +188 -188
  58. package/src/compile/codegen.js +336 -336
  59. package/src/compile/compile-all.js +149 -149
  60. package/src/compile/errors.js +66 -66
  61. package/src/compile/expr.js +409 -409
  62. package/src/compile/index.js +17 -17
  63. package/src/compile/parse.js +541 -541
  64. package/src/compile/resolve.js +211 -211
  65. package/src/compile/scan-exports.js +51 -51
  66. package/src/config/defaults.js +541 -534
  67. package/src/config/index.js +1500 -1469
  68. package/src/config/pattern.js +107 -107
  69. package/src/generate.mjs +163 -163
  70. package/src/http/control-flow.js +71 -71
  71. package/src/http/cookies-entry.js +21 -21
  72. package/src/http/cookies.js +277 -277
  73. package/src/http/request-cache.js +46 -46
  74. package/src/http/request-context.js +165 -165
  75. package/src/http/shared-cookie.js +178 -178
  76. package/src/index.js +101 -101
  77. package/src/init.mjs +232 -230
  78. package/src/migrate/apply.mjs +262 -262
  79. package/src/migrate/babel.mjs +79 -79
  80. package/src/migrate/classify.mjs +155 -155
  81. package/src/migrate/config.mjs +126 -126
  82. package/src/migrate/fs-walk.mjs +191 -191
  83. package/src/migrate/parse.mjs +26 -26
  84. package/src/migrate/scan.mjs +177 -177
  85. package/src/migrate/transform/expr-source.mjs +168 -168
  86. package/src/migrate/transform/island.mjs +67 -67
  87. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  88. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  89. package/src/migrate/transform/page-split.mjs +435 -435
  90. package/src/migrate/write.mjs +81 -81
  91. package/src/migrate.mjs +171 -171
  92. package/src/runtime/alias-hooks.mjs +119 -119
  93. package/src/runtime/register.mjs +4 -4
  94. package/src/server/admin/actions.js +229 -229
  95. package/src/server/admin/auth.js +125 -125
  96. package/src/server/admin/event-log.js +151 -151
  97. package/src/server/admin/gate.js +209 -209
  98. package/src/server/admin/inventory.js +188 -188
  99. package/src/server/admin/mount.js +56 -56
  100. package/src/server/admin/router.js +216 -216
  101. package/src/server/admin/snapshot.js +241 -241
  102. package/src/server/assets.js +147 -147
  103. package/src/server/auth/handoff.js +309 -309
  104. package/src/server/cache-blob.js +70 -70
  105. package/src/server/cache-control.js +45 -0
  106. package/src/server/cache-deps.js +42 -42
  107. package/src/server/cache-vary.js +113 -113
  108. package/src/server/cloudflare.js +607 -607
  109. package/src/server/create-app.js +366 -366
  110. package/src/server/data-cache.js +553 -553
  111. package/src/server/dev/report.js +485 -485
  112. package/src/server/dev/socket.js +170 -170
  113. package/src/server/dev/version-check.mjs +139 -139
  114. package/src/server/disk-cache.js +233 -233
  115. package/src/server/ejs-adapter.js +59 -59
  116. package/src/server/html-cache.js +1196 -1196
  117. package/src/server/image-optimizer.js +500 -500
  118. package/src/server/logs/access-middleware.js +66 -66
  119. package/src/server/logs/file-sink.js +193 -193
  120. package/src/server/logs/pipeline.js +165 -165
  121. package/src/server/logs/s3-put.js +214 -214
  122. package/src/server/logs/s3-sink.js +112 -112
  123. package/src/server/metadata.js +102 -102
  124. package/src/server/middleware/compression.js +205 -205
  125. package/src/server/middleware/csrf.js +134 -134
  126. package/src/server/middleware/dev-gate.js +75 -75
  127. package/src/server/middleware/headers.js +37 -37
  128. package/src/server/middleware/redirects.js +32 -32
  129. package/src/server/middleware/robots-txt.js +341 -341
  130. package/src/server/middleware/static-precompressed.js +121 -121
  131. package/src/server/middleware/trailing-slash.js +53 -53
  132. package/src/server/middleware/upstream-proxy.js +141 -141
  133. package/src/server/og-image.js +369 -356
  134. package/src/server/port-guard.js +255 -255
  135. package/src/server/prewarm.js +1082 -1082
  136. package/src/server/redis.js +588 -588
  137. package/src/server/render.js +910 -910
  138. package/src/server/router.js +157 -157
  139. package/src/server/status-page.js +265 -265
  140. package/src/server/upstream-limiter.js +376 -376
  141. package/src/server/upstream-tracking.js +166 -166
  142. package/src/shared/cookie-domain.js +66 -66
  143. package/src/start.mjs +22 -22
  144. package/src/templates/layout.ejs +30 -30
  145. package/src/templates/layout.jsk +30 -30
  146. package/src/version.mjs +31 -31
  147. package/src/views/components/loader.js +101 -101
  148. package/src/views/helpers/html.js +102 -102
  149. package/src/views/helpers/tags.js +375 -375
  150. package/types/config/defaults.d.ts +6 -0
  151. package/types/config/index.d.ts +6 -0
  152. package/types/server/cache-control.d.ts +28 -0
  153. package/types/server/og-image.d.ts +5 -0
@@ -1,661 +1,667 @@
1
- # 04 — Render ve şablonlar
2
-
3
- Bu belge sunucu HTML'inin nasıl üretildiğini anlatır: EJS motorunun ayarları,
4
- layout dosyasının çözümü ve kullanabildiği local'ler, `views/pages` altındaki
5
- sayfa şablonları, `views/components/**` altındaki bileşenlerin otomatik kaydı,
6
- şablonlara hazır gelen `html`/`tags` yardımcıları, `metadata` nesnesinin `<head>`
7
- etiketlerine çevrilmesi ve üç render hook'u. Controller'ın bu katmana ne
8
- gönderdiği [03-routing.md](./03-routing.md)'de, varlık URL'lerini üreten
9
- `asset()`/`hasAsset()` [08-build.md](./08-build.md)'de anlatılıyor.
10
-
11
- ## Render hattı
12
-
13
- ```
14
- route(controller)
15
- └─ produce()
16
- ├─ controller(ctx) → sayfa tanımı
17
- └─ renderPage(page)
18
- ├─ hooks.metadata(page) + page.metadata → metadata
19
- ├─ Promise.all([
20
- │ renderView(page.view, { …data, metadata }), → body
21
- │ hooks.layoutContext({ pathname, metadata }), → context
22
- │ ])
23
- └─ layout (.jsk derlenmiş veya .ejs) → tam HTML
24
- ```
25
-
26
- Layout bağlamı ve gövde **paralel** üretilir. Sebebi ölçümden geliyor:
27
- navigasyon çoğu projede upstream'den geliyor ve gövde render'ıyla sırayla
28
- beklemek her sayfaya gereksiz gecikme ekliyor.
29
-
30
- ## `.jsk` — build-time derlenmiş şablonlar
31
-
32
- Yeni uygulamalarda varsayılan şablon biçimi `.jsk`'dir. Build sırasında
33
- (`.jskelet/templates/*.mjs`) normal ESM modüllerine çevrilir; **istek anında
34
- parse / `eval` / `new Function` yoktur**. Production yolu:
35
-
36
- ```
37
- controller data → import edilmiş render(data, helpers) → HTML
38
- ```
39
-
40
- ### Sözdizimi özeti
41
-
42
- ```html
43
- <section class="wrapper">
44
- <h1>{{ title }}</h1>
45
- <div>{{{ trustedHtml }}}</div>
46
-
47
- {#if items.length}
48
- <List :items="items" />
49
- {#else}
50
- <p>Boş</p>
51
- {/if}
52
-
53
- {#each items as item, i}
54
- <li :data-i="i">{{ item }}</li>
55
- {/each}
56
-
57
- <Link href="/" text="Home" />
58
- <div data-island="counter" data-island-props='{"start":0}'></div>
59
- </section>
60
- ```
61
-
62
- | Özellik | Yazım |
63
- | --- | --- |
64
- | Kaçışlı metin | `{{ expr }}` |
65
- | Ham HTML | `{{{ expr }}}` |
66
- | Koşul | `{#if expr}` … `{#else}` … `{/if}` |
67
- | Döngü | `{#each list as item}` veya `as item, i` |
68
- | Include | `{#include "partials/header"}` (derlenmiş `.jsk`) |
69
- | Bileşen | PascalCase etiket; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
70
- | Yerleşikler | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
71
-
72
- İfade dili kasıtlı olarak dardır (erişim, karşılaştırma, ternary, `.length`).
73
- Atama, object literal ve rastgele fonksiyon çağrısı yok — mantık controller
74
- veya JS bileşende kalır.
75
-
76
- #### Şablon mu, bileşen mi?
77
-
78
- EJS’den geçerken sınırı erken çizmek işe yarar:
79
-
80
- | Burada kalsın (`.jsk`) | JS bileşene taşı |
81
- | --- | --- |
82
- | Metin, koşul, liste, prop bağlama | Fonksiyon çağrısı, nesne üretimi, biçimlendirme |
83
- | Yerleşik etiketler (`Link`, `Image`, …) | Birden fazla yardımcıdan HTML birleştirme |
84
- | Controller’dan gelen hazır veri | Upstream / hata ayırt eden UI (`LoadErrorState`) |
85
-
86
- Şablonda `format(x)` veya `{ a: 1 }` yazılamıyorsa bu bir eksik değil: o iş
87
- `views/components/*.js` veya controller’ındır. Karmaşık sayfalar bileşene
88
- kaçıyorsa ifade dilini genişletmek yerine bileşen sınırını net tutmak tercih
89
- edilir.
90
-
91
- ### Editör desteği
92
-
93
- Repo içinde `extensions/vscode-jsk` VS Code / Cursor uzantısı vardır: sözdizimi
94
- renklendirme, dil yapılandırması ve snippet'ler. Yerel kurulum:
95
-
96
- ```bash
97
- code --install-extension extensions/vscode-jsk
98
- ```
99
-
100
- Ayrıntılar uzantı README'sinde.
101
-
102
- ### Yerleşik layout etiketleri
103
-
104
- `.jsk` ifade dilinde `asset()` / `hasAsset()` çağrılamaz. Layout’ta stylesheet,
105
- script ve JSON-LD döngüleri için yerleşikler:
106
-
107
- | Etiket | Props | Çıktı |
108
- | --- | --- | --- |
109
- | `Stylesheets` | `styles` | `app.css` + sayfa sheet’leri (`data-jskelet-css`) |
110
- | `BodyScripts` | `entries`, `devtools`, `devBasePath` | `main.js`, entry’ler, isteğe bağlı overlay |
111
- | `JsonLd` | `items` (`structuredData`) | `application/ld+json` script’leri |
112
-
113
- ### EJS ile birlikte yaşam
114
-
115
- Aynı `view` id için derlenmiş `.jsk` varsa o kullanılır; yoksa `.ejs` dosyası
116
- **yalnızca `ejs` peer’i kuruluysa** render edilir. `jskelet init` yeni iskeleti
117
- `.jsk` ile kurar.
118
-
119
- ## EJS motoru (legacy peer)
120
-
121
- EJS opsiyonel peer bağımlılıktır (`npm i ejs`). `.jsk`-only uygulamalar kurmak
122
- zorunda değildir. Bir `.ejs` view veya layout istendiğinde paket uygulamadan
123
- yüklenir; yoksa göç yolunu gösteren bir hata fırlatılır.
124
-
125
- Motor ilk EJS render’da bir kez kurulur. Ayarlar:
126
-
127
- | Ayar | Değer | Sebebi |
128
- | --- | --- | --- |
129
- | `root`, `views` | `views` dizini | `include('partials/header')` çağrıları views kökünden çözülür |
130
- | `cache` | dev'de `false`, prod'da `true` | dev'de şablon düzenlemesi anında görünsün |
131
- | `rmWhitespace` | `true` | çıktı boyutu |
132
- | `async` | `true` | şablon içinde `await` kullanılabilir |
133
-
134
- Gömülü kullanımlar (test, script) için `resetRenderEngine()` dışa açık.
135
-
136
- ## Layout
137
-
138
- ### Layout dosyası nasıl bulunur
139
-
140
- 1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
141
- dizininin üst dizinine** göre çözülür: `views` varsayılansa
142
- `layout: "views/ozel.jsk"` → `<root>/views/ozel.jsk`.
143
- 2. Verilmemişse `views/layout.jsk` (derlenmiş) varsa o kullanılır.
144
- 3. Yoksa `views/layout.ejs` varsa o kullanılır (EJS peer gerekir).
145
- 4. O da yoksa framework'ün kendi minimal layout'u kullanılır
146
- (`node_modules/jskelet/src/templates/layout.jsk`, `jskelet/layout`;
147
- legacy kopya `jskelet/layout/ejs`).
148
-
149
- Üçüncü seçenek yeni bir projenin tek route ile çalışabilmesi için var. Kendi
150
- layout'unuza geçmenin en pratik yolu o dosyayı `views/layout.jsk` olarak
151
- kopyalamaktır.
152
-
153
- ### Framework'ün varsayılan layout'u
154
-
155
- ```jsk
156
- <!DOCTYPE html>
157
- <html :lang="lang">
158
- <head>
159
- <meta charset="utf-8">
160
- <meta name="viewport" content="width=device-width, initial-scale=1">
161
- {{{ extraHead }}}
162
- <Stylesheets :styles="styles" />
163
- {{{ headMeta }}}
164
- <JsonLd :items="structuredData" />
165
- </head>
166
- <body :class="bodyClass">
167
- {{{ body }}}
168
- <BodyScripts :entries="entries" :devtools="devtools" :devBasePath="devBasePath" />
169
- </body>
170
- </html>
171
- ```
172
-
173
- Dikkat edilecek noktalar:
174
-
175
- - **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
176
- geciktirmek doğrudan LCP'ye yazılır.
177
- - **Global `app.css` render-blocking** ve gerekçesi
178
- [02-mimari.md](./02-mimari.md)'de. Controller `styles: [...]` ile ek sayfa
179
- sheet'leri de aynı şekilde basılır. Build çalışmadıysa `hasAsset` false olur
180
- ve etiket hiç basılmaz.
181
- - **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
182
- istememesini sağlar (`Stylesheets` / `BodyScripts` içinde).
183
- - **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
184
- çıktısında hiç yoktur.
185
-
186
- ### Layout local'leri
187
-
188
- | Local | Tip | Kaynağı |
189
- | --- | --- | --- |
190
- | `metadata` | `object` | `hooks.metadata()` + controller `metadata` (controller kazanır) |
191
- | `headMeta` | `string` | `metadata`dan üretilmiş hazır `<head>` etiketleri |
192
- | `extraHead` | `string` | `preconnect` ipuçları + `navigation` ipuçları + controller `head` + `context.extraHead` |
193
- | `structuredData` | `unknown[]` | `hooks.layoutContext()` → `structuredData`; varsayılan `[]` |
194
- | `body` | `string` | Sayfa şablonunun render çıktısı |
195
- | `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
196
- | `entries` | `string[]` | controller `entries`; varsayılan `[]` |
197
- | `styles` | `string[]` | controller `styles`; varsayılan `[]` |
198
- | `pathname` | `string` | `req.path`; **varsayılan boş string** |
199
- | `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
200
- | `devtools` | `boolean` | `NODE_ENV === "development"` |
201
- | `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
202
- | `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
203
- | html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
204
- | `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
205
- | `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
206
-
207
- `pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
208
- sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
209
-
210
- ## Sayfa şablonları
211
-
212
- `view` alanı `views/` altındaki yolu uzantısız verir: `"pages/home"` →
213
- `views/pages/home.ejs`. Şablona geçen local'ler `data` alanının içeriği artı
214
- `metadata`dır — layout local'leri **değil**. Sayfa şablonu yine tüm yardımcılara
215
- ve bileşenlere erişir.
216
-
217
- ```ejs
218
- <%# views/pages/home.ejs %>
219
- <section class="wrapper">
220
- <h1 class="text-3xl font-bold"><%= heading %></h1>
221
-
222
- <%# `list` views/components/list.js içinde tanımlı; import gerekmiyor. %>
223
- <%- list({ items }) %>
224
-
225
- <div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
226
- </section>
227
- ```
228
-
229
- EJS'te iki çıktı biçimini karıştırmayın:
230
-
231
- - `<%= value %>` — HTML kaçışlı. Kullanıcı/upstream verisi için **daima** bu.
232
- - `<%- html %>` — ham. Yalnızca kendi ürettiğin, güvenli HTML string'leri için
233
- (bileşen çağrıları, `headMeta`, `body`).
234
-
235
- `async: true` açık olduğu için şablon içinde `await` da kullanılabilir, ancak
236
- veri çekmeyi controller'da tutmak teşhisi kolaylaştırır.
237
-
238
- ## Bileşenler: `views/components/**`
239
-
240
- Bileşenler EJS partial'ı değil, **HTML string döndüren fonksiyonlardır**.
241
- `views/components/**` altındaki her `.js` dosyası taranır ve **her named export**
242
- şablon local'i olur. Elle bakılan bir barrel dosyası yok: yeni bir bileşen
243
- eklemek için dosyayı oluşturmak yeterli.
244
-
245
- ```js
246
- // views/components/list.js
247
- import { esc } from "jskelet/html";
248
-
249
- /**
250
- * @param {{ items: string[] }} props
251
- * @returns {string}
252
- */
253
- export function list({ items }) {
254
- if (!items?.length) return "";
255
-
256
- const rows = items.map((item) => `<li class="py-1">${esc(item)}</li>`).join("");
257
- return `<ul class="mt-6 list-disc pl-6">${rows}</ul>`;
258
- }
259
- ```
260
-
261
- Şablonda:
262
-
263
- ```ejs
264
- <%- list({ items }) %>
265
- ```
266
-
267
- Kurallar:
268
-
269
- - Tarama özyinelemelidir; alt dizinler de kapsanır.
270
- - `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
271
- - Compile-time bilinen bileşen listesi **dosya adından değil**, kaynak
272
- metindeki named export'lardan okunur. `ui.js` içindeki `sectionHead` →
273
- şablonda `<SectionHead />` (runtime zaten camelCase export'a PascalCase
274
- alias ekler). Dosya adına göre stub re-export eklemeye gerek yoktur.
275
- - `loader.js` ve `index.js` bileşen dosyası sayılmaz.
276
- - `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
277
- önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
278
- bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
279
- - Aynı ad (veya aynı PascalCase etiket) iki farklı bileşen dosyasında
280
- tanımlıysa **uyarı değil hata**: build ve sunucu açılışı
281
- `Component 'card' is defined twice: …` ile durur. Barrel üzerine yazmak
282
- bilinçli istisnadır.
283
- - `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
284
- bir proje de çalışır.
285
-
286
- ## Yardımcılar: `jskelet/html`
287
-
288
- Şablonlara otomatik geçer; bileşen dosyalarında `import { … } from "jskelet/html"`
289
- ile alınır.
290
-
291
- ### `esc(value)`
292
-
293
- Metin içeriği ve attribute değerleri için kaçış (`&`, `<`, `>`, `"`, `'`).
294
- `null`, `undefined` ve `false` boş string'e çevrilir — koşullu render'da
295
- `false && "…"` gibi ifadeler `"false"` basmaz.
296
-
297
- ```js
298
- esc('<b>"x"</b>'); // "&lt;b&gt;&quot;x&quot;&lt;/b&gt;"
299
- ```
300
-
301
- ### `attrs(object)`
302
-
303
- Attribute nesnesini string'e çevirir. `null`/`undefined`/`false` atlanır, `true`
304
- boolean attribute olarak yazılır, geri kalan değerler kaçışlanır. Çıktı boş
305
- değilse **başında bir boşluk** ile döner, böylece `<div${attrs(...)}>` her zaman
306
- doğru biçimlenir.
307
-
308
- ```js
309
- `<input${attrs({ type: "text", required: true, value: null })}>`;
310
- // '<input type="text" required>'
311
- ```
312
-
313
- ### `cx(...inputs)`
314
-
315
- `clsx` karşılığı: string, sayı, dizi ve `{ sınıf: koşul }` nesnesi kabul eder,
316
- falsy değerleri atar. Tailwind çakışması **çözmez**.
317
-
318
- ```js
319
- cx("btn", isActive && "btn-active", { "btn-lg": size === "lg" });
320
- ```
321
-
322
- ### `cn(...inputs)`
323
-
324
- `cx()` ile birleştirir, sonra `tailwind-merge` ile Tailwind çakışmalarını çözer.
325
- Bir bileşenin varsayılan sınıflarının çağıran tarafından ezilmesi gerektiğinde
326
- bunu kullanın.
327
-
328
- ```js
329
- cn("px-4 py-2 bg-slate-100", className); // className "bg-white" ise bg-slate-100 düşer
330
- ```
331
-
332
- `tailwind-merge` çalışma zamanı bağımlılığı olarak korunur çünkü sınıf hesabı
333
- yalnızca sunucuda yapılır; client bundle'a hiç girmez.
334
-
335
- ### `jsonScript(value)`
336
-
337
- `<script type="application/ld+json">` gövdesi için güvenli JSON: `<`, `>`, `&`
338
- ve U+2028/U+2029 kaçırılır, böylece `</script` ya da `<!--` dizileri gövdeyi
339
- kapatamaz.
340
-
341
- ```ejs
342
- <script type="application/ld+json"><%- jsonScript(article) %></script>
343
- ```
344
-
345
- ## Yardımcılar: `jskelet/tags`
346
-
347
- `next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları. Hepsi HTML
348
- string döndürür ve EJS içinden `<%- %>` ile basılır.
349
-
350
- ### `link(props)`
351
-
352
- ```js
353
- link({
354
- href: "/hakkinda",
355
- text: "Hakkında",
356
- class: "font-semibold",
357
- // opsiyonel: html, title, ariaLabel, target, rel, attrs
358
- });
359
- ```
360
-
361
- - `title` verilmezse `ariaLabel` → `text` → `href` sırasıyla otomatik
362
- doldurulur.
363
- - `href` `http://` ya da `https://` ile başlıyorsa `target="_blank"` ve
364
- `rel="noopener noreferrer"` otomatik eklenir; açıkça verirsen senin değerin
365
- kullanılır.
366
- - `html` verilirse içerik ham basılır; `text` verilirse kaçışlanır.
367
- - `attrs` nesnesi ek attribute'ları geçirir ve öncekilerin üzerine biner.
368
-
369
- ### `image(props)`
370
-
371
- ```js
372
- image({
373
- src: "/hero.png",
374
- alt: "Kapak",
375
- priority: true,
376
- // opsiyonel: width, height, class, sizes, srcset, fill, loading,
377
- // unoptimized, attrs
378
- });
379
- ```
380
-
381
- Davranış:
382
-
383
- - `public/` altındaki yerel raster görseller için build'de üretilen webp
384
- varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
385
- `width`/`height` olarak eklenir. Manifest'te olmayan yerel yollar olduğu
386
- gibi basılır.
387
- - `images.remote.allowHosts` açıksa uzak `http(s)` URL'leri
388
- `/_jskelet/image?url=&w=` proxy'sine çevrilir (webp). `width` varsa 1x/2x
389
- + config `widths` ile `srcset` üretilir.
390
- - `srcset` elle verilmişse ya da `unoptimized: true` ise ne manifest ne de
391
- remote proxy kullanılır.
392
- - Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
393
- yazılmaz; gürültüden ibaret olurdu. Remote'da tek genişlikte bile `src`
394
- yine optimize URL'dir.
395
- - `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
396
- genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
397
- (`(max-width: Npx) 100vw, Npx`).
398
- - `priority: true` → `loading="eager"`, `decoding="sync"`,
399
- `fetchpriority="high"`. LCP görseli için.
400
- - `priority` yoksa → `loading="lazy"`, `decoding="async"`.
401
- - `fill: true` → `width`/`height` yazılmaz ve
402
- `absolute inset-0 h-full w-full object-cover` sınıfları `cn()` ile birleştirilir.
403
-
404
- ### `icon(props)`
405
-
406
- Build zamanı üretilen SVG sprite'tan `<use>` çıkarır.
407
-
408
- ```js
409
- icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
410
- // <svg width="20" height="20" class="…" aria-hidden="true" focusable="false"
411
- // fill="currentColor" viewBox="0 0 256 256"><use href="/assets/sprite.<hash>.svg#arrow-right-bold"></use></svg>
412
- ```
413
-
414
- - `name` Phosphor adıdır; `ArrowRightIcon` ve `ArrowRight` biçimleri de kabul
415
- edilir ve `arrow-right`'a çevrilir (`toKebab()`).
416
- - `weight` sprite id'sine dâhildir: `thin`, `light`, `regular` (varsayılan),
417
- `bold`, `fill`, `duotone`.
418
- - `size` varsayılan 24; `width` ve `height` olarak yazılır.
419
- - Development'ta sprite'ta olmayan bir sembol istendiğinde tek seferlik uyarı
420
- basılır. Sprite yalnızca kaynakta **statik olarak** görülen adları içerir; adı
421
- çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
422
- sessizce boşluk kalır ([08-build.md](./08-build.md)).
423
-
424
- ### `preloadImage(props)`
425
-
426
- ```js
427
- preloadImage({ href: "/assets/img/hero-1280.abc.webp", imagesrcset, imagesizes });
428
- // <link rel="preload" as="image" href="…" fetchpriority="high">
429
- ```
430
-
431
- Pratikte doğrudan çağırmak yerine `headHints()` kullanılır:
432
-
433
- ```js
434
- import { headHints } from "jskelet";
435
-
436
- return {
437
- view: "pages/article",
438
- head: headHints({ href: cover, imageSrcSet, imageSizes }),
439
- };
440
- ```
441
-
442
- `headHints()` `href` yoksa boş string döner, yani koşul yazmak gerekmez.
443
- Preconnect'leri layout zaten her sayfaya bastığı için burada tekrarlanmaz.
444
-
445
- ## Metadata → `<head>`
446
-
447
- Controller `metadata` döndürür, framework onu etiketlere çevirir (Next.js'in
448
- Metadata API'sinin karşılığı). Şema bilinçli olarak küçük; daha fazlası
449
- gerekirse `extraTags` ile ham HTML eklenir, böylece framework her yeni meta türü
450
- için sürüm çıkarmak zorunda kalmaz.
451
-
452
- | Alan | Tip | Anlamı |
453
- | --- | --- | --- |
454
- | `title` | `string` | `<title>` |
455
- | `titleTemplate` | `string` | `"%s \| Site"` — `title` buna gömülür. Yalnızca `title` da varsa uygulanır. |
456
- | `description` | `string` | `<meta name="description">` |
457
- | `canonical` | `string` | Mutlak ya da göreli URL |
458
- | `siteUrl` | `string` | Göreli `canonical`ı mutlaklaştırmak için taban |
459
- | `robots` | `{ index?: boolean, follow?: boolean }` | Varsayılan `index, follow` |
460
- | `locale` | `string` | `og:locale` |
461
- | `openGraph` | `{ title, description, url, type, siteName, image, imageWidth, imageHeight }` | `og:*` etiketleri |
462
- | `twitter` | `{ card, site, creator, title, description, image }` | `twitter:*` etiketleri |
463
- | `extraTags` | `string[]` | Olduğu gibi basılacak ham etiketler |
464
-
465
- Üretim kuralları:
466
-
467
- - **Robots varsayılanı indekslenebilir.** Bir sayfayı gizlemek açık bir karar
468
- olmalı: `robots: { index: false }` → `noindex, follow`.
469
- - **OpenGraph `property` kullanır, `name` değil.** Bazı kazıyıcılar `name` ile
470
- yazılmış og etiketlerini görmezden geliyor.
471
- - **Devralma zinciri:** `og:title` yoksa `title`, `og:description` yoksa
472
- `description`, `og:url` yoksa mutlaklaştırılmış `canonical`, `twitter:title`
473
- yoksa `og:title` → `title`, `twitter:image` yoksa `og:image`.
474
- - **`twitter:card`** verilmezse `og:image` varsa `summary_large_image`, yoksa
475
- `summary`.
476
- - **Boş değerler hiç basılmaz:** `null`, `undefined` ve `""` olan alanlar
477
- etiket üretmez.
478
- - `og:type` verilmezse `website`.
479
-
480
- Örnek:
481
-
482
- ```js
483
- return {
484
- view: "pages/article",
485
- metadata: {
486
- title: article.title,
487
- description: article.summary,
488
- canonical: `/haber/${article.slug}`,
489
- openGraph: {
490
- type: "article",
491
- image: article.cover,
492
- imageWidth: 1200,
493
- imageHeight: 630,
494
- },
495
- extraTags: [`<meta property="article:published_time" content="${article.date}">`],
496
- },
497
- };
498
- ```
499
-
500
- `titleTemplate` ve `siteUrl` gibi her sayfada aynı olan alanları
501
- `hooks.metadata()` içine koyun; controller yalnızca sayfaya özel olanı verir.
502
-
503
- `renderHeadMeta(metadata)` fonksiyonu dışa açıktır; layout dışında (ör. bir
504
- fragment ya da e-posta) aynı etiketleri üretmek gerekirse kullanılabilir.
505
-
506
- ## robots.txt
507
-
508
- `robots.txt`'i uygulama yazar: `public/robots.txt` ya da düz bir route.
509
- Framework bu gövdeyi değiştirmez; başarılı metin yanıtının **altına** bir
510
- JSkelet notu ve `Disallow` kuralları ekler. Dosya ya da route yoksa framework
511
- bir `robots.txt` uydurmaz.
512
-
513
- Eklenen yollar:
514
-
515
- - `/_jskelet/` — yönetim paneli, uzak görsel proxy, auth handoff
516
- - `/__jskelet/` — geliştirme araçları
517
- - `/_fragment/` — layout'suz parça yanıtları
518
-
519
- Bu öneklerin dışına taşınmış bir uç da eklenir, ama yalnızca gerçekten
520
- mount edildiyse: `admin.basePath`, `images.remote.path`,
521
- `auth.crossSubdomainHandoff.path`. `brand.devBasePath` yalnızca
522
- development'ta yazılır; production'da o yol uygulamanın kendi sayfası
523
- olabilir.
524
-
525
- Not, config'teki marka adıyla başlar (`brand.name`, varsayılan `JSkelet`).
526
- Alttaki grup `User-agent: *` ile birlikte dosyada adı geçen diğer ajanları
527
- da tekrarlar. Google, belirli bir ajana ait grubu `*` ile birleştirmez;
528
- aynı ajanın ikinci grubunu birleştirir. Not dosyada zaten varsa ikinci kez
529
- eklenmez.
530
-
531
- ## Dinamik OG görselleri
532
-
533
- Next.js `ImageResponse` / `opengraph-image.tsx` karşılığı. JSX yok: kart
534
- alanları (`title`, `description`, `siteName`, renkler) ya da ham `svg` verilir.
535
- `sharp` (opsiyonel peer) kuruluysa PNG, yoksa SVG döner. Sosyal kazıyıcıların
536
- çoğu PNG beklediği için prod'da `sharp` önerilir.
537
-
538
- HTML değil görsel döndüğü için `route()` kullanılmaz — `ogHandler` düz bir
539
- Express handler üretir. `notFound()` ve `null` dönüşü 404 olur.
540
-
541
- ```js
542
- // routes/35-og.mjs
543
- export default function register(app, { ogHandler, notFound }) {
544
- app.get(
545
- "/og/blog/:slug.png",
546
- ogHandler(async ({ params }) => {
547
- const post = getPost(params.slug);
548
- if (!post) notFound();
549
- return {
550
- title: post.title,
551
- description: post.excerpt,
552
- siteName: "Blog",
553
- };
554
- }),
555
- );
556
- }
557
- ```
558
-
559
- Sayfa metadata'sında mutlak URL ve boyut verin:
560
-
561
- ```js
562
- openGraph: {
563
- type: "article",
564
- image: `${SITE_URL}/og/blog/${post.slug}.png`,
565
- imageWidth: 1200,
566
- imageHeight: 630,
567
- },
568
- ```
569
-
570
- Ham SVG veya Next benzeri sınıf:
571
-
572
- ```js
573
- import { ImageResponse, sendOgImage, OG_SIZE } from "jskelet";
574
-
575
- app.get("/og/custom.png", async (req, res) => {
576
- const image = new ImageResponse(
577
- `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630">…</svg>`,
578
- OG_SIZE,
579
- );
580
- await image.send(res);
581
- // veya: await sendOgImage(res, { title: "…", format: "svg" });
582
- });
583
- ```
584
-
585
- Varsayılan `Cache-Control`:
586
- `public, max-age=0, s-maxage=86400, stale-while-revalidate=604800`.
587
- `cacheControl` seçeneğiyle ezilir. Çalışan örnek: `examples/blog/routes/35-og.mjs`.
588
-
589
- ## Hook'lar
590
-
591
- Hook'lar `jskelet.config.mjs` → `hooks` altında tanımlanır. Hepsi opsiyonel,
592
- hepsi `async` olabilir. **Bir hook hata verirse sayfa düşmez:** framework kendi
593
- varsayılanına döner ve uyarır.
594
-
595
- ### `hooks.metadata(page)`
596
-
597
- Her sayfanın metadata varsayılanı. Argüman olarak render edilen sayfa tanımını
598
- alır, bir metadata nesnesi döndürür. Controller'ın `metadata` alanı bunun
599
- **üzerine biner** (alan bazında, sığ birleştirme).
600
-
601
- ```js
602
- hooks: {
603
- metadata() {
604
- return {
605
- titleTemplate: "%s | JSkelet",
606
- description: "JSkelet ile kurulmuş bir site.",
607
- siteUrl: "https://ornek.com",
608
- };
609
- },
610
- }
611
- ```
612
-
613
- ### `hooks.layoutContext({ pathname, metadata })`
614
-
615
- Layout'a her render'da eklenen local'ler. Döndürülen nesnenin **her alanı**
616
- layout local'i olur; ayrıca üç alan özel olarak yorumlanır:
617
-
618
- - `lang` → `<html lang>`
619
- - `structuredData` → JSON-LD script'leri (dizi)
620
- - `extraHead` → `<head>`e eklenir (controller `head`inden sonra)
621
- - `bodyClass` → controller `bodyClass` vermemişse kullanılır
622
-
623
- ```js
624
- hooks: {
625
- async layoutContext({ pathname }) {
626
- return {
627
- bodyClass: "min-h-full",
628
- navigation: await getNavigation(),
629
- isHome: pathname === "/",
630
- };
631
- },
632
- }
633
- ```
634
-
635
- Bu hook gövde render'ıyla **paralel** çalışır; içinde upstream çağırmak sayfaya
636
- sıralı gecikme eklemez.
637
-
638
- ### `hooks.notFound()`
639
-
640
- 404 sayfası tanımı. Döndürdüğü nesne `renderPage`'e `pathname: "/404"` ile
641
- verilir. Ayrıntı: [03-routing.md](./03-routing.md).
642
-
643
- ### Diğer hook'lar
644
-
645
- `hooks.prewarmPaths()` render katmanına değil ısıtmaya aittir; bkz.
646
- [06-cache.md](./06-cache.md).
647
-
648
- ## Overlay portal noktası
649
-
650
- `jskelet/client` → `getOverlayRoot()` modal ve drawer içeriğini taşıyacağı
651
- hedefi verir: layout'ta `<div id="jskelet-overlays"></div>` varsa oraya, yoksa
652
- `body`ye. Portal, `overflow` ya da `transform` taşıyan bir ata elementin
653
- `position: fixed` overlay'i kırpmasını engeller. Modal kullanacaksanız bu div'i
654
- layout'un `<body>` sonuna eklemek yeterli
655
- ([05-islands.md](./05-islands.md)).
656
-
657
- ## Sırada ne var
658
-
659
- - Island'lar ve `entries`: [05-islands.md](./05-islands.md)
660
- - `asset()`, manifest ve Tailwind taraması: [08-build.md](./08-build.md)
661
- - Hook'ların config içindeki yeri: [07-yapilandirma.md](./07-yapilandirma.md)
1
+ # 04 — Render ve şablonlar
2
+
3
+ Bu belge sunucu HTML'inin nasıl üretildiğini anlatır: EJS motorunun ayarları,
4
+ layout dosyasının çözümü ve kullanabildiği local'ler, `views/pages` altındaki
5
+ sayfa şablonları, `views/components/**` altındaki bileşenlerin otomatik kaydı,
6
+ şablonlara hazır gelen `html`/`tags` yardımcıları, `metadata` nesnesinin `<head>`
7
+ etiketlerine çevrilmesi ve üç render hook'u. Controller'ın bu katmana ne
8
+ gönderdiği [03-routing.md](./03-routing.md)'de, varlık URL'lerini üreten
9
+ `asset()`/`hasAsset()` [08-build.md](./08-build.md)'de anlatılıyor.
10
+
11
+ ## Render hattı
12
+
13
+ ```
14
+ route(controller)
15
+ └─ produce()
16
+ ├─ controller(ctx) → sayfa tanımı
17
+ └─ renderPage(page)
18
+ ├─ hooks.metadata(page) + page.metadata → metadata
19
+ ├─ Promise.all([
20
+ │ renderView(page.view, { …data, metadata }), → body
21
+ │ hooks.layoutContext({ pathname, metadata }), → context
22
+ │ ])
23
+ └─ layout (.jsk derlenmiş veya .ejs) → tam HTML
24
+ ```
25
+
26
+ Layout bağlamı ve gövde **paralel** üretilir. Sebebi ölçümden geliyor:
27
+ navigasyon çoğu projede upstream'den geliyor ve gövde render'ıyla sırayla
28
+ beklemek her sayfaya gereksiz gecikme ekliyor.
29
+
30
+ ## `.jsk` — build-time derlenmiş şablonlar
31
+
32
+ Yeni uygulamalarda varsayılan şablon biçimi `.jsk`'dir. Build sırasında
33
+ (`.jskelet/templates/*.mjs`) normal ESM modüllerine çevrilir; **istek anında
34
+ parse / `eval` / `new Function` yoktur**. Production yolu:
35
+
36
+ ```
37
+ controller data → import edilmiş render(data, helpers) → HTML
38
+ ```
39
+
40
+ ### Sözdizimi özeti
41
+
42
+ ```html
43
+ <section class="wrapper">
44
+ <h1>{{ title }}</h1>
45
+ <div>{{{ trustedHtml }}}</div>
46
+
47
+ {#if items.length}
48
+ <List :items="items" />
49
+ {#else}
50
+ <p>Boş</p>
51
+ {/if}
52
+
53
+ {#each items as item, i}
54
+ <li :data-i="i">{{ item }}</li>
55
+ {/each}
56
+
57
+ <Link href="/" text="Home" />
58
+ <div data-island="counter" data-island-props='{"start":0}'></div>
59
+ </section>
60
+ ```
61
+
62
+ | Özellik | Yazım |
63
+ | --- | --- |
64
+ | Kaçışlı metin | `{{ expr }}` |
65
+ | Ham HTML | `{{{ expr }}}` |
66
+ | Koşul | `{#if expr}` … `{#else}` … `{/if}` |
67
+ | Döngü | `{#each list as item}` veya `as item, i` |
68
+ | Include | `{#include "partials/header"}` (derlenmiş `.jsk`) |
69
+ | Bileşen | PascalCase etiket; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
70
+ | Yerleşikler | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
71
+
72
+ İfade dili kasıtlı olarak dardır (erişim, karşılaştırma, ternary, `.length`).
73
+ Atama, object literal ve rastgele fonksiyon çağrısı yok — mantık controller
74
+ veya JS bileşende kalır.
75
+
76
+ #### Şablon mu, bileşen mi?
77
+
78
+ EJS’den geçerken sınırı erken çizmek işe yarar:
79
+
80
+ | Burada kalsın (`.jsk`) | JS bileşene taşı |
81
+ | --- | --- |
82
+ | Metin, koşul, liste, prop bağlama | Fonksiyon çağrısı, nesne üretimi, biçimlendirme |
83
+ | Yerleşik etiketler (`Link`, `Image`, …) | Birden fazla yardımcıdan HTML birleştirme |
84
+ | Controller’dan gelen hazır veri | Upstream / hata ayırt eden UI (`LoadErrorState`) |
85
+
86
+ Şablonda `format(x)` veya `{ a: 1 }` yazılamıyorsa bu bir eksik değil: o iş
87
+ `views/components/*.js` veya controller’ındır. Karmaşık sayfalar bileşene
88
+ kaçıyorsa ifade dilini genişletmek yerine bileşen sınırını net tutmak tercih
89
+ edilir.
90
+
91
+ ### Editör desteği
92
+
93
+ Repo içinde `extensions/vscode-jsk` VS Code / Cursor uzantısı vardır: sözdizimi
94
+ renklendirme, dil yapılandırması ve snippet'ler. Yerel kurulum:
95
+
96
+ ```bash
97
+ code --install-extension extensions/vscode-jsk
98
+ ```
99
+
100
+ Ayrıntılar uzantı README'sinde.
101
+
102
+ ### Yerleşik layout etiketleri
103
+
104
+ `.jsk` ifade dilinde `asset()` / `hasAsset()` çağrılamaz. Layout’ta stylesheet,
105
+ script ve JSON-LD döngüleri için yerleşikler:
106
+
107
+ | Etiket | Props | Çıktı |
108
+ | --- | --- | --- |
109
+ | `Stylesheets` | `styles` | `app.css` + sayfa sheet’leri (`data-jskelet-css`) |
110
+ | `BodyScripts` | `entries`, `devtools`, `devBasePath` | `main.js`, entry’ler, isteğe bağlı overlay |
111
+ | `JsonLd` | `items` (`structuredData`) | `application/ld+json` script’leri |
112
+
113
+ ### EJS ile birlikte yaşam
114
+
115
+ Aynı `view` id için derlenmiş `.jsk` varsa o kullanılır; yoksa `.ejs` dosyası
116
+ **yalnızca `ejs` peer’i kuruluysa** render edilir. `jskelet init` yeni iskeleti
117
+ `.jsk` ile kurar.
118
+
119
+ ## EJS motoru (legacy peer)
120
+
121
+ EJS opsiyonel peer bağımlılıktır (`npm i ejs`). `.jsk`-only uygulamalar kurmak
122
+ zorunda değildir. Bir `.ejs` view veya layout istendiğinde paket uygulamadan
123
+ yüklenir; yoksa göç yolunu gösteren bir hata fırlatılır.
124
+
125
+ Motor ilk EJS render’da bir kez kurulur. Ayarlar:
126
+
127
+ | Ayar | Değer | Sebebi |
128
+ | --- | --- | --- |
129
+ | `root`, `views` | `views` dizini | `include('partials/header')` çağrıları views kökünden çözülür |
130
+ | `cache` | dev'de `false`, prod'da `true` | dev'de şablon düzenlemesi anında görünsün |
131
+ | `rmWhitespace` | `true` | çıktı boyutu |
132
+ | `async` | `true` | şablon içinde `await` kullanılabilir |
133
+
134
+ Gömülü kullanımlar (test, script) için `resetRenderEngine()` dışa açık.
135
+
136
+ ## Layout
137
+
138
+ ### Layout dosyası nasıl bulunur
139
+
140
+ 1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
141
+ dizininin üst dizinine** göre çözülür: `views` varsayılansa
142
+ `layout: "views/ozel.jsk"` → `<root>/views/ozel.jsk`.
143
+ 2. Verilmemişse `views/layout.jsk` (derlenmiş) varsa o kullanılır.
144
+ 3. Yoksa `views/layout.ejs` varsa o kullanılır (EJS peer gerekir).
145
+ 4. O da yoksa framework'ün kendi minimal layout'u kullanılır
146
+ (`node_modules/jskelet/src/templates/layout.jsk`, `jskelet/layout`;
147
+ legacy kopya `jskelet/layout/ejs`).
148
+
149
+ Üçüncü seçenek yeni bir projenin tek route ile çalışabilmesi için var. Kendi
150
+ layout'unuza geçmenin en pratik yolu o dosyayı `views/layout.jsk` olarak
151
+ kopyalamaktır.
152
+
153
+ ### Framework'ün varsayılan layout'u
154
+
155
+ ```jsk
156
+ <!DOCTYPE html>
157
+ <html :lang="lang">
158
+ <head>
159
+ <meta charset="utf-8">
160
+ <meta name="viewport" content="width=device-width, initial-scale=1">
161
+ {{{ extraHead }}}
162
+ <Stylesheets :styles="styles" />
163
+ {{{ headMeta }}}
164
+ <JsonLd :items="structuredData" />
165
+ </head>
166
+ <body :class="bodyClass">
167
+ {{{ body }}}
168
+ <BodyScripts :entries="entries" :devtools="devtools" :devBasePath="devBasePath" />
169
+ </body>
170
+ </html>
171
+ ```
172
+
173
+ Dikkat edilecek noktalar:
174
+
175
+ - **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
176
+ geciktirmek doğrudan LCP'ye yazılır.
177
+ - **Global `app.css` render-blocking** ve gerekçesi
178
+ [02-mimari.md](./02-mimari.md)'de. Controller `styles: [...]` ile ek sayfa
179
+ sheet'leri de aynı şekilde basılır. Build çalışmadıysa `hasAsset` false olur
180
+ ve etiket hiç basılmaz.
181
+ - **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
182
+ istememesini sağlar (`Stylesheets` / `BodyScripts` içinde).
183
+ - **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
184
+ çıktısında hiç yoktur.
185
+
186
+ ### Layout local'leri
187
+
188
+ | Local | Tip | Kaynağı |
189
+ | --- | --- | --- |
190
+ | `metadata` | `object` | `hooks.metadata()` + controller `metadata` (controller kazanır) |
191
+ | `headMeta` | `string` | `metadata`dan üretilmiş hazır `<head>` etiketleri |
192
+ | `extraHead` | `string` | `preconnect` ipuçları + `navigation` ipuçları + controller `head` + `context.extraHead` |
193
+ | `structuredData` | `unknown[]` | `hooks.layoutContext()` → `structuredData`; varsayılan `[]` |
194
+ | `body` | `string` | Sayfa şablonunun render çıktısı |
195
+ | `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
196
+ | `entries` | `string[]` | controller `entries`; varsayılan `[]` |
197
+ | `styles` | `string[]` | controller `styles`; varsayılan `[]` |
198
+ | `pathname` | `string` | `req.path`; **varsayılan boş string** |
199
+ | `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
200
+ | `devtools` | `boolean` | `NODE_ENV === "development"` |
201
+ | `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
202
+ | `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
203
+ | html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
204
+ | `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
205
+ | `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
206
+
207
+ `pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
208
+ sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
209
+
210
+ ## Sayfa şablonları
211
+
212
+ `view` alanı `views/` altındaki yolu uzantısız verir: `"pages/home"` →
213
+ `views/pages/home.ejs`. Şablona geçen local'ler `data` alanının içeriği artı
214
+ `metadata`dır — layout local'leri **değil**. Sayfa şablonu yine tüm yardımcılara
215
+ ve bileşenlere erişir.
216
+
217
+ ```ejs
218
+ <%# views/pages/home.ejs %>
219
+ <section class="wrapper">
220
+ <h1 class="text-3xl font-bold"><%= heading %></h1>
221
+
222
+ <%# `list` views/components/list.js içinde tanımlı; import gerekmiyor. %>
223
+ <%- list({ items }) %>
224
+
225
+ <div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
226
+ </section>
227
+ ```
228
+
229
+ EJS'te iki çıktı biçimini karıştırmayın:
230
+
231
+ - `<%= value %>` — HTML kaçışlı. Kullanıcı/upstream verisi için **daima** bu.
232
+ - `<%- html %>` — ham. Yalnızca kendi ürettiğin, güvenli HTML string'leri için
233
+ (bileşen çağrıları, `headMeta`, `body`).
234
+
235
+ `async: true` açık olduğu için şablon içinde `await` da kullanılabilir, ancak
236
+ veri çekmeyi controller'da tutmak teşhisi kolaylaştırır.
237
+
238
+ ## Bileşenler: `views/components/**`
239
+
240
+ Bileşenler EJS partial'ı değil, **HTML string döndüren fonksiyonlardır**.
241
+ `views/components/**` altındaki her `.js` dosyası taranır ve **her named export**
242
+ şablon local'i olur. Elle bakılan bir barrel dosyası yok: yeni bir bileşen
243
+ eklemek için dosyayı oluşturmak yeterli.
244
+
245
+ ```js
246
+ // views/components/list.js
247
+ import { esc } from "jskelet/html";
248
+
249
+ /**
250
+ * @param {{ items: string[] }} props
251
+ * @returns {string}
252
+ */
253
+ export function list({ items }) {
254
+ if (!items?.length) return "";
255
+
256
+ const rows = items.map((item) => `<li class="py-1">${esc(item)}</li>`).join("");
257
+ return `<ul class="mt-6 list-disc pl-6">${rows}</ul>`;
258
+ }
259
+ ```
260
+
261
+ Şablonda:
262
+
263
+ ```ejs
264
+ <%- list({ items }) %>
265
+ ```
266
+
267
+ Kurallar:
268
+
269
+ - Tarama özyinelemelidir; alt dizinler de kapsanır.
270
+ - `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
271
+ - Compile-time bilinen bileşen listesi **dosya adından değil**, kaynak
272
+ metindeki named export'lardan okunur. `ui.js` içindeki `sectionHead` →
273
+ şablonda `<SectionHead />` (runtime zaten camelCase export'a PascalCase
274
+ alias ekler). Dosya adına göre stub re-export eklemeye gerek yoktur.
275
+ - `loader.js` ve `index.js` bileşen dosyası sayılmaz.
276
+ - `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
277
+ önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
278
+ bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
279
+ - Aynı ad (veya aynı PascalCase etiket) iki farklı bileşen dosyasında
280
+ tanımlıysa **uyarı değil hata**: build ve sunucu açılışı
281
+ `Component 'card' is defined twice: …` ile durur. Barrel üzerine yazmak
282
+ bilinçli istisnadır.
283
+ - `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
284
+ bir proje de çalışır.
285
+
286
+ ## Yardımcılar: `jskelet/html`
287
+
288
+ Şablonlara otomatik geçer; bileşen dosyalarında `import { … } from "jskelet/html"`
289
+ ile alınır.
290
+
291
+ ### `esc(value)`
292
+
293
+ Metin içeriği ve attribute değerleri için kaçış (`&`, `<`, `>`, `"`, `'`).
294
+ `null`, `undefined` ve `false` boş string'e çevrilir — koşullu render'da
295
+ `false && "…"` gibi ifadeler `"false"` basmaz.
296
+
297
+ ```js
298
+ esc('<b>"x"</b>'); // "&lt;b&gt;&quot;x&quot;&lt;/b&gt;"
299
+ ```
300
+
301
+ ### `attrs(object)`
302
+
303
+ Attribute nesnesini string'e çevirir. `null`/`undefined`/`false` atlanır, `true`
304
+ boolean attribute olarak yazılır, geri kalan değerler kaçışlanır. Çıktı boş
305
+ değilse **başında bir boşluk** ile döner, böylece `<div${attrs(...)}>` her zaman
306
+ doğru biçimlenir.
307
+
308
+ ```js
309
+ `<input${attrs({ type: "text", required: true, value: null })}>`;
310
+ // '<input type="text" required>'
311
+ ```
312
+
313
+ ### `cx(...inputs)`
314
+
315
+ `clsx` karşılığı: string, sayı, dizi ve `{ sınıf: koşul }` nesnesi kabul eder,
316
+ falsy değerleri atar. Tailwind çakışması **çözmez**.
317
+
318
+ ```js
319
+ cx("btn", isActive && "btn-active", { "btn-lg": size === "lg" });
320
+ ```
321
+
322
+ ### `cn(...inputs)`
323
+
324
+ `cx()` ile birleştirir, sonra `tailwind-merge` ile Tailwind çakışmalarını çözer.
325
+ Bir bileşenin varsayılan sınıflarının çağıran tarafından ezilmesi gerektiğinde
326
+ bunu kullanın.
327
+
328
+ ```js
329
+ cn("px-4 py-2 bg-slate-100", className); // className "bg-white" ise bg-slate-100 düşer
330
+ ```
331
+
332
+ `tailwind-merge` çalışma zamanı bağımlılığı olarak korunur çünkü sınıf hesabı
333
+ yalnızca sunucuda yapılır; client bundle'a hiç girmez.
334
+
335
+ ### `jsonScript(value)`
336
+
337
+ `<script type="application/ld+json">` gövdesi için güvenli JSON: `<`, `>`, `&`
338
+ ve U+2028/U+2029 kaçırılır, böylece `</script` ya da `<!--` dizileri gövdeyi
339
+ kapatamaz.
340
+
341
+ ```ejs
342
+ <script type="application/ld+json"><%- jsonScript(article) %></script>
343
+ ```
344
+
345
+ ## Yardımcılar: `jskelet/tags`
346
+
347
+ `next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları. Hepsi HTML
348
+ string döndürür ve EJS içinden `<%- %>` ile basılır.
349
+
350
+ ### `link(props)`
351
+
352
+ ```js
353
+ link({
354
+ href: "/hakkinda",
355
+ text: "Hakkında",
356
+ class: "font-semibold",
357
+ // opsiyonel: html, title, ariaLabel, target, rel, attrs
358
+ });
359
+ ```
360
+
361
+ - `title` verilmezse `ariaLabel` → `text` → `href` sırasıyla otomatik
362
+ doldurulur.
363
+ - `href` `http://` ya da `https://` ile başlıyorsa `target="_blank"` ve
364
+ `rel="noopener noreferrer"` otomatik eklenir; açıkça verirsen senin değerin
365
+ kullanılır.
366
+ - `html` verilirse içerik ham basılır; `text` verilirse kaçışlanır.
367
+ - `attrs` nesnesi ek attribute'ları geçirir ve öncekilerin üzerine biner.
368
+
369
+ ### `image(props)`
370
+
371
+ ```js
372
+ image({
373
+ src: "/hero.png",
374
+ alt: "Kapak",
375
+ priority: true,
376
+ // opsiyonel: width, height, class, sizes, srcset, fill, loading,
377
+ // unoptimized, attrs
378
+ });
379
+ ```
380
+
381
+ Davranış:
382
+
383
+ - `public/` altındaki yerel raster görseller için build'de üretilen webp
384
+ varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
385
+ `width`/`height` olarak eklenir. Manifest'te olmayan yerel yollar olduğu
386
+ gibi basılır.
387
+ - `images.remote.allowHosts` açıksa uzak `http(s)` URL'leri
388
+ `/_jskelet/image?url=&w=` proxy'sine çevrilir (webp). `width` varsa 1x/2x
389
+ + config `widths` ile `srcset` üretilir.
390
+ - `srcset` elle verilmişse ya da `unoptimized: true` ise ne manifest ne de
391
+ remote proxy kullanılır.
392
+ - Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
393
+ yazılmaz; gürültüden ibaret olurdu. Remote'da tek genişlikte bile `src`
394
+ yine optimize URL'dir.
395
+ - `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
396
+ genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
397
+ (`(max-width: Npx) 100vw, Npx`).
398
+ - `priority: true` → `loading="eager"`, `decoding="sync"`,
399
+ `fetchpriority="high"`. LCP görseli için.
400
+ - `priority` yoksa → `loading="lazy"`, `decoding="async"`.
401
+ - `fill: true` → `width`/`height` yazılmaz ve
402
+ `absolute inset-0 h-full w-full object-cover` sınıfları `cn()` ile birleştirilir.
403
+
404
+ ### `icon(props)`
405
+
406
+ Build zamanı üretilen SVG sprite'tan `<use>` çıkarır.
407
+
408
+ ```js
409
+ icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
410
+ // <svg width="20" height="20" class="…" aria-hidden="true" focusable="false"
411
+ // fill="currentColor" viewBox="0 0 256 256"><use href="/assets/sprite.<hash>.svg#arrow-right-bold"></use></svg>
412
+ ```
413
+
414
+ - `name` Phosphor adıdır; `ArrowRightIcon` ve `ArrowRight` biçimleri de kabul
415
+ edilir ve `arrow-right`'a çevrilir (`toKebab()`).
416
+ - `weight` sprite id'sine dâhildir: `thin`, `light`, `regular` (varsayılan),
417
+ `bold`, `fill`, `duotone`.
418
+ - `size` varsayılan 24; `width` ve `height` olarak yazılır.
419
+ - Development'ta sprite'ta olmayan bir sembol istendiğinde tek seferlik uyarı
420
+ basılır. Sprite yalnızca kaynakta **statik olarak** görülen adları içerir; adı
421
+ çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
422
+ sessizce boşluk kalır ([08-build.md](./08-build.md)).
423
+
424
+ ### `preloadImage(props)`
425
+
426
+ ```js
427
+ preloadImage({ href: "/assets/img/hero-1280.abc.webp", imagesrcset, imagesizes });
428
+ // <link rel="preload" as="image" href="…" fetchpriority="high">
429
+ ```
430
+
431
+ Pratikte doğrudan çağırmak yerine `headHints()` kullanılır:
432
+
433
+ ```js
434
+ import { headHints } from "jskelet";
435
+
436
+ return {
437
+ view: "pages/article",
438
+ head: headHints({ href: cover, imageSrcSet, imageSizes }),
439
+ };
440
+ ```
441
+
442
+ `headHints()` `href` yoksa boş string döner, yani koşul yazmak gerekmez.
443
+ Preconnect'leri layout zaten her sayfaya bastığı için burada tekrarlanmaz.
444
+
445
+ ## Metadata → `<head>`
446
+
447
+ Controller `metadata` döndürür, framework onu etiketlere çevirir (Next.js'in
448
+ Metadata API'sinin karşılığı). Şema bilinçli olarak küçük; daha fazlası
449
+ gerekirse `extraTags` ile ham HTML eklenir, böylece framework her yeni meta türü
450
+ için sürüm çıkarmak zorunda kalmaz.
451
+
452
+ | Alan | Tip | Anlamı |
453
+ | --- | --- | --- |
454
+ | `title` | `string` | `<title>` |
455
+ | `titleTemplate` | `string` | `"%s \| Site"` — `title` buna gömülür. Yalnızca `title` da varsa uygulanır. |
456
+ | `description` | `string` | `<meta name="description">` |
457
+ | `canonical` | `string` | Mutlak ya da göreli URL |
458
+ | `siteUrl` | `string` | Göreli `canonical`ı mutlaklaştırmak için taban |
459
+ | `robots` | `{ index?: boolean, follow?: boolean }` | Varsayılan `index, follow` |
460
+ | `locale` | `string` | `og:locale` |
461
+ | `openGraph` | `{ title, description, url, type, siteName, image, imageWidth, imageHeight }` | `og:*` etiketleri |
462
+ | `twitter` | `{ card, site, creator, title, description, image }` | `twitter:*` etiketleri |
463
+ | `extraTags` | `string[]` | Olduğu gibi basılacak ham etiketler |
464
+
465
+ Üretim kuralları:
466
+
467
+ - **Robots varsayılanı indekslenebilir.** Bir sayfayı gizlemek açık bir karar
468
+ olmalı: `robots: { index: false }` → `noindex, follow`.
469
+ - **OpenGraph `property` kullanır, `name` değil.** Bazı kazıyıcılar `name` ile
470
+ yazılmış og etiketlerini görmezden geliyor.
471
+ - **Devralma zinciri:** `og:title` yoksa `title`, `og:description` yoksa
472
+ `description`, `og:url` yoksa mutlaklaştırılmış `canonical`, `twitter:title`
473
+ yoksa `og:title` → `title`, `twitter:image` yoksa `og:image`.
474
+ - **`twitter:card`** verilmezse `og:image` varsa `summary_large_image`, yoksa
475
+ `summary`.
476
+ - **Boş değerler hiç basılmaz:** `null`, `undefined` ve `""` olan alanlar
477
+ etiket üretmez.
478
+ - `og:type` verilmezse `website`.
479
+
480
+ Örnek:
481
+
482
+ ```js
483
+ return {
484
+ view: "pages/article",
485
+ metadata: {
486
+ title: article.title,
487
+ description: article.summary,
488
+ canonical: `/haber/${article.slug}`,
489
+ openGraph: {
490
+ type: "article",
491
+ image: article.cover,
492
+ imageWidth: 1200,
493
+ imageHeight: 630,
494
+ },
495
+ extraTags: [`<meta property="article:published_time" content="${article.date}">`],
496
+ },
497
+ };
498
+ ```
499
+
500
+ `titleTemplate` ve `siteUrl` gibi her sayfada aynı olan alanları
501
+ `hooks.metadata()` içine koyun; controller yalnızca sayfaya özel olanı verir.
502
+
503
+ `renderHeadMeta(metadata)` fonksiyonu dışa açıktır; layout dışında (ör. bir
504
+ fragment ya da e-posta) aynı etiketleri üretmek gerekirse kullanılabilir.
505
+
506
+ ## robots.txt
507
+
508
+ `robots.txt`'i uygulama yazar: `public/robots.txt` ya da düz bir route.
509
+ Framework bu gövdeyi değiştirmez; başarılı metin yanıtının **altına** bir
510
+ JSkelet notu ve `Disallow` kuralları ekler. Dosya ya da route yoksa framework
511
+ bir `robots.txt` uydurmaz.
512
+
513
+ Eklenen yollar:
514
+
515
+ - `/_jskelet/` — yönetim paneli, uzak görsel proxy, auth handoff
516
+ - `/__jskelet/` — geliştirme araçları
517
+ - `/_fragment/` — layout'suz parça yanıtları
518
+
519
+ Bu öneklerin dışına taşınmış bir uç da eklenir, ama yalnızca gerçekten
520
+ mount edildiyse: `admin.basePath`, `images.remote.path`,
521
+ `auth.crossSubdomainHandoff.path`. `brand.devBasePath` yalnızca
522
+ development'ta yazılır; production'da o yol uygulamanın kendi sayfası
523
+ olabilir.
524
+
525
+ Not, config'teki marka adıyla başlar (`brand.name`, varsayılan `JSkelet`).
526
+ Alttaki grup `User-agent: *` ile birlikte dosyada adı geçen diğer ajanları
527
+ da tekrarlar. Google, belirli bir ajana ait grubu `*` ile birleştirmez;
528
+ aynı ajanın ikinci grubunu birleştirir. Not dosyada zaten varsa ikinci kez
529
+ eklenmez.
530
+
531
+ ## Dinamik OG görselleri
532
+
533
+ Next.js `ImageResponse` / `opengraph-image.tsx` karşılığı. JSX yok: kart
534
+ alanları (`title`, `description`, `siteName`, renkler) ya da ham `svg` verilir.
535
+ `sharp` (opsiyonel peer) kuruluysa PNG, yoksa SVG döner. Sosyal kazıyıcıların
536
+ çoğu PNG beklediği için prod'da `sharp` önerilir.
537
+
538
+ HTML değil görsel döndüğü için `route()` kullanılmaz — `ogHandler` düz bir
539
+ Express handler üretir. `notFound()` ve `null` dönüşü 404 olur.
540
+
541
+ ```js
542
+ // routes/35-og.mjs
543
+ export default function register(app, { ogHandler, notFound }) {
544
+ app.get(
545
+ "/og/blog/:slug.png",
546
+ ogHandler(async ({ params }) => {
547
+ const post = getPost(params.slug);
548
+ if (!post) notFound();
549
+ return {
550
+ title: post.title,
551
+ description: post.excerpt,
552
+ siteName: "Blog",
553
+ };
554
+ }),
555
+ );
556
+ }
557
+ ```
558
+
559
+ Sayfa metadata'sında mutlak URL ve boyut verin:
560
+
561
+ ```js
562
+ openGraph: {
563
+ type: "article",
564
+ image: `${SITE_URL}/og/blog/${post.slug}.png`,
565
+ imageWidth: 1200,
566
+ imageHeight: 630,
567
+ },
568
+ ```
569
+
570
+ Ham SVG veya Next benzeri sınıf:
571
+
572
+ ```js
573
+ import { ImageResponse, sendOgImage, OG_SIZE } from "jskelet";
574
+
575
+ app.get("/og/custom.png", async (req, res) => {
576
+ const image = new ImageResponse(
577
+ `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630">…</svg>`,
578
+ OG_SIZE,
579
+ );
580
+ await image.send(res);
581
+ // veya: await sendOgImage(res, { title: "…", format: "svg" });
582
+ });
583
+ ```
584
+
585
+ Varsayılan başlıklar (süreler HTML ayarına bağlı değildir):
586
+
587
+ ```
588
+ Cache-Control: public, max-age=0
589
+ CDN-Cache-Control: max-age=86400, stale-while-revalidate=604800
590
+ ```
591
+
592
+ `cacheControl` seçeneği `Cache-Control`'ü ezer; bu durumda `CDN-Cache-Control`
593
+ yazılmaz. Çalışan örnek: `examples/blog/routes/35-og.mjs`.
594
+
595
+ ## Hook'lar
596
+
597
+ Hook'lar `jskelet.config.mjs` → `hooks` altında tanımlanır. Hepsi opsiyonel,
598
+ hepsi `async` olabilir. **Bir hook hata verirse sayfa düşmez:** framework kendi
599
+ varsayılanına döner ve uyarır.
600
+
601
+ ### `hooks.metadata(page)`
602
+
603
+ Her sayfanın metadata varsayılanı. Argüman olarak render edilen sayfa tanımını
604
+ alır, bir metadata nesnesi döndürür. Controller'ın `metadata` alanı bunun
605
+ **üzerine biner** (alan bazında, sığ birleştirme).
606
+
607
+ ```js
608
+ hooks: {
609
+ metadata() {
610
+ return {
611
+ titleTemplate: "%s | JSkelet",
612
+ description: "JSkelet ile kurulmuş bir site.",
613
+ siteUrl: "https://ornek.com",
614
+ };
615
+ },
616
+ }
617
+ ```
618
+
619
+ ### `hooks.layoutContext({ pathname, metadata })`
620
+
621
+ Layout'a her render'da eklenen local'ler. Döndürülen nesnenin **her alanı**
622
+ layout local'i olur; ayrıca üç alan özel olarak yorumlanır:
623
+
624
+ - `lang` → `<html lang>`
625
+ - `structuredData` → JSON-LD script'leri (dizi)
626
+ - `extraHead` → `<head>`e eklenir (controller `head`inden sonra)
627
+ - `bodyClass` → controller `bodyClass` vermemişse kullanılır
628
+
629
+ ```js
630
+ hooks: {
631
+ async layoutContext({ pathname }) {
632
+ return {
633
+ bodyClass: "min-h-full",
634
+ navigation: await getNavigation(),
635
+ isHome: pathname === "/",
636
+ };
637
+ },
638
+ }
639
+ ```
640
+
641
+ Bu hook gövde render'ıyla **paralel** çalışır; içinde upstream çağırmak sayfaya
642
+ sıralı gecikme eklemez.
643
+
644
+ ### `hooks.notFound()`
645
+
646
+ 404 sayfası tanımı. Döndürdüğü nesne `renderPage`'e `pathname: "/404"` ile
647
+ verilir. Ayrıntı: [03-routing.md](./03-routing.md).
648
+
649
+ ### Diğer hook'lar
650
+
651
+ `hooks.prewarmPaths()` render katmanına değil ısıtmaya aittir; bkz.
652
+ [06-cache.md](./06-cache.md).
653
+
654
+ ## Overlay portal noktası
655
+
656
+ `jskelet/client` → `getOverlayRoot()` modal ve drawer içeriğini taşıyacağı
657
+ hedefi verir: layout'ta `<div id="jskelet-overlays"></div>` varsa oraya, yoksa
658
+ `body`ye. Portal, `overflow` ya da `transform` taşıyan bir ata elementin
659
+ `position: fixed` overlay'i kırpmasını engeller. Modal kullanacaksanız bu div'i
660
+ layout'un `<body>` sonuna eklemek yeterli
661
+ ([05-islands.md](./05-islands.md)).
662
+
663
+ ## Sırada ne var
664
+
665
+ - Island'lar ve `entries`: [05-islands.md](./05-islands.md)
666
+ - `asset()`, manifest ve Tailwind taraması: [08-build.md](./08-build.md)
667
+ - Hook'ların config içindeki yeri: [07-yapilandirma.md](./07-yapilandirma.md)