jskelet 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +628 -620
  3. package/LICENSE +21 -21
  4. package/README.md +2 -0
  5. package/bin/jskelet.mjs +130 -130
  6. package/docs/01-baslangic.md +291 -291
  7. package/docs/02-mimari.md +310 -310
  8. package/docs/03-routing.md +515 -515
  9. package/docs/04-render-ve-sablonlar.md +667 -661
  10. package/docs/05-islands.md +486 -486
  11. package/docs/06-cache.md +1467 -1443
  12. package/docs/07-yapilandirma.md +1208 -1197
  13. package/docs/08-build.md +429 -429
  14. package/docs/09-dev-araclari.md +364 -364
  15. package/docs/10-dagitim.md +348 -338
  16. package/docs/12-panel-ve-oturum.md +479 -478
  17. package/docs/README.md +83 -83
  18. package/docs/en/01-getting-started.md +298 -298
  19. package/docs/en/02-architecture.md +329 -329
  20. package/docs/en/03-routing.md +531 -531
  21. package/docs/en/04-rendering.md +675 -669
  22. package/docs/en/05-islands.md +497 -497
  23. package/docs/en/06-caching.md +1476 -1453
  24. package/docs/en/07-configuration.md +1229 -1219
  25. package/docs/en/08-build.md +447 -447
  26. package/docs/en/09-dev-tools.md +373 -373
  27. package/docs/en/10-deployment.md +351 -340
  28. package/docs/en/11-migration.md +398 -398
  29. package/docs/en/12-dashboards-and-sessions.md +489 -488
  30. package/docs/en/README.md +87 -87
  31. package/package.json +137 -137
  32. package/src/build/ensure-build.mjs +19 -19
  33. package/src/build/paths.mjs +153 -153
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +349 -349
  36. package/src/build/tasks/css.mjs +235 -235
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +357 -357
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/build/tasks/templates.mjs +20 -20
  42. package/src/client/admin/i18n.js +764 -764
  43. package/src/client/admin/login.html +74 -74
  44. package/src/client/admin/panel.css +809 -809
  45. package/src/client/admin/panel.html +495 -495
  46. package/src/client/admin/panel.js +1251 -1251
  47. package/src/client/devtools/report.html +185 -185
  48. package/src/client/devtools/report.js +745 -745
  49. package/src/client/devtools/seo.js +628 -628
  50. package/src/client/dom.js +95 -95
  51. package/src/client/form.js +192 -192
  52. package/src/client/index.js +45 -45
  53. package/src/client/registry.js +305 -305
  54. package/src/client/safe-image.js +91 -91
  55. package/src/client/shared-cookie.js +225 -225
  56. package/src/client/store.js +36 -36
  57. package/src/client/swap.js +188 -188
  58. package/src/compile/codegen.js +336 -336
  59. package/src/compile/compile-all.js +149 -149
  60. package/src/compile/errors.js +66 -66
  61. package/src/compile/expr.js +409 -409
  62. package/src/compile/index.js +17 -17
  63. package/src/compile/parse.js +541 -541
  64. package/src/compile/resolve.js +211 -211
  65. package/src/compile/scan-exports.js +51 -51
  66. package/src/config/defaults.js +541 -534
  67. package/src/config/index.js +1500 -1469
  68. package/src/config/pattern.js +107 -107
  69. package/src/generate.mjs +163 -163
  70. package/src/http/control-flow.js +71 -71
  71. package/src/http/cookies-entry.js +21 -21
  72. package/src/http/cookies.js +277 -277
  73. package/src/http/request-cache.js +46 -46
  74. package/src/http/request-context.js +165 -165
  75. package/src/http/shared-cookie.js +178 -178
  76. package/src/index.js +101 -101
  77. package/src/init.mjs +232 -230
  78. package/src/migrate/apply.mjs +262 -262
  79. package/src/migrate/babel.mjs +79 -79
  80. package/src/migrate/classify.mjs +155 -155
  81. package/src/migrate/config.mjs +126 -126
  82. package/src/migrate/fs-walk.mjs +191 -191
  83. package/src/migrate/parse.mjs +26 -26
  84. package/src/migrate/scan.mjs +177 -177
  85. package/src/migrate/transform/expr-source.mjs +168 -168
  86. package/src/migrate/transform/island.mjs +67 -67
  87. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  88. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  89. package/src/migrate/transform/page-split.mjs +435 -435
  90. package/src/migrate/write.mjs +81 -81
  91. package/src/migrate.mjs +171 -171
  92. package/src/runtime/alias-hooks.mjs +119 -119
  93. package/src/runtime/register.mjs +4 -4
  94. package/src/server/admin/actions.js +229 -229
  95. package/src/server/admin/auth.js +125 -125
  96. package/src/server/admin/event-log.js +151 -151
  97. package/src/server/admin/gate.js +209 -209
  98. package/src/server/admin/inventory.js +188 -188
  99. package/src/server/admin/mount.js +56 -56
  100. package/src/server/admin/router.js +216 -216
  101. package/src/server/admin/snapshot.js +241 -241
  102. package/src/server/assets.js +147 -147
  103. package/src/server/auth/handoff.js +309 -309
  104. package/src/server/cache-blob.js +70 -70
  105. package/src/server/cache-control.js +45 -0
  106. package/src/server/cache-deps.js +42 -42
  107. package/src/server/cache-vary.js +113 -113
  108. package/src/server/cloudflare.js +607 -607
  109. package/src/server/create-app.js +366 -366
  110. package/src/server/data-cache.js +553 -553
  111. package/src/server/dev/report.js +485 -485
  112. package/src/server/dev/socket.js +170 -170
  113. package/src/server/dev/version-check.mjs +139 -139
  114. package/src/server/disk-cache.js +233 -233
  115. package/src/server/ejs-adapter.js +59 -59
  116. package/src/server/html-cache.js +1196 -1196
  117. package/src/server/image-optimizer.js +500 -500
  118. package/src/server/logs/access-middleware.js +66 -66
  119. package/src/server/logs/file-sink.js +193 -193
  120. package/src/server/logs/pipeline.js +165 -165
  121. package/src/server/logs/s3-put.js +214 -214
  122. package/src/server/logs/s3-sink.js +112 -112
  123. package/src/server/metadata.js +102 -102
  124. package/src/server/middleware/compression.js +205 -205
  125. package/src/server/middleware/csrf.js +134 -134
  126. package/src/server/middleware/dev-gate.js +75 -75
  127. package/src/server/middleware/headers.js +37 -37
  128. package/src/server/middleware/redirects.js +32 -32
  129. package/src/server/middleware/robots-txt.js +341 -341
  130. package/src/server/middleware/static-precompressed.js +121 -121
  131. package/src/server/middleware/trailing-slash.js +53 -53
  132. package/src/server/middleware/upstream-proxy.js +141 -141
  133. package/src/server/og-image.js +369 -356
  134. package/src/server/port-guard.js +255 -255
  135. package/src/server/prewarm.js +1082 -1082
  136. package/src/server/redis.js +588 -588
  137. package/src/server/render.js +910 -910
  138. package/src/server/router.js +157 -157
  139. package/src/server/status-page.js +265 -265
  140. package/src/server/upstream-limiter.js +376 -376
  141. package/src/server/upstream-tracking.js +166 -166
  142. package/src/shared/cookie-domain.js +66 -66
  143. package/src/start.mjs +22 -22
  144. package/src/templates/layout.ejs +30 -30
  145. package/src/templates/layout.jsk +30 -30
  146. package/src/version.mjs +31 -31
  147. package/src/views/components/loader.js +101 -101
  148. package/src/views/helpers/html.js +102 -102
  149. package/src/views/helpers/tags.js +375 -375
  150. package/types/config/defaults.d.ts +6 -0
  151. package/types/config/index.d.ts +6 -0
  152. package/types/server/cache-control.d.ts +28 -0
  153. package/types/server/og-image.d.ts +5 -0
