jskelet 0.2.5 → 0.3.0

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 (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +293 -287
  7. package/docs/03-routing.md +486 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
@@ -1,490 +1,490 @@
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.ejs render → 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
- ## EJS motoru
31
-
32
- Motor ilk render'da bir kez kurulur; bileşen taraması dosya sistemine
33
- dokunduğu için her istekte yapılamaz ve config yüklenmeden hesaplanamaz.
34
-
35
- Ayarlar:
36
-
37
- | Ayar | Değer | Sebebi |
38
- | --- | --- | --- |
39
- | `root`, `views` | `views` dizini | `include('partials/header')` çağrıları views kökünden çözülür |
40
- | `cache` | dev'de `false`, prod'da `true` | dev'de şablon düzenlemesi anında görünsün |
41
- | `rmWhitespace` | `true` | çıktı boyutu |
42
- | `async` | `true` | şablon içinde `await` kullanılabilir |
43
-
44
- Gömülü kullanımlar (test, script) için `resetRenderEngine()` dışa açık: bileşen
45
- dosyaları değişince kaydı yeniler. Dev sunucusu süreci yeniden başlattığı için
46
- normal akışta gerekmez.
47
-
48
- ## Layout
49
-
50
- ### Layout dosyası nasıl bulunur
51
-
52
- 1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
53
- dizininin üst dizinine** göre çözülür: `views` varsayılansa
54
- `layout: "views/ozel.ejs"` → `<root>/views/ozel.ejs`.
55
- 2. Verilmemişse `views/layout.ejs` varsa o kullanılır.
56
- 3. O da yoksa framework'ün kendi minimal layout'u kullanılır
57
- (`node_modules/jskelet/src/templates/layout.ejs`, ayrıca
58
- `jskelet/layout` belirteciyle de erişilebilir).
59
-
60
- Üçüncü seçenek yeni bir projenin tek route ile çalışabilmesi için var. Kendi
61
- layout'unuza geçmenin en pratik yolu o dosyayı `views/layout.ejs` olarak
62
- kopyalamaktır.
63
-
64
- ### Framework'ün varsayılan layout'u
65
-
66
- ```ejs
67
- <!DOCTYPE html>
68
- <html lang="<%= lang %>">
69
- <head>
70
- <meta charset="utf-8">
71
- <meta name="viewport" content="width=device-width, initial-scale=1">
72
- <%- extraHead %>
73
- <% if (hasAsset('app.css')) { %>
74
- <link rel="stylesheet" href="<%= asset('app.css') %>">
75
- <% } %>
76
- <%- headMeta %>
77
- <% structuredData.forEach(function (item) { %>
78
- <script type="application/ld+json"><%- jsonScript(item) %></script>
79
- <% }); %>
80
- </head>
81
- <body class="<%= bodyClass %>">
82
- <%- body %>
83
- <% if (hasAsset('main.js')) { %>
84
- <script type="module" src="<%= asset('main.js') %>"></script>
85
- <% } %>
86
- <% entries.forEach(function (entry) { %>
87
- <script type="module" src="<%= asset(entry) %>"></script>
88
- <% }); %>
89
- <% if (devtools) { %>
90
- <script type="module" src="<%= devBasePath %>/overlay.js"></script>
91
- <% } %>
92
- </body>
93
- </html>
94
- ```
95
-
96
- Dikkat edilecek noktalar:
97
-
98
- - **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
99
- geciktirmek doğrudan LCP'ye yazılır.
100
- - **Tek, render-blocking stylesheet** ve gerekçesi
101
- [02-mimari.md](./02-mimari.md)'de. Build çalışmadıysa `hasAsset('app.css')`
102
- false olur ve etiket hiç basılmaz.
103
- - **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
104
- istememesini sağlar.
105
- - **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
106
- çıktısında hiç yoktur.
107
-
108
- ### Layout local'leri
109
-
110
- | Local | Tip | Kaynağı |
111
- | --- | --- | --- |
112
- | `metadata` | `object` | `hooks.metadata()` + controller `metadata` (controller kazanır) |
113
- | `headMeta` | `string` | `metadata`dan üretilmiş hazır `<head>` etiketleri |
114
- | `extraHead` | `string` | `preconnect` ipuçları + `navigation` ipuçları + controller `head` + `context.extraHead` |
115
- | `structuredData` | `unknown[]` | `hooks.layoutContext()` → `structuredData`; varsayılan `[]` |
116
- | `body` | `string` | Sayfa şablonunun render çıktısı |
117
- | `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
118
- | `entries` | `string[]` | controller `entries`; varsayılan `[]` |
119
- | `pathname` | `string` | `req.path`; **varsayılan boş string** |
120
- | `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
121
- | `devtools` | `boolean` | `NODE_ENV === "development"` |
122
- | `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
123
- | `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
124
- | html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
125
- | `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
126
- | `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
127
-
128
- `pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
129
- sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
130
-
131
- ## Sayfa şablonları
132
-
133
- `view` alanı `views/` altındaki yolu uzantısız verir: `"pages/home"` →
134
- `views/pages/home.ejs`. Şablona geçen local'ler `data` alanının içeriği artı
135
- `metadata`dır — layout local'leri **değil**. Sayfa şablonu yine tüm yardımcılara
136
- ve bileşenlere erişir.
137
-
138
- ```ejs
139
- <%# views/pages/home.ejs %>
140
- <section class="wrapper">
141
- <h1 class="text-3xl font-bold"><%= heading %></h1>
142
-
143
- <%# `list` views/components/list.js içinde tanımlı; import gerekmiyor. %>
144
- <%- list({ items }) %>
145
-
146
- <div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
147
- </section>
148
- ```
149
-
150
- EJS'te iki çıktı biçimini karıştırmayın:
151
-
152
- - `<%= value %>` — HTML kaçışlı. Kullanıcı/upstream verisi için **daima** bu.
153
- - `<%- html %>` — ham. Yalnızca kendi ürettiğin, güvenli HTML string'leri için
154
- (bileşen çağrıları, `headMeta`, `body`).
155
-
156
- `async: true` açık olduğu için şablon içinde `await` da kullanılabilir, ancak
157
- veri çekmeyi controller'da tutmak teşhisi kolaylaştırır.
158
-
159
- ## Bileşenler: `views/components/**`
160
-
161
- Bileşenler EJS partial'ı değil, **HTML string döndüren fonksiyonlardır**.
162
- `views/components/**` altındaki her `.js` dosyası taranır ve **her named export**
163
- şablon local'i olur. Elle bakılan bir barrel dosyası yok: yeni bir bileşen
164
- eklemek için dosyayı oluşturmak yeterli.
165
-
166
- ```js
167
- // views/components/list.js
168
- import { esc } from "jskelet/html";
169
-
170
- /**
171
- * @param {{ items: string[] }} props
172
- * @returns {string}
173
- */
174
- export function list({ items }) {
175
- if (!items?.length) return "";
176
-
177
- const rows = items.map((item) => `<li class="py-1">${esc(item)}</li>`).join("");
178
- return `<ul class="mt-6 list-disc pl-6">${rows}</ul>`;
179
- }
180
- ```
181
-
182
- Şablonda:
183
-
184
- ```ejs
185
- <%- list({ items }) %>
186
- ```
187
-
188
- Kurallar:
189
-
190
- - Tarama özyinelemelidir; alt dizinler de kapsanır.
191
- - `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
192
- - `loader.js` ve `index.js` bileşen dosyası sayılmaz.
193
- - `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
194
- önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
195
- bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
196
- - Aynı ad iki farklı bileşen dosyasında tanımlıysa uyarı basılır ve **ikincisi
197
- kazanır**: `[components] 'card' is defined twice: a.js and b.js — the second
198
- one wins.`
199
- - `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
200
- bir proje de çalışır.
201
-
202
- ## Yardımcılar: `jskelet/html`
203
-
204
- Şablonlara otomatik geçer; bileşen dosyalarında `import { … } from "jskelet/html"`
205
- ile alınır.
206
-
207
- ### `esc(value)`
208
-
209
- Metin içeriği ve attribute değerleri için kaçış (`&`, `<`, `>`, `"`, `'`).
210
- `null`, `undefined` ve `false` boş string'e çevrilir — koşullu render'da
211
- `false && "…"` gibi ifadeler `"false"` basmaz.
212
-
213
- ```js
214
- esc('<b>"x"</b>'); // "&lt;b&gt;&quot;x&quot;&lt;/b&gt;"
215
- ```
216
-
217
- ### `attrs(object)`
218
-
219
- Attribute nesnesini string'e çevirir. `null`/`undefined`/`false` atlanır, `true`
220
- boolean attribute olarak yazılır, geri kalan değerler kaçışlanır. Çıktı boş
221
- değilse **başında bir boşluk** ile döner, böylece `<div${attrs(...)}>` her zaman
222
- doğru biçimlenir.
223
-
224
- ```js
225
- `<input${attrs({ type: "text", required: true, value: null })}>`;
226
- // '<input type="text" required>'
227
- ```
228
-
229
- ### `cx(...inputs)`
230
-
231
- `clsx` karşılığı: string, sayı, dizi ve `{ sınıf: koşul }` nesnesi kabul eder,
232
- falsy değerleri atar. Tailwind çakışması **çözmez**.
233
-
234
- ```js
235
- cx("btn", isActive && "btn-active", { "btn-lg": size === "lg" });
236
- ```
237
-
238
- ### `cn(...inputs)`
239
-
240
- `cx()` ile birleştirir, sonra `tailwind-merge` ile Tailwind çakışmalarını çözer.
241
- Bir bileşenin varsayılan sınıflarının çağıran tarafından ezilmesi gerektiğinde
242
- bunu kullanın.
243
-
244
- ```js
245
- cn("px-4 py-2 bg-slate-100", className); // className "bg-white" ise bg-slate-100 düşer
246
- ```
247
-
248
- `tailwind-merge` çalışma zamanı bağımlılığı olarak korunur çünkü sınıf hesabı
249
- yalnızca sunucuda yapılır; client bundle'a hiç girmez.
250
-
251
- ### `jsonScript(value)`
252
-
253
- `<script type="application/ld+json">` gövdesi için güvenli JSON: `<`, `>`, `&`
254
- ve U+2028/U+2029 kaçırılır, böylece `</script` ya da `<!--` dizileri gövdeyi
255
- kapatamaz.
256
-
257
- ```ejs
258
- <script type="application/ld+json"><%- jsonScript(article) %></script>
259
- ```
260
-
261
- ## Yardımcılar: `jskelet/tags`
262
-
263
- `next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları. Hepsi HTML
264
- string döndürür ve EJS içinden `<%- %>` ile basılır.
265
-
266
- ### `link(props)`
267
-
268
- ```js
269
- link({
270
- href: "/hakkinda",
271
- text: "Hakkında",
272
- class: "font-semibold",
273
- // opsiyonel: html, title, ariaLabel, target, rel, attrs
274
- });
275
- ```
276
-
277
- - `title` verilmezse `ariaLabel` → `text` → `href` sırasıyla otomatik
278
- doldurulur.
279
- - `href` `http://` ya da `https://` ile başlıyorsa `target="_blank"` ve
280
- `rel="noopener noreferrer"` otomatik eklenir; açıkça verirsen senin değerin
281
- kullanılır.
282
- - `html` verilirse içerik ham basılır; `text` verilirse kaçışlanır.
283
- - `attrs` nesnesi ek attribute'ları geçirir ve öncekilerin üzerine biner.
284
-
285
- ### `image(props)`
286
-
287
- ```js
288
- image({
289
- src: "/hero.png",
290
- alt: "Kapak",
291
- priority: true,
292
- // opsiyonel: width, height, class, sizes, srcset, fill, loading,
293
- // unoptimized, attrs
294
- });
295
- ```
296
-
297
- Davranış:
298
-
299
- - `public/` altındaki yerel raster görseller için build'de üretilen webp
300
- varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
301
- `width`/`height` olarak eklenir. Manifest'te olmayan ya da uzak görseller
302
- olduğu gibi basılır.
303
- - `srcset` elle verilmişse ya da `unoptimized: true` ise manifest'e hiç
304
- bakılmaz.
305
- - Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
306
- yazılmaz; gürültüden ibaret olurdu.
307
- - `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
308
- genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
309
- (`(max-width: Npx) 100vw, Npx`).
310
- - `priority: true` → `loading="eager"`, `decoding="sync"`,
311
- `fetchpriority="high"`. LCP görseli için.
312
- - `priority` yoksa → `loading="lazy"`, `decoding="async"`.
313
- - `fill: true` → `width`/`height` yazılmaz ve
314
- `absolute inset-0 h-full w-full object-cover` sınıfları `cn()` ile birleştirilir.
315
-
316
- ### `icon(props)`
317
-
318
- Build zamanı üretilen SVG sprite'tan `<use>` çıkarır.
319
-
320
- ```js
321
- icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
322
- // <svg width="20" height="20" class="…" aria-hidden="true" focusable="false"
323
- // fill="currentColor" viewBox="0 0 256 256"><use href="/assets/sprite.<hash>.svg#arrow-right-bold"></use></svg>
324
- ```
325
-
326
- - `name` Phosphor adıdır; `ArrowRightIcon` ve `ArrowRight` biçimleri de kabul
327
- edilir ve `arrow-right`'a çevrilir (`toKebab()`).
328
- - `weight` sprite id'sine dâhildir: `thin`, `light`, `regular` (varsayılan),
329
- `bold`, `fill`, `duotone`.
330
- - `size` varsayılan 24; `width` ve `height` olarak yazılır.
331
- - Development'ta sprite'ta olmayan bir sembol istendiğinde tek seferlik uyarı
332
- basılır. Sprite yalnızca kaynakta **statik olarak** görülen adları içerir; adı
333
- çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
334
- sessizce boşluk kalır ([08-build.md](./08-build.md)).
335
-
336
- ### `preloadImage(props)`
337
-
338
- ```js
339
- preloadImage({ href: "/assets/img/hero-1280.abc.webp", imagesrcset, imagesizes });
340
- // <link rel="preload" as="image" href="…" fetchpriority="high">
341
- ```
342
-
343
- Pratikte doğrudan çağırmak yerine `headHints()` kullanılır:
344
-
345
- ```js
346
- import { headHints } from "jskelet";
347
-
348
- return {
349
- view: "pages/article",
350
- head: headHints({ href: cover, imageSrcSet, imageSizes }),
351
- };
352
- ```
353
-
354
- `headHints()` `href` yoksa boş string döner, yani koşul yazmak gerekmez.
355
- Preconnect'leri layout zaten her sayfaya bastığı için burada tekrarlanmaz.
356
-
357
- ## Metadata → `<head>`
358
-
359
- Controller `metadata` döndürür, framework onu etiketlere çevirir (Next.js'in
360
- Metadata API'sinin karşılığı). Şema bilinçli olarak küçük; daha fazlası
361
- gerekirse `extraTags` ile ham HTML eklenir, böylece framework her yeni meta türü
362
- için sürüm çıkarmak zorunda kalmaz.
363
-
364
- | Alan | Tip | Anlamı |
365
- | --- | --- | --- |
366
- | `title` | `string` | `<title>` |
367
- | `titleTemplate` | `string` | `"%s \| Site"` — `title` buna gömülür. Yalnızca `title` da varsa uygulanır. |
368
- | `description` | `string` | `<meta name="description">` |
369
- | `canonical` | `string` | Mutlak ya da göreli URL |
370
- | `siteUrl` | `string` | Göreli `canonical`ı mutlaklaştırmak için taban |
371
- | `robots` | `{ index?: boolean, follow?: boolean }` | Varsayılan `index, follow` |
372
- | `locale` | `string` | `og:locale` |
373
- | `openGraph` | `{ title, description, url, type, siteName, image, imageWidth, imageHeight }` | `og:*` etiketleri |
374
- | `twitter` | `{ card, site, creator, title, description, image }` | `twitter:*` etiketleri |
375
- | `extraTags` | `string[]` | Olduğu gibi basılacak ham etiketler |
376
-
377
- Üretim kuralları:
378
-
379
- - **Robots varsayılanı indekslenebilir.** Bir sayfayı gizlemek açık bir karar
380
- olmalı: `robots: { index: false }` → `noindex, follow`.
381
- - **OpenGraph `property` kullanır, `name` değil.** Bazı kazıyıcılar `name` ile
382
- yazılmış og etiketlerini görmezden geliyor.
383
- - **Devralma zinciri:** `og:title` yoksa `title`, `og:description` yoksa
384
- `description`, `og:url` yoksa mutlaklaştırılmış `canonical`, `twitter:title`
385
- yoksa `og:title` → `title`, `twitter:image` yoksa `og:image`.
386
- - **`twitter:card`** verilmezse `og:image` varsa `summary_large_image`, yoksa
387
- `summary`.
388
- - **Boş değerler hiç basılmaz:** `null`, `undefined` ve `""` olan alanlar
389
- etiket üretmez.
390
- - `og:type` verilmezse `website`.
391
-
392
- Örnek:
393
-
394
- ```js
395
- return {
396
- view: "pages/article",
397
- metadata: {
398
- title: article.title,
399
- description: article.summary,
400
- canonical: `/haber/${article.slug}`,
401
- openGraph: {
402
- type: "article",
403
- image: article.cover,
404
- imageWidth: 1200,
405
- imageHeight: 630,
406
- },
407
- extraTags: [`<meta property="article:published_time" content="${article.date}">`],
408
- },
409
- };
410
- ```
411
-
412
- `titleTemplate` ve `siteUrl` gibi her sayfada aynı olan alanları
413
- `hooks.metadata()` içine koyun; controller yalnızca sayfaya özel olanı verir.
414
-
415
- `renderHeadMeta(metadata)` fonksiyonu dışa açıktır; layout dışında (ör. bir
416
- fragment ya da e-posta) aynı etiketleri üretmek gerekirse kullanılabilir.
417
-
418
- ## Hook'lar
419
-
420
- Hook'lar `jskelet.config.mjs` → `hooks` altında tanımlanır. Hepsi opsiyonel,
421
- hepsi `async` olabilir. **Bir hook hata verirse sayfa düşmez:** framework kendi
422
- varsayılanına döner ve uyarır.
423
-
424
- ### `hooks.metadata(page)`
425
-
426
- Her sayfanın metadata varsayılanı. Argüman olarak render edilen sayfa tanımını
427
- alır, bir metadata nesnesi döndürür. Controller'ın `metadata` alanı bunun
428
- **üzerine biner** (alan bazında, sığ birleştirme).
429
-
430
- ```js
431
- hooks: {
432
- metadata() {
433
- return {
434
- titleTemplate: "%s | JSkelet",
435
- description: "JSkelet ile kurulmuş bir site.",
436
- siteUrl: "https://ornek.com",
437
- };
438
- },
439
- }
440
- ```
441
-
442
- ### `hooks.layoutContext({ pathname, metadata })`
443
-
444
- Layout'a her render'da eklenen local'ler. Döndürülen nesnenin **her alanı**
445
- layout local'i olur; ayrıca üç alan özel olarak yorumlanır:
446
-
447
- - `lang` → `<html lang>`
448
- - `structuredData` → JSON-LD script'leri (dizi)
449
- - `extraHead` → `<head>`e eklenir (controller `head`inden sonra)
450
- - `bodyClass` → controller `bodyClass` vermemişse kullanılır
451
-
452
- ```js
453
- hooks: {
454
- async layoutContext({ pathname }) {
455
- return {
456
- bodyClass: "min-h-full",
457
- navigation: await getNavigation(),
458
- isHome: pathname === "/",
459
- };
460
- },
461
- }
462
- ```
463
-
464
- Bu hook gövde render'ıyla **paralel** çalışır; içinde upstream çağırmak sayfaya
465
- sıralı gecikme eklemez.
466
-
467
- ### `hooks.notFound()`
468
-
469
- 404 sayfası tanımı. Döndürdüğü nesne `renderPage`'e `pathname: "/404"` ile
470
- verilir. Ayrıntı: [03-routing.md](./03-routing.md).
471
-
472
- ### Diğer hook'lar
473
-
474
- `hooks.prewarmPaths()` render katmanına değil ısıtmaya aittir; bkz.
475
- [06-cache.md](./06-cache.md).
476
-
477
- ## Overlay portal noktası
478
-
479
- `jskelet/client` → `getOverlayRoot()` modal ve drawer içeriğini taşıyacağı
480
- hedefi verir: layout'ta `<div id="jskelet-overlays"></div>` varsa oraya, yoksa
481
- `body`ye. Portal, `overflow` ya da `transform` taşıyan bir ata elementin
482
- `position: fixed` overlay'i kırpmasını engeller. Modal kullanacaksanız bu div'i
483
- layout'un `<body>` sonuna eklemek yeterli
484
- ([05-islands.md](./05-islands.md)).
485
-
486
- ## Sırada ne var
487
-
488
- - Island'lar ve `entries`: [05-islands.md](./05-islands.md)
489
- - `asset()`, manifest ve Tailwind taraması: [08-build.md](./08-build.md)
490
- - 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.ejs render → 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
+ ## EJS motoru
31
+
32
+ Motor ilk render'da bir kez kurulur; bileşen taraması dosya sistemine
33
+ dokunduğu için her istekte yapılamaz ve config yüklenmeden hesaplanamaz.
34
+
35
+ Ayarlar:
36
+
37
+ | Ayar | Değer | Sebebi |
38
+ | --- | --- | --- |
39
+ | `root`, `views` | `views` dizini | `include('partials/header')` çağrıları views kökünden çözülür |
40
+ | `cache` | dev'de `false`, prod'da `true` | dev'de şablon düzenlemesi anında görünsün |
41
+ | `rmWhitespace` | `true` | çıktı boyutu |
42
+ | `async` | `true` | şablon içinde `await` kullanılabilir |
43
+
44
+ Gömülü kullanımlar (test, script) için `resetRenderEngine()` dışa açık: bileşen
45
+ dosyaları değişince kaydı yeniler. Dev sunucusu süreci yeniden başlattığı için
46
+ normal akışta gerekmez.
47
+
48
+ ## Layout
49
+
50
+ ### Layout dosyası nasıl bulunur
51
+
52
+ 1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
53
+ dizininin üst dizinine** göre çözülür: `views` varsayılansa
54
+ `layout: "views/ozel.ejs"` → `<root>/views/ozel.ejs`.
55
+ 2. Verilmemişse `views/layout.ejs` varsa o kullanılır.
56
+ 3. O da yoksa framework'ün kendi minimal layout'u kullanılır
57
+ (`node_modules/jskelet/src/templates/layout.ejs`, ayrıca
58
+ `jskelet/layout` belirteciyle de erişilebilir).
59
+
60
+ Üçüncü seçenek yeni bir projenin tek route ile çalışabilmesi için var. Kendi
61
+ layout'unuza geçmenin en pratik yolu o dosyayı `views/layout.ejs` olarak
62
+ kopyalamaktır.
63
+
64
+ ### Framework'ün varsayılan layout'u
65
+
66
+ ```ejs
67
+ <!DOCTYPE html>
68
+ <html lang="<%= lang %>">
69
+ <head>
70
+ <meta charset="utf-8">
71
+ <meta name="viewport" content="width=device-width, initial-scale=1">
72
+ <%- extraHead %>
73
+ <% if (hasAsset('app.css')) { %>
74
+ <link rel="stylesheet" href="<%= asset('app.css') %>">
75
+ <% } %>
76
+ <%- headMeta %>
77
+ <% structuredData.forEach(function (item) { %>
78
+ <script type="application/ld+json"><%- jsonScript(item) %></script>
79
+ <% }); %>
80
+ </head>
81
+ <body class="<%= bodyClass %>">
82
+ <%- body %>
83
+ <% if (hasAsset('main.js')) { %>
84
+ <script type="module" src="<%= asset('main.js') %>"></script>
85
+ <% } %>
86
+ <% entries.forEach(function (entry) { %>
87
+ <script type="module" src="<%= asset(entry) %>"></script>
88
+ <% }); %>
89
+ <% if (devtools) { %>
90
+ <script type="module" src="<%= devBasePath %>/overlay.js"></script>
91
+ <% } %>
92
+ </body>
93
+ </html>
94
+ ```
95
+
96
+ Dikkat edilecek noktalar:
97
+
98
+ - **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
99
+ geciktirmek doğrudan LCP'ye yazılır.
100
+ - **Tek, render-blocking stylesheet** ve gerekçesi
101
+ [02-mimari.md](./02-mimari.md)'de. Build çalışmadıysa `hasAsset('app.css')`
102
+ false olur ve etiket hiç basılmaz.
103
+ - **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
104
+ istememesini sağlar.
105
+ - **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
106
+ çıktısında hiç yoktur.
107
+
108
+ ### Layout local'leri
109
+
110
+ | Local | Tip | Kaynağı |
111
+ | --- | --- | --- |
112
+ | `metadata` | `object` | `hooks.metadata()` + controller `metadata` (controller kazanır) |
113
+ | `headMeta` | `string` | `metadata`dan üretilmiş hazır `<head>` etiketleri |
114
+ | `extraHead` | `string` | `preconnect` ipuçları + `navigation` ipuçları + controller `head` + `context.extraHead` |
115
+ | `structuredData` | `unknown[]` | `hooks.layoutContext()` → `structuredData`; varsayılan `[]` |
116
+ | `body` | `string` | Sayfa şablonunun render çıktısı |
117
+ | `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
118
+ | `entries` | `string[]` | controller `entries`; varsayılan `[]` |
119
+ | `pathname` | `string` | `req.path`; **varsayılan boş string** |
120
+ | `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
121
+ | `devtools` | `boolean` | `NODE_ENV === "development"` |
122
+ | `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
123
+ | `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
124
+ | html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
125
+ | `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
126
+ | `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
127
+
128
+ `pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
129
+ sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
130
+
131
+ ## Sayfa şablonları
132
+
133
+ `view` alanı `views/` altındaki yolu uzantısız verir: `"pages/home"` →
134
+ `views/pages/home.ejs`. Şablona geçen local'ler `data` alanının içeriği artı
135
+ `metadata`dır — layout local'leri **değil**. Sayfa şablonu yine tüm yardımcılara
136
+ ve bileşenlere erişir.
137
+
138
+ ```ejs
139
+ <%# views/pages/home.ejs %>
140
+ <section class="wrapper">
141
+ <h1 class="text-3xl font-bold"><%= heading %></h1>
142
+
143
+ <%# `list` views/components/list.js içinde tanımlı; import gerekmiyor. %>
144
+ <%- list({ items }) %>
145
+
146
+ <div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
147
+ </section>
148
+ ```
149
+
150
+ EJS'te iki çıktı biçimini karıştırmayın:
151
+
152
+ - `<%= value %>` — HTML kaçışlı. Kullanıcı/upstream verisi için **daima** bu.
153
+ - `<%- html %>` — ham. Yalnızca kendi ürettiğin, güvenli HTML string'leri için
154
+ (bileşen çağrıları, `headMeta`, `body`).
155
+
156
+ `async: true` açık olduğu için şablon içinde `await` da kullanılabilir, ancak
157
+ veri çekmeyi controller'da tutmak teşhisi kolaylaştırır.
158
+
159
+ ## Bileşenler: `views/components/**`
160
+
161
+ Bileşenler EJS partial'ı değil, **HTML string döndüren fonksiyonlardır**.
162
+ `views/components/**` altındaki her `.js` dosyası taranır ve **her named export**
163
+ şablon local'i olur. Elle bakılan bir barrel dosyası yok: yeni bir bileşen
164
+ eklemek için dosyayı oluşturmak yeterli.
165
+
166
+ ```js
167
+ // views/components/list.js
168
+ import { esc } from "jskelet/html";
169
+
170
+ /**
171
+ * @param {{ items: string[] }} props
172
+ * @returns {string}
173
+ */
174
+ export function list({ items }) {
175
+ if (!items?.length) return "";
176
+
177
+ const rows = items.map((item) => `<li class="py-1">${esc(item)}</li>`).join("");
178
+ return `<ul class="mt-6 list-disc pl-6">${rows}</ul>`;
179
+ }
180
+ ```
181
+
182
+ Şablonda:
183
+
184
+ ```ejs
185
+ <%- list({ items }) %>
186
+ ```
187
+
188
+ Kurallar:
189
+
190
+ - Tarama özyinelemelidir; alt dizinler de kapsanır.
191
+ - `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
192
+ - `loader.js` ve `index.js` bileşen dosyası sayılmaz.
193
+ - `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
194
+ önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
195
+ bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
196
+ - Aynı ad iki farklı bileşen dosyasında tanımlıysa uyarı basılır ve **ikincisi
197
+ kazanır**: `[components] 'card' is defined twice: a.js and b.js — the second
198
+ one wins.`
199
+ - `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
200
+ bir proje de çalışır.
201
+
202
+ ## Yardımcılar: `jskelet/html`
203
+
204
+ Şablonlara otomatik geçer; bileşen dosyalarında `import { … } from "jskelet/html"`
205
+ ile alınır.
206
+
207
+ ### `esc(value)`
208
+
209
+ Metin içeriği ve attribute değerleri için kaçış (`&`, `<`, `>`, `"`, `'`).
210
+ `null`, `undefined` ve `false` boş string'e çevrilir — koşullu render'da
211
+ `false && "…"` gibi ifadeler `"false"` basmaz.
212
+
213
+ ```js
214
+ esc('<b>"x"</b>'); // "&lt;b&gt;&quot;x&quot;&lt;/b&gt;"
215
+ ```
216
+
217
+ ### `attrs(object)`
218
+
219
+ Attribute nesnesini string'e çevirir. `null`/`undefined`/`false` atlanır, `true`
220
+ boolean attribute olarak yazılır, geri kalan değerler kaçışlanır. Çıktı boş
221
+ değilse **başında bir boşluk** ile döner, böylece `<div${attrs(...)}>` her zaman
222
+ doğru biçimlenir.
223
+
224
+ ```js
225
+ `<input${attrs({ type: "text", required: true, value: null })}>`;
226
+ // '<input type="text" required>'
227
+ ```
228
+
229
+ ### `cx(...inputs)`
230
+
231
+ `clsx` karşılığı: string, sayı, dizi ve `{ sınıf: koşul }` nesnesi kabul eder,
232
+ falsy değerleri atar. Tailwind çakışması **çözmez**.
233
+
234
+ ```js
235
+ cx("btn", isActive && "btn-active", { "btn-lg": size === "lg" });
236
+ ```
237
+
238
+ ### `cn(...inputs)`
239
+
240
+ `cx()` ile birleştirir, sonra `tailwind-merge` ile Tailwind çakışmalarını çözer.
241
+ Bir bileşenin varsayılan sınıflarının çağıran tarafından ezilmesi gerektiğinde
242
+ bunu kullanın.
243
+
244
+ ```js
245
+ cn("px-4 py-2 bg-slate-100", className); // className "bg-white" ise bg-slate-100 düşer
246
+ ```
247
+
248
+ `tailwind-merge` çalışma zamanı bağımlılığı olarak korunur çünkü sınıf hesabı
249
+ yalnızca sunucuda yapılır; client bundle'a hiç girmez.
250
+
251
+ ### `jsonScript(value)`
252
+
253
+ `<script type="application/ld+json">` gövdesi için güvenli JSON: `<`, `>`, `&`
254
+ ve U+2028/U+2029 kaçırılır, böylece `</script` ya da `<!--` dizileri gövdeyi
255
+ kapatamaz.
256
+
257
+ ```ejs
258
+ <script type="application/ld+json"><%- jsonScript(article) %></script>
259
+ ```
260
+
261
+ ## Yardımcılar: `jskelet/tags`
262
+
263
+ `next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları. Hepsi HTML
264
+ string döndürür ve EJS içinden `<%- %>` ile basılır.
265
+
266
+ ### `link(props)`
267
+
268
+ ```js
269
+ link({
270
+ href: "/hakkinda",
271
+ text: "Hakkında",
272
+ class: "font-semibold",
273
+ // opsiyonel: html, title, ariaLabel, target, rel, attrs
274
+ });
275
+ ```
276
+
277
+ - `title` verilmezse `ariaLabel` → `text` → `href` sırasıyla otomatik
278
+ doldurulur.
279
+ - `href` `http://` ya da `https://` ile başlıyorsa `target="_blank"` ve
280
+ `rel="noopener noreferrer"` otomatik eklenir; açıkça verirsen senin değerin
281
+ kullanılır.
282
+ - `html` verilirse içerik ham basılır; `text` verilirse kaçışlanır.
283
+ - `attrs` nesnesi ek attribute'ları geçirir ve öncekilerin üzerine biner.
284
+
285
+ ### `image(props)`
286
+
287
+ ```js
288
+ image({
289
+ src: "/hero.png",
290
+ alt: "Kapak",
291
+ priority: true,
292
+ // opsiyonel: width, height, class, sizes, srcset, fill, loading,
293
+ // unoptimized, attrs
294
+ });
295
+ ```
296
+
297
+ Davranış:
298
+
299
+ - `public/` altındaki yerel raster görseller için build'de üretilen webp
300
+ varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
301
+ `width`/`height` olarak eklenir. Manifest'te olmayan ya da uzak görseller
302
+ olduğu gibi basılır.
303
+ - `srcset` elle verilmişse ya da `unoptimized: true` ise manifest'e hiç
304
+ bakılmaz.
305
+ - Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
306
+ yazılmaz; gürültüden ibaret olurdu.
307
+ - `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
308
+ genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
309
+ (`(max-width: Npx) 100vw, Npx`).
310
+ - `priority: true` → `loading="eager"`, `decoding="sync"`,
311
+ `fetchpriority="high"`. LCP görseli için.
312
+ - `priority` yoksa → `loading="lazy"`, `decoding="async"`.
313
+ - `fill: true` → `width`/`height` yazılmaz ve
314
+ `absolute inset-0 h-full w-full object-cover` sınıfları `cn()` ile birleştirilir.
315
+
316
+ ### `icon(props)`
317
+
318
+ Build zamanı üretilen SVG sprite'tan `<use>` çıkarır.
319
+
320
+ ```js
321
+ icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
322
+ // <svg width="20" height="20" class="…" aria-hidden="true" focusable="false"
323
+ // fill="currentColor" viewBox="0 0 256 256"><use href="/assets/sprite.<hash>.svg#arrow-right-bold"></use></svg>
324
+ ```
325
+
326
+ - `name` Phosphor adıdır; `ArrowRightIcon` ve `ArrowRight` biçimleri de kabul
327
+ edilir ve `arrow-right`'a çevrilir (`toKebab()`).
328
+ - `weight` sprite id'sine dâhildir: `thin`, `light`, `regular` (varsayılan),
329
+ `bold`, `fill`, `duotone`.
330
+ - `size` varsayılan 24; `width` ve `height` olarak yazılır.
331
+ - Development'ta sprite'ta olmayan bir sembol istendiğinde tek seferlik uyarı
332
+ basılır. Sprite yalnızca kaynakta **statik olarak** görülen adları içerir; adı
333
+ çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
334
+ sessizce boşluk kalır ([08-build.md](./08-build.md)).
335
+
336
+ ### `preloadImage(props)`
337
+
338
+ ```js
339
+ preloadImage({ href: "/assets/img/hero-1280.abc.webp", imagesrcset, imagesizes });
340
+ // <link rel="preload" as="image" href="…" fetchpriority="high">
341
+ ```
342
+
343
+ Pratikte doğrudan çağırmak yerine `headHints()` kullanılır:
344
+
345
+ ```js
346
+ import { headHints } from "jskelet";
347
+
348
+ return {
349
+ view: "pages/article",
350
+ head: headHints({ href: cover, imageSrcSet, imageSizes }),
351
+ };
352
+ ```
353
+
354
+ `headHints()` `href` yoksa boş string döner, yani koşul yazmak gerekmez.
355
+ Preconnect'leri layout zaten her sayfaya bastığı için burada tekrarlanmaz.
356
+
357
+ ## Metadata → `<head>`
358
+
359
+ Controller `metadata` döndürür, framework onu etiketlere çevirir (Next.js'in
360
+ Metadata API'sinin karşılığı). Şema bilinçli olarak küçük; daha fazlası
361
+ gerekirse `extraTags` ile ham HTML eklenir, böylece framework her yeni meta türü
362
+ için sürüm çıkarmak zorunda kalmaz.
363
+
364
+ | Alan | Tip | Anlamı |
365
+ | --- | --- | --- |
366
+ | `title` | `string` | `<title>` |
367
+ | `titleTemplate` | `string` | `"%s \| Site"` — `title` buna gömülür. Yalnızca `title` da varsa uygulanır. |
368
+ | `description` | `string` | `<meta name="description">` |
369
+ | `canonical` | `string` | Mutlak ya da göreli URL |
370
+ | `siteUrl` | `string` | Göreli `canonical`ı mutlaklaştırmak için taban |
371
+ | `robots` | `{ index?: boolean, follow?: boolean }` | Varsayılan `index, follow` |
372
+ | `locale` | `string` | `og:locale` |
373
+ | `openGraph` | `{ title, description, url, type, siteName, image, imageWidth, imageHeight }` | `og:*` etiketleri |
374
+ | `twitter` | `{ card, site, creator, title, description, image }` | `twitter:*` etiketleri |
375
+ | `extraTags` | `string[]` | Olduğu gibi basılacak ham etiketler |
376
+
377
+ Üretim kuralları:
378
+
379
+ - **Robots varsayılanı indekslenebilir.** Bir sayfayı gizlemek açık bir karar
380
+ olmalı: `robots: { index: false }` → `noindex, follow`.
381
+ - **OpenGraph `property` kullanır, `name` değil.** Bazı kazıyıcılar `name` ile
382
+ yazılmış og etiketlerini görmezden geliyor.
383
+ - **Devralma zinciri:** `og:title` yoksa `title`, `og:description` yoksa
384
+ `description`, `og:url` yoksa mutlaklaştırılmış `canonical`, `twitter:title`
385
+ yoksa `og:title` → `title`, `twitter:image` yoksa `og:image`.
386
+ - **`twitter:card`** verilmezse `og:image` varsa `summary_large_image`, yoksa
387
+ `summary`.
388
+ - **Boş değerler hiç basılmaz:** `null`, `undefined` ve `""` olan alanlar
389
+ etiket üretmez.
390
+ - `og:type` verilmezse `website`.
391
+
392
+ Örnek:
393
+
394
+ ```js
395
+ return {
396
+ view: "pages/article",
397
+ metadata: {
398
+ title: article.title,
399
+ description: article.summary,
400
+ canonical: `/haber/${article.slug}`,
401
+ openGraph: {
402
+ type: "article",
403
+ image: article.cover,
404
+ imageWidth: 1200,
405
+ imageHeight: 630,
406
+ },
407
+ extraTags: [`<meta property="article:published_time" content="${article.date}">`],
408
+ },
409
+ };
410
+ ```
411
+
412
+ `titleTemplate` ve `siteUrl` gibi her sayfada aynı olan alanları
413
+ `hooks.metadata()` içine koyun; controller yalnızca sayfaya özel olanı verir.
414
+
415
+ `renderHeadMeta(metadata)` fonksiyonu dışa açıktır; layout dışında (ör. bir
416
+ fragment ya da e-posta) aynı etiketleri üretmek gerekirse kullanılabilir.
417
+
418
+ ## Hook'lar
419
+
420
+ Hook'lar `jskelet.config.mjs` → `hooks` altında tanımlanır. Hepsi opsiyonel,
421
+ hepsi `async` olabilir. **Bir hook hata verirse sayfa düşmez:** framework kendi
422
+ varsayılanına döner ve uyarır.
423
+
424
+ ### `hooks.metadata(page)`
425
+
426
+ Her sayfanın metadata varsayılanı. Argüman olarak render edilen sayfa tanımını
427
+ alır, bir metadata nesnesi döndürür. Controller'ın `metadata` alanı bunun
428
+ **üzerine biner** (alan bazında, sığ birleştirme).
429
+
430
+ ```js
431
+ hooks: {
432
+ metadata() {
433
+ return {
434
+ titleTemplate: "%s | JSkelet",
435
+ description: "JSkelet ile kurulmuş bir site.",
436
+ siteUrl: "https://ornek.com",
437
+ };
438
+ },
439
+ }
440
+ ```
441
+
442
+ ### `hooks.layoutContext({ pathname, metadata })`
443
+
444
+ Layout'a her render'da eklenen local'ler. Döndürülen nesnenin **her alanı**
445
+ layout local'i olur; ayrıca üç alan özel olarak yorumlanır:
446
+
447
+ - `lang` → `<html lang>`
448
+ - `structuredData` → JSON-LD script'leri (dizi)
449
+ - `extraHead` → `<head>`e eklenir (controller `head`inden sonra)
450
+ - `bodyClass` → controller `bodyClass` vermemişse kullanılır
451
+
452
+ ```js
453
+ hooks: {
454
+ async layoutContext({ pathname }) {
455
+ return {
456
+ bodyClass: "min-h-full",
457
+ navigation: await getNavigation(),
458
+ isHome: pathname === "/",
459
+ };
460
+ },
461
+ }
462
+ ```
463
+
464
+ Bu hook gövde render'ıyla **paralel** çalışır; içinde upstream çağırmak sayfaya
465
+ sıralı gecikme eklemez.
466
+
467
+ ### `hooks.notFound()`
468
+
469
+ 404 sayfası tanımı. Döndürdüğü nesne `renderPage`'e `pathname: "/404"` ile
470
+ verilir. Ayrıntı: [03-routing.md](./03-routing.md).
471
+
472
+ ### Diğer hook'lar
473
+
474
+ `hooks.prewarmPaths()` render katmanına değil ısıtmaya aittir; bkz.
475
+ [06-cache.md](./06-cache.md).
476
+
477
+ ## Overlay portal noktası
478
+
479
+ `jskelet/client` → `getOverlayRoot()` modal ve drawer içeriğini taşıyacağı
480
+ hedefi verir: layout'ta `<div id="jskelet-overlays"></div>` varsa oraya, yoksa
481
+ `body`ye. Portal, `overflow` ya da `transform` taşıyan bir ata elementin
482
+ `position: fixed` overlay'i kırpmasını engeller. Modal kullanacaksanız bu div'i
483
+ layout'un `<body>` sonuna eklemek yeterli
484
+ ([05-islands.md](./05-islands.md)).
485
+
486
+ ## Sırada ne var
487
+
488
+ - Island'lar ve `entries`: [05-islands.md](./05-islands.md)
489
+ - `asset()`, manifest ve Tailwind taraması: [08-build.md](./08-build.md)
490
+ - Hook'ların config içindeki yeri: [07-yapilandirma.md](./07-yapilandirma.md)