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,661 +1,661 @@
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 `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)