jskelet 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -1,338 +1,338 @@
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ı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)