jskelet 0.6.1 → 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,515 +1,515 @@
1
- # 03 — Routing
2
-
3
- Bu belge bir isteğin hangi controller'a düştüğünü belirleyen her mekanizmayı
4
- anlatır: route modüllerinin sözleşmesi ve yükleme sırası, `route()` sarmalayıcısı,
5
- controller'ın döndürdüğü sayfa tanımı, `ctx` nesnesi, `params`, `notFound()` ve
6
- `redirect()` kontrol akışı, ve `jskelet.config.mjs` üzerinden gelen
7
- redirect/rewrite kuralları. Sayfa tanımının şablon tarafı
8
- [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)'de, `revalidate`
9
- davranışı [06-cache.md](./06-cache.md)'de anlatılıyor.
10
-
11
- ## Route modülü sözleşmesi
12
-
13
- Bir route modülü, **default export** ya da `register` adlı **named export**
14
- olarak `(app, api) => void | Promise<void>` imzalı bir fonksiyon açar.
15
-
16
- ```js
17
- // routes/10-pages.mjs
18
- export default function register(app, { route }) {
19
- app.get("/", route(async () => ({ view: "pages/home" })));
20
- }
21
- ```
22
-
23
- `app` doğrudan Express uygulamasıdır: `app.get`, `app.post`, `app.use`,
24
- `app.all` — Express 5'in tüm yüzeyi kullanılabilir. `api` ise framework'ün route
25
- dosyalarına geçirdiği hazır yüzeydir, böylece her dosyada tek tek import yapmak
26
- gerekmez:
27
-
28
- | Alan | Karşılığı |
29
- | --- | --- |
30
- | `route` | `jskelet` → `route` |
31
- | `fragment` | `jskelet` → `fragment` |
32
- | `renderView` | `jskelet` → `renderView` |
33
- | `renderPage` | `jskelet` → `renderPage` |
34
- | `notFound` | `jskelet` → `notFound` |
35
- | `redirect` | `jskelet` → `redirect` |
36
- | `permanentRedirect` | `jskelet` → `permanentRedirect` |
37
- | `seeOther` | `jskelet` → `seeOther` |
38
- | `ogHandler` | `jskelet` → `ogHandler` |
39
- | `ogImage` | `jskelet` → `ogImage` |
40
- | `sendOgImage` | `jskelet` → `sendOgImage` |
41
- | `ImageResponse` | `jskelet` → `ImageResponse` |
42
-
43
- İstersen doğrudan import da edebilirsin; `api` yalnızca kolaylık:
44
-
45
- ```js
46
- import { route, notFound } from "jskelet";
47
-
48
- export function register(app) {
49
- app.get("/haber/:slug", route(async ({ params }) => { /* … */ }));
50
- }
51
- ```
52
-
53
- Modül geçerli bir fonksiyon açmazsa uyarı basılır ve atlanır:
54
- `[router] <file> exports neither a default nor a 'register' function, skipped`.
55
-
56
- ## Yükleme sırası
57
-
58
- Dosya sistemine dayalı otomatik URL türetme **yok**. Sıra iki şekilde
59
- belirlenir:
60
-
61
- **1. Açık liste (`jskelet.config.mjs` → `routes`).** Proje köküne göre göreli
62
- yollar, verdiğin sırada yüklenir:
63
-
64
- ```js
65
- export default {
66
- routes: ["./routes/api.js", "./routes/pages.js", "./routes/catch-all.js"],
67
- };
68
- ```
69
-
70
- **2. Liste yoksa `routes/` dizini alfabetik taranır**, ardından her
71
- `features/<name>/index.js` (veya `.mjs`) alfabetik eklenir. Tarama
72
- özyinelemelidir (alt dizinler de dâhil), yalnızca `.js` ve `.mjs` dosyaları
73
- alınır ve adı `_` ile başlayan dosyalar atlanır (`_helpers.js` gibi paylaşılan
74
- modüller için).
75
-
76
- Feature-first düzen zorunlu değildir; bir feature örneği:
77
-
78
- ```
79
- features/piyasalar/
80
- index.js # register(app, api) — URL'ler açıkça yazılır
81
- server/
82
- views/pages/… # .jsk veya .ejs
83
- views/components/
84
- client/ # island'lar; client/entries'ten registerAll
85
- ```
86
-
87
- `jskelet generate feature|page|island` iskelet üretir. Filesystem URL routing
88
- yoktur.
89
-
90
- Bu durumda dosya adlarına sayısal önek verin:
91
-
92
- ```
93
- routes/
94
- ├── 10-pages.mjs
95
- ├── 50-blog.mjs
96
- └── 99-catch-all.mjs
97
- ```
98
-
99
- Sıranın açık olması bir tasarım kararı: `/:slug` gibi tek segmentli bir
100
- yakalayıcı `/hakkinda` rotasından önce kaydedilirse "hakkinda" bir slug sanılır.
101
- Sırayı dosya adına gizlemek yerine görünür kılmak teşhisi kolaylaştırıyor
102
- ([02-mimari.md](./02-mimari.md)).
103
-
104
- Hiç route modülü bulunamazsa uyarı basılır ve sunucu yalnızca statik dosyalar +
105
- 404 ile ayağa kalkar.
106
-
107
- ### Bozuk modül davranışı
108
-
109
- - **Development:** modül import edilemezse uyarı basılır ve atlanır; sunucu
110
- ayakta kalır.
111
- - **Production:** hata fırlatılır ve süreç açılmaz. Yarım route tablosuyla
112
- yayına çıkmak, sessizce 404 dönen sayfalar demek.
113
-
114
- ## `route()` — controller sarmalayıcısı
115
-
116
- `route(controller, options?)` bir Express request handler döndürür ve şu işleri
117
- üstlenir:
118
-
119
- - `ctx` nesnesini kurar ve controller'ı çağırır.
120
- - HTML TTL cache'ini uygular (`revalidate` varsa ve metot `GET` ise).
121
- - `notFound()` / `redirect()` kontrol akışını yakalar.
122
- - Yanıt başlıklarını yazar: `Content-Type` ve cache durumuna göre
123
- `Cache-Control` (+ önbelleklenebilir yanıtlarda `X-JSkelet-Cache`).
124
- - Önbellekte saklanan sıkıştırılmış gövdeyi kullanarak yanıtı gönderir.
125
-
126
- ```js
127
- app.get(
128
- "/hakkinda",
129
- route(
130
- async () => ({
131
- view: "pages/about",
132
- metadata: { title: "Hakkında", canonical: "/hakkinda" },
133
- }),
134
- { revalidate: 300 },
135
- ),
136
- );
137
- ```
138
-
139
- `options` iki alan kabul eder:
140
-
141
- | Alan | Tip | Anlamı |
142
- | --- | --- | --- |
143
- | `revalidate` | `number` (saniye) | HTML önbellek TTL'i. Verilmezse ya da 0 ise bu route önbelleklenmez. `jskelet.config.mjs` → `cache().html` içindeki eşleşen bir kural bu değeri **ezer**. |
144
- | `private` | `boolean` | Sayfa ziyaretçiye bağlı. Önbellek devre dışı kalır, `cache().html` deseni bunu **ezemez**, yanıt `private, no-store` ve `Vary: Cookie` ile ETag'siz gider. |
145
-
146
- `revalidate` verilse bile **query parametresi taşıyan istek varsayılan olarak
147
- dinamiktir**; o yol için `cache().query` altında bir izin listesi tanımlamak
148
- gerekir ([06-cache.md](./06-cache.md)).
149
-
150
- Oturuma bağlı her sayfa `private: true` almalı; önbellek anahtarında kimlik
151
- olmadığı için bayrak olmadan bir kullanıcının HTML'i bir başkasına servis
152
- edilir. Framework bu hatayı çalışma zamanında da yakalıyor (controller cookie
153
- okuduğunda render önbelleğe yazılmaz), ama doğru yer bayrak. Ayrıntılar
154
- [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
155
-
156
- ## `fragment()` — layout'suz parça
157
-
158
- Bir bölgeyi tazeleyen uçlar için. Layout basılmaz, yanıt `private, no-store` ve
159
- ETag'siz gider, HTML önbelleğine hiç uğramaz. `/_fragment/` öneki
160
- `robots.txt`'in altına eklenir; parça uçları dizine girmez
161
- ([04](./04-render-ve-sablonlar.md#robotstxt)).
162
-
163
- ```js
164
- app.get(
165
- "/_fragment/satirlar",
166
- fragment(async ({ query }) => ({
167
- view: "partials/rows",
168
- data: { rows: getRows(Number(query.sayfa ?? 1)) },
169
- })),
170
- );
171
- ```
172
-
173
- Controller `{ view, data?, status? }` ya da doğrudan bir HTML string döner.
174
- Hata durumunda tüm sayfa yerine küçük bir uyarı parçası döner
175
- (`<div role="alert" data-fragment-error>`), çünkü takas edilen bölge bir hata
176
- sayfasının tamamını içine almamalı.
177
-
178
- `fragment()` POST için de kullanılabilir: form gönderiminin cevabı olarak
179
- güncellenmiş parçayı döndürmenin yolu bu, ve şablonda `csrfField()`
180
- çalışabilmesi için gereken istek bağlamını da kuruyor.
181
-
182
- ## `ctx` — controller bağlamı
183
-
184
- Controller tek argüman alır:
185
-
186
- ```js
187
- {
188
- params, // Express route parametreleri (req.params)
189
- query, // Ayrıştırılmış query string (req.query)
190
- pathname, // req.path — query'siz yol
191
- req, // Express Request; ihtiyaç olursa tam erişim
192
- }
193
- ```
194
-
195
- `params` Express'in kendi desen sözdizimini kullanır (Express 5 /
196
- `path-to-regexp`), config'teki `source` sözdizimini değil:
197
-
198
- ```js
199
- app.get("/haber/:slug", route(async ({ params }) => {
200
- const article = await getArticle(params.slug);
201
- if (!article) notFound();
202
- return { view: "pages/article", data: { article } };
203
- }));
204
- ```
205
-
206
- `pathname` hem cache anahtarında hem de `renderPage`'e geçen `pathname`
207
- local'inde kullanılır; layout'un "bu ana sayfa mı" gibi kararları buna bakar.
208
-
209
- ## Controller'ın döndürdüğü sayfa tanımı
210
-
211
- Controller `async (ctx) => sayfa` biçimindedir ve şu alanları döndürebilir:
212
-
213
- | Alan | Tip | Varsayılan | Anlamı |
214
- | --- | --- | --- | --- |
215
- | `view` | `string` | — | `views/` altındaki şablon yolu, uzantısız: `"pages/home"` → `views/pages/home.jsk` (yoksa legacy `.ejs`). |
216
- | `data` | `object` | `{}` | Şablona local olarak geçen veriler. |
217
- | `metadata` | `object` | `{}` | `<head>` etiketlerine çevrilir; `hooks.metadata()` çıktısının üzerine biner. Şema: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md). |
218
- | `status` | `number` | `200` | HTTP durum kodu. Yalnızca 200 önbelleğe yazılır. |
219
- | `head` | `string` | `""` | `<head>`e olduğu gibi basılacak ham HTML (ör. LCP preload'ı). |
220
- | `bodyClass` | `string` | `hooks.layoutContext().bodyClass ?? ""` | `<body class="…">`. |
221
- | `entries` | `string[]` | `[]` | Bu sayfada ek olarak yüklenecek client entry adları: `["chart.js"]`. |
222
- | `styles` | `string[]` | `[]` | Bu sayfada ek olarak yüklenecek stylesheet adları: `["home.css"]` → `styles/pages/home.css`. |
223
-
224
- `revalidate` **`route()`'un ikinci argümanıdır**, controller'ın döndürdüğü
225
- nesnenin alanı değil.
226
-
227
- Örnek, hepsi bir arada:
228
-
229
- ```js
230
- import { headHints } from "jskelet";
231
-
232
- app.get(
233
- "/piyasalar",
234
- route(
235
- async ({ query }) => {
236
- const data = await getMarkets(query.tab ?? "hisse");
237
-
238
- return {
239
- view: "pages/markets",
240
- data: { markets: data.items, tab: query.tab ?? "hisse" },
241
- metadata: {
242
- title: "Piyasalar",
243
- canonical: "/piyasalar",
244
- openGraph: { image: data.cover },
245
- },
246
- head: headHints({ href: data.cover }),
247
- bodyClass: "bg-slate-50",
248
- entries: ["chart.js"],
249
- styles: ["markets.css"],
250
- };
251
- },
252
- { revalidate: 30 },
253
- ),
254
- );
255
- ```
256
-
257
- ## `notFound()` ve `redirect()`
258
-
259
- `next/navigation` içindeki kontrol akışının karşılığı: derinlerdeki bir
260
- fonksiyon `throw` eder, framework yakalar. Böylece veri katmanındaki bir
261
- fonksiyon, controller'a dönüş değeri taşımak zorunda kalmadan 404 üretebilir.
262
-
263
- ```js
264
- import { notFound, redirect, permanentRedirect, seeOther } from "jskelet";
265
-
266
- notFound(); // 404 → hooks.notFound() sayfası
267
- redirect("/yeni-adres"); // 307 (geçici, metodu korur)
268
- permanentRedirect("/yeni"); // 308 (kalıcı, metodu korur)
269
- seeOther("/panel"); // 303 (POST sonrası)
270
- ```
271
-
272
- Dördü de `never` döner (her zaman fırlatır). Ayrıntı:
273
-
274
- - `notFound()` → `NotFoundError` (`statusCode: 404`)
275
- - `redirect(location)` → `RedirectError` (`statusCode: 307`)
276
- - `permanentRedirect(location)` → `RedirectError` (`statusCode: 308`)
277
- - `seeOther(location)` → `RedirectError` (`statusCode: 303`)
278
-
279
- Bir POST handler'ında `redirect()` değil `seeOther()` kullanılır: 307 metodu
280
- koruyor, yani tarayıcı hedefe yeniden POST ediyor. "Post/redirect/get" akışı —
281
- geri tuşunun formu yeniden göndermediği akış — 303 gerektiriyor.
282
-
283
- Özel bir durum kodu gerekiyorsa sınıfı doğrudan kullanabilirsin:
284
-
285
- ```js
286
- import { RedirectError } from "jskelet";
287
-
288
- throw new RedirectError("/eski-kurulum-uyumu", 301);
289
- ```
290
-
291
- Ayırt etmek için `isNotFoundError(error)` ve `isRedirectError(error)` dışa açık.
292
-
293
- Yakalanma noktaları:
294
-
295
- 1. **`route()` içinde:** redirect doğrudan yanıta yazılır; notFound `produce()`
296
- içinde yakalanır ve 404 sayfası üretilir (bu çıktı önbelleğe **yazılmaz**,
297
- çünkü yalnızca 200 saklanır).
298
- 2. **Express hata yöneticisinde:** bir middleware ya da route dışı kodda
299
- fırlatılmışsa burada karşılanır.
300
-
301
- ## 404 sayfası
302
-
303
- Bir istek hiçbir route'a düşmezse framework `hooks.notFound()` hook'unu çağırır
304
- ve dönen sayfa tanımını `pathname: "/404"` ile render eder.
305
-
306
- ```js
307
- // jskelet.config.mjs
308
- export default {
309
- hooks: {
310
- notFound() {
311
- return {
312
- view: "pages/not-found",
313
- metadata: { title: "Sayfa bulunamadı", robots: { index: false } },
314
- };
315
- },
316
- },
317
- };
318
- ```
319
-
320
- Hook tanımlı değilse ya da 404 render'ı da hata verirse framework şablonsuz,
321
- minimal bir HTML döner. Bu geri dönüş bilinçli olarak şablonsuz: 404 render'ı da
322
- patlarsa ziyaretçi boş yanıt görmesin.
323
-
324
- ## Hata sayfaları (500 ve diğerleri)
325
-
326
- Bir controller ya da middleware beklenmeyen bir hata fırlattığında Express'in
327
- hata yöneticisi devreye girer, hatayı loglar ve framework'ün kendi hata sayfasını
328
- `Cache-Control: no-store` ile döner. Durum kodu hatanın `statusCode` (ya da
329
- `status`) alanından okunur; 400–599 aralığında değilse 500 kullanılır.
330
-
331
- **Development** (`NODE_ENV=development`, yani `jskelet dev`): 5xx yanıtlarında
332
- gömülü 500 sayfası ve `hooks.error()` atlanır; mesaj, yığın izi ve varsa
333
- `cause` zinciri içeren bir teşhis sayfası döner. 4xx (404 vb.) development'ta
334
- da her zamanki gibi durum sayfasıdır.
335
-
336
- **Production**: framework'ün sayfası bilinçli olarak yalın — durum kodu, tek
337
- satır başlık ve tek satır açıklama. Marka adı, gezinme ya da hata ayrıntısı
338
- taşımaz; sunucunun içi ziyaretçiye açılmaz. Dil `brand.lang`ten gelir (`tr` ve
339
- `en` hazır, diğerleri `en`e düşer).
340
-
341
- Kendi sayfanı vermek için `hooks.error()` (yalnızca production / 4xx):
342
-
343
- ```js
344
- // jskelet.config.mjs
345
- export default {
346
- hooks: {
347
- error({ status }) {
348
- return {
349
- view: "pages/error",
350
- data: { status },
351
- metadata: { title: "Bir hata oluştu", robots: { index: false } },
352
- };
353
- },
354
- },
355
- };
356
- ```
357
-
358
- Hook bir sayfa tanımı yerine doğrudan HTML string de döndürebilir; layout'a
359
- bağlı olmayan bir hata sayfası istiyorsan bu yol daha güvenli, çünkü layout'un
360
- kendisi hata veriyorsa sayfa tanımı da render edilemez. Hook yoksa, `null`
361
- dönerse ya da render'ı patlarsa framework gömülü sayfaya düşer.
362
-
363
- 404 için `hooks.notFound()` önceliklidir; yalnızca o tanımlı değilse
364
- `hooks.error()` `status: 404` ile çağrılır.
365
-
366
- Sayfayı programatik olarak da üretebilirsin:
367
-
368
- ```js
369
- import { renderStatusPage } from "jskelet";
370
-
371
- const html = await renderStatusPage(503);
372
- ```
373
-
374
- ## Layout'suz render: `renderView`
375
-
376
- `renderView(view, data)` tek bir şablonu layout olmadan render eder ve string
377
- döner. Fragment uçları, e-posta şablonları ve island'ların sonradan çektiği
378
- HTML parçaları için:
379
-
380
- ```js
381
- export default function register(app, { renderView }) {
382
- app.get("/_fragment/yorumlar/:id", async (req, res) => {
383
- const comments = await getComments(req.params.id);
384
- res.type("html").send(await renderView("fragments/comments", { comments }));
385
- });
386
- }
387
- ```
388
-
389
- `/_fragment/` öneki varsayılan `prewarmSkip` listesinde yer alır, yani ısıtma
390
- turu bu uçları taramaz ([06-cache.md](./06-cache.md)).
391
-
392
- ## Config: `redirects()`
393
-
394
- `jskelet.config.mjs` → `redirects()` bir dizi döndürür ve middleware zincirinde
395
- route'lardan **önce** çalışır (bkz. [02-mimari.md](./02-mimari.md)).
396
-
397
- ```js
398
- export default {
399
- async redirects() {
400
- return [
401
- { source: "/eski-blog/:slug", destination: "/blog/:slug", permanent: true },
402
- { source: "/kampanya", destination: "/kampanyalar" },
403
- { source: "/legacy", destination: "/", statusCode: 301 },
404
- ];
405
- },
406
- };
407
- ```
408
-
409
- Davranış:
410
-
411
- - **İlk eşleşen kural kazanır**, sonrası denenmez. Sıralama config'teki yazım
412
- sırasıdır.
413
- - **Query string korunur:** `/eski-blog/x?utm=a` → `/blog/x?utm=a`. Yönlendirme
414
- kampanya parametrelerini düşürürse trafik kaynağı kaybolur.
415
- - **Durum kodu:** `permanent: true` → 308, aksi hâlde 307 (Next semantiği).
416
- Farklı bir kod isteyen `statusCode` verebilir; örneğin eski kurulumlarla uyum
417
- için 301.
418
- - `source` ya da `destination` geçersizse kural sessizce düşmez, uyarı basılır.
419
-
420
- ## Config: `trailingSlash`
421
-
422
- `trailingSlash: true` iken kanonik sayfa URL'leri `/` ile biter ve **200**
423
- döner; slash'sız istek **308** ile slash'lıya gider. Ayrıntı ve istisnalar:
424
- [07-yapilandirma.md](./07-yapilandirma.md#trailingslash).
425
-
426
- ## Config: `rewrites()`
427
-
428
- Rewrite, tarayıcının adres çubuğunu değiştirmeden isteği başka bir yere taşır.
429
- İki faz vardır:
430
-
431
- ```js
432
- export default {
433
- async rewrites() {
434
- return {
435
- beforeFiles: [
436
- { source: "/sitemap-:page.xml", destination: "/sitemap?page=:page" },
437
- ],
438
- afterFiles: [
439
- { source: "/api/:path*", destination: "https://api.example.com/:path*" },
440
- ],
441
- };
442
- },
443
- };
444
- ```
445
-
446
- Bir dizi döndürürsen tamamı `afterFiles` sayılır:
447
-
448
- ```js
449
- async rewrites() {
450
- return [{ source: "/api/:path*", destination: "https://api.example.com/:path*" }];
451
- }
452
- ```
453
-
454
- - **`beforeFiles`** statik dosyalardan da önce çalışır. `/assets/…` gibi yolları
455
- yeniden yazmak gerekiyorsa buraya konmalı.
456
- - **`afterFiles`** statik denendikten sonra, route'lardan önce çalışır.
457
-
458
- Hedefin biçimi davranışı belirler:
459
-
460
- - **Mutlak (`http://` / `https://`):** istek gömülü ters proxy ile dışa taşınır.
461
- Harici paket yok; `fetch` ile stream eden ince bir katman. Hop-by-hop
462
- başlıklar (`host`, `connection`, `content-length`, `accept-encoding`)
463
- temizlenir; yanıtta `content-encoding`, `content-length`,
464
- `transfer-encoding`, `connection` düşürülür. `redirect: "manual"` sayesinde
465
- upstream'in 302'si burada tüketilmez, tarayıcıya iletilir.
466
- - **Göreli:** yalnızca `req.url` değiştirilir ve istek kendi route tablosunda
467
- devam eder. Bu fazda ilk eşleşen kural döngüyü kırar.
468
-
469
- Tipik kullanım `/api/*` yolunu backend'e taşımaktır. Tarayıcı bunu same-origin
470
- çağırdığı için CORS ve third-party cookie sorunları oluşmaz.
471
-
472
- ### Elle proxy: `createProxy`
473
-
474
- Aynı proxy'yi kendi route'unda da kullanabilirsin:
475
-
476
- ```js
477
- import { createProxy } from "jskelet";
478
-
479
- export default function register(app) {
480
- app.use("/ws-api", createProxy((req) => `${process.env.API_ORIGIN}${req.url}`));
481
- }
482
- ```
483
-
484
- `resolveTarget` fırlatırsa ya da boş döndürürse istek proxy'lenmez ve zincire
485
- devam eder: hedef origin yapılandırılmamış bir kurulumda 500 yerine normal bir
486
- 404 almak daha doğru.
487
-
488
- ## `source` desen sözdizimi
489
-
490
- `redirects()`, `rewrites()`, `headers()` ve `cache().html` aynı küçük desen
491
- derleyicisini kullanır. Bu, Next'in tam `path-to-regexp` yüzeyi değil; config'te
492
- fiilen kullanılan alt küme bilinçli olarak seçildi.
493
-
494
- | Desen | Anlamı |
495
- | --- | --- |
496
- | `/haber/:slug` | Tek segment yakalar (`[^/]+`) |
497
- | `/:path*` | Sıfır veya daha fazla segment yakalar; öndeki `/` opsiyoneldir, yani `/blog/:path*` `/blog`u da kapsar |
498
- | `/:path*.svg` | Joker + sabit son ek; uzantı kuralları böyle yazılır |
499
- | `/etiket-:slug` | Segment ortasında parametre |
500
-
501
- Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
502
- Parametre adı `[A-Za-z_][A-Za-z0-9_]*` kalıbına uymalıdır.
503
-
504
- `source` mutlaka `/` ile başlamalı; başlamazsa kural yok sayılır ve uyarı
505
- basılır (``[config] invalid source (must start with `/`): …``). Tanınmayan bir
506
- sözdizimi sessizce literal kabul edilmez.
507
-
508
- Tam desen listesi ve config referansı: [07-yapilandirma.md](./07-yapilandirma.md).
509
-
510
- ## Sırada ne var
511
-
512
- - Şablon katmanı, bileşenler ve metadata:
513
- [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
514
- - `revalidate`, cache anahtarı ve `X-JSkelet-Cache`: [06-cache.md](./06-cache.md)
515
- - Config alanlarının tam referansı: [07-yapilandirma.md](./07-yapilandirma.md)
1
+ # 03 — Routing
2
+
3
+ Bu belge bir isteğin hangi controller'a düştüğünü belirleyen her mekanizmayı
4
+ anlatır: route modüllerinin sözleşmesi ve yükleme sırası, `route()` sarmalayıcısı,
5
+ controller'ın döndürdüğü sayfa tanımı, `ctx` nesnesi, `params`, `notFound()` ve
6
+ `redirect()` kontrol akışı, ve `jskelet.config.mjs` üzerinden gelen
7
+ redirect/rewrite kuralları. Sayfa tanımının şablon tarafı
8
+ [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)'de, `revalidate`
9
+ davranışı [06-cache.md](./06-cache.md)'de anlatılıyor.
10
+
11
+ ## Route modülü sözleşmesi
12
+
13
+ Bir route modülü, **default export** ya da `register` adlı **named export**
14
+ olarak `(app, api) => void | Promise<void>` imzalı bir fonksiyon açar.
15
+
16
+ ```js
17
+ // routes/10-pages.mjs
18
+ export default function register(app, { route }) {
19
+ app.get("/", route(async () => ({ view: "pages/home" })));
20
+ }
21
+ ```
22
+
23
+ `app` doğrudan Express uygulamasıdır: `app.get`, `app.post`, `app.use`,
24
+ `app.all` — Express 5'in tüm yüzeyi kullanılabilir. `api` ise framework'ün route
25
+ dosyalarına geçirdiği hazır yüzeydir, böylece her dosyada tek tek import yapmak
26
+ gerekmez:
27
+
28
+ | Alan | Karşılığı |
29
+ | --- | --- |
30
+ | `route` | `jskelet` → `route` |
31
+ | `fragment` | `jskelet` → `fragment` |
32
+ | `renderView` | `jskelet` → `renderView` |
33
+ | `renderPage` | `jskelet` → `renderPage` |
34
+ | `notFound` | `jskelet` → `notFound` |
35
+ | `redirect` | `jskelet` → `redirect` |
36
+ | `permanentRedirect` | `jskelet` → `permanentRedirect` |
37
+ | `seeOther` | `jskelet` → `seeOther` |
38
+ | `ogHandler` | `jskelet` → `ogHandler` |
39
+ | `ogImage` | `jskelet` → `ogImage` |
40
+ | `sendOgImage` | `jskelet` → `sendOgImage` |
41
+ | `ImageResponse` | `jskelet` → `ImageResponse` |
42
+
43
+ İstersen doğrudan import da edebilirsin; `api` yalnızca kolaylık:
44
+
45
+ ```js
46
+ import { route, notFound } from "jskelet";
47
+
48
+ export function register(app) {
49
+ app.get("/haber/:slug", route(async ({ params }) => { /* … */ }));
50
+ }
51
+ ```
52
+
53
+ Modül geçerli bir fonksiyon açmazsa uyarı basılır ve atlanır:
54
+ `[router] <file> exports neither a default nor a 'register' function, skipped`.
55
+
56
+ ## Yükleme sırası
57
+
58
+ Dosya sistemine dayalı otomatik URL türetme **yok**. Sıra iki şekilde
59
+ belirlenir:
60
+
61
+ **1. Açık liste (`jskelet.config.mjs` → `routes`).** Proje köküne göre göreli
62
+ yollar, verdiğin sırada yüklenir:
63
+
64
+ ```js
65
+ export default {
66
+ routes: ["./routes/api.js", "./routes/pages.js", "./routes/catch-all.js"],
67
+ };
68
+ ```
69
+
70
+ **2. Liste yoksa `routes/` dizini alfabetik taranır**, ardından her
71
+ `features/<name>/index.js` (veya `.mjs`) alfabetik eklenir. Tarama
72
+ özyinelemelidir (alt dizinler de dâhil), yalnızca `.js` ve `.mjs` dosyaları
73
+ alınır ve adı `_` ile başlayan dosyalar atlanır (`_helpers.js` gibi paylaşılan
74
+ modüller için).
75
+
76
+ Feature-first düzen zorunlu değildir; bir feature örneği:
77
+
78
+ ```
79
+ features/piyasalar/
80
+ index.js # register(app, api) — URL'ler açıkça yazılır
81
+ server/
82
+ views/pages/… # .jsk veya .ejs
83
+ views/components/
84
+ client/ # island'lar; client/entries'ten registerAll
85
+ ```
86
+
87
+ `jskelet generate feature|page|island` iskelet üretir. Filesystem URL routing
88
+ yoktur.
89
+
90
+ Bu durumda dosya adlarına sayısal önek verin:
91
+
92
+ ```
93
+ routes/
94
+ ├── 10-pages.mjs
95
+ ├── 50-blog.mjs
96
+ └── 99-catch-all.mjs
97
+ ```
98
+
99
+ Sıranın açık olması bir tasarım kararı: `/:slug` gibi tek segmentli bir
100
+ yakalayıcı `/hakkinda` rotasından önce kaydedilirse "hakkinda" bir slug sanılır.
101
+ Sırayı dosya adına gizlemek yerine görünür kılmak teşhisi kolaylaştırıyor
102
+ ([02-mimari.md](./02-mimari.md)).
103
+
104
+ Hiç route modülü bulunamazsa uyarı basılır ve sunucu yalnızca statik dosyalar +
105
+ 404 ile ayağa kalkar.
106
+
107
+ ### Bozuk modül davranışı
108
+
109
+ - **Development:** modül import edilemezse uyarı basılır ve atlanır; sunucu
110
+ ayakta kalır.
111
+ - **Production:** hata fırlatılır ve süreç açılmaz. Yarım route tablosuyla
112
+ yayına çıkmak, sessizce 404 dönen sayfalar demek.
113
+
114
+ ## `route()` — controller sarmalayıcısı
115
+
116
+ `route(controller, options?)` bir Express request handler döndürür ve şu işleri
117
+ üstlenir:
118
+
119
+ - `ctx` nesnesini kurar ve controller'ı çağırır.
120
+ - HTML TTL cache'ini uygular (`revalidate` varsa ve metot `GET` ise).
121
+ - `notFound()` / `redirect()` kontrol akışını yakalar.
122
+ - Yanıt başlıklarını yazar: `Content-Type` ve cache durumuna göre
123
+ `Cache-Control` (+ önbelleklenebilir yanıtlarda `X-JSkelet-Cache`).
124
+ - Önbellekte saklanan sıkıştırılmış gövdeyi kullanarak yanıtı gönderir.
125
+
126
+ ```js
127
+ app.get(
128
+ "/hakkinda",
129
+ route(
130
+ async () => ({
131
+ view: "pages/about",
132
+ metadata: { title: "Hakkında", canonical: "/hakkinda" },
133
+ }),
134
+ { revalidate: 300 },
135
+ ),
136
+ );
137
+ ```
138
+
139
+ `options` iki alan kabul eder:
140
+
141
+ | Alan | Tip | Anlamı |
142
+ | --- | --- | --- |
143
+ | `revalidate` | `number` (saniye) | HTML önbellek TTL'i. Verilmezse ya da 0 ise bu route önbelleklenmez. `jskelet.config.mjs` → `cache().html` içindeki eşleşen bir kural bu değeri **ezer**. |
144
+ | `private` | `boolean` | Sayfa ziyaretçiye bağlı. Önbellek devre dışı kalır, `cache().html` deseni bunu **ezemez**, yanıt `private, no-store` ve `Vary: Cookie` ile ETag'siz gider. |
145
+
146
+ `revalidate` verilse bile **query parametresi taşıyan istek varsayılan olarak
147
+ dinamiktir**; o yol için `cache().query` altında bir izin listesi tanımlamak
148
+ gerekir ([06-cache.md](./06-cache.md)).
149
+
150
+ Oturuma bağlı her sayfa `private: true` almalı; önbellek anahtarında kimlik
151
+ olmadığı için bayrak olmadan bir kullanıcının HTML'i bir başkasına servis
152
+ edilir. Framework bu hatayı çalışma zamanında da yakalıyor (controller cookie
153
+ okuduğunda render önbelleğe yazılmaz), ama doğru yer bayrak. Ayrıntılar
154
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
155
+
156
+ ## `fragment()` — layout'suz parça
157
+
158
+ Bir bölgeyi tazeleyen uçlar için. Layout basılmaz, yanıt `private, no-store` ve
159
+ ETag'siz gider, HTML önbelleğine hiç uğramaz. `/_fragment/` öneki
160
+ `robots.txt`'in altına eklenir; parça uçları dizine girmez
161
+ ([04](./04-render-ve-sablonlar.md#robotstxt)).
162
+
163
+ ```js
164
+ app.get(
165
+ "/_fragment/satirlar",
166
+ fragment(async ({ query }) => ({
167
+ view: "partials/rows",
168
+ data: { rows: getRows(Number(query.sayfa ?? 1)) },
169
+ })),
170
+ );
171
+ ```
172
+
173
+ Controller `{ view, data?, status? }` ya da doğrudan bir HTML string döner.
174
+ Hata durumunda tüm sayfa yerine küçük bir uyarı parçası döner
175
+ (`<div role="alert" data-fragment-error>`), çünkü takas edilen bölge bir hata
176
+ sayfasının tamamını içine almamalı.
177
+
178
+ `fragment()` POST için de kullanılabilir: form gönderiminin cevabı olarak
179
+ güncellenmiş parçayı döndürmenin yolu bu, ve şablonda `csrfField()`
180
+ çalışabilmesi için gereken istek bağlamını da kuruyor.
181
+
182
+ ## `ctx` — controller bağlamı
183
+
184
+ Controller tek argüman alır:
185
+
186
+ ```js
187
+ {
188
+ params, // Express route parametreleri (req.params)
189
+ query, // Ayrıştırılmış query string (req.query)
190
+ pathname, // req.path — query'siz yol
191
+ req, // Express Request; ihtiyaç olursa tam erişim
192
+ }
193
+ ```
194
+
195
+ `params` Express'in kendi desen sözdizimini kullanır (Express 5 /
196
+ `path-to-regexp`), config'teki `source` sözdizimini değil:
197
+
198
+ ```js
199
+ app.get("/haber/:slug", route(async ({ params }) => {
200
+ const article = await getArticle(params.slug);
201
+ if (!article) notFound();
202
+ return { view: "pages/article", data: { article } };
203
+ }));
204
+ ```
205
+
206
+ `pathname` hem cache anahtarında hem de `renderPage`'e geçen `pathname`
207
+ local'inde kullanılır; layout'un "bu ana sayfa mı" gibi kararları buna bakar.
208
+
209
+ ## Controller'ın döndürdüğü sayfa tanımı
210
+
211
+ Controller `async (ctx) => sayfa` biçimindedir ve şu alanları döndürebilir:
212
+
213
+ | Alan | Tip | Varsayılan | Anlamı |
214
+ | --- | --- | --- | --- |
215
+ | `view` | `string` | — | `views/` altındaki şablon yolu, uzantısız: `"pages/home"` → `views/pages/home.jsk` (yoksa legacy `.ejs`). |
216
+ | `data` | `object` | `{}` | Şablona local olarak geçen veriler. |
217
+ | `metadata` | `object` | `{}` | `<head>` etiketlerine çevrilir; `hooks.metadata()` çıktısının üzerine biner. Şema: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md). |
218
+ | `status` | `number` | `200` | HTTP durum kodu. Yalnızca 200 önbelleğe yazılır. |
219
+ | `head` | `string` | `""` | `<head>`e olduğu gibi basılacak ham HTML (ör. LCP preload'ı). |
220
+ | `bodyClass` | `string` | `hooks.layoutContext().bodyClass ?? ""` | `<body class="…">`. |
221
+ | `entries` | `string[]` | `[]` | Bu sayfada ek olarak yüklenecek client entry adları: `["chart.js"]`. |
222
+ | `styles` | `string[]` | `[]` | Bu sayfada ek olarak yüklenecek stylesheet adları: `["home.css"]` → `styles/pages/home.css`. |
223
+
224
+ `revalidate` **`route()`'un ikinci argümanıdır**, controller'ın döndürdüğü
225
+ nesnenin alanı değil.
226
+
227
+ Örnek, hepsi bir arada:
228
+
229
+ ```js
230
+ import { headHints } from "jskelet";
231
+
232
+ app.get(
233
+ "/piyasalar",
234
+ route(
235
+ async ({ query }) => {
236
+ const data = await getMarkets(query.tab ?? "hisse");
237
+
238
+ return {
239
+ view: "pages/markets",
240
+ data: { markets: data.items, tab: query.tab ?? "hisse" },
241
+ metadata: {
242
+ title: "Piyasalar",
243
+ canonical: "/piyasalar",
244
+ openGraph: { image: data.cover },
245
+ },
246
+ head: headHints({ href: data.cover }),
247
+ bodyClass: "bg-slate-50",
248
+ entries: ["chart.js"],
249
+ styles: ["markets.css"],
250
+ };
251
+ },
252
+ { revalidate: 30 },
253
+ ),
254
+ );
255
+ ```
256
+
257
+ ## `notFound()` ve `redirect()`
258
+
259
+ `next/navigation` içindeki kontrol akışının karşılığı: derinlerdeki bir
260
+ fonksiyon `throw` eder, framework yakalar. Böylece veri katmanındaki bir
261
+ fonksiyon, controller'a dönüş değeri taşımak zorunda kalmadan 404 üretebilir.
262
+
263
+ ```js
264
+ import { notFound, redirect, permanentRedirect, seeOther } from "jskelet";
265
+
266
+ notFound(); // 404 → hooks.notFound() sayfası
267
+ redirect("/yeni-adres"); // 307 (geçici, metodu korur)
268
+ permanentRedirect("/yeni"); // 308 (kalıcı, metodu korur)
269
+ seeOther("/panel"); // 303 (POST sonrası)
270
+ ```
271
+
272
+ Dördü de `never` döner (her zaman fırlatır). Ayrıntı:
273
+
274
+ - `notFound()` → `NotFoundError` (`statusCode: 404`)
275
+ - `redirect(location)` → `RedirectError` (`statusCode: 307`)
276
+ - `permanentRedirect(location)` → `RedirectError` (`statusCode: 308`)
277
+ - `seeOther(location)` → `RedirectError` (`statusCode: 303`)
278
+
279
+ Bir POST handler'ında `redirect()` değil `seeOther()` kullanılır: 307 metodu
280
+ koruyor, yani tarayıcı hedefe yeniden POST ediyor. "Post/redirect/get" akışı —
281
+ geri tuşunun formu yeniden göndermediği akış — 303 gerektiriyor.
282
+
283
+ Özel bir durum kodu gerekiyorsa sınıfı doğrudan kullanabilirsin:
284
+
285
+ ```js
286
+ import { RedirectError } from "jskelet";
287
+
288
+ throw new RedirectError("/eski-kurulum-uyumu", 301);
289
+ ```
290
+
291
+ Ayırt etmek için `isNotFoundError(error)` ve `isRedirectError(error)` dışa açık.
292
+
293
+ Yakalanma noktaları:
294
+
295
+ 1. **`route()` içinde:** redirect doğrudan yanıta yazılır; notFound `produce()`
296
+ içinde yakalanır ve 404 sayfası üretilir (bu çıktı önbelleğe **yazılmaz**,
297
+ çünkü yalnızca 200 saklanır).
298
+ 2. **Express hata yöneticisinde:** bir middleware ya da route dışı kodda
299
+ fırlatılmışsa burada karşılanır.
300
+
301
+ ## 404 sayfası
302
+
303
+ Bir istek hiçbir route'a düşmezse framework `hooks.notFound()` hook'unu çağırır
304
+ ve dönen sayfa tanımını `pathname: "/404"` ile render eder.
305
+
306
+ ```js
307
+ // jskelet.config.mjs
308
+ export default {
309
+ hooks: {
310
+ notFound() {
311
+ return {
312
+ view: "pages/not-found",
313
+ metadata: { title: "Sayfa bulunamadı", robots: { index: false } },
314
+ };
315
+ },
316
+ },
317
+ };
318
+ ```
319
+
320
+ Hook tanımlı değilse ya da 404 render'ı da hata verirse framework şablonsuz,
321
+ minimal bir HTML döner. Bu geri dönüş bilinçli olarak şablonsuz: 404 render'ı da
322
+ patlarsa ziyaretçi boş yanıt görmesin.
323
+
324
+ ## Hata sayfaları (500 ve diğerleri)
325
+
326
+ Bir controller ya da middleware beklenmeyen bir hata fırlattığında Express'in
327
+ hata yöneticisi devreye girer, hatayı loglar ve framework'ün kendi hata sayfasını
328
+ `Cache-Control: no-store` ile döner. Durum kodu hatanın `statusCode` (ya da
329
+ `status`) alanından okunur; 400–599 aralığında değilse 500 kullanılır.
330
+
331
+ **Development** (`NODE_ENV=development`, yani `jskelet dev`): 5xx yanıtlarında
332
+ gömülü 500 sayfası ve `hooks.error()` atlanır; mesaj, yığın izi ve varsa
333
+ `cause` zinciri içeren bir teşhis sayfası döner. 4xx (404 vb.) development'ta
334
+ da her zamanki gibi durum sayfasıdır.
335
+
336
+ **Production**: framework'ün sayfası bilinçli olarak yalın — durum kodu, tek
337
+ satır başlık ve tek satır açıklama. Marka adı, gezinme ya da hata ayrıntısı
338
+ taşımaz; sunucunun içi ziyaretçiye açılmaz. Dil `brand.lang`ten gelir (`tr` ve
339
+ `en` hazır, diğerleri `en`e düşer).
340
+
341
+ Kendi sayfanı vermek için `hooks.error()` (yalnızca production / 4xx):
342
+
343
+ ```js
344
+ // jskelet.config.mjs
345
+ export default {
346
+ hooks: {
347
+ error({ status }) {
348
+ return {
349
+ view: "pages/error",
350
+ data: { status },
351
+ metadata: { title: "Bir hata oluştu", robots: { index: false } },
352
+ };
353
+ },
354
+ },
355
+ };
356
+ ```
357
+
358
+ Hook bir sayfa tanımı yerine doğrudan HTML string de döndürebilir; layout'a
359
+ bağlı olmayan bir hata sayfası istiyorsan bu yol daha güvenli, çünkü layout'un
360
+ kendisi hata veriyorsa sayfa tanımı da render edilemez. Hook yoksa, `null`
361
+ dönerse ya da render'ı patlarsa framework gömülü sayfaya düşer.
362
+
363
+ 404 için `hooks.notFound()` önceliklidir; yalnızca o tanımlı değilse
364
+ `hooks.error()` `status: 404` ile çağrılır.
365
+
366
+ Sayfayı programatik olarak da üretebilirsin:
367
+
368
+ ```js
369
+ import { renderStatusPage } from "jskelet";
370
+
371
+ const html = await renderStatusPage(503);
372
+ ```
373
+
374
+ ## Layout'suz render: `renderView`
375
+
376
+ `renderView(view, data)` tek bir şablonu layout olmadan render eder ve string
377
+ döner. Fragment uçları, e-posta şablonları ve island'ların sonradan çektiği
378
+ HTML parçaları için:
379
+
380
+ ```js
381
+ export default function register(app, { renderView }) {
382
+ app.get("/_fragment/yorumlar/:id", async (req, res) => {
383
+ const comments = await getComments(req.params.id);
384
+ res.type("html").send(await renderView("fragments/comments", { comments }));
385
+ });
386
+ }
387
+ ```
388
+
389
+ `/_fragment/` öneki varsayılan `prewarmSkip` listesinde yer alır, yani ısıtma
390
+ turu bu uçları taramaz ([06-cache.md](./06-cache.md)).
391
+
392
+ ## Config: `redirects()`
393
+
394
+ `jskelet.config.mjs` → `redirects()` bir dizi döndürür ve middleware zincirinde
395
+ route'lardan **önce** çalışır (bkz. [02-mimari.md](./02-mimari.md)).
396
+
397
+ ```js
398
+ export default {
399
+ async redirects() {
400
+ return [
401
+ { source: "/eski-blog/:slug", destination: "/blog/:slug", permanent: true },
402
+ { source: "/kampanya", destination: "/kampanyalar" },
403
+ { source: "/legacy", destination: "/", statusCode: 301 },
404
+ ];
405
+ },
406
+ };
407
+ ```
408
+
409
+ Davranış:
410
+
411
+ - **İlk eşleşen kural kazanır**, sonrası denenmez. Sıralama config'teki yazım
412
+ sırasıdır.
413
+ - **Query string korunur:** `/eski-blog/x?utm=a` → `/blog/x?utm=a`. Yönlendirme
414
+ kampanya parametrelerini düşürürse trafik kaynağı kaybolur.
415
+ - **Durum kodu:** `permanent: true` → 308, aksi hâlde 307 (Next semantiği).
416
+ Farklı bir kod isteyen `statusCode` verebilir; örneğin eski kurulumlarla uyum
417
+ için 301.
418
+ - `source` ya da `destination` geçersizse kural sessizce düşmez, uyarı basılır.
419
+
420
+ ## Config: `trailingSlash`
421
+
422
+ `trailingSlash: true` iken kanonik sayfa URL'leri `/` ile biter ve **200**
423
+ döner; slash'sız istek **308** ile slash'lıya gider. Ayrıntı ve istisnalar:
424
+ [07-yapilandirma.md](./07-yapilandirma.md#trailingslash).
425
+
426
+ ## Config: `rewrites()`
427
+
428
+ Rewrite, tarayıcının adres çubuğunu değiştirmeden isteği başka bir yere taşır.
429
+ İki faz vardır:
430
+
431
+ ```js
432
+ export default {
433
+ async rewrites() {
434
+ return {
435
+ beforeFiles: [
436
+ { source: "/sitemap-:page.xml", destination: "/sitemap?page=:page" },
437
+ ],
438
+ afterFiles: [
439
+ { source: "/api/:path*", destination: "https://api.example.com/:path*" },
440
+ ],
441
+ };
442
+ },
443
+ };
444
+ ```
445
+
446
+ Bir dizi döndürürsen tamamı `afterFiles` sayılır:
447
+
448
+ ```js
449
+ async rewrites() {
450
+ return [{ source: "/api/:path*", destination: "https://api.example.com/:path*" }];
451
+ }
452
+ ```
453
+
454
+ - **`beforeFiles`** statik dosyalardan da önce çalışır. `/assets/…` gibi yolları
455
+ yeniden yazmak gerekiyorsa buraya konmalı.
456
+ - **`afterFiles`** statik denendikten sonra, route'lardan önce çalışır.
457
+
458
+ Hedefin biçimi davranışı belirler:
459
+
460
+ - **Mutlak (`http://` / `https://`):** istek gömülü ters proxy ile dışa taşınır.
461
+ Harici paket yok; `fetch` ile stream eden ince bir katman. Hop-by-hop
462
+ başlıklar (`host`, `connection`, `content-length`, `accept-encoding`)
463
+ temizlenir; yanıtta `content-encoding`, `content-length`,
464
+ `transfer-encoding`, `connection` düşürülür. `redirect: "manual"` sayesinde
465
+ upstream'in 302'si burada tüketilmez, tarayıcıya iletilir.
466
+ - **Göreli:** yalnızca `req.url` değiştirilir ve istek kendi route tablosunda
467
+ devam eder. Bu fazda ilk eşleşen kural döngüyü kırar.
468
+
469
+ Tipik kullanım `/api/*` yolunu backend'e taşımaktır. Tarayıcı bunu same-origin
470
+ çağırdığı için CORS ve third-party cookie sorunları oluşmaz.
471
+
472
+ ### Elle proxy: `createProxy`
473
+
474
+ Aynı proxy'yi kendi route'unda da kullanabilirsin:
475
+
476
+ ```js
477
+ import { createProxy } from "jskelet";
478
+
479
+ export default function register(app) {
480
+ app.use("/ws-api", createProxy((req) => `${process.env.API_ORIGIN}${req.url}`));
481
+ }
482
+ ```
483
+
484
+ `resolveTarget` fırlatırsa ya da boş döndürürse istek proxy'lenmez ve zincire
485
+ devam eder: hedef origin yapılandırılmamış bir kurulumda 500 yerine normal bir
486
+ 404 almak daha doğru.
487
+
488
+ ## `source` desen sözdizimi
489
+
490
+ `redirects()`, `rewrites()`, `headers()` ve `cache().html` aynı küçük desen
491
+ derleyicisini kullanır. Bu, Next'in tam `path-to-regexp` yüzeyi değil; config'te
492
+ fiilen kullanılan alt küme bilinçli olarak seçildi.
493
+
494
+ | Desen | Anlamı |
495
+ | --- | --- |
496
+ | `/haber/:slug` | Tek segment yakalar (`[^/]+`) |
497
+ | `/:path*` | Sıfır veya daha fazla segment yakalar; öndeki `/` opsiyoneldir, yani `/blog/:path*` `/blog`u da kapsar |
498
+ | `/:path*.svg` | Joker + sabit son ek; uzantı kuralları böyle yazılır |
499
+ | `/etiket-:slug` | Segment ortasında parametre |
500
+
501
+ Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
502
+ Parametre adı `[A-Za-z_][A-Za-z0-9_]*` kalıbına uymalıdır.
503
+
504
+ `source` mutlaka `/` ile başlamalı; başlamazsa kural yok sayılır ve uyarı
505
+ basılır (``[config] invalid source (must start with `/`): …``). Tanınmayan bir
506
+ sözdizimi sessizce literal kabul edilmez.
507
+
508
+ Tam desen listesi ve config referansı: [07-yapilandirma.md](./07-yapilandirma.md).
509
+
510
+ ## Sırada ne var
511
+
512
+ - Şablon katmanı, bileşenler ve metadata:
513
+ [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
514
+ - `revalidate`, cache anahtarı ve `X-JSkelet-Cache`: [06-cache.md](./06-cache.md)
515
+ - Config alanlarının tam referansı: [07-yapilandirma.md](./07-yapilandirma.md)