jskelet 0.2.3 → 0.2.5

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