jskelet 0.1.1

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 (72) hide show
  1. package/AGENTS.md +127 -0
  2. package/CHANGELOG.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +342 -0
  5. package/bin/jskelet.mjs +104 -0
  6. package/docs/01-baslangic.md +285 -0
  7. package/docs/02-mimari.md +287 -0
  8. package/docs/03-routing.md +437 -0
  9. package/docs/04-render-ve-sablonlar.md +490 -0
  10. package/docs/05-islands.md +429 -0
  11. package/docs/06-cache.md +409 -0
  12. package/docs/07-yapilandirma.md +673 -0
  13. package/docs/08-build.md +366 -0
  14. package/docs/09-dev-araclari.md +302 -0
  15. package/docs/10-dagitim.md +329 -0
  16. package/docs/11-tasima.md +352 -0
  17. package/docs/README.md +82 -0
  18. package/package.json +97 -0
  19. package/src/build/build.mjs +138 -0
  20. package/src/build/ensure-build.mjs +15 -0
  21. package/src/build/paths.mjs +118 -0
  22. package/src/build/resolve-peer.mjs +36 -0
  23. package/src/build/tasks/client.mjs +268 -0
  24. package/src/build/tasks/css.mjs +124 -0
  25. package/src/build/tasks/fonts.mjs +146 -0
  26. package/src/build/tasks/icons.mjs +224 -0
  27. package/src/build/tasks/images.mjs +244 -0
  28. package/src/build/tasks/precompress.mjs +78 -0
  29. package/src/client/devtools/overlay.js +1763 -0
  30. package/src/client/devtools/report.html +185 -0
  31. package/src/client/devtools/report.js +712 -0
  32. package/src/client/dom.js +95 -0
  33. package/src/client/index.js +26 -0
  34. package/src/client/registry.js +223 -0
  35. package/src/client/safe-image.js +91 -0
  36. package/src/client/store.js +36 -0
  37. package/src/config/defaults.js +102 -0
  38. package/src/config/index.js +433 -0
  39. package/src/config/pattern.js +107 -0
  40. package/src/dev-server.mjs +383 -0
  41. package/src/http/control-flow.js +56 -0
  42. package/src/http/request-cache.js +46 -0
  43. package/src/index.js +35 -0
  44. package/src/init.mjs +220 -0
  45. package/src/log.mjs +332 -0
  46. package/src/logo.png +0 -0
  47. package/src/runtime/alias-hooks.mjs +119 -0
  48. package/src/runtime/register.mjs +4 -0
  49. package/src/server/assets.js +119 -0
  50. package/src/server/create-app.js +167 -0
  51. package/src/server/dev/devtools.js +383 -0
  52. package/src/server/dev/report.js +351 -0
  53. package/src/server/head-hints.js +132 -0
  54. package/src/server/html-cache.js +166 -0
  55. package/src/server/metadata.js +102 -0
  56. package/src/server/middleware/compression.js +205 -0
  57. package/src/server/middleware/dev-gate.js +62 -0
  58. package/src/server/middleware/headers.js +37 -0
  59. package/src/server/middleware/redirects.js +32 -0
  60. package/src/server/middleware/static-precompressed.js +100 -0
  61. package/src/server/middleware/upstream-proxy.js +141 -0
  62. package/src/server/prewarm.js +283 -0
  63. package/src/server/render.js +356 -0
  64. package/src/server/router.js +121 -0
  65. package/src/server/status-page.js +164 -0
  66. package/src/server/upstream-tracking.js +51 -0
  67. package/src/start.mjs +7 -0
  68. package/src/templates/layout.ejs +44 -0
  69. package/src/version.mjs +17 -0
  70. package/src/views/components/loader.js +85 -0
  71. package/src/views/helpers/html.js +102 -0
  72. package/src/views/helpers/tags.js +193 -0
