jskelet 0.2.5 → 0.3.0

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 (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  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 +293 -287
  7. package/docs/03-routing.md +486 -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 +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
@@ -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)