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