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,338 +1,348 @@
1
- # 10 — Dağıtım
2
-
3
- Bu belge bir JSkelet uygulamasını yayına almayı anlatır: prod build ve start
4
- akışı, ayarlanması gereken ortam değişkenleri, çalışan bir Docker kurulumu, ters
5
- proxy ve `trust proxy` notları, sağlık kontrolü ucunun nasıl eklendiği ve
6
- ölçekleme sırasında önbelleğin nasıl davrandığı. Build adımlarının içeriği
7
- [08-build.md](./08-build.md)'de, önbellek davranışı [06-cache.md](./06-cache.md)'de.
8
-
9
- ## Prod akışı
10
-
11
- ```bash
12
- npm ci
13
- npm run build # jskelet build
14
- npm start # jskelet start
15
- ```
16
-
17
- `jskelet build` `NODE_ENV` verilmemişse `production` ayarlar ve tüm adımları
18
- çalıştırır: fontlar, ikon sprite, CSS, client JS, görseller, manifest,
19
- precompress.
20
-
21
- `jskelet start` önce `.jskelet/manifest.json` dosyasına bakar; yoksa build'i
22
- kendisi çalıştırır. Docker imajında build zaten yapıldığı için bu bir no-op;
23
- amaç `npm start`ı doğrudan çalıştıran birinin stilsiz bir sayfayla
24
- karşılaşmaması.
25
-
26
- Port doluysa süreç **başlamaz** (PID + ipucu). `jskelet start --murder` o
27
- porttaki dinleyiciyi öldürüp bağlar — geliştirmede unutulmuş bir süreç için;
28
- üretim orkestratöründe genelde gerekmez.
29
-
30
- Sunucu hazır olduğunda tek satır basar:
31
-
32
- ```
33
- jskelet → http://localhost:3000 (production)
34
- ```
35
-
36
- Süreç iki güvenlik ağıyla korunur: `unhandledRejection` ve `uncaughtException`
37
- loglanır ve süreç ayakta kalır. Bir haber sitesinde tek sayfanın hatası tüm
38
- siteyi indirmemeli. Kendi hata izleme aracınıza (Sentry vb.) bağlanmak
39
- istiyorsanız aynı olaylara kendi dinleyicinizi de ekleyebilirsiniz.
40
-
41
- ## Ortam değişkenleri
42
-
43
- Zorunlu hiçbir değişken yok; hepsinin makul bir varsayılanı var. Prod'da
44
- ayarlamayı düşünmeniz gerekenler:
45
-
46
- | Değişken | Öneri | Neden |
47
- | --- | --- | --- |
48
- | `NODE_ENV` | `production` | Şablon cache'i, manifest'in bir kez okunması, bozuk route modülünde fırlatma |
49
- | `PORT` | `3000` | Orkestratörünüzün beklediği port |
50
- | `HOST` | `0.0.0.0` | Yalnızca IPv4 dinlemek gerekiyorsa. Varsayılan `::` zaten çift yığın dinler |
51
- | `PREWARM_MAX` | Site boyutuna göre | Açılışta ısıtılacak sayfa sayısı |
52
- | `PREWARM_INTERVAL_SECONDS` | `0` ya da uzun bir değer | Hiç ziyaret edilmeyen sayfaları sıcak tutmak isterseniz |
53
- | `DEV_GATE` + `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler. Token tek başına siteyi kilitlemez |
54
- | `JSKELET_S3_*` | Access log'u S3'e yazıyorsanız | Bucket + credential; ayrıntı [07](./07-yapilandirma.md) |
55
-
56
- Production'da dosya veya S3 sink açıldığında HTTP access log middleware
57
- otomatik mount edilir (`logs.kinds` içinde `http` varsa). Admin paneli ring'i
58
- ayrıdır — disk/S3'e yazılan satırlar panele akmaz.
59
-
60
- Tam liste ve prewarm ayarlarının öncelik sırası:
61
- [07-yapilandirma.md](./07-yapilandirma.md).
62
-
63
- CLI `--env-file-if-exists=.env` ile çalıştığı için `.env` dosyası varsa otomatik
64
- yüklenir; yoksa hata verilmez. Kapsayıcıda genelde bu dosya yerine ortam
65
- değişkenleri doğrudan enjekte edilir. İki kaynağı birlikte kullanmak hangi
66
- değerin geçerli olduğunu belirsizleştirir; prod imajında `.env` bulundurmamak en
67
- temizidir.
68
-
69
- **Gizli anahtarlar `clientEnv` listesine konmamalıdır:** oradaki değerler client
70
- bundle'a düz metin olarak gömülür ([08-build.md](./08-build.md)). Secret benzeri
71
- isimler (`SECRET`, `API_KEY`, …) artık build'i düşürür.
72
-
73
- ## Docker
74
-
75
- Çok aşamalı bir imaj: build aşaması dev bağımlılıklarıyla derler, çalışma
76
- aşaması yalnızca üretim bağımlılıklarını ve build çıktısını taşır.
77
-
78
- ```dockerfile
79
- # syntax=docker/dockerfile:1
80
-
81
- # ---------- build ----------
82
- FROM node:22-bookworm-slim AS build
83
- WORKDIR /app
84
-
85
- # Bağımlılıklar ayrı katmanda: kaynak değişince yeniden kurulum yapılmasın.
86
- COPY package.json package-lock.json ./
87
- RUN npm ci
88
-
89
- # `public/fonts/` commit edilmiş olmalı: build'in ağa çıkması gerekmesin.
90
- COPY . .
91
-
92
- ENV NODE_ENV=production
93
- RUN npx jskelet build
94
-
95
- # ---------- runtime ----------
96
- FROM node:22-bookworm-slim AS runtime
97
- WORKDIR /app
98
-
99
- ENV NODE_ENV=production
100
- ENV PORT=3000
101
- ENV HOST=0.0.0.0
102
-
103
- COPY package.json package-lock.json ./
104
- # sharp ve tailwind yalnızca build zamanı gerekli; çalışma imajına girmesin.
105
- # images.remote kullanıyorsanız sharp'ı production dependencies'e alın.
106
- RUN npm ci --omit=dev && npm cache clean --force
107
-
108
- COPY --from=build /app/jskelet.config.mjs ./jskelet.config.mjs
109
- COPY --from=build /app/jsconfig.json ./jsconfig.json
110
- COPY --from=build /app/routes ./routes
111
- COPY --from=build /app/views ./views
112
- COPY --from=build /app/lib ./lib
113
- COPY --from=build /app/public ./public
114
- COPY --from=build /app/.jskelet ./.jskelet
115
-
116
- # Root olmayan kullanıcı.
117
- USER node
118
-
119
- EXPOSE 3000
120
-
121
- # Sağlık kontrolü: aşağıdaki route'u eklediğinizi varsayar.
122
- HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
123
- CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/healthcheck').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
124
-
125
- CMD ["npx", "jskelet", "start"]
126
- ```
127
-
128
- Notlar:
129
-
130
- - **`client/` ve `styles/` çalışma imajına gerekmez:** çıktıları
131
- `public/assets/` altında. `views/` ve `routes/` gerekir, çünkü render çalışma
132
- anında yapılıyor. `lib/` yalnızca projenizde varsa kopyalayın.
133
- - **`.jskelet/` gerekir:** `manifest.json` olmadan `asset()` hash'li URL'leri
134
- bulamaz ve `jskelet start` build'i baştan çalıştırmaya kalkar.
135
- - **`sharp` çalışma imajında genelde gerekmez:** yalnızca build zamanı görsel
136
- optimizasyonu için. `--omit=dev` ile dışarıda kalır (devDependency olarak
137
- kurulmuşsa). **`images.remote` açıksa** sharp runtime bağımlılığıdır —
138
- production `dependencies`'e alın ya da runtime imajında ayrıca kurun; yoksa
139
- optimizer kaynak URL'ye 302 yönlendirir.
140
- - `jskelet start`ı `npx` olmadan çağırmak isterseniz
141
- `CMD ["node", "node_modules/jskelet/bin/jskelet.mjs", "start"]` de çalışır.
142
-
143
- `.dockerignore`:
144
-
145
- ```
146
- node_modules
147
- .git
148
- .jskelet
149
- public/assets
150
- .env
151
- ```
152
-
153
- Build aşaması `npx jskelet build` ile bunları kendisi üretir.
154
-
155
- ### Depo alt dizininden dağıtım
156
-
157
- Bu depodaki örnekler jskelet'i npm'den değil `"jskelet": "file:../.."` ile
158
- alıyor. Coolify, Railway, Render gibi araçlarda "base directory" olarak
159
- `examples/blog` verilirse build context yalnızca o dizin olur, `../..`
160
- context'in dışında kalır ve kurulum `npm ci`de düşer. Doğru ayar: **base
161
- directory `/`** (depo kökü) ve imajı yukarıdaki çok aşamalı Dockerfile ile
162
- uygulama dizinine göre uyarlamak — ya da jskelet'i npm bağımlılığı olarak
163
- kurup context'i uygulamanın kendi dizini yapmak.
164
-
165
- Kendi uygulamanızda jskelet normal bir bağımlılık olacağı için bu kısıt yoktur;
166
- yukarıdaki çok aşamalı imaj yeterli.
167
-
168
- ## Sağlık kontrolü
169
-
170
- Framework hazır bir sağlık kontrolü ucu **eklemez**; kendi route'unuza koymanız
171
- gerekir. Varsayılan `devGateBypass` listesi `/api/healthcheck` yolunu içerdiği
172
- için bu adı kullanmak en az sürprizli seçenektir: dev gate açıkken bile
173
- erişilebilir kalır.
174
-
175
- ```js
176
- // routes/00-health.mjs
177
- import { getHtmlCacheSize } from "jskelet";
178
-
179
- export default function register(app) {
180
- app.get("/api/healthcheck", (req, res) => {
181
- res.setHeader("Cache-Control", "no-store");
182
- res.json({
183
- ok: true,
184
- uptime: process.uptime(),
185
- cache: getHtmlCacheSize(),
186
- });
187
- });
188
- }
189
- ```
190
-
191
- Dosya adındaki `00-` öneki, bu route'un herhangi bir yakalayıcıdan önce
192
- kaydedilmesini sağlar ([03-routing.md](./03-routing.md)).
193
-
194
- Farklı bir yol kullanacaksanız `devGateBypass` listesini güncelleyin, aksi hâlde
195
- staging'de orkestratör 404 görür:
196
-
197
- ```js
198
- devGateBypass: ["/healthz", "/robots.txt", "/sitemap.xml", "/favicon.ico"]
199
- ```
200
-
201
- Isıtma turu sağlık kontrolünü etkilemez: prewarm başarısız olsa bile süreç ayakta
202
- kalır ve sayfalar (soğuk da olsa) servis edilir.
203
-
204
- Hazırlık (readiness) ile canlılık (liveness) ayrımı gerekiyorsa ısıtmanın
205
- durumunu de raporlayabilirsiniz:
206
-
207
- ```js
208
- import { prewarmProgress } from "jskelet";
209
-
210
- app.get("/api/ready", (req, res) => {
211
- const warmedUp = !prewarmProgress.active && prewarmProgress.finishedAt !== null;
212
- res.status(warmedUp ? 200 : 503).json({ warmedUp, ...prewarmProgress });
213
- });
214
- ```
215
-
216
- Bu ucun yolunu `prewarmSkip` ile ısıtma dışında bırakmayı unutmayın (varsayılan
217
- `/api/` öneki zaten kapsıyor).
218
-
219
- ## Ters proxy
220
-
221
- Express uygulaması `trust proxy`yi **açık** olarak kurar (`app.set("trust proxy", true)`).
222
- Bunun sonuçları:
223
-
224
- - `req.protocol` `X-Forwarded-Proto` başlığından okunur, yani proxy TLS'i
225
- sonlandırıyorsa `https` doğru döner.
226
- - `req.ip` `X-Forwarded-For` zincirinden çözülür.
227
- - `res.redirect()` ile üretilen mutlak URL'ler doğru şemayı taşır.
228
-
229
- Bu ayar **proxy'nin bu başlıkları güvenilir biçimde yazdığını varsayar.**
230
- Uygulamayı doğrudan internete açacaksanız istemcinin `X-Forwarded-*` başlıklarını
231
- uydurabileceğini unutmayın; her zaman bir proxy ya da yük dengeleyici arkasında
232
- çalıştırın ve proxy'nin gelen `X-Forwarded-For` başlığını üzerine yazdığından
233
- emin olun.
234
-
235
- Örnek nginx yapılandırması:
236
-
237
- ```nginx
238
- upstream jskelet {
239
- server 127.0.0.1:3000;
240
- keepalive 32;
241
- }
242
-
243
- server {
244
- listen 443 ssl http2;
245
- server_name ornek.com;
246
-
247
- # Yanıt gövdeleri zaten sıkıştırılmış geliyor; ikinci kez sıkıştırma yapma.
248
- gzip off;
249
-
250
- location / {
251
- proxy_pass http://jskelet;
252
- proxy_http_version 1.1;
253
-
254
- proxy_set_header Host $host;
255
- proxy_set_header X-Real-IP $remote_addr;
256
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
257
- proxy_set_header X-Forwarded-Proto $scheme;
258
- proxy_set_header Connection "";
259
-
260
- # Sıkıştırılmış yanıt alabilmek için upstream'e ilet.
261
- proxy_set_header Accept-Encoding $http_accept_encoding;
262
- }
263
- }
264
- ```
265
-
266
- Önemli noktalar:
267
-
268
- - **Sıkıştırmayı iki kez yapmayın.** JSkelet brotli/gzip pazarlığını kendisi
269
- yapıyor ve önbelleklenmiş sayfalarda sıkıştırılmış gövdeyi saklıyor. nginx'in
270
- kendi `gzip`ini açık bırakmak brotli'yi çözüp yeniden gzip'lemeye yol açabilir.
271
- - **`Accept-Encoding`i iletin**, yoksa uygulama sıkıştırma yapmaz ve önbellekteki
272
- hazır sıkıştırılmış gövdeler kullanılmaz.
273
- - `Vary: Accept-Encoding` uygulama tarafından yazılır; proxy önbelleği bunu
274
- dikkate alır.
275
-
276
- ### CDN ile birlikte
277
-
278
- Önbelleklenebilir sayfalara yazılan başlık:
279
-
280
- ```
281
- Cache-Control: public, max-age=0, s-maxage=<revalidate>, stale-while-revalidate=60
282
- ```
283
-
284
- `max-age=0` tarayıcıda saklamayı kapatır, `s-maxage` CDN'e süreyi bildirir. Yani
285
- aynı tazelik modeli iki katmanda birlikte çalışır: CDN `s-maxage` boyunca kendi
286
- kopyasını verir, süresi geçtiğinde origin'e sorar ve origin de kendi
287
- önbelleğinden anında yanıtlar.
288
-
289
- `X-JSkelet-Cache` başlığı hangi katmanın yanıtladığını teşhis etmeyi
290
- kolaylaştırır; CDN'in kendi cache başlığıyla birlikte okuyun
291
- ([06-cache.md](./06-cache.md)).
292
-
293
- Statik varlıklar (`/assets/`, `/fonts/`) `immutable` işaretli olduğu için CDN'de
294
- süresiz tutulabilir; hash değiştiğinde URL de değişir.
295
-
296
- ## Ölçekleme
297
-
298
- HTML önbelleği **süreç belleğinde** yaşar. Birden fazla kopya çalıştırdığınızda:
299
-
300
- - Her kopyanın kendi önbelleği olur; bellek kullanımı kopya sayısıyla çarpılır
301
- (en fazla 500 girdi + sıkıştırılmış kopyaları).
302
- - Her kopya açılışta kendi ısıtma turunu yapar. `PREWARM_MAX` ve
303
- `PREWARM_CONCURRENCY` değerlerini upstream API'nizin kopya sayısıyla çarpılmış
304
- yükü kaldırabileceği şekilde ayarlayın.
305
- - `clearHtmlCache()` yalnızca çağrıldığı süreci etkiler. Tüm kopyaları
306
- temizlemek gerekiyorsa bunu orkestratör düzeyinde (yeniden başlatma) ya da
307
- kendi yazacağınız bir yayın mekanizmasıyla çözmeniz gerekir.
308
- - Önünde bir CDN varsa çoğu istek origin'e hiç gelmez ve kopya başına önbellek
309
- farkı görünmez hâle gelir.
310
-
311
- Tek kopyanın kapasitesini artırmak için `revalidate` sürelerini yükseltmek,
312
- kopya eklemekten genellikle daha etkilidir: önbellek isabet oranı arttıkça
313
- istek başına iş neredeyse sıfıra iner.
314
-
315
- ## Yayın öncesi kontrol listesi
316
-
317
- - [ ] `NODE_ENV=production`
318
- - [ ] `npm run build` çalıştı ve `.jskelet/manifest.json` üretildi
319
- - [ ] `public/fonts/` içindeki woff2 dosyaları commit edilmiş
320
- ([08-build.md](./08-build.md))
321
- - [ ] `styles/globals.css` içindeki `@source` direktifleri tüm şablon
322
- dizinlerini kapsıyor
323
- - [ ] `hooks.notFound()` tanımlı ve 404 şablonu var
324
- - [ ] `hooks.metadata()` içinde `siteUrl` var (göreli `canonical`lar
325
- mutlaklaşsın)
326
- - [ ] `cache().html` desenleri sitenin tazelik profiline uygun
327
- - [ ] `hooks.prewarmPaths()` en önemli sayfaları başa koyuyor
328
- - [ ] `headers()` içinde CSP ve güvenlik başlıkları tanımlı
329
- - [ ] Sağlık kontrolü ucu var ve `devGateBypass` listesinde
330
- - [ ] Staging'de `DEV_GATE=1` ve `DEV_TOKEN` ayarlı, prod'da gate **kapalı**
331
- - [ ] Ters proxy `Accept-Encoding`i iletiyor ve kendi sıkıştırmasını yapmıyor
332
- - [ ] `clientEnv` listesinde gizli anahtar yok
333
-
334
- ## Sırada ne var
335
-
336
- - Önbellek ayarları ve prewarm: [06-cache.md](./06-cache.md)
337
- - Ortam değişkenlerinin tamamı: [07-yapilandirma.md](./07-yapilandirma.md)
338
- - Next.js'ten taşıma: [11-tasima.md](./11-tasima.md)
1
+ # 10 — Dağıtım
2
+
3
+ Bu belge bir JSkelet uygulamasını yayına almayı anlatır: prod build ve start
4
+ akışı, ayarlanması gereken ortam değişkenleri, çalışan bir Docker kurulumu, ters
5
+ proxy ve `trust proxy` notları, sağlık kontrolü ucunun nasıl eklendiği ve
6
+ ölçekleme sırasında önbelleğin nasıl davrandığı. Build adımlarının içeriği
7
+ [08-build.md](./08-build.md)'de, önbellek davranışı [06-cache.md](./06-cache.md)'de.
8
+
9
+ ## Prod akışı
10
+
11
+ ```bash
12
+ npm ci
13
+ npm run build # jskelet build
14
+ npm start # jskelet start
15
+ ```
16
+
17
+ `jskelet build` `NODE_ENV` verilmemişse `production` ayarlar ve tüm adımları
18
+ çalıştırır: fontlar, ikon sprite, CSS, client JS, görseller, manifest,
19
+ precompress.
20
+
21
+ `jskelet start` önce `.jskelet/manifest.json` dosyasına bakar; yoksa build'i
22
+ kendisi çalıştırır. Docker imajında build zaten yapıldığı için bu bir no-op;
23
+ amaç `npm start`ı doğrudan çalıştıran birinin stilsiz bir sayfayla
24
+ karşılaşmaması.
25
+
26
+ Port doluysa süreç **başlamaz** (PID + ipucu). `jskelet start --murder` o
27
+ porttaki dinleyiciyi öldürüp bağlar — geliştirmede unutulmuş bir süreç için;
28
+ üretim orkestratöründe genelde gerekmez.
29
+
30
+ Sunucu hazır olduğunda tek satır basar:
31
+
32
+ ```
33
+ jskelet → http://localhost:3000 (production)
34
+ ```
35
+
36
+ Süreç iki güvenlik ağıyla korunur: `unhandledRejection` ve `uncaughtException`
37
+ loglanır ve süreç ayakta kalır. Bir haber sitesinde tek sayfanın hatası tüm
38
+ siteyi indirmemeli. Kendi hata izleme aracınıza (Sentry vb.) bağlanmak
39
+ istiyorsanız aynı olaylara kendi dinleyicinizi de ekleyebilirsiniz.
40
+
41
+ ## Ortam değişkenleri
42
+
43
+ Zorunlu hiçbir değişken yok; hepsinin makul bir varsayılanı var. Prod'da
44
+ ayarlamayı düşünmeniz gerekenler:
45
+
46
+ | Değişken | Öneri | Neden |
47
+ | --- | --- | --- |
48
+ | `NODE_ENV` | `production` | Şablon cache'i, manifest'in bir kez okunması, bozuk route modülünde fırlatma |
49
+ | `PORT` | `3000` | Orkestratörünüzün beklediği port |
50
+ | `HOST` | `0.0.0.0` | Yalnızca IPv4 dinlemek gerekiyorsa. Varsayılan `::` zaten çift yığın dinler |
51
+ | `PREWARM_MAX` | Site boyutuna göre | Açılışta ısıtılacak sayfa sayısı |
52
+ | `PREWARM_INTERVAL_SECONDS` | `0` ya da uzun bir değer | Hiç ziyaret edilmeyen sayfaları sıcak tutmak isterseniz |
53
+ | `DEV_GATE` + `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler. Token tek başına siteyi kilitlemez |
54
+ | `JSKELET_S3_*` | Access log'u S3'e yazıyorsanız | Bucket + credential; ayrıntı [07](./07-yapilandirma.md) |
55
+
56
+ Production'da dosya veya S3 sink açıldığında HTTP access log middleware
57
+ otomatik mount edilir (`logs.kinds` içinde `http` varsa). Admin paneli ring'i
58
+ ayrıdır — disk/S3'e yazılan satırlar panele akmaz.
59
+
60
+ Tam liste ve prewarm ayarlarının öncelik sırası:
61
+ [07-yapilandirma.md](./07-yapilandirma.md).
62
+
63
+ CLI `--env-file-if-exists=.env` ile çalıştığı için `.env` dosyası varsa otomatik
64
+ yüklenir; yoksa hata verilmez. Kapsayıcıda genelde bu dosya yerine ortam
65
+ değişkenleri doğrudan enjekte edilir. İki kaynağı birlikte kullanmak hangi
66
+ değerin geçerli olduğunu belirsizleştirir; prod imajında `.env` bulundurmamak en
67
+ temizidir.
68
+
69
+ **Gizli anahtarlar `clientEnv` listesine konmamalıdır:** oradaki değerler client
70
+ bundle'a düz metin olarak gömülür ([08-build.md](./08-build.md)). Secret benzeri
71
+ isimler (`SECRET`, `API_KEY`, …) artık build'i düşürür.
72
+
73
+ ## Docker
74
+
75
+ Çok aşamalı bir imaj: build aşaması dev bağımlılıklarıyla derler, çalışma
76
+ aşaması yalnızca üretim bağımlılıklarını ve build çıktısını taşır.
77
+
78
+ ```dockerfile
79
+ # syntax=docker/dockerfile:1
80
+
81
+ # ---------- build ----------
82
+ FROM node:22-bookworm-slim AS build
83
+ WORKDIR /app
84
+
85
+ # Bağımlılıklar ayrı katmanda: kaynak değişince yeniden kurulum yapılmasın.
86
+ COPY package.json package-lock.json ./
87
+ RUN npm ci
88
+
89
+ # `public/fonts/` commit edilmiş olmalı: build'in ağa çıkması gerekmesin.
90
+ COPY . .
91
+
92
+ ENV NODE_ENV=production
93
+ RUN npx jskelet build
94
+
95
+ # ---------- runtime ----------
96
+ FROM node:22-bookworm-slim AS runtime
97
+ WORKDIR /app
98
+
99
+ ENV NODE_ENV=production
100
+ ENV PORT=3000
101
+ ENV HOST=0.0.0.0
102
+
103
+ COPY package.json package-lock.json ./
104
+ # sharp ve tailwind yalnızca build zamanı gerekli; çalışma imajına girmesin.
105
+ # images.remote kullanıyorsanız sharp'ı production dependencies'e alın.
106
+ RUN npm ci --omit=dev && npm cache clean --force
107
+
108
+ COPY --from=build /app/jskelet.config.mjs ./jskelet.config.mjs
109
+ COPY --from=build /app/jsconfig.json ./jsconfig.json
110
+ COPY --from=build /app/routes ./routes
111
+ COPY --from=build /app/views ./views
112
+ COPY --from=build /app/lib ./lib
113
+ COPY --from=build /app/public ./public
114
+ COPY --from=build /app/.jskelet ./.jskelet
115
+
116
+ # Root olmayan kullanıcı.
117
+ USER node
118
+
119
+ EXPOSE 3000
120
+
121
+ # Sağlık kontrolü: aşağıdaki route'u eklediğinizi varsayar.
122
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
123
+ CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/healthcheck').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
124
+
125
+ CMD ["npx", "jskelet", "start"]
126
+ ```
127
+
128
+ Notlar:
129
+
130
+ - **`client/` ve `styles/` çalışma imajına gerekmez:** çıktıları
131
+ `public/assets/` altında. `views/` ve `routes/` gerekir, çünkü render çalışma
132
+ anında yapılıyor. `lib/` yalnızca projenizde varsa kopyalayın.
133
+ - **`.jskelet/` gerekir:** `manifest.json` olmadan `asset()` hash'li URL'leri
134
+ bulamaz ve `jskelet start` build'i baştan çalıştırmaya kalkar.
135
+ - **`sharp` çalışma imajında genelde gerekmez:** yalnızca build zamanı görsel
136
+ optimizasyonu için. `--omit=dev` ile dışarıda kalır (devDependency olarak
137
+ kurulmuşsa). **`images.remote` açıksa** sharp runtime bağımlılığıdır —
138
+ production `dependencies`'e alın ya da runtime imajında ayrıca kurun; yoksa
139
+ optimizer kaynak URL'ye 302 yönlendirir.
140
+ - `jskelet start`ı `npx` olmadan çağırmak isterseniz
141
+ `CMD ["node", "node_modules/jskelet/bin/jskelet.mjs", "start"]` de çalışır.
142
+
143
+ `.dockerignore`:
144
+
145
+ ```
146
+ node_modules
147
+ .git
148
+ .jskelet
149
+ public/assets
150
+ .env
151
+ ```
152
+
153
+ Build aşaması `npx jskelet build` ile bunları kendisi üretir.
154
+
155
+ ### Depo alt dizininden dağıtım
156
+
157
+ Bu depodaki örnekler jskelet'i npm'den değil `"jskelet": "file:../.."` ile
158
+ alıyor. Coolify, Railway, Render gibi araçlarda "base directory" olarak
159
+ `examples/blog` verilirse build context yalnızca o dizin olur, `../..`
160
+ context'in dışında kalır ve kurulum `npm ci`de düşer. Doğru ayar: **base
161
+ directory `/`** (depo kökü) ve imajı yukarıdaki çok aşamalı Dockerfile ile
162
+ uygulama dizinine göre uyarlamak — ya da jskelet'i npm bağımlılığı olarak
163
+ kurup context'i uygulamanın kendi dizini yapmak.
164
+
165
+ Kendi uygulamanızda jskelet normal bir bağımlılık olacağı için bu kısıt yoktur;
166
+ yukarıdaki çok aşamalı imaj yeterli.
167
+
168
+ ## Sağlık kontrolü
169
+
170
+ Framework hazır bir sağlık kontrolü ucu **eklemez**; kendi route'unuza koymanız
171
+ gerekir. Varsayılan `devGateBypass` listesi `/api/healthcheck` yolunu içerdiği
172
+ için bu adı kullanmak en az sürprizli seçenektir: dev gate açıkken bile
173
+ erişilebilir kalır.
174
+
175
+ ```js
176
+ // routes/00-health.mjs
177
+ import { getHtmlCacheSize } from "jskelet";
178
+
179
+ export default function register(app) {
180
+ app.get("/api/healthcheck", (req, res) => {
181
+ res.setHeader("Cache-Control", "no-store");
182
+ res.json({
183
+ ok: true,
184
+ uptime: process.uptime(),
185
+ cache: getHtmlCacheSize(),
186
+ });
187
+ });
188
+ }
189
+ ```
190
+
191
+ Dosya adındaki `00-` öneki, bu route'un herhangi bir yakalayıcıdan önce
192
+ kaydedilmesini sağlar ([03-routing.md](./03-routing.md)).
193
+
194
+ Farklı bir yol kullanacaksanız `devGateBypass` listesini güncelleyin, aksi hâlde
195
+ staging'de orkestratör 404 görür:
196
+
197
+ ```js
198
+ devGateBypass: ["/healthz", "/robots.txt", "/sitemap.xml", "/favicon.ico"]
199
+ ```
200
+
201
+ Isıtma turu sağlık kontrolünü etkilemez: prewarm başarısız olsa bile süreç ayakta
202
+ kalır ve sayfalar (soğuk da olsa) servis edilir.
203
+
204
+ Hazırlık (readiness) ile canlılık (liveness) ayrımı gerekiyorsa ısıtmanın
205
+ durumunu de raporlayabilirsiniz:
206
+
207
+ ```js
208
+ import { prewarmProgress } from "jskelet";
209
+
210
+ app.get("/api/ready", (req, res) => {
211
+ const warmedUp = !prewarmProgress.active && prewarmProgress.finishedAt !== null;
212
+ res.status(warmedUp ? 200 : 503).json({ warmedUp, ...prewarmProgress });
213
+ });
214
+ ```
215
+
216
+ Bu ucun yolunu `prewarmSkip` ile ısıtma dışında bırakmayı unutmayın (varsayılan
217
+ `/api/` öneki zaten kapsıyor).
218
+
219
+ ## Ters proxy
220
+
221
+ Express uygulaması `trust proxy`yi **açık** olarak kurar (`app.set("trust proxy", true)`).
222
+ Bunun sonuçları:
223
+
224
+ - `req.protocol` `X-Forwarded-Proto` başlığından okunur, yani proxy TLS'i
225
+ sonlandırıyorsa `https` doğru döner.
226
+ - `req.ip` `X-Forwarded-For` zincirinden çözülür.
227
+ - `res.redirect()` ile üretilen mutlak URL'ler doğru şemayı taşır.
228
+
229
+ Bu ayar **proxy'nin bu başlıkları güvenilir biçimde yazdığını varsayar.**
230
+ Uygulamayı doğrudan internete açacaksanız istemcinin `X-Forwarded-*` başlıklarını
231
+ uydurabileceğini unutmayın; her zaman bir proxy ya da yük dengeleyici arkasında
232
+ çalıştırın ve proxy'nin gelen `X-Forwarded-For` başlığını üzerine yazdığından
233
+ emin olun.
234
+
235
+ Örnek nginx yapılandırması:
236
+
237
+ ```nginx
238
+ upstream jskelet {
239
+ server 127.0.0.1:3000;
240
+ keepalive 32;
241
+ }
242
+
243
+ server {
244
+ listen 443 ssl http2;
245
+ server_name ornek.com;
246
+
247
+ # Yanıt gövdeleri zaten sıkıştırılmış geliyor; ikinci kez sıkıştırma yapma.
248
+ gzip off;
249
+
250
+ location / {
251
+ proxy_pass http://jskelet;
252
+ proxy_http_version 1.1;
253
+
254
+ proxy_set_header Host $host;
255
+ proxy_set_header X-Real-IP $remote_addr;
256
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
257
+ proxy_set_header X-Forwarded-Proto $scheme;
258
+ proxy_set_header Connection "";
259
+
260
+ # Sıkıştırılmış yanıt alabilmek için upstream'e ilet.
261
+ proxy_set_header Accept-Encoding $http_accept_encoding;
262
+ }
263
+ }
264
+ ```
265
+
266
+ Önemli noktalar:
267
+
268
+ - **Sıkıştırmayı iki kez yapmayın.** JSkelet brotli/gzip pazarlığını kendisi
269
+ yapıyor ve önbelleklenmiş sayfalarda sıkıştırılmış gövdeyi saklıyor. nginx'in
270
+ kendi `gzip`ini açık bırakmak brotli'yi çözüp yeniden gzip'lemeye yol açabilir.
271
+ - **`Accept-Encoding`i iletin**, yoksa uygulama sıkıştırma yapmaz ve önbellekteki
272
+ hazır sıkıştırılmış gövdeler kullanılmaz.
273
+ - `Vary: Accept-Encoding` uygulama tarafından yazılır; proxy önbelleği bunu
274
+ dikkate alır.
275
+
276
+ ### CDN ile birlikte
277
+
278
+ Önbelleklenebilir sayfalara yazılan başlıklar:
279
+
280
+ ```
281
+ Cache-Control: public, max-age=0
282
+ CDN-Cache-Control: max-age=<html ttl>, stale-while-revalidate=<staleWhileRevalidate>
283
+ ```
284
+
285
+ `max-age=0` tarayıcıda saklamayı kapatır. Edge süresi `CDN-Cache-Control`
286
+ üzerindeki `max-age`'dir ve mevcut HTML TTL'dir. `stale-while-revalidate`
287
+ `cache().staleWhileRevalidate` değeridir (varsayılan 60; `0` direktifi basmaz).
288
+ `s-maxage` yazılmaz: Cloudflare `max-age=0` ile birlikte onu `EXPIRED` sayar.
289
+ `must-revalidate`, `proxy-revalidate` ve `no-cache` aynı yanıtta yoktur.
290
+
291
+ CDN `max-age` boyunca kendi kopyasını verir, taze pencere bitince
292
+ `stale-while-revalidate` süresince eski HTML'i sunar ve arkada origin'e sorar.
293
+ Origin da kendi önbelleğinden anında yanıtlar.
294
+
295
+ **Kırılma.** Yalnızca `Cache-Control` / `s-maxage` okuyan bir ara katman
296
+ (nginx `proxy_cache`) bu HTML'i artık önbelleklemez. Cloudflare
297
+ `CDN-Cache-Control` okur. Süreç içi önbellek ve `X-JSkelet-Cache` aynı kalır.
298
+
299
+ `X-JSkelet-Cache` başlığı hangi katmanın yanıtladığını teşhis etmeyi
300
+ kolaylaştırır; CDN'in kendi cache başlığıyla birlikte okuyun
301
+ ([06-cache.md](./06-cache.md)).
302
+
303
+ Statik varlıklar (`/assets/`, `/fonts/`) `immutable` işaretli olduğu için CDN'de
304
+ süresiz tutulabilir; hash değiştiğinde URL de değişir.
305
+
306
+ ## Ölçekleme
307
+
308
+ HTML önbelleği **süreç belleğinde** yaşar. Birden fazla kopya çalıştırdığınızda:
309
+
310
+ - Her kopyanın kendi önbelleği olur; bellek kullanımı kopya sayısıyla çarpılır
311
+ (en fazla 500 girdi + sıkıştırılmış kopyaları).
312
+ - Her kopya açılışta kendi ısıtma turunu yapar. `PREWARM_MAX` ve
313
+ `PREWARM_CONCURRENCY` değerlerini upstream API'nizin kopya sayısıyla çarpılmış
314
+ yükü kaldırabileceği şekilde ayarlayın.
315
+ - `clearHtmlCache()` yalnızca çağrıldığı süreci etkiler. Tüm kopyaları
316
+ temizlemek gerekiyorsa bunu orkestratör düzeyinde (yeniden başlatma) ya da
317
+ kendi yazacağınız bir yayın mekanizmasıyla çözmeniz gerekir.
318
+ - Önünde bir CDN varsa çoğu istek origin'e hiç gelmez ve kopya başına önbellek
319
+ farkı görünmez hâle gelir.
320
+
321
+ Tek kopyanın kapasitesini artırmak için `revalidate` sürelerini yükseltmek,
322
+ kopya eklemekten genellikle daha etkilidir: önbellek isabet oranı arttıkça
323
+ istek başına iş neredeyse sıfıra iner.
324
+
325
+ ## Yayın öncesi kontrol listesi
326
+
327
+ - [ ] `NODE_ENV=production`
328
+ - [ ] `npm run build` çalıştı ve `.jskelet/manifest.json` üretildi
329
+ - [ ] `public/fonts/` içindeki woff2 dosyaları commit edilmiş
330
+ ([08-build.md](./08-build.md))
331
+ - [ ] `styles/globals.css` içindeki `@source` direktifleri tüm şablon
332
+ dizinlerini kapsıyor
333
+ - [ ] `hooks.notFound()` tanımlı ve 404 şablonu var
334
+ - [ ] `hooks.metadata()` içinde `siteUrl` var (göreli `canonical`lar
335
+ mutlaklaşsın)
336
+ - [ ] `cache().html` desenleri sitenin tazelik profiline uygun
337
+ - [ ] `hooks.prewarmPaths()` en önemli sayfaları başa koyuyor
338
+ - [ ] `headers()` içinde CSP ve güvenlik başlıkları tanımlı
339
+ - [ ] Sağlık kontrolü ucu var ve `devGateBypass` listesinde
340
+ - [ ] Staging'de `DEV_GATE=1` ve `DEV_TOKEN` ayarlı, prod'da gate **kapalı**
341
+ - [ ] Ters proxy `Accept-Encoding`i iletiyor ve kendi sıkıştırmasını yapmıyor
342
+ - [ ] `clientEnv` listesinde gizli anahtar yok
343
+
344
+ ## Sırada ne var
345
+
346
+ - Önbellek ayarları ve prewarm: [06-cache.md](./06-cache.md)
347
+ - Ortam değişkenlerinin tamamı: [07-yapilandirma.md](./07-yapilandirma.md)
348
+ - Next.js'ten taşıma: [11-tasima.md](./11-tasima.md)