@@ -1,1197 +1,1208 @@
1
- # 07 — Yapılandırma referansı
2
-
3
- Bu belge `jskelet.config.mjs`'in tam referansıdır: her alan, tipi, varsayılanı
4
- ve örneği. Ardından `source` desen sözdizimi ve framework'ün okuduğu tüm ortam
5
- değişkenleri tablosu geliyor. Alanların davranışsal ayrıntıları için ilgili
6
- belgelere bağlantı verildi; buradaki amaç tek bakışta tam liste sunmak.
7
-
8
- ## Dosyanın konumu ve yüklenmesi
9
-
10
- Config dosyası proje kökünde `jskelet.config.mjs` adıyla aranır ve **zorunlu
11
- değildir**. Yoksa ya da okunamıyorsa uyarı basılır ve sunucu varsayılanlarla
12
- ayağa kalkar; bozuk bir düzenleme siteyi açılamaz hâle getirmemeli.
13
-
14
- ```js
15
- // jskelet.config.mjs
16
- export default {
17
- // …
18
- };
19
- ```
20
-
21
- Default export yoksa modülün kendisi config olarak kullanılır (named export'lar).
22
-
23
- `headers()`, `redirects()`, `rewrites()` ve `cache()` bölümleri fonksiyon **ya da
24
- düz değer** olabilir; fonksiyon olmaları hâlinde `async` olabilirler ve `this`
25
- config nesnesine bağlıdır. Bir bölüm hata verirse yalnızca o bölüm yok sayılır.
26
-
27
- Config başarıyla yüklendiğinde bir özet basılır:
28
- `[config] jskelet.config.mjs loaded — 3 headers, 2 redirects, 1 cache rule`
29
-
30
- ## Tam örnek
31
-
32
- ```js
33
- // jskelet.config.mjs
34
- export default {
35
- paths: {
36
- views: "views",
37
- public: "public",
38
- client: "client",
39
- routes: "routes",
40
- styles: "styles/globals.css",
41
- generated: ".jskelet",
42
- },
43
-
44
- brand: {
45
- name: "Örnek",
46
- poweredBy: "Örnek",
47
- cacheHeader: "X-Ornek-Cache",
48
- devBasePath: "/__ornek/dev",
49
- prewarmUserAgent: "ornek-prewarm",
50
- devTokenCookie: "dev_token",
51
- lang: "tr",
52
- },
53
-
54
- layout: "views/layout.jsk",
55
- routes: ["./routes/10-pages.mjs", "./routes/99-catch-all.mjs"],
56
- trailingSlash: false,
57
-
58
- static: {
59
- extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2"],
60
- prefixes: ["/assets/", "/fonts/"],
61
- },
62
-
63
- devGateBypass: ["/api/healthcheck", "/robots.txt"],
64
- preconnect: ["https://cdn.ornek.com"],
65
-
66
- security: {
67
- trustProxy: true,
68
- cookieSecret: process.env.JSKELET_SECRET,
69
- csrf: {
70
- enabled: true,
71
- token: false,
72
- allowedOrigins: [],
73
- exclude: ["/webhook/:path*"],
74
- cookieName: "csrf_token",
75
- fieldName: "_csrf",
76
- headerName: "x-csrf-token",
77
- },
78
- },
79
-
80
- navigation: {
81
- prefetch: "moderate",
82
- prerender: "conservative",
83
- viewTransition: true,
84
- exclude: ["/cikis"],
85
- },
86
-
87
- prewarmSkip: ["/api/", "/_fragment/", "/__ornek/"],
88
- watch: ["data"],
89
-
90
- fonts: [{ family: "Inter", weights: [400, 600, 700] }],
91
- icons: { dir: "icons", scan: ["views", "client", "routes", "lib"] },
92
- images: { widths: [400, 800, 1200], quality: 78, skip: ["indirmeler"] },
93
- clientEnv: ["PUBLIC_WS_URL"],
94
-
95
- async headers() {
96
- return [
97
- {
98
- source: "/:path*",
99
- headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }],
100
- },
101
- ];
102
- },
103
-
104
- async redirects() {
105
- return [{ source: "/eski/:slug", destination: "/yeni/:slug", permanent: true }];
106
- },
107
-
108
- async rewrites() {
109
- return {
110
- afterFiles: [
111
- { source: "/api/:path*", destination: "https://api.ornek.com/:path*" },
112
- ],
113
- };
114
- },
115
-
116
- async cache() {
117
- return {
118
- html: { "/": 60, "/haber/:slug": 300 },
119
- query: { "/arama": ["q", "page"] },
120
- maxEntries: 500,
121
- data: { maxEntries: 10000, staleFactor: 10 },
122
- prewarm: {
123
- enabled: true,
124
- max: 400,
125
- concurrency: 4,
126
- rps: 0,
127
- intervalSeconds: 0,
128
- rotate: true,
129
- priority: ["/", "/haber/:slug"],
130
- },
131
- };
132
- },
133
-
134
- hooks: {
135
- metadata() { /* … */ },
136
- layoutContext() { /* … */ },
137
- notFound() { /* … */ },
138
- error() { /* … */ },
139
- prewarmPaths() { /* … */ },
140
- },
141
- };
142
- ```
143
-
144
- ## `paths`
145
-
146
- **Tip:** `Record<string, string>` — **Varsayılan:** aşağıdaki tablo
147
-
148
- Proje kökündeki dizin (ve `styles` için dosya) adları. Değerler proje köküne
149
- göre çözülür ve içeride mutlak yola çevrilir.
150
-
151
- | Anahtar | Varsayılan | İçeriği |
152
- | --- | --- | --- |
153
- | `views` | `"views"` | Layout, sayfalar, bileşenler (klasik kök; `.jsk` / `.ejs`) |
154
- | `features` | `"features"` | Feature-first dilimler (`<name>/{server,views,client}`) |
155
- | `shared` | `"shared"` | Özellikler arası paylaşılan server/views/client |
156
- | `public` | `"public"` | Statik dosyalar; build çıktısı da buraya yazılır |
157
- | `client` | `"client"` | Island runtime kaynakları ve entry'ler |
158
- | `routes` | `"routes"` | Route modülleri |
159
- | `styles` | `"styles/globals.css"` | Tailwind/PostCSS giriş **dosyası** |
160
- | `generated` | `".jskelet"` | `manifest.json`, `templates/`, `metafile.json`, `images.json` |
161
-
162
- `styles` bir dosya yolu olduğu hâlde aynı çözümlemeden geçer; ayrı bir alan
163
- tutmaya değmiyor.
164
-
165
- İki yol her zaman türetilir ve ezilemez: `public/assets` (hash'li build
166
- çıktısı) ve `public/fonts` (self-host fontlar).
167
-
168
- ```js
169
- paths: { views: "src/views", routes: "src/routes", styles: "src/styles/main.css" }
170
- ```
171
-
172
- ## `brand`
173
-
174
- **Tip:** `object` — **Varsayılan:** aşağıdaki tablo
175
-
176
- Markalama ve tek yerden değiştirilebilir isimler. Fork eden ya da beyaz etiket
177
- kullanan projeler kendi adını verebilir. Verilen alanlar varsayılanlarla sığ
178
- birleştirilir.
179
-
180
- | Alan | Tip | Varsayılan | Anlamı |
181
- | --- | --- | --- | --- |
182
- | `name` | `string` | `"JSkelet"` | Görüntü adı |
183
- | `poweredBy` | `string` | `"JSkelet"` | `X-Powered-By` başlığının değeri |
184
- | `cacheHeader` | `string` | `"X-JSkelet-Cache"` | HTML cache durumu başlığı ([06-cache.md](./06-cache.md)) |
185
- | `devBasePath` | `string` | `"/__jskelet/dev"` | Dev overlay ve rapor uçlarının kökü |
186
- | `prewarmUserAgent` | `string` | `"jskelet-prewarm"` | Isıtma isteklerinin UA'sı; dev paneli bunu filtreler |
187
- | `devTokenCookie` | `string` | `"dev_token"` | Dev gate'in çerez ve query parametresi adı |
188
- | `lang` | `string` | — | `<html lang>` varsayılanı. Verilmezse layout `"en"` kullanır. |
189
- | `sharedCookieRoots` | `string[]` | `[]` | Paylaşımlı cookie Domain kökleri (örn. `.investvio.com`, `.localhost`). [12](./12-panel-ve-oturum.md) |
190
-
191
- `lang` için öncelik sırası: `hooks.layoutContext()` → `lang` **>** `brand.lang`
192
- **>** `"en"`.
193
-
194
- ```js
195
- brand: {
196
- lang: "tr",
197
- poweredBy: "Örnek",
198
- sharedCookieRoots: [".investvio.com", ".localhost"],
199
- }
200
- ```
201
-
202
- ## `auth`
203
-
204
- **Tip:** `object` — **Varsayılan:** `{ crossSubdomainHandoff: false }`
205
-
206
- Kimlik framework'te yok; bu bölüm yalnızca alt alan adları arasında kısa
207
- session id taşımak için handoff köprüsünü açar.
208
-
209
- | Alan | Tip | Varsayılan | Anlamı |
210
- | --- | --- | --- | --- |
211
- | `crossSubdomainHandoff` | `boolean \| object` | `false` | Açıkken `POST /_jskelet/auth/handoff` + `?handoff=` redeem. Object: `allowedCookieNames` (zorunlu), `ttlSeconds?`, `path?`, `maxValueBytes?`, `maxPendingTickets?`, `maxMintsPerIpPerMinute?` |
212
-
213
- ```js
214
- auth: {
215
- crossSubdomainHandoff: {
216
- allowedCookieNames: ["sid"],
217
- ttlSeconds: 60,
218
- },
219
- },
220
- ```
221
-
222
- Mint uç noktası CSRF middleware'inden **sonra** mount edilir (origin kontrolü).
223
- Cookie adı allowlist dışındaysa veya RFC 6265 token değilse 400. Ayrıntı:
224
- [12-panel-ve-oturum.md](./12-panel-ve-oturum.md).
225
-
226
- ## `layout`
227
-
228
- **Tip:** `string` — **Varsayılan:** yok (otomatik çözüm)
229
-
230
- Layout dosyasının yolu (`.jsk` veya legacy `.ejs`). Verilen değer **views
231
- dizininin üst dizinine** göre çözülür, yani varsayılan `views` ile
232
- `"views/ozel.jsk"` → `<root>/views/ozel.jsk`.
233
-
234
- Verilmezse sırayla: `views/layout.jsk`, `views/layout.ejs` (legacy), yoksa
235
- framework'ün `src/templates/layout.jsk` varsayılanı. Ayrıntı:
236
- [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md).
237
-
238
- ## `routes`
239
-
240
- **Tip:** `string[]` — **Varsayılan:** `null` (dizin taraması)
241
-
242
- Route modüllerinin açık listesi, proje köküne göre. Verilen sırada yüklenir.
243
- Verilmezse `paths.routes` dizini alfabetik ve özyinelemeli olarak taranır.
244
- Ayrıntı: [03-routing.md](./03-routing.md).
245
-
246
- ```js
247
- routes: ["./routes/api.js", "./routes/pages.js", "./routes/catch-all.js"]
248
- ```
249
-
250
- ## `trailingSlash`
251
-
252
- **Tip:** `boolean` — **Varsayılan:** `false`
253
-
254
- `true` iken kanonik URL'ler `/` ile biter: `/hakkinda/` doğrudan **200**
255
- döner; slash'sız `/hakkinda` **308** ile `/hakkinda/` adresine gider (301
256
- değil — metodu koruyan kalıcı yönlendirme, framework'ün diğer `permanent`
257
- redirect'leriyle aynı). Query string korunur.
258
-
259
- İstisnalar: kök `/`, uzantılı dosya yolları (`/robots.txt`, `/assets/app.js`)
260
- ve `/.well-known/**`. Bunlara slash eklenmez.
261
-
262
- `false` iken (varsayılan) slash dayatılmaz. Express non-strict eşleşme ile
263
- `/x` ve `/x/` ikisi de 200 olabilir — Next.js'in varsayılan "slash'ı kırp"
264
- davranışından bilinçli fark; mevcut siteleri kırmamak için.
265
-
266
- Açıkken şablonlardaki `href`, sitemap ve `redirects()` hedeflerini de
267
- slash'lı yazın; aksi hâlde tarayıcı her tıklamada ekstra bir 308 görür.
268
-
269
- ```js
270
- trailingSlash: true
271
- ```
272
-
273
- ## `static`
274
-
275
- **Tip:** `{ extensions?: string[], prefixes?: string[] }` — **Varsayılan:**
276
- aşağıda
277
-
278
- Uzantı ve önek bazlı statik dosya tespiti. Bu listeye uyan yollara
279
- `Cache-Control: public, max-age=31536000, immutable` yazılır.
280
-
281
- | Alan | Varsayılan |
282
- | --- | --- |
283
- | `extensions` | `[".svg", ".png", ".webp", ".avif", ".ico", ".woff2"]` |
284
- | `prefixes` | `["/assets/", "/fonts/"]` |
285
-
286
- Verilirse varsayılanın **yerine** geçer (birleştirilmez), yani varsayılana ek
287
- yapmak isterseniz tam listeyi yazın.
288
-
289
- ```js
290
- static: {
291
- extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2", ".mp4"],
292
- prefixes: ["/assets/", "/fonts/", "/video/"],
293
- }
294
- ```
295
-
296
- ## `devGate`
297
-
298
- **Tip:** `boolean` — **Varsayılan:** `false`
299
-
300
- Yayına açılmamış ortamı gizler. **`DEV_TOKEN` tek başına siteyi kilitlemez.**
301
- Paylaşılan bir task tanımı production'a da aynı değişkeni taşıyabilir; o
302
- durumda ziyaretçi token vermek zorunda kalmaz, site açık kalır.
303
-
304
- Gate'i açmak için `devGate: true` ya da `DEV_GATE=1`. İkisi de varken token
305
- taşımayan isteğe 404 döner. `DEV_GATE=0` config'teki açığı da kapatır. Token
306
- boşsa gate açık olsa da istekler geçer.
307
-
308
- Ayrıntı: [09-dev-araclari.md](./09-dev-araclari.md).
309
-
310
- ## `devGateBypass`
311
-
312
- **Tip:** `string[]` — **Varsayılan:**
313
- `["/api/healthcheck", "/robots.txt", "/sitemap.xml", "/site.webmanifest", "/favicon.ico"]`
314
-
315
- Dev gate'in hiçbir koşulda kapatmadığı **tam** yollar (önek değil, birebir
316
- eşleşme). Gate açıkken sağlık kontrolünün ve robots dosyalarının erişilebilir
317
- kalması için. Verilirse varsayılanın yerine geçer.
318
-
319
- Ayrıntı: [09-dev-araclari.md](./09-dev-araclari.md).
320
-
321
- ## `preconnect`
322
-
323
- **Tip:** `string[]` — **Varsayılan:** `[]`
324
-
325
- Üçüncü taraf origin'ler; her sayfanın `<head>`inde `<link rel="preconnect">`
326
- olarak basılır. Görsel CDN'i, API origin'i, font host'u buraya yazılır. Değerler
327
- `new URL(...).origin` ile normalize edilir; geçersiz bir URL atlanır ve uyarı
328
- basılır.
329
-
330
- Liste her sayfada aynı olduğu için bir kez hesaplanıp saklanır. Boş liste geçerli
331
- bir yapılandırmadır.
332
-
333
- ```js
334
- preconnect: ["https://cdn.ornek.com", "https://api.ornek.com"]
335
- ```
336
-
337
- ## `security`
338
-
339
- **Tip:** `object` — **Varsayılan:**
340
- `{ trustProxy: true, cookieSecret: null, csrf: { enabled: true, token: false, … } }`
341
-
342
- Kişiye özel sayfaların tamamı ve gerekçeleri
343
- [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de; burada alanların referansı
344
- var.
345
-
346
- | Alan | Tip | Varsayılan | Anlamı |
347
- | --- | --- | --- | --- |
348
- | `trustProxy` | `boolean` | `true` | Express'in `trust proxy` ayarı. Ters proxy arkasında doğru protokol ve istemci IP'si için gerekli. |
349
- | `cookieSecret` | `string \| null` | `null` | İmzalı cookie sırrı. Verilmezse `JSKELET_SECRET` okunur. |
350
- | `csrf.enabled` | `boolean` | `true` | Origin/`Sec-Fetch-Site` kontrolü. |
351
- | `csrf.token` | `boolean` | `false` | Çift gönderim token'ı katmanı. Cookie oturumlu formlarda **açın**. |
352
- | `csrf.allowedOrigins` | `string[]` | `[]` | Kendi host'umuzun yanında kabul edilen origin'ler. |
353
- | `csrf.exclude` | `string[]` | `[]` | Kontrolden muaf yollar; `source` desen sözdizimi. |
354
- | `csrf.cookieName` | `string` | `"csrf_token"` | Token cookie'sinin adı. |
355
- | `csrf.fieldName` | `string` | `"_csrf"` | `csrfField()`in bastığı alan adı. |
356
- | `csrf.headerName` | `string` | `"x-csrf-token"` | Token'ın kabul edildiği başlık. |
357
-
358
- `trustProxy` doğrudan internete açık bir sunucuda **kapatılmalı**: açıkken
359
- istemci kendi `X-Forwarded-For` / `X-Forwarded-Proto` / Host başlığını
360
- uydurabilir; rate limit, admin IP allowlist, Secure cookie ve cache `vary.host`
361
- yanlış adresi görür. Ters proxy (nginx, Caddy, Cloudflare) arkasındaysa `true`
362
- doğru varsayılandır.
363
-
364
- CSRF kontrolü yalnızca çapraz site olduğu **belli** olan istekleri reddeder —
365
- `Origin` uyuşmuyorsa ya da `Sec-Fetch-Site: cross-site` geldiyse. İkisi de yoksa
366
- istek geçer, çünkü tarayıcılar çapraz origin bir POST'ta `Origin`'i her zaman
367
- gönderirken webhook'lar hiç göndermez. Cookie ile oturum açan panel/form
368
- uygulamalarında `csrf.token: true` + `csrfField()` ikinci katmandır; webhook
369
- uçlarını `csrf.exclude` listesine yazın.
370
-
371
- ## `navigation`
372
-
373
- **Tip:** `object` — **Varsayılan:**
374
- `{ prefetch: "moderate", prerender: false, viewTransition: false, exclude: [] }`
375
-
376
- Site içi gezinmeyi hızlandıran `<head>` ipuçları. JSkelet klasik MPA olduğu için
377
- her tıklama tam sayfa yüklemesidir; bu bölüm o yüklemeyi tarayıcının **önceden**
378
- yapmasını sağlar. Client runtime'ı eklenmez — Speculation Rules ve view
379
- transition tarayıcı yetenekleridir, desteklemeyen tarayıcıda sessizce yok
380
- sayılırlar.
381
-
382
- | Alan | Tip | Varsayılan | Anlamı |
383
- | --- | --- | --- | --- |
384
- | `prefetch` | `false \| "conservative" \| "moderate" \| "eager"` | `"moderate"` | Bağlantı hedefinin **belgesini** önceden indirir |
385
- | `prerender` | aynı | `false` | Hedefi arka planda **tam render eder**; tıklama anında açılır |
386
- | `viewTransition` | `boolean` | `false` | `@view-transition { navigation: auto }` basar |
387
- | `exclude` | `string[]` | `[]` | Spekülasyon dışı bırakılacak href desenleri |
388
-
389
- `true` verilirse `prefetch`/`prerender` varsayılan eagerness'a düşer; tanınmayan
390
- bir değer uyarı basıp varsayılana döner.
391
-
392
- **Eagerness ne demek:** `conservative` bağlantıya basıldığı an, `moderate`
393
- bağlantı üzerinde bir süre duraksandığında, `eager` bağlantı görünür olur olmaz
394
- tetikler. Yukarı çıktıkça isabet artar, boşa giden istek de artar.
395
-
396
- **`prerender` neden kapalı geliyor.** Prerender edilen sayfanın script'leri
397
- gerçekten çalışır. Ölçüm kodunu `prerenderingchange` olayına bağlamayan bir
398
- uygulamada ziyaret sayıları şişer. Açmadan önce analytics'i gözden geçirin;
399
- sunucu tarafındaki maliyeti düşüktür, çünkü spekülatif istek de HTML
400
- önbelleğinden karşılanır ([06-cache.md](./06-cache.md)).
401
-
402
- **Her koşulda muaf olanlar.** `/api/*`, `/_fragment/*` ve `brand.devBasePath`
403
- altındaki yollar otomatik dışlanır; `exclude` bunların üstüne eklenir. Ayrıca
404
- `rel="nofollow"`, `target="_blank"` ve `data-no-prefetch` taşıyan bağlantılar
405
- hiçbir kurala girmez. Yan etkisi olan tek bir bağlantıyı dışarıda bırakmanın en
406
- kolay yolu sonuncusu:
407
-
408
- ```html
409
- <a href="/cikis" data-no-prefetch>Çıkış</a>
410
- ```
411
-
412
- **`viewTransition` açarken arka planı `<html>`e verin.** Geçiş sırasında tarayıcı
413
- eski ve yeni sayfanın anlık görüntülerini çapraz geçirir; `<body>`ye verilmiş bir
414
- arka plan bu görüntünün içinde kalır ve altta kalan canvas görünür. Sonuç, her
415
- geçişte bir kare beyaz flaştır ve koyu temada gözden kaçmaz. Renk `<html>` (ya da
416
- `:root`) üzerindeyse böyle bir boşluk oluşmaz:
417
-
418
- ```html
419
- <html lang="tr" class="bg-white dark:bg-slate-950">
420
- <body class="text-slate-900 dark:text-slate-100">
421
- ```
422
-
423
- Hareket azaltma tercihi framework tarafından karşılanır: `prefers-reduced-motion:
424
- reduce` altında geçiş kapatılır, ayrıca bir şey yazmanız gerekmez.
425
-
426
- **Geçişi içerikle sınırlayın.** Varsayılan davranış tüm belgeyi tek parça olarak
427
- çapraz geçirir, yani gezinme boyunca hiç değişmeyen header ve footer da titrer.
428
- Bu bölgelere bir `view-transition-name` vermek onları kendi grubuna alır;
429
- tarayıcı aynı adı iki belgede de gördüğü için "aynı öğe" sayar. Adlandırılan
430
- öğenin animasyonunu kapatınca geçiş yalnızca içerikte kalır:
431
-
432
- ```css
433
- body > header { view-transition-name: site-header; }
434
- body > footer { view-transition-name: site-footer; }
435
-
436
- ::view-transition-old(site-header),
437
- ::view-transition-old(site-footer) { animation: none; opacity: 0; }
438
- ::view-transition-new(site-header),
439
- ::view-transition-new(site-footer) { animation: none; opacity: 1; }
440
-
441
- /* Kalan içerik; varsayılan 250ms gezinmeyi yavaş hissettiriyor. */
442
- ::view-transition-old(root),
443
- ::view-transition-new(root) { animation-duration: 180ms; }
444
- ```
445
-
446
- Çalışan bir örnek için Tailwind `@source` ve view-transition CSS'ini kendi
447
- uygulamanızın `styles/globals.css` dosyasına taşıyın; yukarıdaki bloklar
448
- başlangıç noktasıdır.
449
-
450
- **CSP kullanıyorsanız** kurallar satır içi bir `<script type="speculationrules">`
451
- olarak basılır; `script-src` politikanızın buna izin vermesi gerekir.
452
-
453
- ```js
454
- navigation: {
455
- prefetch: "moderate",
456
- prerender: "conservative",
457
- viewTransition: true,
458
- exclude: ["/cikis", "/sepet/*"],
459
- }
460
- ```
461
-
462
- ## `prewarmSkip`
463
-
464
- **Tip:** `string[]` — **Varsayılan:** `["/api/", "/_fragment/", "/__jskelet/"]`
465
-
466
- Isıtmanın atlayacağı yol **önekleri**. Oturuma bağlı ya da fragment uçları
467
- ısıtılmamalı. Verilirse varsayılanın yerine geçer — `brand.devBasePath`i
468
- değiştirdiyseniz bu listeyi de güncellemeyi unutmayın.
469
-
470
- Ayrıntı: [06-cache.md](./06-cache.md).
471
-
472
- ## `watch`
473
-
474
- **Tip:** `string[]` — **Varsayılan:** `[]`
475
-
476
- `jskelet dev`in sunucu yeniden başlatma için izleyeceği **ek** dizinler, proje
477
- köküne göre. `routes`, `views` ve `lib` zaten izlenir; `client/` ve `styles/`
478
- esbuild ve CSS watcher'ları tarafından ele alınır, buraya konmamalı.
479
-
480
- Yalnızca `.js`, `.mjs`, `.json` ve `.ejs` uzantılı dosyalar tetikleyicidir.
481
-
482
- ```js
483
- watch: ["data", "content"]
484
- ```
485
-
486
- Ayrıntı: [09-dev-araclari.md](./09-dev-araclari.md).
487
-
488
- ## `fonts`
489
-
490
- **Tip:** `{ family: string, slug?: string, weights?: number[] }[]` —
491
- **Varsayılan:** `[]`
492
-
493
- Self-host edilecek Google Fonts aileleri. Boş bırakılırsa font adımı hiç
494
- çalışmaz.
495
-
496
- | Alan | Tip | Varsayılan | Anlamı |
497
- | --- | --- | --- | --- |
498
- | `family` | `string` | — | Google Fonts aile adı: `"Inter"`, `"Noto Sans"` |
499
- | `slug` | `string` | `family`den türetilir (küçük harf, boşluk → `-`) | Dosya adı öneki |
500
- | `weights` | `number[]` | `[400]` | İndirilecek ağırlıklar |
501
-
502
- Çıktı: `public/fonts/<slug>-<weight>.woff2`, manifest anahtarı aynı dosya adı.
503
- Dosyalar **sabit isimlidir** (hash yok) ve **commit edilmesi beklenir**.
504
- Ayrıntı: [08-build.md](./08-build.md).
505
-
506
- ```js
507
- fonts: [
508
- { family: "Inter", weights: [400, 600, 700] },
509
- { family: "Noto Serif", slug: "serif", weights: [400] },
510
- ]
511
- ```
512
-
513
- ## `icons`
514
-
515
- **Tip:** `{ scan?: string[], dir?: string } | false` — **Varsayılan:** `{ dir: "icons" }`
516
-
517
- SVG ikon sprite üretimi. Kaynak **XOR** seçilir: `icons.dir` dizini varsa
518
- yalnızca oradaki düz SVG'ler; yoksa `@phosphor-icons/core` (kuruluysa).
519
-
520
- | Değer | Sonuç |
521
- | --- | --- |
522
- | `{}` (varsayılan) | `dir: "icons"`; taranan dizinler `["views", "client", "routes", "lib", "features", "shared"]` |
523
- | `{ dir: "assets/icons" }` | Yerel SVG kökü değiştirilir |
524
- | `{ scan: [...] }` | Taranan dizinler değiştirilir |
525
- | `false` | Sprite adımı tamamen atlanır |
526
-
527
- Yerel dizin (varsa) düz dosya adları kullanır: `house.svg` → `house:regular`,
528
- `house-bold.svg` → `house:bold`. Boş bir `icons/` dizini Phosphor'a düşmez —
529
- dizini silmek fallback'i açar. Ayrıntı: [08-build.md](./08-build.md).
530
-
531
- ```js
532
- icons: {
533
- dir: "icons",
534
- scan: ["views", "client", "routes", "lib", "content"],
535
- }
536
- ```
537
-
538
- ## `images`
539
-
540
- **Tip:**
541
- `{ widths?: number[], quality?: number, skip?: string[], remote?: { allowHosts: string[], path?: string, maxWidth?: number, cacheMaxAge?: number, fetchTimeoutMs?: number, maxBytes?: number } | false } | false`
542
- — **Varsayılan:** `{ widths, quality, skip, remote: false }` (remote kapalı)
543
-
544
- `public/` altındaki png/jpg görsellerin webp varyantlarını **build**'de üretir.
545
- `remote.allowHosts` verilirse çalışma anında uzak görselleri de proxy eder
546
- (`/_jskelet/image?url=&w=&q=` → webp).
547
-
548
- | Alan | Tip | Varsayılan | Anlamı |
549
- | --- | --- | --- | --- |
550
- | `widths` | `number[]` | `[400, 640, 960, 1280, 1920]` | Build ve remote `srcset` adayları. Kaynaktan büyük olanlar build'de elenir; kaynağın kendi genişliği (en fazla 1920) her zaman eklenir. |
551
- | `quality` | `number` | `78` | webp kalitesi. Build'de imzaya girer; remote uçta `q` varsayılanı. |
552
- | `skip` | `string[]` | `[]` | Build'de taranmayacak **dizin adları**. `assets` ve `fonts` her zaman atlanır. |
553
- | `remote` | `object \| false` | kapalı | Runtime optimizer. `allowHosts` **zorunlu**; boşsa uç mount edilmez. |
554
-
555
- ### `images.remote`
556
-
557
- | Alan | Tip | Varsayılan | Anlamı |
558
- | --- | --- | --- | --- |
559
- | `allowHosts` | `string[]` | `[]` | Çekilebilecek host'lar. `*.cdn.example.com` sonek jokerini destekler. |
560
- | `path` | `string` | `/_jskelet/image` | Optimizer GET yolu. |
561
- | `maxWidth` | `number` | `1920` | `w` üst sınırı. |
562
- | `cacheMaxAge` | `number` | `2592000` (30 gün) | Yanıt `Cache-Control` max-age (saniye). Disk önbelleği `.jskelet/image-cache/`; 256 MB'yi geçince en eski dosya düşer. |
563
- | `fetchTimeoutMs` | `number` | `10000` | Upstream fetch zaman aşımı. |
564
- | `maxBytes` | `number` | `10485760` (10 MiB) | Upstream gövde üst sınırı. |
565
-
566
- `false` verilirse görsel adımı hiç çalışmaz. Build adımı `sharp` gerektirir ve
567
- watch turunda hiç çalışmaz. Remote açıksa `sharp` **runtime**'da da gerekir;
568
- yoksa optimizer kaynak URL'ye 302 yönlendirir. Fetch, redirect'leri otomatik
569
- takip etmez: her hop `allowHosts` ve private adres kontrolünden geçer.
570
- Ayrıntı: [08-build.md](./08-build.md).
571
-
572
- ```js
573
- images: {
574
- widths: [400, 800, 1200],
575
- quality: 82,
576
- skip: ["indirmeler"],
577
- remote: {
578
- allowHosts: ["static.ornek.com", "*.cdn.ornek.com"],
579
- },
580
- }
581
- ```
582
-
583
- `image({ src: "https://static.ornek.com/a.jpg", width: 96, alt: "…" })` bu
584
- ayarla `src` / `srcset`'i `/_jskelet/image?url=…&w=96` biçimine çevirir.
585
- Elle URL kurmak için `remoteImageUrl(src, { width })` (`jskelet`).
586
-
587
- ## `clientEnv`
588
-
589
- **Tip:** `string[]` — **Varsayılan:** `[]`
590
-
591
- Client bundle'a build zamanında gömülecek ortam değişkeni anahtarları. Next'teki
592
- `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın herkese açık olduğu
593
- isimden değil config'ten belli. `NODE_ENV` her zaman gömülür.
594
-
595
- `process.env`in tamamı tek nesne olarak define edildiği için listede olmayan bir
596
- anahtar okunduğunda çökme yerine `undefined` döner.
597
-
598
- ```js
599
- clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN_ORIGIN"]
600
- ```
601
-
602
- **Buraya gizli anahtar koymayın** — değerler bundle'da düz metin olarak durur.
603
- İsimlerinde `SECRET`, `PASSWORD`, `TOKEN`, `API_KEY`, `PRIVATE` vb. geçen
604
- anahtarlar build sırasında **reddeder** (`PUBLIC` / `PUBLISHABLE` içerenler
605
- muaf).
606
-
607
- ## `headers()`
608
-
609
- **Tip:** `() => { source: string, headers: { key: string, value: string }[] }[]`
610
- — **Varsayılan:** `[]`
611
-
612
- Yol desenine göre yanıt başlıkları. Framework yalnızca statik dosyalara uzun
613
- ömürlü cache yazar; bunun dışındaki her başlık (CSP, COOP, HSTS,
614
- X-Frame-Options…) buradan gelir ve varsayılanların üstüne biner. Üretim
615
- sitelerinde en azından aşağıdaki güvenlik başlıklarını tanımlayın.
616
-
617
- Eşleşen **tüm** kurallar uygulanır (redirect'lerin aksine ilk eşleşmede
618
- durulmaz), sırayla; aynı başlığı iki kural yazarsa sonraki kazanır.
619
-
620
- `key`i olmayan ya da `value`u `undefined` olan girdiler atlanır; hiç geçerli
621
- başlığı kalmayan bir kural hiç eklenmez.
622
-
623
- ```js
624
- async headers() {
625
- return [
626
- {
627
- source: "/:path*",
628
- headers: [
629
- { key: "X-Frame-Options", value: "SAMEORIGIN" },
630
- { key: "X-Content-Type-Options", value: "nosniff" },
631
- { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
632
- {
633
- key: "Permissions-Policy",
634
- value: "camera=(), microphone=(), geolocation=()",
635
- },
636
- {
637
- key: "Content-Security-Policy",
638
- value: "default-src 'self'; img-src 'self' https://cdn.ornek.com data:; script-src 'self'",
639
- },
640
- // Yalnızca HTTPS terminasyonu sizin kontrolünüzdeyse:
641
- // { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
642
- ],
643
- },
644
- {
645
- source: "/indirme/:path*",
646
- headers: [{ key: "Cache-Control", value: "no-store" }],
647
- },
648
- ];
649
- }
650
- ```
651
-
652
- ## `redirects()`
653
-
654
- **Tip:**
655
- `() => { source: string, destination: string, permanent?: boolean, statusCode?: number }[]`
656
- — **Varsayılan:** `[]`
657
-
658
- | Alan | Tip | Anlamı |
659
- | --- | --- | --- |
660
- | `source` | `string` | Desen (aşağıdaki sözdizimi) |
661
- | `destination` | `string` | Hedef; `:param` yer tutucuları doldurulur |
662
- | `permanent` | `boolean` | `true` → 308, aksi hâlde 307 |
663
- | `statusCode` | `number` | Açık durum kodu; `permanent`i ezer |
664
-
665
- İlk eşleşen kural kazanır ve query string korunur. Ayrıntı:
666
- [03-routing.md](./03-routing.md).
667
-
668
- ## `rewrites()`
669
-
670
- **Tip:** `() => Rule[] | { beforeFiles?: Rule[], afterFiles?: Rule[] }`
671
- burada `Rule = { source: string, destination: string }` — **Varsayılan:** `[]`
672
-
673
- Dizi döndürülürse tamamı `afterFiles` sayılır.
674
-
675
- - `beforeFiles` statik dosyalardan da önce çalışır.
676
- - `afterFiles` statik denendikten sonra, route'lardan önce çalışır.
677
- - Mutlak hedef (`http://`/`https://`) → gömülü ters proxy.
678
- - Göreli hedef → yalnızca `req.url` değişir.
679
-
680
- Ayrıntı: [03-routing.md](./03-routing.md).
681
-
682
- ## `cache()`
683
-
684
- **Tip:**
685
- `() => { html?: Record<string, number>, query?: Record<string, string[] | true>, vary?: { host?: boolean, headers?: string[], fn?: (req) => string | null }, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, upstream?: object, redis?: object, prewarm?: object }` —
686
- **Varsayılan:**
687
- `{ html: {}, query: {}, vary: { host: false }, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300 }, upstream: { rate: 0 }, redis: { enabled: false }, prewarm: { enabled: true, max: 400, intervalSeconds: 0, origins: [] } }`
688
-
689
- ### `cache().html`
690
-
691
- Desen → saniye eşlemesi. Eşleşen kural, route'un kendi `revalidate` değerini
692
- **ezer**. Negatif ya da sonlu olmayan değerler yok sayılır; `0` "önbellekleme"
693
- anlamına gelir. TTL dolmadan önce framework, son render süresine göre erken
694
- arka plan tazelemesi başlatır (ayrı bir config alanı yok; ayrıntı
695
- [06-cache.md](./06-cache.md)).
696
-
697
- ```js
698
- html: {
699
- "/": 60,
700
- "/haber/:slug": 300,
701
- "/arama": 0,
702
- }
703
- ```
704
-
705
- Tek istisna `route(fn, { private: true })`: bu route'ta desen eşleşse bile yok
706
- sayılır. Kilidin tek yönlü olması bilinçli — ters yönde bir hata, bir
707
- kullanıcının HTML'inin bir başkasına servis edilmesi anlamına geliyor.
708
-
709
- ### `cache().query`
710
-
711
- Desen → cache anahtarına girmesine izin verilen query parametreleri.
712
-
713
- **Varsayılan olarak query parametresi taşıyan istek dinamiktir**: `cache().html`
714
- o yolu kapsıyor olsa bile HTML önbelleğine hiç girmez, `private, no-store` ile
715
- gider. Sebebi basit — bir yolun bütün varyantlarını cache'lemek `?utm_source=…`
716
- gibi sonsuz sayıda anahtar üretiyor ve `maxEntries` sınırına dayandığında
717
- LRU'daki gerçek sayfaları dışarı atıyor. Hangi parametrenin çıktıyı gerçekten
718
- değiştirdiğini yalnızca uygulama bilir.
719
-
720
- ```js
721
- query: {
722
- "/arama": ["q", "page"], // yalnızca bu ikisi anahtara girer
723
- "/urunler": ["kategori"],
724
- "/rapor/:id": true, // bütün parametreler anahtara girer
725
- "/kampanya": [], // query tamamen yok sayılır
726
- }
727
- ```
728
-
729
- - **İzin listesi** (`string[]`): listedeki parametreler anahtara girer, her
730
- farklı değer kendi girdisini alır. Listede olmayan parametreler **yok
731
- sayılır** — sayfa yine cache'lenir ve bütün kampanya varyantları tek kopyayı
732
- paylaşır.
733
- - **`true`**: bütün parametreler anahtara girer. Anahtar sayısını sınırlayan
734
- tek şey `maxEntries` olur; yalnızca değer kümesi kapalı olan yollarda kullan.
735
- - **`[]`**: query hiç dikkate alınmaz, bütün varyantlar query'siz sürümün
736
- HTML'ini alır.
737
-
738
- Parametreler anahtara **sıralı** yazılır: `?a=1&b=2` ile `?b=2&a=1` aynı girdiyi
739
- paylaşır. `route(fn, { private: true })` bu bölümden etkilenmez; private route
740
- hiçbir koşulda cache'lenmez.
741
-
742
- ### `cache().vary`
743
-
744
- HTML cache anahtarına query allowlist'ten **bağımsız** sabit parçalar ekler.
745
- Host'tan locale üreten sitelerde `host: true` **zorunlu**; aksi halde ilk
746
- locale'in HTML'i diğer host'a servis edilir. CDN zaten tam URL ile ayırır —
747
- bu ayar origin L1 ve Redis HTML anahtarı içindir.
748
-
749
- ```js
750
- vary: {
751
- host: true, // h=tr.example.com|…
752
- // headers: ["x-locale"],
753
- // fn: (req) => req.hostname.startsWith("tr.") ? "l=tr" : "l=en",
754
- }
755
- ```
756
-
757
- | Alan | Tip | Varsayılan | Anlamı |
758
- | --- | --- | --- | --- |
759
- | `host` | `boolean` | `false` | Public Host (`x-forwarded-host` yoksa `Host`), lowercase, portsuz → `h=…` |
760
- | `headers` | `string[]` | `[]` | İstek başlıkları `ad=değer` olarak eklenir |
761
- | `fn` | `(req) => string \| null` | — | Dönüş bir segment olarak eklenir |
762
-
763
- Anahtar biçimi: `${vary}|${yol}?${query}` (vary yoksa önek yok). Ayrıntı:
764
- [06-cache.md](./06-cache.md).
765
-
766
- ### `cache().maxEntries`
767
-
768
- **Tip:** `number` — **Varsayılan:** `500`
769
-
770
- HTML önbelleğinin girdi sınırı. Girdi başına yüz kilobayt düştüğü için bu sayıyı
771
- yükseltmek belleği hızla tüketir; on binlerce yollu bir siteyi buradan çözmeye
772
- çalışmak yanlış katman, doğru yer `cache().data`.
773
-
774
- **Tavan 800.** Daha yükseği yüklemede uyarıyla 800'e çekilir. Süreç içi HTML +
775
- sıkıştırılmış gövde ayrıca 256 MB'yi geçemez; bu bütçe config'den yükseltilmez.
776
- Sıkıştırılmış kopya tektir (brotli veya gzip).
777
-
778
- ### `cache().data`
779
-
780
- Upstream veri önbelleği (`withDataCache`). Ayrıntı: [06-cache.md](./06-cache.md).
781
-
782
- | Alan | Tip | Varsayılan | Anlamı |
783
- | --- | --- | --- | --- |
784
- | `maxEntries` | `number` | `10000` | LRU girdi sınırı. JSON, HTML'e göre onlarca kat küçük olduğu için sınır yüksek. **Tavan 20000**; üstü uyarıyla kesilir. Süreç içi JSON ayrıca **64 MB**'yi geçemez; bu bütçe config'den yükseltilmez. |
785
- | `staleFactor` | `number` | `10` | TTL dolduktan sonra girdinin kaç TTL boyunca daha kullanılabileceği. `0` → bayat servis yok. |
786
-
787
- ### `cache().trackUpstream`
788
-
789
- **Tip:** `boolean` — **Varsayılan:** `true`
790
-
791
- Açıkken `globalThis.fetch` sarılır ve render sırasındaki geçici upstream
792
- hataları (`429`, `5xx`, ağ) kendiliğinden bildirilir; `reportUpstreamFailure()`
793
- çağırmak gerekmez. `fetch`i kendisi saran bir uygulama bunu kapatabilir.
794
-
795
- ### `cache().trackDependencies`
796
-
797
- **Tip:** `boolean` — **Varsayılan:** `true`
798
-
799
- Açıkken bir render'ın okuduğu `withDataCache` anahtarları kaydedilir ve
800
- `clearDataCache()` o veriyi okumuş HTML sayfalarını da bayatlatır — hedefli
801
- invalidation için uygulamanın hiçbir şey bildirmesi gerekmez
802
- ([06-cache.md](./06-cache.md)). `withDataCache` kullanmayan bir uygulamada
803
- kaydedilecek bir şey yok; kapatmak bağlam kurma maliyetini de kaldırır.
804
-
805
- ### `cache().transientRetry`
806
-
807
- **Tip:** `{ attempts?: number, delayMs?: number } | false` —
808
- **Varsayılan:** `{ attempts: 1, delayMs: 300 }`
809
-
810
- Geçici bir upstream hatası yüzünden `notFound()` çağrılan sayfa kaç kez daha
811
- denenir. Amaç var olan bir sayfanın 404'e dönüşmemesi; denemeler tükenirse yanıt
812
- önbelleğe girmeyen bir 503 olur. `false` ya da `attempts: 0` tekrarı kapatır.
813
- Ayrıntı: [06-cache.md](./06-cache.md).
814
-
815
- ### `cache().upstream`
816
-
817
- Upstream API'ye giden `fetch` çağrılarının host başına hız freni. Varsayılan
818
- **kapalı**: `rate` verilmedikçe hiçbir istek beklemez. `rate` bir tavandır;
819
- gerçek hız 429 cevaplarına göre kendini aşağı çeker ve temiz geçen pencerelerde
820
- kademe kademe geri çıkar.
821
-
822
- | Alan | Tip | Varsayılan | Anlamı |
823
- | --- | --- | --- | --- |
824
- | `rate` | `number` | `0` | Saniyedeki en fazla çağrı. `0` → fren kapalı |
825
- | `burst` | `number` | `0` | Kova boyu; `0` → bir saniyelik bütçe kadar patlama |
826
- | `concurrency` | `number` | `8` | Aynı anda uçabilecek çağrı |
827
- | `minRate` | `number` | `0.5` | Azalmanın dibi; hız buranın altına inmez |
828
- | `increaseStep` | `number` | `1` | Toplamsal artışın adımı (çağrı/saniye) |
829
- | `increaseIntervalMs` | `number` | `5000` | Artış periyodu |
830
- | `decreaseIntervalMs` | `number` | `1000` | İki azalma arasındaki en kısa süre |
831
- | `breakerFailures` | `number` | `5` | Art arda kaç 429'dan sonra host baypas edilir |
832
- | `breakerCooldownMs` | `number` | `10000` | Baypasın süresi |
833
- | `hosts` | `Record<string, object>` | `{}` | Host bazlı override; aynı alanlar geçerli |
834
-
835
- Yalnızca `429` ve `503` hızı cezalandırır: `400`/`404`/`500` bir kota sorunu
836
- değil. Durumu `getUpstreamLimiterStatus()` ile ya da dev panelinin **Server**
837
- sekmesinden okuyabilirsin. Ayrıntı ve freni açmadan önce bakılacak yer:
838
- [06-cache.md](./06-cache.md).
839
-
840
- ```js
841
- upstream: {
842
- rate: 10,
843
- concurrency: 4,
844
- hosts: { "api.example.com": { rate: 3 } },
845
- }
846
- ```
847
-
848
- ### `cache().redis`
849
-
850
- Opsiyonel Redis ikinci kademesi (L2). Bellek içi önbellek birincil kalır; Redis
851
- yalnızca L1'de bulunmayan bir yol için render'ı atlatır ve invalidation'ı diğer
852
- instance'lara yayar. `ioredis` uygulamaya kurulmalı (`npm install ioredis`);
853
- kurulmadıysa ya da bağlanılamıyorsa uyarı basılır ve site bellek içi önbellekle
854
- çalışmaya devam eder.
855
-
856
- | Alan | Tip | Varsayılan | Anlamı |
857
- | --- | --- | --- | --- |
858
- | `enabled` | `boolean` | `false` | Yalnızca açıkça `true` verildiğinde açılır |
859
- | `url` | `string \| null` | `null` | `redis://` ya da `rediss://`. Boşsa ioredis varsayılanı (`localhost:6379`) |
860
- | `namespace` | `string` | `"default"` | Aynı Redis'i paylaşan uygulamaları ayırır |
861
- | `keyPrefix` | `string` | `"_jskelet"` | Anahtar düzeninin kökü |
862
- | `html` | `boolean` | `true` | HTML gövdeleri paylaşılsın mı |
863
- | `data` | `boolean` | `true` | `withDataCache` girdileri paylaşılsın mı |
864
- | `storeEncoded` | `boolean` | `false` | Brotli/gzip gövdeleri de paylaşılsın mı; girdi başına boyutu iki-üç katına çıkarır |
865
- | `events` | `boolean` | `true` | pub/sub üzerinden invalidation yayını |
866
- | `commandTimeoutMs` | `number` | `200` | Tek bir komutun en fazla bekletebileceği süre |
867
-
868
- Anahtarlar `_jskelet:{namespace}:{buildId}:html:{yol}?{query}` biçiminde yaşar.
869
- `buildId` her build'de değişir, böylece deploy sonrası eski HTML kendiliğinden
870
- geçersiz olur. Kişiye özel (`storable: false`), `degraded` ve 200 dışındaki
871
- yanıtlar paylaşımlı kademeye hiç yazılmaz. Takaslar ve teşhis:
872
- [06-cache.md](./06-cache.md).
873
-
874
- ```js
875
- redis: {
876
- enabled: process.env.NODE_ENV === "production",
877
- url: process.env.REDIS_URL,
878
- namespace: "haber-sitesi",
879
- }
880
- ```
881
-
882
- ### `logs`
883
-
884
- Kalıcı log sink'leri. Varsayılan her şey kapalı: stdout ve admin paneli ring'i
885
- mevcut davranışını korur. Açıldığında HTTP access log ile framework olayları
886
- (`event` / `error`) NDJSON olarak dosyaya, `drainLog`'a ve/veya S3'e gider.
887
- Dosya parçaları zstd'dir ve en fazla 5 dakika durur; süresi dolan en eski
888
- parça silinir.
889
-
890
- | Alan | Tip | Varsayılan | Anlamı |
891
- | --- | --- | --- | --- |
892
- | `console` | `boolean` | `true` | Runtime `http` / `event` / `error` satırları stdout'a basılsın mı (banner/build satırları etkilenmez) |
893
- | `kinds` | `("http" \| "event" \| "error")[]` | hepsi | Sink'lere giden kayıt türleri |
894
- | `file.enabled` | `boolean` | `false` | Dosya spool'u. Satırlar ~1 sn veya 32 satırda bir `jskelet-<zaman>-<n>.ndjson.zst` olur. En fazla 5 dakika tutulur; en eski parça silinir. |
895
- | `file.dir` | `string` | `"logs"` | Proje köküne göre dizin |
896
- | `drainLog` | `(chunk) => void \| Promise<void>` | `null` | Mühürlenen zstd parçasını (`{ body, encoding, bytes, lines, at }`) istenen yere aktarır. Hata uyarı basar, siteyi düşürmez. Dosya kapalıysa diske yazılmaz. |
897
- | `s3.enabled` | `boolean` | `false` | S3 batch PutObject sink'i |
898
- | `s3.bucket` | `string \| null` | `null` | Bucket ya da `bucket/prefix/…` yolu; `JSKELET_LOG_BUCKET` ezer |
899
- | `s3.prefix` | `string` | `"jskelet/logs/"` | Nesne anahtarı öneki (yolda verilmediyse) |
900
- | `s3.region` | `string \| null` | `"auto"` | Bölge; verilmezse `JSKELET_S3_REGION`, yoksa `auto` |
901
- | `s3.endpoint` | `string \| null` | `null` | S3-uyumlu API adresi; `JSKELET_S3_API_URL` ezer |
902
- | `s3.flushIntervalMs` | `number` | `5000` | Batch flush aralığı |
903
- | `s3.maxBatch` | `number` | `100` | Bu kadar satırda erken flush |
904
-
905
- S3 credential'ları config'e yazılmaz: `JSKELET_S3_ACCESS_KEY_ID`,
906
- `JSKELET_S3_SECRET_ACCESS_KEY`, isteğe bağlı `JSKELET_S3_SESSION_TOKEN`.
907
- Bucket/region/credential eksikse uyarı basılır ve S3 sink kapanır; site ayağa
908
- kalkmaya devam eder. Framework `@aws-sdk` taşımaz — PutObject SigV4 ile
909
- gömülüdür.
910
-
911
- ```js
912
- logs: {
913
- console: true,
914
- kinds: ["http", "error"],
915
- file: { enabled: true, dir: "logs" },
916
- async drainLog(chunk) {
917
- // chunk.body zstd NDJSON. Dosya 5 dakika sonra silinir; kalıcı kopya burada.
918
- },
919
- s3: {
920
- enabled: process.env.NODE_ENV === "production",
921
- bucket: process.env.JSKELET_LOG_BUCKET,
922
- prefix: "my-app/logs/",
923
- region: process.env.JSKELET_S3_REGION,
924
- endpoint: process.env.JSKELET_S3_API_URL,
925
- },
926
- }
927
- ```
928
-
929
- ### `admin()`
930
-
931
- Framework yönetim paneli (`/_jskelet/admin`). Bellek içi / Redis / Cloudflare
932
- önbelleğini yönetir; route ve view envanteri ile canlı log kuyruğu sunar.
933
-
934
- Ortama bakmaz: `enabled` verilmedikçe **hiç mount edilmez** ve yol da yoktur.
935
- Açıkken production'da da çalışır — asıl sorular ("bu sayfa neden bayat",
936
- "webhook purge'ü geçti mi") orada soruluyor. `cache()` bölümünden ayrıdır.
937
-
938
- | Alan | Tip | Varsayılan | Anlamı |
939
- | --- | --- | --- | --- |
940
- | `enabled` | `boolean` | `false` | Yalnızca açıkça `true` verildiğinde açılır (`JSKELET_ADMIN` ezer) |
941
- | `basePath` | `string` | `"/_jskelet/admin"` | Panelin kökü |
942
- | `allowIps` | `string[]` | `[]` | Exact IP veya CIDR; boş = kısıt yok. Listede olmayan her istek 404 |
943
- | `blockBots` | `boolean` | `true` | Bilinen crawler UA'ları 404 |
944
- | `banAttempts` | `number` | `3` | Kaç başarısız denemeden sonra IP yasaklanır |
945
- | `banHours` | `number` | `24` | Yasağın süresi |
946
- | `sessionHours` | `number` | `12` | Oturum çerezinin ömrü |
947
- | `logSize` | `number` | `500` | Canlı log ring boyutu |
948
-
949
- Şifre **her süreç başlangıcında** üretilir ve yalnızca sunucu logundaki
950
- `ADMIN` kutusunda görünür. Yasaklı ve yetkisiz her cevap `404`'tür. Kullanım
951
- ve ekran ayrıntıları: [06-cache.md](./06-cache.md).
952
-
953
- ```js
954
- admin() {
955
- return {
956
- enabled: process.env.JSKELET_ADMIN === "1",
957
- allowIps: ["203.0.113.10", "10.0.0.0/8"],
958
- };
959
- }
960
- ```
961
-
962
- ### `cache().cloudflare`
963
-
964
- CDN kademesi. JSkelet'in önbelleği origin önbelleği; ziyaretçinin gördüğü kopya
965
- edge'de duruyor. Bu bölüm bağlıysa panelden edge purge'ü, cache ile ilgili zone
966
- ayarları ve cache isabet oranı yönetilebilir.
967
-
968
- | Alan | Tip | Varsayılan | Anlamı |
969
- | --- | --- | --- | --- |
970
- | `enabled` | `boolean` | `true` | `false` verilirse env'de token olsa bile yüzey kapalı kalır |
971
- | `zoneId` | `string \| null` | `null` | Zone kimliği (`JSKELET_CLOUDFLARE_ZONE_ID` ezer) |
972
- | `apiToken` | `string \| null` | `null` | Token; **env tercih edilir**, config'e yazmak sırrı repoya sokar |
973
- | `hostname` | `string \| null` | `null` | Purge tam URL ister; yol → URL çevrimi bu ad üzerinden yapılır. Verilmezse panelin açıldığı origin kullanılır |
974
- | `analyticsHours` | `number` | `24` | Analitik penceresi, en çok `72` |
975
-
976
- Token yalnızca `JSKELET_CLOUDFLARE_KEY` ile verildiğinde config dosyası temiz
977
- kalır; izinler yapılacak işe göre: purge için `Zone.Cache Purge`, ayarlar için
978
- `Zone.Zone Settings`, isabet oranı için `Zone.Analytics` (salt okunur). Token
979
- hiçbir panel cevabında dönmez, yalnızca "env'den geldi" bilgisi görünür.
980
-
981
- Zone bağlı değilse panel bir uyarı değil kurulum önerisi gösterir; Cloudflare
982
- hata dönerse ilgili bölüm hatayı yazar ve panelin kalanı çalışmaya devam eder.
983
- Neyin sorulabildiği — özellikle "bu sayfa kaç edge'de cache'li" sorusunun neden
984
- tam cevabı olmadığı — [06-cache.md](./06-cache.md) içinde.
985
-
986
- ### `cache().prewarm`
987
-
988
- İki mod: **klasik** (liste + açılış turu) veya **`onVisit`** (ziyaret edilen
989
- sayfadaki linkler). Birlikte verilemez — config yüklenirken hata.
990
-
991
- #### Klasik alanlar
992
-
993
- | Alan | Tip | Varsayılan | Anlamı |
994
- | --- | --- | --- | --- |
995
- | `enabled` | `boolean` | `true` | `false` ise ısıtma yapılmaz (`PREWARM=1` ile ezilebilir) |
996
- | `max` | `number` | `400` | Bir turda en fazla kaç yol ısıtılır |
997
- | `concurrency` | `number` | prod 4, dev 1 | Paralel işçi sayısı |
998
- | `rps` | `number` | prod `0`, dev 4 | Saniyedeki en fazla ısıtma isteği; `0` sınırsız. Upstream kotasını koruyan ayar bu. Dev'deki varsayılan fren, ısıtmanın sayfa isteklerini bekletmemesi için. |
999
- | `delayMs` | `number` | prod 500, dev 3000 | Açılıştan sonra ilk turun gecikmesi |
1000
- | `retryDelayMs` | `number` | `2000` | Tekrar turundan önce beklenen süre |
1001
- | `intervalSeconds` | `number` | `0` | 0'dan büyükse tur periyodik tekrarlanır |
1002
- | `rotate` | `boolean` | `true` | Liste `max`'tan uzunsa periyodik turlar kaldığı yerden devam eder |
1003
- | `priority` | `(string \| RegExp)[]` | `[]` | Isıtma sırası; eşleşen yollar her turda başa alınır |
1004
- | `origins` | `string[]` | `[]` | Klasik turda ısıtılacak origin'ler. Boşsa `http://127.0.0.1:<port>`. `vary.host` açıksa locale host'ları buraya yazın |
1005
-
1006
- `priority` iki biçim kabul eder: config'in her yerinde geçerli olan desen
1007
- sözdizimi ve doğrudan `RegExp`. Önce yazılan önce ısınır.
1008
-
1009
- ```js
1010
- prewarm: {
1011
- max: 500,
1012
- rps: 4,
1013
- intervalSeconds: 300,
1014
- // vary.host açıksa loopback tek başına yetmez:
1015
- origins: ["http://localhost", "http://tr.localhost"],
1016
- priority: [
1017
- "/", // ana sayfa
1018
- "/piyasalar/:path*", // tüm piyasa bölümü
1019
- /-yorumlar$/, // desen sözdiziminin karşılamadığı kural
1020
- ],
1021
- }
1022
- ```
1023
-
1024
- #### `onVisit`
1025
-
1026
- | Alan | Tip | Varsayılan | Anlamı |
1027
- | --- | --- | --- | --- |
1028
- | `onVisit` | `true \| false \| object` | kapalı | Ziyaret tabanlı ısıtma |
1029
- | `onVisit.perPage` | `number` | `20` | Sayfa başına üstten alta en fazla link. **Tavan 20** |
1030
- | `onVisit.concurrency` | `number` | `2` | Paralel işçi. **Tavan 2** |
1031
- | `onVisit.rps` | `number` | `2` | Saniyedeki tavan. **Tavan 2**; `0` da 2'ye çekilir |
1032
-
1033
- ```js
1034
- prewarm: {
1035
- onVisit: { perPage: 20, rps: 2 },
1036
- }
1037
- ```
1038
-
1039
- `hooks.prewarmPaths` ve klasik alanlar (`max`, `priority`, …) `onVisit` ile
1040
- **yasaktır**. Ayrıntı: [06-cache.md](./06-cache.md).
1041
-
1042
- Sayısal alanların her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env
1043
- önceliklidir. Ayrıntı: [06-cache.md](./06-cache.md).
1044
-
1045
- ## `hooks`
1046
-
1047
- **Tip:** `Record<string, Function>` — **Varsayılan:** `{}`
1048
-
1049
- Hepsi opsiyonel, hepsi `async` olabilir. Bir hook hata verirse framework kendi
1050
- varsayılanına döner ve uyarır — sayfa düşmez.
1051
-
1052
- | Hook | İmza | Döndürdüğü | Belge |
1053
- | --- | --- | --- | --- |
1054
- | `metadata` | `(page) => object` | Her sayfanın metadata varsayılanı; controller `metadata`sı üzerine biner | [04](./04-render-ve-sablonlar.md) |
1055
- | `layoutContext` | `({ pathname, metadata }) => object` | Layout local'leri; `lang`, `structuredData`, `extraHead`, `bodyClass` özel yorumlanır | [04](./04-render-ve-sablonlar.md) |
1056
- | `notFound` | `() => object \| null` | 404 sayfa tanımı; `null` ise framework'ün hata sayfası | [03](./03-routing.md) |
1057
- | `error` | `({ status, error }) => object \| string \| null` | 404 dışındaki hata sayfaları (ve `notFound` yoksa 404); sayfa tanımı ya da doğrudan HTML | [03](./03-routing.md) |
1058
- | `prewarmPaths` | `() => string[]` | Klasik ısıtmada ısıtılacak yollar; tanımlı değilse klasik tur kurulmaz. `onVisit` ile birlikte **yasak** | [06](./06-cache.md) |
1059
-
1060
- ```js
1061
- hooks: {
1062
- metadata() {
1063
- return { titleTemplate: "%s | Örnek", siteUrl: "https://ornek.com" };
1064
- },
1065
-
1066
- async layoutContext({ pathname }) {
1067
- return { navigation: await getNavigation(), isHome: pathname === "/" };
1068
- },
1069
-
1070
- notFound() {
1071
- return {
1072
- view: "pages/not-found",
1073
- metadata: { title: "Sayfa bulunamadı", robots: { index: false } },
1074
- };
1075
- },
1076
-
1077
- error({ status }) {
1078
- return {
1079
- view: "pages/error",
1080
- data: { status },
1081
- metadata: { title: "Bir hata oluştu", robots: { index: false } },
1082
- };
1083
- },
1084
-
1085
- async prewarmPaths() {
1086
- return ["/", ...(await getArticlePaths())];
1087
- },
1088
- }
1089
- ```
1090
-
1091
- ## `source` desen sözdizimi
1092
-
1093
- `headers()`, `redirects()`, `rewrites()` ve `cache().html` aynı küçük derleyiciyi
1094
- kullanır. Bu, Next'in tam `path-to-regexp` yüzeyi değil; config'te fiilen
1095
- kullanılan alt küme bilinçli olarak seçildi ve tanınmayan bir sözdizimi sessizce
1096
- literal kabul edilmez, uyarı üretir.
1097
-
1098
- | Desen | Regex karşılığı | Örnek eşleşme |
1099
- | --- | --- | --- |
1100
- | `/hakkinda` | tam eşleşme | `/hakkinda` |
1101
- | `/haber/:slug` | `([^/]+)` — tek segment | `/haber/abc` (✗ `/haber/a/b`) |
1102
- | `/:path*` | `(.*)` — sıfır veya daha fazla segment | `/`, `/a`, `/a/b/c` |
1103
- | `/blog/:path*` | joker alt yol; öndeki `/` opsiyonel | `/blog`, `/blog/`, `/blog/a/b` |
1104
- | `/:path*.svg` | joker + sabit son ek | `/ikon.svg`, `/a/b/c.svg` |
1105
- | `/etiket-:slug` | segment ortasında parametre | `/etiket-finans` |
1106
-
1107
- Kurallar:
1108
-
1109
- - `source` **`/` ile başlamak zorundadır**; başlamazsa kural yok sayılır ve
1110
- uyarı basılır.
1111
- - Parametre adı `[A-Za-z_][A-Za-z0-9_]*` kalıbına uymalıdır.
1112
- - Desen daima **baştan sona** eşleşir (`^…$`); önek eşleşmesi için `:path*`
1113
- kullanın.
1114
- - `:path*` sıfır segment de yakalar ve hemen öncesindeki `/` opsiyoneldir:
1115
- `/hesabim/:path*` bölümün kök yolunu (`/hesabim`) da kapsar. Aksi hâlde bir
1116
- bölümü tamamen kapatmak isteyen kural tam da giriş sayfasını atlıyordu.
1117
- - Parametreler dışındaki tüm karakterler literal kabul edilir ve regex için
1118
- kaçışlanır — `.` gerçekten nokta demektir.
1119
- - Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
1120
- Karşılığı olmayan bir yer tutucu olduğu gibi bırakılır.
1121
-
1122
- ## Ortam değişkenleri
1123
-
1124
- Framework'ün okuduğu tüm değişkenler. `.env` dosyası varsa CLI tarafından
1125
- otomatik yüklenir (`--env-file=.env`); yoksa bayrak hiç geçilmez ve uyarı
1126
- basılmaz.
1127
-
1128
- | Değişken | Kim okur | Varsayılan | Anlamı |
1129
- | --- | --- | --- | --- |
1130
- | `NODE_ENV` | her yer | `production` (start/build), `development` (dev) | Dev overlay, EJS cache, manifest yeniden okuma, route hata davranışı ve prewarm varsayılanlarını belirler. `jskelet dev` bunu kendisi ayarlar — `cross-env` gerekmez. |
1131
- | `PORT` | `startServer` | `3000` | Dinlenecek port. Doluysa süreç başlamaz; `jskelet start|dev --murder` dinleyiciyi öldürür |
1132
- | `HOST` | `startServer` | `::` | Bağlanılacak arayüz. Varsayılan çift yığın dinler (IPv6 + IPv4); IPv6 yoksa `0.0.0.0`'a düşer |
1133
- | `JSKELET_SECRET` | `jskelet/cookies` | — | İmzalı cookie sırrı. `security.cookieSecret` verilmediğinde buradan okunur; ikisi de yoksa imzalı cookie API'si hata verir. [12](./12-panel-ve-oturum.md) |
1134
- | `DEV_GATE` | `devGate` | kapalı | `1` gate'i açar, `0` config'te açık olsa da kapatır. `DEV_TOKEN` tek başına açmaz. [09](./09-dev-araclari.md) |
1135
- | `DEV_TOKEN` | `devGate`, `prewarm` | — | Gate açıkken beklenen sır. Yoksa veya gate kapalıysa site açık kalır. Isıtma, gate açıkken token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
1136
- | `JSKELET_ADMIN` | `createApp` | — | Ayarlıysa yönetim panelini açar; `0` config'te açık olan paneli kapatır. Env config'i ezer, çünkü panel genelde bir arıza sırasında tek seferlik açılır. [06](./06-cache.md) |
1137
- | `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log hedefi: bucket ya da `bucket/prefix` yolu. Credential ile birlikte varsa sink otomatik açılır |
1138
- | `JSKELET_S3_BUCKET` | `logs.s3` | — | `JSKELET_LOG_BUCKET` yoksa bucket; `JSKELET_S3_KEY_PREFIX` ile birleşir |
1139
- | `JSKELET_S3_KEY_PREFIX` | `logs.s3` | — | `JSKELET_S3_BUCKET` ile kullanılır (`bucket/prefix`) |
1140
- | `JSKELET_S3_ACCESS_KEY_ID` | `logs.s3` | — | PutObject imzası |
1141
- | `JSKELET_S3_SECRET_ACCESS_KEY` | `logs.s3` | — | İmza sırrı (`JSKELET_S3_ACCESS_SECRET` yedek ad) |
1142
- | `JSKELET_S3_SESSION_TOKEN` | `logs.s3` | — | Geçici credential için isteğe bağlı |
1143
- | `JSKELET_S3_REGION` | `logs.s3` | `auto` | Verilmezse `auto` |
1144
- | `JSKELET_S3_API_URL` | `logs.s3` | — | S3-uyumlu endpoint; `logs.s3.endpoint`'i ezer |
1145
- | `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache yüzeyi | — | API token. Verilene kadar CDN purge'ü ve edge analitiği kapalıdır; config'teki `apiToken`'ı ezer. Token hiçbir cevapta dönmez. [06](./06-cache.md) |
1146
- | `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache yüzeyi | — | Zone kimliği. Token'la birlikte verilmedikçe hiçbir Cloudflare ucu çağrılmaz |
1147
- | `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache yüzeyi | — | Purge URL'lerinin kökü. Panel iç bir adresten açılıyorsa gerekir |
1148
- | `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
1149
- | `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
1150
- | `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 1 | Paralel işçi sayısı |
1151
- | `PREWARM_RPS` | `prewarm` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız |
1152
- | `PREWARM_DELAY_MS` | `startPrewarm` | prod 500, dev 3000 | İlk turun gecikmesi |
1153
- | `PREWARM_RETRY_DELAY_MS` | `prewarm` | `2000` | Tekrar turundan önceki bekleme |
1154
- | `PREWARM_INTERVAL_SECONDS` | `startPrewarm` | `0` | 0'dan büyükse periyodik tur |
1155
- | `JSKELET_VERBOSE` | `jskelet dev` | — | `1` ise restart'ta değişen dosyaların tamamı listelenir |
1156
- | `JSKELET_COLOR` | `jskelet/log` | — | `1` ise renk zorlanır. Alt süreçler boruya yazdığı için renk algılaması kapanır; `jskelet dev` bunu kendisi ayarlar. |
1157
- | `JSKELET_CHILD` | `jskelet build` | — | Dev script'i tarafından ayarlanır; build banner'ı ve "Ready" özetini bastırır |
1158
- | `NO_COLOR` | `jskelet/log` | — | Ayarlıysa renk hiç kullanılmaz (`JSKELET_COLOR`u da ezer) |
1159
-
1160
- Uygulamanızın kendi değişkenleri (API origin'i, token'lar) framework tarafından
1161
- okunmaz; doğrudan `process.env` üzerinden kullanın. Tarayıcıya ulaşması
1162
- gerekenleri `clientEnv` ile bildirin.
1163
-
1164
- Sayısal prewarm ayarları yalnızca **pozitif ve sonlu** değer kabul eder;
1165
- geçersiz bir değer sessizce bir sonraki katmana (config → kod varsayılanı)
1166
- düşer.
1167
-
1168
- ## Programatik erişim
1169
-
1170
- ```js
1171
- import { getConfig, loadConfig } from "jskelet";
1172
-
1173
- await loadConfig(); // proje kökünden okur
1174
- await loadConfig({ root: "/baska/proje" }); // farklı kök
1175
- await loadConfig({ configFile: "jskelet.test.mjs" });
1176
- await loadConfig({ force: true }); // önbelleği atlayıp yeniden oku
1177
-
1178
- const config = getConfig(); // çözümlenmiş config
1179
- ```
1180
-
1181
- `loadConfig()` aynı süreçte ikinci çağrıda önbelleğe düşer: `jskelet start` hem
1182
- `ensure-build` hem `createApp` üzerinden çağırıyor ve config'i iki kez okuyup iki
1183
- kez loglamanın faydası yok.
1184
-
1185
- `getConfig()` `loadConfig()` çağrılmadan kullanılırsa **hata verir**: sessiz
1186
- yanlış yol, "stylesheet neden yok" gibi teşhisi zor sorunlara dönüşüyor.
1187
-
1188
- Çözümlenmiş config'te dizinler mutlak yol olarak `config.dirs` altındadır
1189
- (`views`, `public`, `client`, `routes`, `styles`, `generated`, `assets`,
1190
- `fonts`), desenler derlenmiş hâldedir ve `config.loaded` dosyanın gerçekten
1191
- okunup okunmadığını söyler.
1192
-
1193
- ## Sırada ne var
1194
-
1195
- - Build tarafındaki alanların etkisi: [08-build.md](./08-build.md)
1196
- - Dev akışı ve `DEV_TOKEN`: [09-dev-araclari.md](./09-dev-araclari.md)
1197
- - Ortam değişkenlerinin dağıtımda kullanımı: [10-dagitim.md](./10-dagitim.md)
1
+ # 07 — Yapılandırma referansı
2
+
3
+ Bu belge `jskelet.config.mjs`'in tam referansıdır: her alan, tipi, varsayılanı
4
+ ve örneği. Ardından `source` desen sözdizimi ve framework'ün okuduğu tüm ortam
5
+ değişkenleri tablosu geliyor. Alanların davranışsal ayrıntıları için ilgili
6
+ belgelere bağlantı verildi; buradaki amaç tek bakışta tam liste sunmak.
7
+
8
+ ## Dosyanın konumu ve yüklenmesi
9
+
10
+ Config dosyası proje kökünde `jskelet.config.mjs` adıyla aranır ve **zorunlu
11
+ değildir**. Yoksa ya da okunamıyorsa uyarı basılır ve sunucu varsayılanlarla
12
+ ayağa kalkar; bozuk bir düzenleme siteyi açılamaz hâle getirmemeli.
13
+
14
+ ```js
15
+ // jskelet.config.mjs
16
+ export default {
17
+ // …
18
+ };
19
+ ```
20
+
21
+ Default export yoksa modülün kendisi config olarak kullanılır (named export'lar).
22
+
23
+ `headers()`, `redirects()`, `rewrites()` ve `cache()` bölümleri fonksiyon **ya da
24
+ düz değer** olabilir; fonksiyon olmaları hâlinde `async` olabilirler ve `this`
25
+ config nesnesine bağlıdır. Bir bölüm hata verirse yalnızca o bölüm yok sayılır.
26
+
27
+ Config başarıyla yüklendiğinde bir özet basılır:
28
+ `[config] jskelet.config.mjs loaded — 3 headers, 2 redirects, 1 cache rule`
29
+
30
+ ## Tam örnek
31
+
32
+ ```js
33
+ // jskelet.config.mjs
34
+ export default {
35
+ paths: {
36
+ views: "views",
37
+ public: "public",
38
+ client: "client",
39
+ routes: "routes",
40
+ styles: "styles/globals.css",
41
+ generated: ".jskelet",
42
+ },
43
+
44
+ brand: {
45
+ name: "Örnek",
46
+ poweredBy: "Örnek",
47
+ cacheHeader: "X-Ornek-Cache",
48
+ devBasePath: "/__ornek/dev",
49
+ prewarmUserAgent: "ornek-prewarm",
50
+ devTokenCookie: "dev_token",
51
+ lang: "tr",
52
+ },
53
+
54
+ layout: "views/layout.jsk",
55
+ routes: ["./routes/10-pages.mjs", "./routes/99-catch-all.mjs"],
56
+ trailingSlash: false,
57
+
58
+ static: {
59
+ extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2"],
60
+ prefixes: ["/assets/", "/fonts/"],
61
+ },
62
+
63
+ devGateBypass: ["/api/healthcheck", "/robots.txt"],
64
+ preconnect: ["https://cdn.ornek.com"],
65
+
66
+ security: {
67
+ trustProxy: true,
68
+ cookieSecret: process.env.JSKELET_SECRET,
69
+ csrf: {
70
+ enabled: true,
71
+ token: false,
72
+ allowedOrigins: [],
73
+ exclude: ["/webhook/:path*"],
74
+ cookieName: "csrf_token",
75
+ fieldName: "_csrf",
76
+ headerName: "x-csrf-token",
77
+ },
78
+ },
79
+
80
+ navigation: {
81
+ prefetch: "moderate",
82
+ prerender: "conservative",
83
+ viewTransition: true,
84
+ exclude: ["/cikis"],
85
+ },
86
+
87
+ prewarmSkip: ["/api/", "/_fragment/", "/__ornek/"],
88
+ watch: ["data"],
89
+
90
+ fonts: [{ family: "Inter", weights: [400, 600, 700] }],
91
+ icons: { dir: "icons", scan: ["views", "client", "routes", "lib"] },
92
+ images: { widths: [400, 800, 1200], quality: 78, skip: ["indirmeler"] },
93
+ clientEnv: ["PUBLIC_WS_URL"],
94
+
95
+ async headers() {
96
+ return [
97
+ {
98
+ source: "/:path*",
99
+ headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }],
100
+ },
101
+ ];
102
+ },
103
+
104
+ async redirects() {
105
+ return [{ source: "/eski/:slug", destination: "/yeni/:slug", permanent: true }];
106
+ },
107
+
108
+ async rewrites() {
109
+ return {
110
+ afterFiles: [
111
+ { source: "/api/:path*", destination: "https://api.ornek.com/:path*" },
112
+ ],
113
+ };
114
+ },
115
+
116
+ async cache() {
117
+ return {
118
+ html: { "/": 60, "/haber/:slug": 300 },
119
+ staleWhileRevalidate: 60,
120
+ query: { "/arama": ["q", "page"] },
121
+ maxEntries: 500,
122
+ data: { maxEntries: 10000, staleFactor: 10 },
123
+ prewarm: {
124
+ enabled: true,
125
+ max: 400,
126
+ concurrency: 4,
127
+ rps: 0,
128
+ intervalSeconds: 0,
129
+ rotate: true,
130
+ priority: ["/", "/haber/:slug"],
131
+ },
132
+ };
133
+ },
134
+
135
+ hooks: {
136
+ metadata() { /* … */ },
137
+ layoutContext() { /* … */ },
138
+ notFound() { /* … */ },
139
+ error() { /* … */ },
140
+ prewarmPaths() { /* … */ },
141
+ },
142
+ };
143
+ ```
144
+
145
+ ## `paths`
146
+
147
+ **Tip:** `Record<string, string>` — **Varsayılan:** aşağıdaki tablo
148
+
149
+ Proje kökündeki dizin (ve `styles` için dosya) adları. Değerler proje köküne
150
+ göre çözülür ve içeride mutlak yola çevrilir.
151
+
152
+ | Anahtar | Varsayılan | İçeriği |
153
+ | --- | --- | --- |
154
+ | `views` | `"views"` | Layout, sayfalar, bileşenler (klasik kök; `.jsk` / `.ejs`) |
155
+ | `features` | `"features"` | Feature-first dilimler (`<name>/{server,views,client}`) |
156
+ | `shared` | `"shared"` | Özellikler arası paylaşılan server/views/client |
157
+ | `public` | `"public"` | Statik dosyalar; build çıktısı da buraya yazılır |
158
+ | `client` | `"client"` | Island runtime kaynakları ve entry'ler |
159
+ | `routes` | `"routes"` | Route modülleri |
160
+ | `styles` | `"styles/globals.css"` | Tailwind/PostCSS giriş **dosyası** |
161
+ | `generated` | `".jskelet"` | `manifest.json`, `templates/`, `metafile.json`, `images.json` |
162
+
163
+ `styles` bir dosya yolu olduğu hâlde aynı çözümlemeden geçer; ayrı bir alan
164
+ tutmaya değmiyor.
165
+
166
+ İki yol her zaman türetilir ve ezilemez: `public/assets` (hash'li build
167
+ çıktısı) ve `public/fonts` (self-host fontlar).
168
+
169
+ ```js
170
+ paths: { views: "src/views", routes: "src/routes", styles: "src/styles/main.css" }
171
+ ```
172
+
173
+ ## `brand`
174
+
175
+ **Tip:** `object` — **Varsayılan:** aşağıdaki tablo
176
+
177
+ Markalama ve tek yerden değiştirilebilir isimler. Fork eden ya da beyaz etiket
178
+ kullanan projeler kendi adını verebilir. Verilen alanlar varsayılanlarla sığ
179
+ birleştirilir.
180
+
181
+ | Alan | Tip | Varsayılan | Anlamı |
182
+ | --- | --- | --- | --- |
183
+ | `name` | `string` | `"JSkelet"` | Görüntü adı |
184
+ | `poweredBy` | `string` | `"JSkelet"` | `X-Powered-By` başlığının değeri |
185
+ | `cacheHeader` | `string` | `"X-JSkelet-Cache"` | HTML cache durumu başlığı ([06-cache.md](./06-cache.md)) |
186
+ | `devBasePath` | `string` | `"/__jskelet/dev"` | Dev overlay ve rapor uçlarının kökü |
187
+ | `prewarmUserAgent` | `string` | `"jskelet-prewarm"` | Isıtma isteklerinin UA'sı; dev paneli bunu filtreler |
188
+ | `devTokenCookie` | `string` | `"dev_token"` | Dev gate'in çerez ve query parametresi adı |
189
+ | `lang` | `string` | — | `<html lang>` varsayılanı. Verilmezse layout `"en"` kullanır. |
190
+ | `sharedCookieRoots` | `string[]` | `[]` | Paylaşımlı cookie Domain kökleri (örn. `.investvio.com`, `.localhost`). [12](./12-panel-ve-oturum.md) |
191
+
192
+ `lang` için öncelik sırası: `hooks.layoutContext()` → `lang` **>** `brand.lang`
193
+ **>** `"en"`.
194
+
195
+ ```js
196
+ brand: {
197
+ lang: "tr",
198
+ poweredBy: "Örnek",
199
+ sharedCookieRoots: [".investvio.com", ".localhost"],
200
+ }
201
+ ```
202
+
203
+ ## `auth`
204
+
205
+ **Tip:** `object` — **Varsayılan:** `{ crossSubdomainHandoff: false }`
206
+
207
+ Kimlik framework'te yok; bu bölüm yalnızca alt alan adları arasında kısa
208
+ session id taşımak için handoff köprüsünü açar.
209
+
210
+ | Alan | Tip | Varsayılan | Anlamı |
211
+ | --- | --- | --- | --- |
212
+ | `crossSubdomainHandoff` | `boolean \| object` | `false` | Açıkken `POST /_jskelet/auth/handoff` + `?handoff=` redeem. Object: `allowedCookieNames` (zorunlu), `ttlSeconds?`, `path?`, `maxValueBytes?`, `maxPendingTickets?`, `maxMintsPerIpPerMinute?` |
213
+
214
+ ```js
215
+ auth: {
216
+ crossSubdomainHandoff: {
217
+ allowedCookieNames: ["sid"],
218
+ ttlSeconds: 60,
219
+ },
220
+ },
221
+ ```
222
+
223
+ Mint uç noktası CSRF middleware'inden **sonra** mount edilir (origin kontrolü).
224
+ Cookie adı allowlist dışındaysa veya RFC 6265 token değilse 400. Ayrıntı:
225
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md).
226
+
227
+ ## `layout`
228
+
229
+ **Tip:** `string` — **Varsayılan:** yok (otomatik çözüm)
230
+
231
+ Layout dosyasının yolu (`.jsk` veya legacy `.ejs`). Verilen değer **views
232
+ dizininin üst dizinine** göre çözülür, yani varsayılan `views` ile
233
+ `"views/ozel.jsk"` → `<root>/views/ozel.jsk`.
234
+
235
+ Verilmezse sırayla: `views/layout.jsk`, `views/layout.ejs` (legacy), yoksa
236
+ framework'ün `src/templates/layout.jsk` varsayılanı. Ayrıntı:
237
+ [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md).
238
+
239
+ ## `routes`
240
+
241
+ **Tip:** `string[]` — **Varsayılan:** `null` (dizin taraması)
242
+
243
+ Route modüllerinin açık listesi, proje köküne göre. Verilen sırada yüklenir.
244
+ Verilmezse `paths.routes` dizini alfabetik ve özyinelemeli olarak taranır.
245
+ Ayrıntı: [03-routing.md](./03-routing.md).
246
+
247
+ ```js
248
+ routes: ["./routes/api.js", "./routes/pages.js", "./routes/catch-all.js"]
249
+ ```
250
+
251
+ ## `trailingSlash`
252
+
253
+ **Tip:** `boolean` — **Varsayılan:** `false`
254
+
255
+ `true` iken kanonik URL'ler `/` ile biter: `/hakkinda/` doğrudan **200**
256
+ döner; slash'sız `/hakkinda` **308** ile `/hakkinda/` adresine gider (301
257
+ değil — metodu koruyan kalıcı yönlendirme, framework'ün diğer `permanent`
258
+ redirect'leriyle aynı). Query string korunur.
259
+
260
+ İstisnalar: kök `/`, uzantılı dosya yolları (`/robots.txt`, `/assets/app.js`)
261
+ ve `/.well-known/**`. Bunlara slash eklenmez.
262
+
263
+ `false` iken (varsayılan) slash dayatılmaz. Express non-strict eşleşme ile
264
+ `/x` ve `/x/` ikisi de 200 olabilir — Next.js'in varsayılan "slash'ı kırp"
265
+ davranışından bilinçli fark; mevcut siteleri kırmamak için.
266
+
267
+ Açıkken şablonlardaki `href`, sitemap ve `redirects()` hedeflerini de
268
+ slash'lı yazın; aksi hâlde tarayıcı her tıklamada ekstra bir 308 görür.
269
+
270
+ ```js
271
+ trailingSlash: true
272
+ ```
273
+
274
+ ## `static`
275
+
276
+ **Tip:** `{ extensions?: string[], prefixes?: string[] }` — **Varsayılan:**
277
+ aşağıda
278
+
279
+ Uzantı ve önek bazlı statik dosya tespiti. Bu listeye uyan yollara
280
+ `Cache-Control: public, max-age=31536000, immutable` yazılır.
281
+
282
+ | Alan | Varsayılan |
283
+ | --- | --- |
284
+ | `extensions` | `[".svg", ".png", ".webp", ".avif", ".ico", ".woff2"]` |
285
+ | `prefixes` | `["/assets/", "/fonts/"]` |
286
+
287
+ Verilirse varsayılanın **yerine** geçer (birleştirilmez), yani varsayılana ek
288
+ yapmak isterseniz tam listeyi yazın.
289
+
290
+ ```js
291
+ static: {
292
+ extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2", ".mp4"],
293
+ prefixes: ["/assets/", "/fonts/", "/video/"],
294
+ }
295
+ ```
296
+
297
+ ## `devGate`
298
+
299
+ **Tip:** `boolean` — **Varsayılan:** `false`
300
+
301
+ Yayına açılmamış ortamı gizler. **`DEV_TOKEN` tek başına siteyi kilitlemez.**
302
+ Paylaşılan bir task tanımı production'a da aynı değişkeni taşıyabilir; o
303
+ durumda ziyaretçi token vermek zorunda kalmaz, site açık kalır.
304
+
305
+ Gate'i açmak için `devGate: true` ya da `DEV_GATE=1`. İkisi de varken token
306
+ taşımayan isteğe 404 döner. `DEV_GATE=0` config'teki açığı da kapatır. Token
307
+ boşsa gate açık olsa da istekler geçer.
308
+
309
+ Ayrıntı: [09-dev-araclari.md](./09-dev-araclari.md).
310
+
311
+ ## `devGateBypass`
312
+
313
+ **Tip:** `string[]` — **Varsayılan:**
314
+ `["/api/healthcheck", "/robots.txt", "/sitemap.xml", "/site.webmanifest", "/favicon.ico"]`
315
+
316
+ Dev gate'in hiçbir koşulda kapatmadığı **tam** yollar (önek değil, birebir
317
+ eşleşme). Gate açıkken sağlık kontrolünün ve robots dosyalarının erişilebilir
318
+ kalması için. Verilirse varsayılanın yerine geçer.
319
+
320
+ Ayrıntı: [09-dev-araclari.md](./09-dev-araclari.md).
321
+
322
+ ## `preconnect`
323
+
324
+ **Tip:** `string[]` — **Varsayılan:** `[]`
325
+
326
+ Üçüncü taraf origin'ler; her sayfanın `<head>`inde `<link rel="preconnect">`
327
+ olarak basılır. Görsel CDN'i, API origin'i, font host'u buraya yazılır. Değerler
328
+ `new URL(...).origin` ile normalize edilir; geçersiz bir URL atlanır ve uyarı
329
+ basılır.
330
+
331
+ Liste her sayfada aynı olduğu için bir kez hesaplanıp saklanır. Boş liste geçerli
332
+ bir yapılandırmadır.
333
+
334
+ ```js
335
+ preconnect: ["https://cdn.ornek.com", "https://api.ornek.com"]
336
+ ```
337
+
338
+ ## `security`
339
+
340
+ **Tip:** `object` — **Varsayılan:**
341
+ `{ trustProxy: true, cookieSecret: null, csrf: { enabled: true, token: false, … } }`
342
+
343
+ Kişiye özel sayfaların tamamı ve gerekçeleri
344
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de; burada alanların referansı
345
+ var.
346
+
347
+ | Alan | Tip | Varsayılan | Anlamı |
348
+ | --- | --- | --- | --- |
349
+ | `trustProxy` | `boolean` | `true` | Express'in `trust proxy` ayarı. Ters proxy arkasında doğru protokol ve istemci IP'si için gerekli. |
350
+ | `cookieSecret` | `string \| null` | `null` | İmzalı cookie sırrı. Verilmezse `JSKELET_SECRET` okunur. |
351
+ | `csrf.enabled` | `boolean` | `true` | Origin/`Sec-Fetch-Site` kontrolü. |
352
+ | `csrf.token` | `boolean` | `false` | Çift gönderim token'ı katmanı. Cookie oturumlu formlarda **açın**. |
353
+ | `csrf.allowedOrigins` | `string[]` | `[]` | Kendi host'umuzun yanında kabul edilen origin'ler. |
354
+ | `csrf.exclude` | `string[]` | `[]` | Kontrolden muaf yollar; `source` desen sözdizimi. |
355
+ | `csrf.cookieName` | `string` | `"csrf_token"` | Token cookie'sinin adı. |
356
+ | `csrf.fieldName` | `string` | `"_csrf"` | `csrfField()`in bastığı alan adı. |
357
+ | `csrf.headerName` | `string` | `"x-csrf-token"` | Token'ın kabul edildiği başlık. |
358
+
359
+ `trustProxy` doğrudan internete açık bir sunucuda **kapatılmalı**: açıkken
360
+ istemci kendi `X-Forwarded-For` / `X-Forwarded-Proto` / Host başlığını
361
+ uydurabilir; rate limit, admin IP allowlist, Secure cookie ve cache `vary.host`
362
+ yanlış adresi görür. Ters proxy (nginx, Caddy, Cloudflare) arkasındaysa `true`
363
+ doğru varsayılandır.
364
+
365
+ CSRF kontrolü yalnızca çapraz site olduğu **belli** olan istekleri reddeder —
366
+ `Origin` uyuşmuyorsa ya da `Sec-Fetch-Site: cross-site` geldiyse. İkisi de yoksa
367
+ istek geçer, çünkü tarayıcılar çapraz origin bir POST'ta `Origin`'i her zaman
368
+ gönderirken webhook'lar hiç göndermez. Cookie ile oturum açan panel/form
369
+ uygulamalarında `csrf.token: true` + `csrfField()` ikinci katmandır; webhook
370
+ uçlarını `csrf.exclude` listesine yazın.
371
+
372
+ ## `navigation`
373
+
374
+ **Tip:** `object` — **Varsayılan:**
375
+ `{ prefetch: "moderate", prerender: false, viewTransition: false, exclude: [] }`
376
+
377
+ Site içi gezinmeyi hızlandıran `<head>` ipuçları. JSkelet klasik MPA olduğu için
378
+ her tıklama tam sayfa yüklemesidir; bu bölüm o yüklemeyi tarayıcının **önceden**
379
+ yapmasını sağlar. Client runtime'ı eklenmez — Speculation Rules ve view
380
+ transition tarayıcı yetenekleridir, desteklemeyen tarayıcıda sessizce yok
381
+ sayılırlar.
382
+
383
+ | Alan | Tip | Varsayılan | Anlamı |
384
+ | --- | --- | --- | --- |
385
+ | `prefetch` | `false \| "conservative" \| "moderate" \| "eager"` | `"moderate"` | Bağlantı hedefinin **belgesini** önceden indirir |
386
+ | `prerender` | aynı | `false` | Hedefi arka planda **tam render eder**; tıklama anında açılır |
387
+ | `viewTransition` | `boolean` | `false` | `@view-transition { navigation: auto }` basar |
388
+ | `exclude` | `string[]` | `[]` | Spekülasyon dışı bırakılacak href desenleri |
389
+
390
+ `true` verilirse `prefetch`/`prerender` varsayılan eagerness'a düşer; tanınmayan
391
+ bir değer uyarı basıp varsayılana döner.
392
+
393
+ **Eagerness ne demek:** `conservative` bağlantıya basıldığı an, `moderate`
394
+ bağlantı üzerinde bir süre duraksandığında, `eager` bağlantı görünür olur olmaz
395
+ tetikler. Yukarı çıktıkça isabet artar, boşa giden istek de artar.
396
+
397
+ **`prerender` neden kapalı geliyor.** Prerender edilen sayfanın script'leri
398
+ gerçekten çalışır. Ölçüm kodunu `prerenderingchange` olayına bağlamayan bir
399
+ uygulamada ziyaret sayıları şişer. Açmadan önce analytics'i gözden geçirin;
400
+ sunucu tarafındaki maliyeti düşüktür, çünkü spekülatif istek de HTML
401
+ önbelleğinden karşılanır ([06-cache.md](./06-cache.md)).
402
+
403
+ **Her koşulda muaf olanlar.** `/api/*`, `/_fragment/*` ve `brand.devBasePath`
404
+ altındaki yollar otomatik dışlanır; `exclude` bunların üstüne eklenir. Ayrıca
405
+ `rel="nofollow"`, `target="_blank"` ve `data-no-prefetch` taşıyan bağlantılar
406
+ hiçbir kurala girmez. Yan etkisi olan tek bir bağlantıyı dışarıda bırakmanın en
407
+ kolay yolu sonuncusu:
408
+
409
+ ```html
410
+ <a href="/cikis" data-no-prefetch>Çıkış</a>
411
+ ```
412
+
413
+ **`viewTransition` açarken arka planı `<html>`e verin.** Geçiş sırasında tarayıcı
414
+ eski ve yeni sayfanın anlık görüntülerini çapraz geçirir; `<body>`ye verilmiş bir
415
+ arka plan bu görüntünün içinde kalır ve altta kalan canvas görünür. Sonuç, her
416
+ geçişte bir kare beyaz flaştır ve koyu temada gözden kaçmaz. Renk `<html>` (ya da
417
+ `:root`) üzerindeyse böyle bir boşluk oluşmaz:
418
+
419
+ ```html
420
+ <html lang="tr" class="bg-white dark:bg-slate-950">
421
+ <body class="text-slate-900 dark:text-slate-100">
422
+ ```
423
+
424
+ Hareket azaltma tercihi framework tarafından karşılanır: `prefers-reduced-motion:
425
+ reduce` altında geçiş kapatılır, ayrıca bir şey yazmanız gerekmez.
426
+
427
+ **Geçişi içerikle sınırlayın.** Varsayılan davranış tüm belgeyi tek parça olarak
428
+ çapraz geçirir, yani gezinme boyunca hiç değişmeyen header ve footer da titrer.
429
+ Bu bölgelere bir `view-transition-name` vermek onları kendi grubuna alır;
430
+ tarayıcı aynı adı iki belgede de gördüğü için "aynı öğe" sayar. Adlandırılan
431
+ öğenin animasyonunu kapatınca geçiş yalnızca içerikte kalır:
432
+
433
+ ```css
434
+ body > header { view-transition-name: site-header; }
435
+ body > footer { view-transition-name: site-footer; }
436
+
437
+ ::view-transition-old(site-header),
438
+ ::view-transition-old(site-footer) { animation: none; opacity: 0; }
439
+ ::view-transition-new(site-header),
440
+ ::view-transition-new(site-footer) { animation: none; opacity: 1; }
441
+
442
+ /* Kalan içerik; varsayılan 250ms gezinmeyi yavaş hissettiriyor. */
443
+ ::view-transition-old(root),
444
+ ::view-transition-new(root) { animation-duration: 180ms; }
445
+ ```
446
+
447
+ Çalışan bir örnek için Tailwind `@source` ve view-transition CSS'ini kendi
448
+ uygulamanızın `styles/globals.css` dosyasına taşıyın; yukarıdaki bloklar
449
+ başlangıç noktasıdır.
450
+
451
+ **CSP kullanıyorsanız** kurallar satır içi bir `<script type="speculationrules">`
452
+ olarak basılır; `script-src` politikanızın buna izin vermesi gerekir.
453
+
454
+ ```js
455
+ navigation: {
456
+ prefetch: "moderate",
457
+ prerender: "conservative",
458
+ viewTransition: true,
459
+ exclude: ["/cikis", "/sepet/*"],
460
+ }
461
+ ```
462
+
463
+ ## `prewarmSkip`
464
+
465
+ **Tip:** `string[]` — **Varsayılan:** `["/api/", "/_fragment/", "/__jskelet/"]`
466
+
467
+ Isıtmanın atlayacağı yol **önekleri**. Oturuma bağlı ya da fragment uçları
468
+ ısıtılmamalı. Verilirse varsayılanın yerine geçer — `brand.devBasePath`i
469
+ değiştirdiyseniz bu listeyi de güncellemeyi unutmayın.
470
+
471
+ Ayrıntı: [06-cache.md](./06-cache.md).
472
+
473
+ ## `watch`
474
+
475
+ **Tip:** `string[]` — **Varsayılan:** `[]`
476
+
477
+ `jskelet dev`in sunucu yeniden başlatma için izleyeceği **ek** dizinler, proje
478
+ köküne göre. `routes`, `views` ve `lib` zaten izlenir; `client/` ve `styles/`
479
+ esbuild ve CSS watcher'ları tarafından ele alınır, buraya konmamalı.
480
+
481
+ Yalnızca `.js`, `.mjs`, `.json` ve `.ejs` uzantılı dosyalar tetikleyicidir.
482
+
483
+ ```js
484
+ watch: ["data", "content"]
485
+ ```
486
+
487
+ Ayrıntı: [09-dev-araclari.md](./09-dev-araclari.md).
488
+
489
+ ## `fonts`
490
+
491
+ **Tip:** `{ family: string, slug?: string, weights?: number[] }[]` —
492
+ **Varsayılan:** `[]`
493
+
494
+ Self-host edilecek Google Fonts aileleri. Boş bırakılırsa font adımı hiç
495
+ çalışmaz.
496
+
497
+ | Alan | Tip | Varsayılan | Anlamı |
498
+ | --- | --- | --- | --- |
499
+ | `family` | `string` | — | Google Fonts aile adı: `"Inter"`, `"Noto Sans"` |
500
+ | `slug` | `string` | `family`den türetilir (küçük harf, boşluk → `-`) | Dosya adı öneki |
501
+ | `weights` | `number[]` | `[400]` | İndirilecek ağırlıklar |
502
+
503
+ Çıktı: `public/fonts/<slug>-<weight>.woff2`, manifest anahtarı aynı dosya adı.
504
+ Dosyalar **sabit isimlidir** (hash yok) ve **commit edilmesi beklenir**.
505
+ Ayrıntı: [08-build.md](./08-build.md).
506
+
507
+ ```js
508
+ fonts: [
509
+ { family: "Inter", weights: [400, 600, 700] },
510
+ { family: "Noto Serif", slug: "serif", weights: [400] },
511
+ ]
512
+ ```
513
+
514
+ ## `icons`
515
+
516
+ **Tip:** `{ scan?: string[], dir?: string } | false` — **Varsayılan:** `{ dir: "icons" }`
517
+
518
+ SVG ikon sprite üretimi. Kaynak **XOR** seçilir: `icons.dir` dizini varsa
519
+ yalnızca oradaki düz SVG'ler; yoksa `@phosphor-icons/core` (kuruluysa).
520
+
521
+ | Değer | Sonuç |
522
+ | --- | --- |
523
+ | `{}` (varsayılan) | `dir: "icons"`; taranan dizinler `["views", "client", "routes", "lib", "features", "shared"]` |
524
+ | `{ dir: "assets/icons" }` | Yerel SVG kökü değiştirilir |
525
+ | `{ scan: [...] }` | Taranan dizinler değiştirilir |
526
+ | `false` | Sprite adımı tamamen atlanır |
527
+
528
+ Yerel dizin (varsa) düz dosya adları kullanır: `house.svg` → `house:regular`,
529
+ `house-bold.svg` → `house:bold`. Boş bir `icons/` dizini Phosphor'a düşmez —
530
+ dizini silmek fallback'i açar. Ayrıntı: [08-build.md](./08-build.md).
531
+
532
+ ```js
533
+ icons: {
534
+ dir: "icons",
535
+ scan: ["views", "client", "routes", "lib", "content"],
536
+ }
537
+ ```
538
+
539
+ ## `images`
540
+
541
+ **Tip:**
542
+ `{ widths?: number[], quality?: number, skip?: string[], remote?: { allowHosts: string[], path?: string, maxWidth?: number, cacheMaxAge?: number, fetchTimeoutMs?: number, maxBytes?: number } | false } | false`
543
+ — **Varsayılan:** `{ widths, quality, skip, remote: false }` (remote kapalı)
544
+
545
+ `public/` altındaki png/jpg görsellerin webp varyantlarını **build**'de üretir.
546
+ `remote.allowHosts` verilirse çalışma anında uzak görselleri de proxy eder
547
+ (`/_jskelet/image?url=&w=&q=` → webp).
548
+
549
+ | Alan | Tip | Varsayılan | Anlamı |
550
+ | --- | --- | --- | --- |
551
+ | `widths` | `number[]` | `[400, 640, 960, 1280, 1920]` | Build ve remote `srcset` adayları. Kaynaktan büyük olanlar build'de elenir; kaynağın kendi genişliği (en fazla 1920) her zaman eklenir. |
552
+ | `quality` | `number` | `78` | webp kalitesi. Build'de imzaya girer; remote uçta `q` varsayılanı. |
553
+ | `skip` | `string[]` | `[]` | Build'de taranmayacak **dizin adları**. `assets` ve `fonts` her zaman atlanır. |
554
+ | `remote` | `object \| false` | kapalı | Runtime optimizer. `allowHosts` **zorunlu**; boşsa uç mount edilmez. |
555
+
556
+ ### `images.remote`
557
+
558
+ | Alan | Tip | Varsayılan | Anlamı |
559
+ | --- | --- | --- | --- |
560
+ | `allowHosts` | `string[]` | `[]` | Çekilebilecek host'lar. `*.cdn.example.com` sonek jokerini destekler. |
561
+ | `path` | `string` | `/_jskelet/image` | Optimizer GET yolu. |
562
+ | `maxWidth` | `number` | `1920` | `w` üst sınırı. |
563
+ | `cacheMaxAge` | `number` | `2592000` (30 gün) | Yanıt `Cache-Control` max-age (saniye). Disk önbelleği `.jskelet/image-cache/`; 256 MB'yi geçince en eski dosya düşer. |
564
+ | `fetchTimeoutMs` | `number` | `10000` | Upstream fetch zaman aşımı. |
565
+ | `maxBytes` | `number` | `10485760` (10 MiB) | Upstream gövde üst sınırı. |
566
+
567
+ `false` verilirse görsel adımı hiç çalışmaz. Build adımı `sharp` gerektirir ve
568
+ watch turunda hiç çalışmaz. Remote açıksa `sharp` **runtime**'da da gerekir;
569
+ yoksa optimizer kaynak URL'ye 302 yönlendirir. Fetch, redirect'leri otomatik
570
+ takip etmez: her hop `allowHosts` ve private adres kontrolünden geçer.
571
+ Ayrıntı: [08-build.md](./08-build.md).
572
+
573
+ ```js
574
+ images: {
575
+ widths: [400, 800, 1200],
576
+ quality: 82,
577
+ skip: ["indirmeler"],
578
+ remote: {
579
+ allowHosts: ["static.ornek.com", "*.cdn.ornek.com"],
580
+ },
581
+ }
582
+ ```
583
+
584
+ `image({ src: "https://static.ornek.com/a.jpg", width: 96, alt: "…" })` bu
585
+ ayarla `src` / `srcset`'i `/_jskelet/image?url=…&w=96` biçimine çevirir.
586
+ Elle URL kurmak için `remoteImageUrl(src, { width })` (`jskelet`).
587
+
588
+ ## `clientEnv`
589
+
590
+ **Tip:** `string[]` — **Varsayılan:** `[]`
591
+
592
+ Client bundle'a build zamanında gömülecek ortam değişkeni anahtarları. Next'teki
593
+ `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın herkese açık olduğu
594
+ isimden değil config'ten belli. `NODE_ENV` her zaman gömülür.
595
+
596
+ `process.env`in tamamı tek nesne olarak define edildiği için listede olmayan bir
597
+ anahtar okunduğunda çökme yerine `undefined` döner.
598
+
599
+ ```js
600
+ clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN_ORIGIN"]
601
+ ```
602
+
603
+ **Buraya gizli anahtar koymayın** — değerler bundle'da düz metin olarak durur.
604
+ İsimlerinde `SECRET`, `PASSWORD`, `TOKEN`, `API_KEY`, `PRIVATE` vb. geçen
605
+ anahtarlar build sırasında **reddeder** (`PUBLIC` / `PUBLISHABLE` içerenler
606
+ muaf).
607
+
608
+ ## `headers()`
609
+
610
+ **Tip:** `() => { source: string, headers: { key: string, value: string }[] }[]`
611
+ — **Varsayılan:** `[]`
612
+
613
+ Yol desenine göre yanıt başlıkları. Framework yalnızca statik dosyalara uzun
614
+ ömürlü cache yazar; bunun dışındaki her başlık (CSP, COOP, HSTS,
615
+ X-Frame-Options…) buradan gelir ve varsayılanların üstüne biner. Üretim
616
+ sitelerinde en azından aşağıdaki güvenlik başlıklarını tanımlayın.
617
+
618
+ Eşleşen **tüm** kurallar uygulanır (redirect'lerin aksine ilk eşleşmede
619
+ durulmaz), sırayla; aynı başlığı iki kural yazarsa sonraki kazanır.
620
+
621
+ `key`i olmayan ya da `value`u `undefined` olan girdiler atlanır; hiç geçerli
622
+ başlığı kalmayan bir kural hiç eklenmez.
623
+
624
+ ```js
625
+ async headers() {
626
+ return [
627
+ {
628
+ source: "/:path*",
629
+ headers: [
630
+ { key: "X-Frame-Options", value: "SAMEORIGIN" },
631
+ { key: "X-Content-Type-Options", value: "nosniff" },
632
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
633
+ {
634
+ key: "Permissions-Policy",
635
+ value: "camera=(), microphone=(), geolocation=()",
636
+ },
637
+ {
638
+ key: "Content-Security-Policy",
639
+ value: "default-src 'self'; img-src 'self' https://cdn.ornek.com data:; script-src 'self'",
640
+ },
641
+ // Yalnızca HTTPS terminasyonu sizin kontrolünüzdeyse:
642
+ // { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
643
+ ],
644
+ },
645
+ {
646
+ source: "/indirme/:path*",
647
+ headers: [{ key: "Cache-Control", value: "no-store" }],
648
+ },
649
+ ];
650
+ }
651
+ ```
652
+
653
+ ## `redirects()`
654
+
655
+ **Tip:**
656
+ `() => { source: string, destination: string, permanent?: boolean, statusCode?: number }[]`
657
+ — **Varsayılan:** `[]`
658
+
659
+ | Alan | Tip | Anlamı |
660
+ | --- | --- | --- |
661
+ | `source` | `string` | Desen (aşağıdaki sözdizimi) |
662
+ | `destination` | `string` | Hedef; `:param` yer tutucuları doldurulur |
663
+ | `permanent` | `boolean` | `true` → 308, aksi hâlde 307 |
664
+ | `statusCode` | `number` | Açık durum kodu; `permanent`i ezer |
665
+
666
+ İlk eşleşen kural kazanır ve query string korunur. Ayrıntı:
667
+ [03-routing.md](./03-routing.md).
668
+
669
+ ## `rewrites()`
670
+
671
+ **Tip:** `() => Rule[] | { beforeFiles?: Rule[], afterFiles?: Rule[] }`
672
+ burada `Rule = { source: string, destination: string }` — **Varsayılan:** `[]`
673
+
674
+ Dizi döndürülürse tamamı `afterFiles` sayılır.
675
+
676
+ - `beforeFiles` statik dosyalardan da önce çalışır.
677
+ - `afterFiles` statik denendikten sonra, route'lardan önce çalışır.
678
+ - Mutlak hedef (`http://`/`https://`) → gömülü ters proxy.
679
+ - Göreli hedef → yalnızca `req.url` değişir.
680
+
681
+ Ayrıntı: [03-routing.md](./03-routing.md).
682
+
683
+ ## `cache()`
684
+
685
+ **Tip:**
686
+ `() => { html?: Record<string, number>, staleWhileRevalidate?: number, query?: Record<string, string[] | true>, vary?: { host?: boolean, headers?: string[], fn?: (req) => string | null }, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, upstream?: object, redis?: object, prewarm?: object }` —
687
+ **Varsayılan:**
688
+ `{ html: {}, staleWhileRevalidate: 60, query: {}, vary: { host: false }, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300 }, upstream: { rate: 0 }, redis: { enabled: false }, prewarm: { enabled: true, max: 400, intervalSeconds: 0, origins: [] } }`
689
+
690
+ ### `cache().html`
691
+
692
+ Desen → saniye eşlemesi. Eşleşen kural, route'un kendi `revalidate` değerini
693
+ **ezer**. Negatif ya da sonlu olmayan değerler yok sayılır; `0` "önbellekleme"
694
+ anlamına gelir. TTL dolmadan önce framework, son render süresine göre erken
695
+ arka plan tazelemesi başlatır (ayrı bir config alanı yok; ayrıntı
696
+ [06-cache.md](./06-cache.md)).
697
+
698
+ ```js
699
+ html: {
700
+ "/": 60,
701
+ "/haber/:slug": 300,
702
+ "/arama": 0,
703
+ }
704
+ ```
705
+
706
+ Tek istisna `route(fn, { private: true })`: bu route'ta desen eşleşse bile yok
707
+ sayılır. Kilidin tek yönlü olması bilinçli — ters yönde bir hata, bir
708
+ kullanıcının HTML'inin bir başkasına servis edilmesi anlamına geliyor.
709
+
710
+ ### `cache().staleWhileRevalidate`
711
+
712
+ **Tip:** `number` — **Varsayılan:** `60`
713
+
714
+ Edge'in taze penceresi (`cache().html` / `revalidate`) bittikten sonra eski
715
+ HTML'i sunacağı süre, saniye. `CDN-Cache-Control` üzerindeki
716
+ `stale-while-revalidate` direktifine yazılır. `0` ise direktif basılmaz.
717
+ Süreç içi HTML önbelleğinin stale penceresini değiştirmez. Ayrıntı:
718
+ [06-cache.md](./06-cache.md).
719
+
720
+ ### `cache().query`
721
+
722
+ Desen → cache anahtarına girmesine izin verilen query parametreleri.
723
+
724
+ **Varsayılan olarak query parametresi taşıyan istek dinamiktir**: `cache().html`
725
+ o yolu kapsıyor olsa bile HTML önbelleğine hiç girmez, `private, no-store` ile
726
+ gider. Sebebi basit — bir yolun bütün varyantlarını cache'lemek `?utm_source=…`
727
+ gibi sonsuz sayıda anahtar üretiyor ve `maxEntries` sınırına dayandığında
728
+ LRU'daki gerçek sayfaları dışarı atıyor. Hangi parametrenin çıktıyı gerçekten
729
+ değiştirdiğini yalnızca uygulama bilir.
730
+
731
+ ```js
732
+ query: {
733
+ "/arama": ["q", "page"], // yalnızca bu ikisi anahtara girer
734
+ "/urunler": ["kategori"],
735
+ "/rapor/:id": true, // bütün parametreler anahtara girer
736
+ "/kampanya": [], // query tamamen yok sayılır
737
+ }
738
+ ```
739
+
740
+ - **İzin listesi** (`string[]`): listedeki parametreler anahtara girer, her
741
+ farklı değer kendi girdisini alır. Listede olmayan parametreler **yok
742
+ sayılır** — sayfa yine cache'lenir ve bütün kampanya varyantları tek kopyayı
743
+ paylaşır.
744
+ - **`true`**: bütün parametreler anahtara girer. Anahtar sayısını sınırlayan
745
+ tek şey `maxEntries` olur; yalnızca değer kümesi kapalı olan yollarda kullan.
746
+ - **`[]`**: query hiç dikkate alınmaz, bütün varyantlar query'siz sürümün
747
+ HTML'ini alır.
748
+
749
+ Parametreler anahtara **sıralı** yazılır: `?a=1&b=2` ile `?b=2&a=1` aynı girdiyi
750
+ paylaşır. `route(fn, { private: true })` bu bölümden etkilenmez; private route
751
+ hiçbir koşulda cache'lenmez.
752
+
753
+ ### `cache().vary`
754
+
755
+ HTML cache anahtarına query allowlist'ten **bağımsız** sabit parçalar ekler.
756
+ Host'tan locale üreten sitelerde `host: true` **zorunlu**; aksi halde ilk
757
+ locale'in HTML'i diğer host'a servis edilir. CDN zaten tam URL ile ayırır —
758
+ bu ayar origin L1 ve Redis HTML anahtarı içindir.
759
+
760
+ ```js
761
+ vary: {
762
+ host: true, // h=tr.example.com|…
763
+ // headers: ["x-locale"],
764
+ // fn: (req) => req.hostname.startsWith("tr.") ? "l=tr" : "l=en",
765
+ }
766
+ ```
767
+
768
+ | Alan | Tip | Varsayılan | Anlamı |
769
+ | --- | --- | --- | --- |
770
+ | `host` | `boolean` | `false` | Public Host (`x-forwarded-host` yoksa `Host`), lowercase, portsuz → `h=…` |
771
+ | `headers` | `string[]` | `[]` | İstek başlıkları `ad=değer` olarak eklenir |
772
+ | `fn` | `(req) => string \| null` | — | Dönüş bir segment olarak eklenir |
773
+
774
+ Anahtar biçimi: `${vary}|${yol}?${query}` (vary yoksa önek yok). Ayrıntı:
775
+ [06-cache.md](./06-cache.md).
776
+
777
+ ### `cache().maxEntries`
778
+
779
+ **Tip:** `number` — **Varsayılan:** `500`
780
+
781
+ HTML önbelleğinin girdi sınırı. Girdi başına yüz kilobayt düştüğü için bu sayıyı
782
+ yükseltmek belleği hızla tüketir; on binlerce yollu bir siteyi buradan çözmeye
783
+ çalışmak yanlış katman, doğru yer `cache().data`.
784
+
785
+ **Tavan 800.** Daha yükseği yüklemede uyarıyla 800'e çekilir. Süreç içi HTML +
786
+ sıkıştırılmış gövde ayrıca 256 MB'yi geçemez; bu bütçe config'den yükseltilmez.
787
+ Sıkıştırılmış kopya tektir (brotli veya gzip).
788
+
789
+ ### `cache().data`
790
+
791
+ Upstream veri önbelleği (`withDataCache`). Ayrıntı: [06-cache.md](./06-cache.md).
792
+
793
+ | Alan | Tip | Varsayılan | Anlamı |
794
+ | --- | --- | --- | --- |
795
+ | `maxEntries` | `number` | `10000` | LRU girdi sınırı. JSON, HTML'e göre onlarca kat küçük olduğu için sınır yüksek. **Tavan 20000**; üstü uyarıyla kesilir. Süreç içi JSON ayrıca **64 MB**'yi geçemez; bu bütçe config'den yükseltilmez. |
796
+ | `staleFactor` | `number` | `10` | TTL dolduktan sonra girdinin kaç TTL boyunca daha kullanılabileceği. `0` → bayat servis yok. |
797
+
798
+ ### `cache().trackUpstream`
799
+
800
+ **Tip:** `boolean` — **Varsayılan:** `true`
801
+
802
+ Açıkken `globalThis.fetch` sarılır ve render sırasındaki geçici upstream
803
+ hataları (`429`, `5xx`, ağ) kendiliğinden bildirilir; `reportUpstreamFailure()`
804
+ çağırmak gerekmez. `fetch`i kendisi saran bir uygulama bunu kapatabilir.
805
+
806
+ ### `cache().trackDependencies`
807
+
808
+ **Tip:** `boolean` — **Varsayılan:** `true`
809
+
810
+ Açıkken bir render'ın okuduğu `withDataCache` anahtarları kaydedilir ve
811
+ `clearDataCache()` o veriyi okumuş HTML sayfalarını da bayatlatır — hedefli
812
+ invalidation için uygulamanın hiçbir şey bildirmesi gerekmez
813
+ ([06-cache.md](./06-cache.md)). `withDataCache` kullanmayan bir uygulamada
814
+ kaydedilecek bir şey yok; kapatmak bağlam kurma maliyetini de kaldırır.
815
+
816
+ ### `cache().transientRetry`
817
+
818
+ **Tip:** `{ attempts?: number, delayMs?: number } | false` —
819
+ **Varsayılan:** `{ attempts: 1, delayMs: 300 }`
820
+
821
+ Geçici bir upstream hatası yüzünden `notFound()` çağrılan sayfa kaç kez daha
822
+ denenir. Amaç var olan bir sayfanın 404'e dönüşmemesi; denemeler tükenirse yanıt
823
+ önbelleğe girmeyen bir 503 olur. `false` ya da `attempts: 0` tekrarı kapatır.
824
+ Ayrıntı: [06-cache.md](./06-cache.md).
825
+
826
+ ### `cache().upstream`
827
+
828
+ Upstream API'ye giden `fetch` çağrılarının host başına hız freni. Varsayılan
829
+ **kapalı**: `rate` verilmedikçe hiçbir istek beklemez. `rate` bir tavandır;
830
+ gerçek hız 429 cevaplarına göre kendini aşağı çeker ve temiz geçen pencerelerde
831
+ kademe kademe geri çıkar.
832
+
833
+ | Alan | Tip | Varsayılan | Anlamı |
834
+ | --- | --- | --- | --- |
835
+ | `rate` | `number` | `0` | Saniyedeki en fazla çağrı. `0` → fren kapalı |
836
+ | `burst` | `number` | `0` | Kova boyu; `0` → bir saniyelik bütçe kadar patlama |
837
+ | `concurrency` | `number` | `8` | Aynı anda uçabilecek çağrı |
838
+ | `minRate` | `number` | `0.5` | Azalmanın dibi; hız buranın altına inmez |
839
+ | `increaseStep` | `number` | `1` | Toplamsal artışın adımı (çağrı/saniye) |
840
+ | `increaseIntervalMs` | `number` | `5000` | Artış periyodu |
841
+ | `decreaseIntervalMs` | `number` | `1000` | İki azalma arasındaki en kısa süre |
842
+ | `breakerFailures` | `number` | `5` | Art arda kaç 429'dan sonra host baypas edilir |
843
+ | `breakerCooldownMs` | `number` | `10000` | Baypasın süresi |
844
+ | `hosts` | `Record<string, object>` | `{}` | Host bazlı override; aynı alanlar geçerli |
845
+
846
+ Yalnızca `429` ve `503` hızı cezalandırır: `400`/`404`/`500` bir kota sorunu
847
+ değil. Durumu `getUpstreamLimiterStatus()` ile ya da dev panelinin **Server**
848
+ sekmesinden okuyabilirsin. Ayrıntı ve freni açmadan önce bakılacak yer:
849
+ [06-cache.md](./06-cache.md).
850
+
851
+ ```js
852
+ upstream: {
853
+ rate: 10,
854
+ concurrency: 4,
855
+ hosts: { "api.example.com": { rate: 3 } },
856
+ }
857
+ ```
858
+
859
+ ### `cache().redis`
860
+
861
+ Opsiyonel Redis ikinci kademesi (L2). Bellek içi önbellek birincil kalır; Redis
862
+ yalnızca L1'de bulunmayan bir yol için render'ı atlatır ve invalidation'ı diğer
863
+ instance'lara yayar. `ioredis` uygulamaya kurulmalı (`npm install ioredis`);
864
+ kurulmadıysa ya da bağlanılamıyorsa uyarı basılır ve site bellek içi önbellekle
865
+ çalışmaya devam eder.
866
+
867
+ | Alan | Tip | Varsayılan | Anlamı |
868
+ | --- | --- | --- | --- |
869
+ | `enabled` | `boolean` | `false` | Yalnızca açıkça `true` verildiğinde açılır |
870
+ | `url` | `string \| null` | `null` | `redis://` ya da `rediss://`. Boşsa ioredis varsayılanı (`localhost:6379`) |
871
+ | `namespace` | `string` | `"default"` | Aynı Redis'i paylaşan uygulamaları ayırır |
872
+ | `keyPrefix` | `string` | `"_jskelet"` | Anahtar düzeninin kökü |
873
+ | `html` | `boolean` | `true` | HTML gövdeleri paylaşılsın mı |
874
+ | `data` | `boolean` | `true` | `withDataCache` girdileri paylaşılsın mı |
875
+ | `storeEncoded` | `boolean` | `false` | Brotli/gzip gövdeleri de paylaşılsın mı; girdi başına boyutu iki-üç katına çıkarır |
876
+ | `events` | `boolean` | `true` | pub/sub üzerinden invalidation yayını |
877
+ | `commandTimeoutMs` | `number` | `200` | Tek bir komutun en fazla bekletebileceği süre |
878
+
879
+ Anahtarlar `_jskelet:{namespace}:{buildId}:html:{yol}?{query}` biçiminde yaşar.
880
+ `buildId` her build'de değişir, böylece deploy sonrası eski HTML kendiliğinden
881
+ geçersiz olur. Kişiye özel (`storable: false`), `degraded` ve 200 dışındaki
882
+ yanıtlar paylaşımlı kademeye hiç yazılmaz. Takaslar ve teşhis:
883
+ [06-cache.md](./06-cache.md).
884
+
885
+ ```js
886
+ redis: {
887
+ enabled: process.env.NODE_ENV === "production",
888
+ url: process.env.REDIS_URL,
889
+ namespace: "haber-sitesi",
890
+ }
891
+ ```
892
+
893
+ ### `logs`
894
+
895
+ Kalıcı log sink'leri. Varsayılan her şey kapalı: stdout ve admin paneli ring'i
896
+ mevcut davranışını korur. Açıldığında HTTP access log ile framework olayları
897
+ (`event` / `error`) NDJSON olarak dosyaya, `drainLog`'a ve/veya S3'e gider.
898
+ Dosya parçaları zstd'dir ve en fazla 5 dakika durur; süresi dolan en eski
899
+ parça silinir.
900
+
901
+ | Alan | Tip | Varsayılan | Anlamı |
902
+ | --- | --- | --- | --- |
903
+ | `console` | `boolean` | `true` | Runtime `http` / `event` / `error` satırları stdout'a basılsın mı (banner/build satırları etkilenmez) |
904
+ | `kinds` | `("http" \| "event" \| "error")[]` | hepsi | Sink'lere giden kayıt türleri |
905
+ | `file.enabled` | `boolean` | `false` | Dosya spool'u. Satırlar ~1 sn veya 32 satırda bir `jskelet-<zaman>-<n>.ndjson.zst` olur. En fazla 5 dakika tutulur; en eski parça silinir. |
906
+ | `file.dir` | `string` | `"logs"` | Proje köküne göre dizin |
907
+ | `drainLog` | `(chunk) => void \| Promise<void>` | `null` | Mühürlenen zstd parçasını (`{ body, encoding, bytes, lines, at }`) istenen yere aktarır. Hata uyarı basar, siteyi düşürmez. Dosya kapalıysa diske yazılmaz. |
908
+ | `s3.enabled` | `boolean` | `false` | S3 batch PutObject sink'i |
909
+ | `s3.bucket` | `string \| null` | `null` | Bucket ya da `bucket/prefix/…` yolu; `JSKELET_LOG_BUCKET` ezer |
910
+ | `s3.prefix` | `string` | `"jskelet/logs/"` | Nesne anahtarı öneki (yolda verilmediyse) |
911
+ | `s3.region` | `string \| null` | `"auto"` | Bölge; verilmezse `JSKELET_S3_REGION`, yoksa `auto` |
912
+ | `s3.endpoint` | `string \| null` | `null` | S3-uyumlu API adresi; `JSKELET_S3_API_URL` ezer |
913
+ | `s3.flushIntervalMs` | `number` | `5000` | Batch flush aralığı |
914
+ | `s3.maxBatch` | `number` | `100` | Bu kadar satırda erken flush |
915
+
916
+ S3 credential'ları config'e yazılmaz: `JSKELET_S3_ACCESS_KEY_ID`,
917
+ `JSKELET_S3_SECRET_ACCESS_KEY`, isteğe bağlı `JSKELET_S3_SESSION_TOKEN`.
918
+ Bucket/region/credential eksikse uyarı basılır ve S3 sink kapanır; site ayağa
919
+ kalkmaya devam eder. Framework `@aws-sdk` taşımaz — PutObject SigV4 ile
920
+ gömülüdür.
921
+
922
+ ```js
923
+ logs: {
924
+ console: true,
925
+ kinds: ["http", "error"],
926
+ file: { enabled: true, dir: "logs" },
927
+ async drainLog(chunk) {
928
+ // chunk.body zstd NDJSON. Dosya 5 dakika sonra silinir; kalıcı kopya burada.
929
+ },
930
+ s3: {
931
+ enabled: process.env.NODE_ENV === "production",
932
+ bucket: process.env.JSKELET_LOG_BUCKET,
933
+ prefix: "my-app/logs/",
934
+ region: process.env.JSKELET_S3_REGION,
935
+ endpoint: process.env.JSKELET_S3_API_URL,
936
+ },
937
+ }
938
+ ```
939
+
940
+ ### `admin()`
941
+
942
+ Framework yönetim paneli (`/_jskelet/admin`). Bellek içi / Redis / Cloudflare
943
+ önbelleğini yönetir; route ve view envanteri ile canlı log kuyruğu sunar.
944
+
945
+ Ortama bakmaz: `enabled` verilmedikçe **hiç mount edilmez** ve yol da yoktur.
946
+ Açıkken production'da da çalışır — asıl sorular ("bu sayfa neden bayat",
947
+ "webhook purge'ü geçti mi") orada soruluyor. `cache()` bölümünden ayrıdır.
948
+
949
+ | Alan | Tip | Varsayılan | Anlamı |
950
+ | --- | --- | --- | --- |
951
+ | `enabled` | `boolean` | `false` | Yalnızca açıkça `true` verildiğinde açılır (`JSKELET_ADMIN` ezer) |
952
+ | `basePath` | `string` | `"/_jskelet/admin"` | Panelin kökü |
953
+ | `allowIps` | `string[]` | `[]` | Exact IP veya CIDR; boş = kısıt yok. Listede olmayan her istek 404 |
954
+ | `blockBots` | `boolean` | `true` | Bilinen crawler UA'ları 404 |
955
+ | `banAttempts` | `number` | `3` | Kaç başarısız denemeden sonra IP yasaklanır |
956
+ | `banHours` | `number` | `24` | Yasağın süresi |
957
+ | `sessionHours` | `number` | `12` | Oturum çerezinin ömrü |
958
+ | `logSize` | `number` | `500` | Canlı log ring boyutu |
959
+
960
+ Şifre **her süreç başlangıcında** üretilir ve yalnızca sunucu logundaki
961
+ `ADMIN` kutusunda görünür. Yasaklı ve yetkisiz her cevap `404`'tür. Kullanım
962
+ ve ekran ayrıntıları: [06-cache.md](./06-cache.md).
963
+
964
+ ```js
965
+ admin() {
966
+ return {
967
+ enabled: process.env.JSKELET_ADMIN === "1",
968
+ allowIps: ["203.0.113.10", "10.0.0.0/8"],
969
+ };
970
+ }
971
+ ```
972
+
973
+ ### `cache().cloudflare`
974
+
975
+ CDN kademesi. JSkelet'in önbelleği origin önbelleği; ziyaretçinin gördüğü kopya
976
+ edge'de duruyor. Bu bölüm bağlıysa panelden edge purge'ü, cache ile ilgili zone
977
+ ayarları ve cache isabet oranı yönetilebilir.
978
+
979
+ | Alan | Tip | Varsayılan | Anlamı |
980
+ | --- | --- | --- | --- |
981
+ | `enabled` | `boolean` | `true` | `false` verilirse env'de token olsa bile yüzey kapalı kalır |
982
+ | `zoneId` | `string \| null` | `null` | Zone kimliği (`JSKELET_CLOUDFLARE_ZONE_ID` ezer) |
983
+ | `apiToken` | `string \| null` | `null` | Token; **env tercih edilir**, config'e yazmak sırrı repoya sokar |
984
+ | `hostname` | `string \| null` | `null` | Purge tam URL ister; yol → URL çevrimi bu ad üzerinden yapılır. Verilmezse panelin açıldığı origin kullanılır |
985
+ | `analyticsHours` | `number` | `24` | Analitik penceresi, en çok `72` |
986
+
987
+ Token yalnızca `JSKELET_CLOUDFLARE_KEY` ile verildiğinde config dosyası temiz
988
+ kalır; izinler yapılacak işe göre: purge için `Zone.Cache Purge`, ayarlar için
989
+ `Zone.Zone Settings`, isabet oranı için `Zone.Analytics` (salt okunur). Token
990
+ hiçbir panel cevabında dönmez, yalnızca "env'den geldi" bilgisi görünür.
991
+
992
+ Zone bağlı değilse panel bir uyarı değil kurulum önerisi gösterir; Cloudflare
993
+ hata dönerse ilgili bölüm hatayı yazar ve panelin kalanı çalışmaya devam eder.
994
+ Neyin sorulabildiği — özellikle "bu sayfa kaç edge'de cache'li" sorusunun neden
995
+ tam cevabı olmadığı — [06-cache.md](./06-cache.md) içinde.
996
+
997
+ ### `cache().prewarm`
998
+
999
+ İki mod: **klasik** (liste + açılış turu) veya **`onVisit`** (ziyaret edilen
1000
+ sayfadaki linkler). Birlikte verilemez — config yüklenirken hata.
1001
+
1002
+ #### Klasik alanlar
1003
+
1004
+ | Alan | Tip | Varsayılan | Anlamı |
1005
+ | --- | --- | --- | --- |
1006
+ | `enabled` | `boolean` | `true` | `false` ise ısıtma yapılmaz (`PREWARM=1` ile ezilebilir) |
1007
+ | `max` | `number` | `400` | Bir turda en fazla kaç yol ısıtılır |
1008
+ | `concurrency` | `number` | prod 4, dev 1 | Paralel işçi sayısı |
1009
+ | `rps` | `number` | prod `0`, dev 4 | Saniyedeki en fazla ısıtma isteği; `0` sınırsız. Upstream kotasını koruyan ayar bu. Dev'deki varsayılan fren, ısıtmanın sayfa isteklerini bekletmemesi için. |
1010
+ | `delayMs` | `number` | prod 500, dev 3000 | Açılıştan sonra ilk turun gecikmesi |
1011
+ | `retryDelayMs` | `number` | `2000` | Tekrar turundan önce beklenen süre |
1012
+ | `intervalSeconds` | `number` | `0` | 0'dan büyükse tur periyodik tekrarlanır |
1013
+ | `rotate` | `boolean` | `true` | Liste `max`'tan uzunsa periyodik turlar kaldığı yerden devam eder |
1014
+ | `priority` | `(string \| RegExp)[]` | `[]` | Isıtma sırası; eşleşen yollar her turda başa alınır |
1015
+ | `origins` | `string[]` | `[]` | Klasik turda ısıtılacak origin'ler. Boşsa `http://127.0.0.1:<port>`. `vary.host` açıksa locale host'ları buraya yazın |
1016
+
1017
+ `priority` iki biçim kabul eder: config'in her yerinde geçerli olan desen
1018
+ sözdizimi ve doğrudan `RegExp`. Önce yazılan önce ısınır.
1019
+
1020
+ ```js
1021
+ prewarm: {
1022
+ max: 500,
1023
+ rps: 4,
1024
+ intervalSeconds: 300,
1025
+ // vary.host açıksa loopback tek başına yetmez:
1026
+ origins: ["http://localhost", "http://tr.localhost"],
1027
+ priority: [
1028
+ "/", // ana sayfa
1029
+ "/piyasalar/:path*", // tüm piyasa bölümü
1030
+ /-yorumlar$/, // desen sözdiziminin karşılamadığı kural
1031
+ ],
1032
+ }
1033
+ ```
1034
+
1035
+ #### `onVisit`
1036
+
1037
+ | Alan | Tip | Varsayılan | Anlamı |
1038
+ | --- | --- | --- | --- |
1039
+ | `onVisit` | `true \| false \| object` | kapalı | Ziyaret tabanlı ısıtma |
1040
+ | `onVisit.perPage` | `number` | `20` | Sayfa başına üstten alta en fazla link. **Tavan 20** |
1041
+ | `onVisit.concurrency` | `number` | `2` | Paralel işçi. **Tavan 2** |
1042
+ | `onVisit.rps` | `number` | `2` | Saniyedeki tavan. **Tavan 2**; `0` da 2'ye çekilir |
1043
+
1044
+ ```js
1045
+ prewarm: {
1046
+ onVisit: { perPage: 20, rps: 2 },
1047
+ }
1048
+ ```
1049
+
1050
+ `hooks.prewarmPaths` ve klasik alanlar (`max`, `priority`, …) `onVisit` ile
1051
+ **yasaktır**. Ayrıntı: [06-cache.md](./06-cache.md).
1052
+
1053
+ Sayısal alanların her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env
1054
+ önceliklidir. Ayrıntı: [06-cache.md](./06-cache.md).
1055
+
1056
+ ## `hooks`
1057
+
1058
+ **Tip:** `Record<string, Function>` — **Varsayılan:** `{}`
1059
+
1060
+ Hepsi opsiyonel, hepsi `async` olabilir. Bir hook hata verirse framework kendi
1061
+ varsayılanına döner ve uyarır — sayfa düşmez.
1062
+
1063
+ | Hook | İmza | Döndürdüğü | Belge |
1064
+ | --- | --- | --- | --- |
1065
+ | `metadata` | `(page) => object` | Her sayfanın metadata varsayılanı; controller `metadata`sı üzerine biner | [04](./04-render-ve-sablonlar.md) |
1066
+ | `layoutContext` | `({ pathname, metadata }) => object` | Layout local'leri; `lang`, `structuredData`, `extraHead`, `bodyClass` özel yorumlanır | [04](./04-render-ve-sablonlar.md) |
1067
+ | `notFound` | `() => object \| null` | 404 sayfa tanımı; `null` ise framework'ün hata sayfası | [03](./03-routing.md) |
1068
+ | `error` | `({ status, error }) => object \| string \| null` | 404 dışındaki hata sayfaları (ve `notFound` yoksa 404); sayfa tanımı ya da doğrudan HTML | [03](./03-routing.md) |
1069
+ | `prewarmPaths` | `() => string[]` | Klasik ısıtmada ısıtılacak yollar; tanımlı değilse klasik tur kurulmaz. `onVisit` ile birlikte **yasak** | [06](./06-cache.md) |
1070
+
1071
+ ```js
1072
+ hooks: {
1073
+ metadata() {
1074
+ return { titleTemplate: "%s | Örnek", siteUrl: "https://ornek.com" };
1075
+ },
1076
+
1077
+ async layoutContext({ pathname }) {
1078
+ return { navigation: await getNavigation(), isHome: pathname === "/" };
1079
+ },
1080
+
1081
+ notFound() {
1082
+ return {
1083
+ view: "pages/not-found",
1084
+ metadata: { title: "Sayfa bulunamadı", robots: { index: false } },
1085
+ };
1086
+ },
1087
+
1088
+ error({ status }) {
1089
+ return {
1090
+ view: "pages/error",
1091
+ data: { status },
1092
+ metadata: { title: "Bir hata oluştu", robots: { index: false } },
1093
+ };
1094
+ },
1095
+
1096
+ async prewarmPaths() {
1097
+ return ["/", ...(await getArticlePaths())];
1098
+ },
1099
+ }
1100
+ ```
1101
+
1102
+ ## `source` desen sözdizimi
1103
+
1104
+ `headers()`, `redirects()`, `rewrites()` ve `cache().html` aynı küçük derleyiciyi
1105
+ kullanır. Bu, Next'in tam `path-to-regexp` yüzeyi değil; config'te fiilen
1106
+ kullanılan alt küme bilinçli olarak seçildi ve tanınmayan bir sözdizimi sessizce
1107
+ literal kabul edilmez, uyarı üretir.
1108
+
1109
+ | Desen | Regex karşılığı | Örnek eşleşme |
1110
+ | --- | --- | --- |
1111
+ | `/hakkinda` | tam eşleşme | `/hakkinda` |
1112
+ | `/haber/:slug` | `([^/]+)` — tek segment | `/haber/abc` (✗ `/haber/a/b`) |
1113
+ | `/:path*` | `(.*)` — sıfır veya daha fazla segment | `/`, `/a`, `/a/b/c` |
1114
+ | `/blog/:path*` | joker alt yol; öndeki `/` opsiyonel | `/blog`, `/blog/`, `/blog/a/b` |
1115
+ | `/:path*.svg` | joker + sabit son ek | `/ikon.svg`, `/a/b/c.svg` |
1116
+ | `/etiket-:slug` | segment ortasında parametre | `/etiket-finans` |
1117
+
1118
+ Kurallar:
1119
+
1120
+ - `source` **`/` ile başlamak zorundadır**; başlamazsa kural yok sayılır ve
1121
+ uyarı basılır.
1122
+ - Parametre adı `[A-Za-z_][A-Za-z0-9_]*` kalıbına uymalıdır.
1123
+ - Desen daima **baştan sona** eşleşir (`^…$`); önek eşleşmesi için `:path*`
1124
+ kullanın.
1125
+ - `:path*` sıfır segment de yakalar ve hemen öncesindeki `/` opsiyoneldir:
1126
+ `/hesabim/:path*` bölümün kök yolunu (`/hesabim`) da kapsar. Aksi hâlde bir
1127
+ bölümü tamamen kapatmak isteyen kural tam da giriş sayfasını atlıyordu.
1128
+ - Parametreler dışındaki tüm karakterler literal kabul edilir ve regex için
1129
+ kaçışlanır — `.` gerçekten nokta demektir.
1130
+ - Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
1131
+ Karşılığı olmayan bir yer tutucu olduğu gibi bırakılır.
1132
+
1133
+ ## Ortam değişkenleri
1134
+
1135
+ Framework'ün okuduğu tüm değişkenler. `.env` dosyası varsa CLI tarafından
1136
+ otomatik yüklenir (`--env-file=.env`); yoksa bayrak hiç geçilmez ve uyarı
1137
+ basılmaz.
1138
+
1139
+ | Değişken | Kim okur | Varsayılan | Anlamı |
1140
+ | --- | --- | --- | --- |
1141
+ | `NODE_ENV` | her yer | `production` (start/build), `development` (dev) | Dev overlay, EJS cache, manifest yeniden okuma, route hata davranışı ve prewarm varsayılanlarını belirler. `jskelet dev` bunu kendisi ayarlar — `cross-env` gerekmez. |
1142
+ | `PORT` | `startServer` | `3000` | Dinlenecek port. Doluysa süreç başlamaz; `jskelet start|dev --murder` dinleyiciyi öldürür |
1143
+ | `HOST` | `startServer` | `::` | Bağlanılacak arayüz. Varsayılan çift yığın dinler (IPv6 + IPv4); IPv6 yoksa `0.0.0.0`'a düşer |
1144
+ | `JSKELET_SECRET` | `jskelet/cookies` | — | İmzalı cookie sırrı. `security.cookieSecret` verilmediğinde buradan okunur; ikisi de yoksa imzalı cookie API'si hata verir. [12](./12-panel-ve-oturum.md) |
1145
+ | `DEV_GATE` | `devGate` | kapalı | `1` gate'i açar, `0` config'te açık olsa da kapatır. `DEV_TOKEN` tek başına açmaz. [09](./09-dev-araclari.md) |
1146
+ | `DEV_TOKEN` | `devGate`, `prewarm` | — | Gate açıkken beklenen sır. Yoksa veya gate kapalıysa site açık kalır. Isıtma, gate açıkken token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
1147
+ | `JSKELET_ADMIN` | `createApp` | — | Ayarlıysa yönetim panelini açar; `0` config'te açık olan paneli kapatır. Env config'i ezer, çünkü panel genelde bir arıza sırasında tek seferlik açılır. [06](./06-cache.md) |
1148
+ | `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log hedefi: bucket ya da `bucket/prefix` yolu. Credential ile birlikte varsa sink otomatik açılır |
1149
+ | `JSKELET_S3_BUCKET` | `logs.s3` | — | `JSKELET_LOG_BUCKET` yoksa bucket; `JSKELET_S3_KEY_PREFIX` ile birleşir |
1150
+ | `JSKELET_S3_KEY_PREFIX` | `logs.s3` | — | `JSKELET_S3_BUCKET` ile kullanılır (`bucket/prefix`) |
1151
+ | `JSKELET_S3_ACCESS_KEY_ID` | `logs.s3` | — | PutObject imzası |
1152
+ | `JSKELET_S3_SECRET_ACCESS_KEY` | `logs.s3` | — | İmza sırrı (`JSKELET_S3_ACCESS_SECRET` yedek ad) |
1153
+ | `JSKELET_S3_SESSION_TOKEN` | `logs.s3` | — | Geçici credential için isteğe bağlı |
1154
+ | `JSKELET_S3_REGION` | `logs.s3` | `auto` | Verilmezse `auto` |
1155
+ | `JSKELET_S3_API_URL` | `logs.s3` | — | S3-uyumlu endpoint; `logs.s3.endpoint`'i ezer |
1156
+ | `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache yüzeyi | — | API token. Verilene kadar CDN purge'ü ve edge analitiği kapalıdır; config'teki `apiToken`'ı ezer. Token hiçbir cevapta dönmez. [06](./06-cache.md) |
1157
+ | `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache yüzeyi | — | Zone kimliği. Token'la birlikte verilmedikçe hiçbir Cloudflare ucu çağrılmaz |
1158
+ | `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache yüzeyi | — | Purge URL'lerinin kökü. Panel iç bir adresten açılıyorsa gerekir |
1159
+ | `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
1160
+ | `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
1161
+ | `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 1 | Paralel işçi sayısı |
1162
+ | `PREWARM_RPS` | `prewarm` | `0` | Saniyedeki en fazla ısıtma isteği; `0` sınırsız |
1163
+ | `PREWARM_DELAY_MS` | `startPrewarm` | prod 500, dev 3000 | İlk turun gecikmesi |
1164
+ | `PREWARM_RETRY_DELAY_MS` | `prewarm` | `2000` | Tekrar turundan önceki bekleme |
1165
+ | `PREWARM_INTERVAL_SECONDS` | `startPrewarm` | `0` | 0'dan büyükse periyodik tur |
1166
+ | `JSKELET_VERBOSE` | `jskelet dev` | — | `1` ise restart'ta değişen dosyaların tamamı listelenir |
1167
+ | `JSKELET_COLOR` | `jskelet/log` | — | `1` ise renk zorlanır. Alt süreçler boruya yazdığı için renk algılaması kapanır; `jskelet dev` bunu kendisi ayarlar. |
1168
+ | `JSKELET_CHILD` | `jskelet build` | — | Dev script'i tarafından ayarlanır; build banner'ı ve "Ready" özetini bastırır |
1169
+ | `NO_COLOR` | `jskelet/log` | — | Ayarlıysa renk hiç kullanılmaz (`JSKELET_COLOR`u da ezer) |
1170
+
1171
+ Uygulamanızın kendi değişkenleri (API origin'i, token'lar) framework tarafından
1172
+ okunmaz; doğrudan `process.env` üzerinden kullanın. Tarayıcıya ulaşması
1173
+ gerekenleri `clientEnv` ile bildirin.
1174
+
1175
+ Sayısal prewarm ayarları yalnızca **pozitif ve sonlu** değer kabul eder;
1176
+ geçersiz bir değer sessizce bir sonraki katmana (config → kod varsayılanı)
1177
+ düşer.
1178
+
1179
+ ## Programatik erişim
1180
+
1181
+ ```js
1182
+ import { getConfig, loadConfig } from "jskelet";
1183
+
1184
+ await loadConfig(); // proje kökünden okur
1185
+ await loadConfig({ root: "/baska/proje" }); // farklı kök
1186
+ await loadConfig({ configFile: "jskelet.test.mjs" });
1187
+ await loadConfig({ force: true }); // önbelleği atlayıp yeniden oku
1188
+
1189
+ const config = getConfig(); // çözümlenmiş config
1190
+ ```
1191
+
1192
+ `loadConfig()` aynı süreçte ikinci çağrıda önbelleğe düşer: `jskelet start` hem
1193
+ `ensure-build` hem `createApp` üzerinden çağırıyor ve config'i iki kez okuyup iki
1194
+ kez loglamanın faydası yok.
1195
+
1196
+ `getConfig()` `loadConfig()` çağrılmadan kullanılırsa **hata verir**: sessiz
1197
+ yanlış yol, "stylesheet neden yok" gibi teşhisi zor sorunlara dönüşüyor.
1198
+
1199
+ Çözümlenmiş config'te dizinler mutlak yol olarak `config.dirs` altındadır
1200
+ (`views`, `public`, `client`, `routes`, `styles`, `generated`, `assets`,
1201
+ `fonts`), desenler derlenmiş hâldedir ve `config.loaded` dosyanın gerçekten
1202
+ okunup okunmadığını söyler.
1203
+
1204
+ ## Sırada ne var
1205
+
1206
+ - Build tarafındaki alanların etkisi: [08-build.md](./08-build.md)
1207
+ - Dev akışı ve `DEV_TOKEN`: [09-dev-araclari.md](./09-dev-araclari.md)
1208
+ - Ortam değişkenlerinin dağıtımda kullanımı: [10-dagitim.md](./10-dagitim.md)