@@ -0,0 +1,673 @@
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 yüklendi — 3 header, 2 redirect, 1 cache kuralı`
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.ejs",
55
+ routes: ["./routes/10-pages.mjs", "./routes/99-catch-all.mjs"],
56
+
57
+ static: {
58
+ extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2"],
59
+ prefixes: ["/assets/", "/fonts/"],
60
+ },
61
+
62
+ devGateBypass: ["/api/healthcheck", "/robots.txt"],
63
+ preconnect: ["https://cdn.ornek.com"],
64
+
65
+ navigation: {
66
+ prefetch: "moderate",
67
+ prerender: "conservative",
68
+ viewTransition: true,
69
+ exclude: ["/cikis"],
70
+ },
71
+
72
+ prewarmSkip: ["/api/", "/_fragment/", "/__ornek/"],
73
+ watch: ["data"],
74
+
75
+ fonts: [{ family: "Inter", weights: [400, 600, 700] }],
76
+ icons: { scan: ["views", "client", "routes", "lib"] },
77
+ images: { widths: [400, 800, 1200], quality: 78, skip: ["indirmeler"] },
78
+ clientEnv: ["PUBLIC_WS_URL"],
79
+
80
+ async headers() {
81
+ return [
82
+ {
83
+ source: "/:path*",
84
+ headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }],
85
+ },
86
+ ];
87
+ },
88
+
89
+ async redirects() {
90
+ return [{ source: "/eski/:slug", destination: "/yeni/:slug", permanent: true }];
91
+ },
92
+
93
+ async rewrites() {
94
+ return {
95
+ afterFiles: [
96
+ { source: "/api/:path*", destination: "https://api.ornek.com/:path*" },
97
+ ],
98
+ };
99
+ },
100
+
101
+ async cache() {
102
+ return {
103
+ html: { "/": 60, "/haber/:slug": 300 },
104
+ prewarm: { enabled: true, max: 400, concurrency: 4, intervalSeconds: 0 },
105
+ };
106
+ },
107
+
108
+ hooks: {
109
+ metadata() { /* … */ },
110
+ layoutContext() { /* … */ },
111
+ notFound() { /* … */ },
112
+ error() { /* … */ },
113
+ prewarmPaths() { /* … */ },
114
+ },
115
+ };
116
+ ```
117
+
118
+ ## `paths`
119
+
120
+ **Tip:** `Record<string, string>` — **Varsayılan:** aşağıdaki tablo
121
+
122
+ Proje kökündeki dizin (ve `styles` için dosya) adları. Değerler proje köküne
123
+ göre çözülür ve içeride mutlak yola çevrilir.
124
+
125
+ | Anahtar | Varsayılan | İçeriği |
126
+ | --- | --- | --- |
127
+ | `views` | `"views"` | EJS layout, sayfalar, bileşenler |
128
+ | `public` | `"public"` | Statik dosyalar; build çıktısı da buraya yazılır |
129
+ | `client` | `"client"` | Island runtime kaynakları ve entry'ler |
130
+ | `routes` | `"routes"` | Route modülleri |
131
+ | `styles` | `"styles/globals.css"` | Tailwind/PostCSS giriş **dosyası** |
132
+ | `generated` | `".jskelet"` | `manifest.json`, `metafile.json`, `images.json` |
133
+
134
+ `styles` bir dosya yolu olduğu hâlde aynı çözümlemeden geçer; ayrı bir alan
135
+ tutmaya değmiyor.
136
+
137
+ İki yol her zaman türetilir ve ezilemez: `public/assets` (hash'li build
138
+ çıktısı) ve `public/fonts` (self-host fontlar).
139
+
140
+ ```js
141
+ paths: { views: "src/views", routes: "src/routes", styles: "src/styles/main.css" }
142
+ ```
143
+
144
+ ## `brand`
145
+
146
+ **Tip:** `object` — **Varsayılan:** aşağıdaki tablo
147
+
148
+ Markalama ve tek yerden değiştirilebilir isimler. Fork eden ya da beyaz etiket
149
+ kullanan projeler kendi adını verebilir. Verilen alanlar varsayılanlarla sığ
150
+ birleştirilir.
151
+
152
+ | Alan | Tip | Varsayılan | Anlamı |
153
+ | --- | --- | --- | --- |
154
+ | `name` | `string` | `"JSkelet"` | Görüntü adı |
155
+ | `poweredBy` | `string` | `"JSkelet"` | `X-Powered-By` başlığının değeri |
156
+ | `cacheHeader` | `string` | `"X-JSkelet-Cache"` | HTML cache durumu başlığı ([06-cache.md](./06-cache.md)) |
157
+ | `devBasePath` | `string` | `"/__jskelet/dev"` | Dev overlay ve rapor uçlarının kökü |
158
+ | `prewarmUserAgent` | `string` | `"jskelet-prewarm"` | Isıtma isteklerinin UA'sı; dev paneli bunu filtreler |
159
+ | `devTokenCookie` | `string` | `"dev_token"` | Dev gate'in çerez ve query parametresi adı |
160
+ | `lang` | `string` | — | `<html lang>` varsayılanı. Verilmezse layout `"en"` kullanır. |
161
+
162
+ `lang` için öncelik sırası: `hooks.layoutContext()` → `lang` **>** `brand.lang`
163
+ **>** `"en"`.
164
+
165
+ ```js
166
+ brand: { lang: "tr", poweredBy: "Örnek", cacheHeader: "X-Ornek-Cache" }
167
+ ```
168
+
169
+ ## `layout`
170
+
171
+ **Tip:** `string` — **Varsayılan:** yok (otomatik çözüm)
172
+
173
+ Layout `.ejs` dosyasının yolu. Verilen değer **views dizininin üst dizinine**
174
+ göre çözülür, yani varsayılan `views` ile `"views/ozel.ejs"` →
175
+ `<root>/views/ozel.ejs`.
176
+
177
+ Verilmezse sırayla: `views/layout.ejs` varsa o, yoksa framework'ün minimal
178
+ layout'u. Ayrıntı: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md).
179
+
180
+ ## `routes`
181
+
182
+ **Tip:** `string[]` — **Varsayılan:** `null` (dizin taraması)
183
+
184
+ Route modüllerinin açık listesi, proje köküne göre. Verilen sırada yüklenir.
185
+ Verilmezse `paths.routes` dizini alfabetik ve özyinelemeli olarak taranır.
186
+ Ayrıntı: [03-routing.md](./03-routing.md).
187
+
188
+ ```js
189
+ routes: ["./routes/api.js", "./routes/pages.js", "./routes/catch-all.js"]
190
+ ```
191
+
192
+ ## `static`
193
+
194
+ **Tip:** `{ extensions?: string[], prefixes?: string[] }` — **Varsayılan:**
195
+ aşağıda
196
+
197
+ Uzantı ve önek bazlı statik dosya tespiti. Bu listeye uyan yollara
198
+ `Cache-Control: public, max-age=31536000, immutable` yazılır.
199
+
200
+ | Alan | Varsayılan |
201
+ | --- | --- |
202
+ | `extensions` | `[".svg", ".png", ".webp", ".avif", ".ico", ".woff2"]` |
203
+ | `prefixes` | `["/assets/", "/fonts/"]` |
204
+
205
+ Verilirse varsayılanın **yerine** geçer (birleştirilmez), yani varsayılana ek
206
+ yapmak isterseniz tam listeyi yazın.
207
+
208
+ ```js
209
+ static: {
210
+ extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2", ".mp4"],
211
+ prefixes: ["/assets/", "/fonts/", "/video/"],
212
+ }
213
+ ```
214
+
215
+ ## `devGateBypass`
216
+
217
+ **Tip:** `string[]` — **Varsayılan:**
218
+ `["/api/healthcheck", "/robots.txt", "/sitemap.xml", "/site.webmanifest", "/favicon.ico"]`
219
+
220
+ Dev gate'in hiçbir koşulda kapatmadığı **tam** yollar (önek değil, birebir
221
+ eşleşme). `DEV_TOKEN` ayarlı bir ortamda sağlık kontrolünün ve robots
222
+ dosyalarının erişilebilir kalması için. Verilirse varsayılanın yerine geçer.
223
+
224
+ Ayrıntı: [09-dev-araclari.md](./09-dev-araclari.md).
225
+
226
+ ## `preconnect`
227
+
228
+ **Tip:** `string[]` — **Varsayılan:** `[]`
229
+
230
+ Üçüncü taraf origin'ler; her sayfanın `<head>`inde `<link rel="preconnect">`
231
+ olarak basılır. Görsel CDN'i, API origin'i, font host'u buraya yazılır. Değerler
232
+ `new URL(...).origin` ile normalize edilir; geçersiz bir URL atlanır ve uyarı
233
+ basılır.
234
+
235
+ Liste her sayfada aynı olduğu için bir kez hesaplanıp saklanır. Boş liste geçerli
236
+ bir yapılandırmadır.
237
+
238
+ ```js
239
+ preconnect: ["https://cdn.ornek.com", "https://api.ornek.com"]
240
+ ```
241
+
242
+ ## `navigation`
243
+
244
+ **Tip:** `object` — **Varsayılan:**
245
+ `{ prefetch: "moderate", prerender: false, viewTransition: false, exclude: [] }`
246
+
247
+ Site içi gezinmeyi hızlandıran `<head>` ipuçları. JSkelet klasik MPA olduğu için
248
+ her tıklama tam sayfa yüklemesidir; bu bölüm o yüklemeyi tarayıcının **önceden**
249
+ yapmasını sağlar. Client runtime'ı eklenmez — Speculation Rules ve view
250
+ transition tarayıcı yetenekleridir, desteklemeyen tarayıcıda sessizce yok
251
+ sayılırlar.
252
+
253
+ | Alan | Tip | Varsayılan | Anlamı |
254
+ | --- | --- | --- | --- |
255
+ | `prefetch` | `false \| "conservative" \| "moderate" \| "eager"` | `"moderate"` | Bağlantı hedefinin **belgesini** önceden indirir |
256
+ | `prerender` | aynı | `false` | Hedefi arka planda **tam render eder**; tıklama anında açılır |
257
+ | `viewTransition` | `boolean` | `false` | `@view-transition { navigation: auto }` basar |
258
+ | `exclude` | `string[]` | `[]` | Spekülasyon dışı bırakılacak href desenleri |
259
+
260
+ `true` verilirse `prefetch`/`prerender` varsayılan eagerness'a düşer; tanınmayan
261
+ bir değer uyarı basıp varsayılana döner.
262
+
263
+ **Eagerness ne demek:** `conservative` bağlantıya basıldığı an, `moderate`
264
+ bağlantı üzerinde bir süre duraksandığında, `eager` bağlantı görünür olur olmaz
265
+ tetikler. Yukarı çıktıkça isabet artar, boşa giden istek de artar.
266
+
267
+ **`prerender` neden kapalı geliyor.** Prerender edilen sayfanın script'leri
268
+ gerçekten çalışır. Ölçüm kodunu `prerenderingchange` olayına bağlamayan bir
269
+ uygulamada ziyaret sayıları şişer. Açmadan önce analytics'i gözden geçirin;
270
+ sunucu tarafındaki maliyeti düşüktür, çünkü spekülatif istek de HTML
271
+ önbelleğinden karşılanır ([06-cache.md](./06-cache.md)).
272
+
273
+ **Her koşulda muaf olanlar.** `/api/*`, `/_fragment/*` ve `brand.devBasePath`
274
+ altındaki yollar otomatik dışlanır; `exclude` bunların üstüne eklenir. Ayrıca
275
+ `rel="nofollow"`, `target="_blank"` ve `data-no-prefetch` taşıyan bağlantılar
276
+ hiçbir kurala girmez. Yan etkisi olan tek bir bağlantıyı dışarıda bırakmanın en
277
+ kolay yolu sonuncusu:
278
+
279
+ ```html
280
+ <a href="/cikis" data-no-prefetch>Çıkış</a>
281
+ ```
282
+
283
+ **`viewTransition` açarken arka planı `<html>`e verin.** Geçiş sırasında tarayıcı
284
+ eski ve yeni sayfanın anlık görüntülerini çapraz geçirir; `<body>`ye verilmiş bir
285
+ arka plan bu görüntünün içinde kalır ve altta kalan canvas görünür. Sonuç, her
286
+ geçişte bir kare beyaz flaştır ve koyu temada gözden kaçmaz. Renk `<html>` (ya da
287
+ `:root`) üzerindeyse böyle bir boşluk oluşmaz:
288
+
289
+ ```html
290
+ <html lang="tr" class="bg-white dark:bg-slate-950">
291
+ <body class="text-slate-900 dark:text-slate-100">
292
+ ```
293
+
294
+ Hareket azaltma tercihi framework tarafından karşılanır: `prefers-reduced-motion:
295
+ reduce` altında geçiş kapatılır, ayrıca bir şey yazmanız gerekmez.
296
+
297
+ **Geçişi içerikle sınırlayın.** Varsayılan davranış tüm belgeyi tek parça olarak
298
+ çapraz geçirir, yani gezinme boyunca hiç değişmeyen header ve footer da titrer.
299
+ Bu bölgelere bir `view-transition-name` vermek onları kendi grubuna alır;
300
+ tarayıcı aynı adı iki belgede de gördüğü için "aynı öğe" sayar. Adlandırılan
301
+ öğenin animasyonunu kapatınca geçiş yalnızca içerikte kalır:
302
+
303
+ ```css
304
+ body > header { view-transition-name: site-header; }
305
+ body > footer { view-transition-name: site-footer; }
306
+
307
+ ::view-transition-old(site-header),
308
+ ::view-transition-old(site-footer) { animation: none; opacity: 0; }
309
+ ::view-transition-new(site-header),
310
+ ::view-transition-new(site-footer) { animation: none; opacity: 1; }
311
+
312
+ /* Kalan içerik; varsayılan 250ms gezinmeyi yavaş hissettiriyor. */
313
+ ::view-transition-old(root),
314
+ ::view-transition-new(root) { animation-duration: 180ms; }
315
+ ```
316
+
317
+ Çalışan hâli `examples/marketing/styles/globals.css` içinde.
318
+
319
+ **CSP kullanıyorsanız** kurallar satır içi bir `<script type="speculationrules">`
320
+ olarak basılır; `script-src` politikanızın buna izin vermesi gerekir.
321
+
322
+ ```js
323
+ navigation: {
324
+ prefetch: "moderate",
325
+ prerender: "conservative",
326
+ viewTransition: true,
327
+ exclude: ["/cikis", "/sepet/*"],
328
+ }
329
+ ```
330
+
331
+ ## `prewarmSkip`
332
+
333
+ **Tip:** `string[]` — **Varsayılan:** `["/api/", "/_fragment/", "/__jskelet/"]`
334
+
335
+ Isıtmanın atlayacağı yol **önekleri**. Oturuma bağlı ya da fragment uçları
336
+ ısıtılmamalı. Verilirse varsayılanın yerine geçer — `brand.devBasePath`i
337
+ değiştirdiyseniz bu listeyi de güncellemeyi unutmayın.
338
+
339
+ Ayrıntı: [06-cache.md](./06-cache.md).
340
+
341
+ ## `watch`
342
+
343
+ **Tip:** `string[]` — **Varsayılan:** `[]`
344
+
345
+ `jskelet dev`in sunucu yeniden başlatma için izleyeceği **ek** dizinler, proje
346
+ köküne göre. `routes`, `views` ve `lib` zaten izlenir; `client/` ve `styles/`
347
+ esbuild ve CSS watcher'ları tarafından ele alınır, buraya konmamalı.
348
+
349
+ Yalnızca `.js`, `.mjs`, `.json` ve `.ejs` uzantılı dosyalar tetikleyicidir.
350
+
351
+ ```js
352
+ watch: ["data", "content"]
353
+ ```
354
+
355
+ Ayrıntı: [09-dev-araclari.md](./09-dev-araclari.md).
356
+
357
+ ## `fonts`
358
+
359
+ **Tip:** `{ family: string, slug?: string, weights?: number[] }[]` —
360
+ **Varsayılan:** `[]`
361
+
362
+ Self-host edilecek Google Fonts aileleri. Boş bırakılırsa font adımı hiç
363
+ çalışmaz.
364
+
365
+ | Alan | Tip | Varsayılan | Anlamı |
366
+ | --- | --- | --- | --- |
367
+ | `family` | `string` | — | Google Fonts aile adı: `"Inter"`, `"Noto Sans"` |
368
+ | `slug` | `string` | `family`den türetilir (küçük harf, boşluk → `-`) | Dosya adı öneki |
369
+ | `weights` | `number[]` | `[400]` | İndirilecek ağırlıklar |
370
+
371
+ Çıktı: `public/fonts/<slug>-<weight>.woff2`, manifest anahtarı aynı dosya adı.
372
+ Dosyalar **sabit isimlidir** (hash yok) ve **commit edilmesi beklenir**.
373
+ Ayrıntı: [08-build.md](./08-build.md).
374
+
375
+ ```js
376
+ fonts: [
377
+ { family: "Inter", weights: [400, 600, 700] },
378
+ { family: "Noto Serif", slug: "serif", weights: [400] },
379
+ ]
380
+ ```
381
+
382
+ ## `icons`
383
+
384
+ **Tip:** `{ scan?: string[] } | false` — **Varsayılan:** `{}`
385
+
386
+ Phosphor SVG sprite üretimi.
387
+
388
+ | Değer | Sonuç |
389
+ | --- | --- |
390
+ | `{}` (varsayılan) | Sprite üretilir; taranan dizinler `["views", "client", "routes", "lib"]` |
391
+ | `{ scan: [...] }` | Taranan dizinler değiştirilir |
392
+ | `false` | Sprite adımı tamamen atlanır |
393
+
394
+ `@phosphor-icons/core` uygulamanın `node_modules`'ünde yoksa adım sessizce
395
+ atlanır. Ayrıntı: [08-build.md](./08-build.md).
396
+
397
+ ```js
398
+ icons: { scan: ["views", "client", "routes", "lib", "content"] }
399
+ ```
400
+
401
+ ## `images`
402
+
403
+ **Tip:** `{ widths?: number[], quality?: number, skip?: string[] } | false` —
404
+ **Varsayılan:** `{}`
405
+
406
+ `public/` altındaki png/jpg görsellerin webp varyantlarını üretir.
407
+
408
+ | Alan | Tip | Varsayılan | Anlamı |
409
+ | --- | --- | --- | --- |
410
+ | `widths` | `number[]` | `[400, 640, 960, 1280, 1920]` | Üretilecek genişlikler. Kaynaktan büyük olanlar elenir; kaynağın kendi genişliği (en fazla 1920) her zaman eklenir. |
411
+ | `quality` | `number` | `78` | webp kalitesi. Değişince kodlayıcı imzası değişir ve tüm görseller yeniden kodlanır. |
412
+ | `skip` | `string[]` | `[]` | Taranmayacak **dizin adları**. `assets` ve `fonts` her zaman atlanır. |
413
+
414
+ `false` verilirse görsel adımı hiç çalışmaz. Adım `sharp` gerektirir ve watch
415
+ turunda hiç çalışmaz. Ayrıntı: [08-build.md](./08-build.md).
416
+
417
+ ```js
418
+ images: { widths: [400, 800, 1200], quality: 82, skip: ["indirmeler"] }
419
+ ```
420
+
421
+ ## `clientEnv`
422
+
423
+ **Tip:** `string[]` — **Varsayılan:** `[]`
424
+
425
+ Client bundle'a build zamanında gömülecek ortam değişkeni anahtarları. Next'teki
426
+ `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın herkese açık olduğu
427
+ isimden değil config'ten belli. `NODE_ENV` her zaman gömülür.
428
+
429
+ `process.env`in tamamı tek nesne olarak define edildiği için listede olmayan bir
430
+ anahtar okunduğunda çökme yerine `undefined` döner.
431
+
432
+ ```js
433
+ clientEnv: ["PUBLIC_WS_URL", "PUBLIC_CDN_ORIGIN"]
434
+ ```
435
+
436
+ **Buraya gizli anahtar koymayın** — değerler bundle'da düz metin olarak durur.
437
+
438
+ ## `headers()`
439
+
440
+ **Tip:** `() => { source: string, headers: { key: string, value: string }[] }[]`
441
+ — **Varsayılan:** `[]`
442
+
443
+ Yol desenine göre yanıt başlıkları. Framework yalnızca statik dosyalara uzun
444
+ ömürlü cache yazar; bunun dışındaki her başlık (CSP, COOP, HSTS,
445
+ X-Frame-Options…) buradan gelir ve varsayılanların üstüne biner.
446
+
447
+ Eşleşen **tüm** kurallar uygulanır (redirect'lerin aksine ilk eşleşmede
448
+ durulmaz), sırayla; aynı başlığı iki kural yazarsa sonraki kazanır.
449
+
450
+ `key`i olmayan ya da `value`u `undefined` olan girdiler atlanır; hiç geçerli
451
+ başlığı kalmayan bir kural hiç eklenmez.
452
+
453
+ ```js
454
+ async headers() {
455
+ return [
456
+ {
457
+ source: "/:path*",
458
+ headers: [
459
+ { key: "X-Frame-Options", value: "SAMEORIGIN" },
460
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
461
+ {
462
+ key: "Content-Security-Policy",
463
+ value: "default-src 'self'; img-src 'self' https://cdn.ornek.com data:",
464
+ },
465
+ ],
466
+ },
467
+ {
468
+ source: "/indirme/:path*",
469
+ headers: [{ key: "Cache-Control", value: "no-store" }],
470
+ },
471
+ ];
472
+ }
473
+ ```
474
+
475
+ ## `redirects()`
476
+
477
+ **Tip:**
478
+ `() => { source: string, destination: string, permanent?: boolean, statusCode?: number }[]`
479
+ — **Varsayılan:** `[]`
480
+
481
+ | Alan | Tip | Anlamı |
482
+ | --- | --- | --- |
483
+ | `source` | `string` | Desen (aşağıdaki sözdizimi) |
484
+ | `destination` | `string` | Hedef; `:param` yer tutucuları doldurulur |
485
+ | `permanent` | `boolean` | `true` → 308, aksi hâlde 307 |
486
+ | `statusCode` | `number` | Açık durum kodu; `permanent`i ezer |
487
+
488
+ İlk eşleşen kural kazanır ve query string korunur. Ayrıntı:
489
+ [03-routing.md](./03-routing.md).
490
+
491
+ ## `rewrites()`
492
+
493
+ **Tip:** `() => Rule[] | { beforeFiles?: Rule[], afterFiles?: Rule[] }`
494
+ burada `Rule = { source: string, destination: string }` — **Varsayılan:** `[]`
495
+
496
+ Dizi döndürülürse tamamı `afterFiles` sayılır.
497
+
498
+ - `beforeFiles` statik dosyalardan da önce çalışır.
499
+ - `afterFiles` statik denendikten sonra, route'lardan önce çalışır.
500
+ - Mutlak hedef (`http://`/`https://`) → gömülü ters proxy.
501
+ - Göreli hedef → yalnızca `req.url` değişir.
502
+
503
+ Ayrıntı: [03-routing.md](./03-routing.md).
504
+
505
+ ## `cache()`
506
+
507
+ **Tip:** `() => { html?: Record<string, number>, prewarm?: object }` —
508
+ **Varsayılan:** `{ html: {}, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
509
+
510
+ ### `cache().html`
511
+
512
+ Desen → saniye eşlemesi. Eşleşen kural, route'un kendi `revalidate` değerini
513
+ **ezer**. Negatif ya da sonlu olmayan değerler yok sayılır; `0` "önbellekleme"
514
+ anlamına gelir.
515
+
516
+ ```js
517
+ html: {
518
+ "/": 60,
519
+ "/haber/:slug": 300,
520
+ "/arama": 0,
521
+ }
522
+ ```
523
+
524
+ ### `cache().prewarm`
525
+
526
+ | Alan | Tip | Varsayılan | Anlamı |
527
+ | --- | --- | --- | --- |
528
+ | `enabled` | `boolean` | `true` | `false` ise ısıtma yapılmaz (`PREWARM=1` ile ezilebilir) |
529
+ | `max` | `number` | `400` | En fazla kaç yol ısıtılır |
530
+ | `concurrency` | `number` | prod 4, dev 2 | Paralel işçi sayısı |
531
+ | `delayMs` | `number` | prod 500, dev 3000 | Açılıştan sonra ilk turun gecikmesi |
532
+ | `intervalSeconds` | `number` | `0` | 0'dan büyükse tur periyodik tekrarlanır |
533
+
534
+ Her biri aynı adı taşıyan ortam değişkeniyle ezilebilir; env önceliklidir.
535
+ Ayrıntı: [06-cache.md](./06-cache.md).
536
+
537
+ ## `hooks`
538
+
539
+ **Tip:** `Record<string, Function>` — **Varsayılan:** `{}`
540
+
541
+ Hepsi opsiyonel, hepsi `async` olabilir. Bir hook hata verirse framework kendi
542
+ varsayılanına döner ve uyarır — sayfa düşmez.
543
+
544
+ | Hook | İmza | Döndürdüğü | Belge |
545
+ | --- | --- | --- | --- |
546
+ | `metadata` | `(page) => object` | Her sayfanın metadata varsayılanı; controller `metadata`sı üzerine biner | [04](./04-render-ve-sablonlar.md) |
547
+ | `layoutContext` | `({ pathname, metadata }) => object` | Layout local'leri; `lang`, `structuredData`, `extraHead`, `bodyClass` özel yorumlanır | [04](./04-render-ve-sablonlar.md) |
548
+ | `notFound` | `() => object \| null` | 404 sayfa tanımı; `null` ise framework'ün hata sayfası | [03](./03-routing.md) |
549
+ | `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) |
550
+ | `prewarmPaths` | `() => string[]` | Isıtılacak yollar; tanımlı değilse ısıtma hiç kurulmaz | [06](./06-cache.md) |
551
+
552
+ ```js
553
+ hooks: {
554
+ metadata() {
555
+ return { titleTemplate: "%s | Örnek", siteUrl: "https://ornek.com" };
556
+ },
557
+
558
+ async layoutContext({ pathname }) {
559
+ return { navigation: await getNavigation(), isHome: pathname === "/" };
560
+ },
561
+
562
+ notFound() {
563
+ return {
564
+ view: "pages/not-found",
565
+ metadata: { title: "Sayfa bulunamadı", robots: { index: false } },
566
+ };
567
+ },
568
+
569
+ error({ status }) {
570
+ return {
571
+ view: "pages/error",
572
+ data: { status },
573
+ metadata: { title: "Bir hata oluştu", robots: { index: false } },
574
+ };
575
+ },
576
+
577
+ async prewarmPaths() {
578
+ return ["/", ...(await getArticlePaths())];
579
+ },
580
+ }
581
+ ```
582
+
583
+ ## `source` desen sözdizimi
584
+
585
+ `headers()`, `redirects()`, `rewrites()` ve `cache().html` aynı küçük derleyiciyi
586
+ kullanır. Bu, Next'in tam `path-to-regexp` yüzeyi değil; config'te fiilen
587
+ kullanılan alt küme bilinçli olarak seçildi ve tanınmayan bir sözdizimi sessizce
588
+ literal kabul edilmez, uyarı üretir.
589
+
590
+ | Desen | Regex karşılığı | Örnek eşleşme |
591
+ | --- | --- | --- |
592
+ | `/hakkinda` | tam eşleşme | `/hakkinda` |
593
+ | `/haber/:slug` | `([^/]+)` — tek segment | `/haber/abc` (✗ `/haber/a/b`) |
594
+ | `/:path*` | `(.*)` — sıfır veya daha fazla segment | `/`, `/a`, `/a/b/c` |
595
+ | `/blog/:path*` | joker alt yol; öndeki `/` opsiyonel | `/blog`, `/blog/`, `/blog/a/b` |
596
+ | `/:path*.svg` | joker + sabit son ek | `/ikon.svg`, `/a/b/c.svg` |
597
+ | `/etiket-:slug` | segment ortasında parametre | `/etiket-finans` |
598
+
599
+ Kurallar:
600
+
601
+ - `source` **`/` ile başlamak zorundadır**; başlamazsa kural yok sayılır ve
602
+ uyarı basılır.
603
+ - Parametre adı `[A-Za-z_][A-Za-z0-9_]*` kalıbına uymalıdır.
604
+ - Desen daima **baştan sona** eşleşir (`^…$`); önek eşleşmesi için `:path*`
605
+ kullanın.
606
+ - `:path*` sıfır segment de yakalar ve hemen öncesindeki `/` opsiyoneldir:
607
+ `/hesabim/:path*` bölümün kök yolunu (`/hesabim`) da kapsar. Aksi hâlde bir
608
+ bölümü tamamen kapatmak isteyen kural tam da giriş sayfasını atlıyordu.
609
+ - Parametreler dışındaki tüm karakterler literal kabul edilir ve regex için
610
+ kaçışlanır — `.` gerçekten nokta demektir.
611
+ - Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
612
+ Karşılığı olmayan bir yer tutucu olduğu gibi bırakılır.
613
+
614
+ ## Ortam değişkenleri
615
+
616
+ Framework'ün okuduğu tüm değişkenler. `.env` dosyası varsa CLI tarafından
617
+ otomatik yüklenir (`--env-file=.env`); yoksa bayrak hiç geçilmez ve uyarı
618
+ basılmaz.
619
+
620
+ | Değişken | Kim okur | Varsayılan | Anlamı |
621
+ | --- | --- | --- | --- |
622
+ | `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. |
623
+ | `PORT` | `startServer` | `3000` | Dinlenecek port |
624
+ | `HOST` | `startServer` | `0.0.0.0` | Bağlanılacak arayüz |
625
+ | `DEV_TOKEN` | `devGate`, `prewarm` | — | Ayarlıysa token taşımayan her isteğe 404 döner. Isıtma token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
626
+ | `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
627
+ | `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
628
+ | `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 2 | Paralel işçi sayısı |
629
+ | `PREWARM_DELAY_MS` | `startPrewarm` | prod 500, dev 3000 | İlk turun gecikmesi |
630
+ | `PREWARM_INTERVAL_SECONDS` | `startPrewarm` | `0` | 0'dan büyükse periyodik tur |
631
+ | `JSKELET_VERBOSE` | `jskelet dev` | — | `1` ise restart'ta değişen dosyaların tamamı listelenir |
632
+ | `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. |
633
+ | `JSKELET_CHILD` | `jskelet build` | — | Dev script'i tarafından ayarlanır; build banner'ı ve "Ready" özetini bastırır |
634
+ | `NO_COLOR` | `jskelet/log` | — | Ayarlıysa renk hiç kullanılmaz (`JSKELET_COLOR`u da ezer) |
635
+
636
+ Uygulamanızın kendi değişkenleri (API origin'i, token'lar) framework tarafından
637
+ okunmaz; doğrudan `process.env` üzerinden kullanın. Tarayıcıya ulaşması
638
+ gerekenleri `clientEnv` ile bildirin.
639
+
640
+ Sayısal prewarm ayarları yalnızca **pozitif ve sonlu** değer kabul eder;
641
+ geçersiz bir değer sessizce bir sonraki katmana (config → kod varsayılanı)
642
+ düşer.
643
+
644
+ ## Programatik erişim
645
+
646
+ ```js
647
+ import { getConfig, loadConfig } from "jskelet";
648
+
649
+ await loadConfig(); // proje kökünden okur
650
+ await loadConfig({ root: "/baska/proje" }); // farklı kök
651
+ await loadConfig({ configFile: "jskelet.test.mjs" });
652
+ await loadConfig({ force: true }); // önbelleği atlayıp yeniden oku
653
+
654
+ const config = getConfig(); // çözümlenmiş config
655
+ ```
656
+
657
+ `loadConfig()` aynı süreçte ikinci çağrıda önbelleğe düşer: `jskelet start` hem
658
+ `ensure-build` hem `createApp` üzerinden çağırıyor ve config'i iki kez okuyup iki
659
+ kez loglamanın faydası yok.
660
+
661
+ `getConfig()` `loadConfig()` çağrılmadan kullanılırsa **hata verir**: sessiz
662
+ yanlış yol, "stylesheet neden yok" gibi teşhisi zor sorunlara dönüşüyor.
663
+
664
+ Çözümlenmiş config'te dizinler mutlak yol olarak `config.dirs` altındadır
665
+ (`views`, `public`, `client`, `routes`, `styles`, `generated`, `assets`,
666
+ `fonts`), desenler derlenmiş hâldedir ve `config.loaded` dosyanın gerçekten
667
+ okunup okunmadığını söyler.
668
+
669
+ ## Sırada ne var
670
+
671
+ - Build tarafındaki alanların etkisi: [08-build.md](./08-build.md)
672
+ - Dev akışı ve `DEV_TOKEN`: [09-dev-araclari.md](./09-dev-araclari.md)
673
+ - Ortam değişkenlerinin dağıtımda kullanımı: [10-dagitim.md](./10-dagitim.md)