jskelet 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +127 -0
  2. package/CHANGELOG.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +342 -0
  5. package/bin/jskelet.mjs +104 -0
  6. package/docs/01-baslangic.md +285 -0
  7. package/docs/02-mimari.md +287 -0
  8. package/docs/03-routing.md +437 -0
  9. package/docs/04-render-ve-sablonlar.md +490 -0
  10. package/docs/05-islands.md +429 -0
  11. package/docs/06-cache.md +409 -0
  12. package/docs/07-yapilandirma.md +673 -0
  13. package/docs/08-build.md +366 -0
  14. package/docs/09-dev-araclari.md +302 -0
  15. package/docs/10-dagitim.md +329 -0
  16. package/docs/11-tasima.md +352 -0
  17. package/docs/README.md +82 -0
  18. package/package.json +97 -0
  19. package/src/build/build.mjs +138 -0
  20. package/src/build/ensure-build.mjs +15 -0
  21. package/src/build/paths.mjs +118 -0
  22. package/src/build/resolve-peer.mjs +36 -0
  23. package/src/build/tasks/client.mjs +268 -0
  24. package/src/build/tasks/css.mjs +124 -0
  25. package/src/build/tasks/fonts.mjs +146 -0
  26. package/src/build/tasks/icons.mjs +224 -0
  27. package/src/build/tasks/images.mjs +244 -0
  28. package/src/build/tasks/precompress.mjs +78 -0
  29. package/src/client/devtools/overlay.js +1763 -0
  30. package/src/client/devtools/report.html +185 -0
  31. package/src/client/devtools/report.js +712 -0
  32. package/src/client/dom.js +95 -0
  33. package/src/client/index.js +26 -0
  34. package/src/client/registry.js +223 -0
  35. package/src/client/safe-image.js +91 -0
  36. package/src/client/store.js +36 -0
  37. package/src/config/defaults.js +102 -0
  38. package/src/config/index.js +433 -0
  39. package/src/config/pattern.js +107 -0
  40. package/src/dev-server.mjs +383 -0
  41. package/src/http/control-flow.js +56 -0
  42. package/src/http/request-cache.js +46 -0
  43. package/src/index.js +35 -0
  44. package/src/init.mjs +220 -0
  45. package/src/log.mjs +332 -0
  46. package/src/logo.png +0 -0
  47. package/src/runtime/alias-hooks.mjs +119 -0
  48. package/src/runtime/register.mjs +4 -0
  49. package/src/server/assets.js +119 -0
  50. package/src/server/create-app.js +167 -0
  51. package/src/server/dev/devtools.js +383 -0
  52. package/src/server/dev/report.js +351 -0
  53. package/src/server/head-hints.js +132 -0
  54. package/src/server/html-cache.js +166 -0
  55. package/src/server/metadata.js +102 -0
  56. package/src/server/middleware/compression.js +205 -0
  57. package/src/server/middleware/dev-gate.js +62 -0
  58. package/src/server/middleware/headers.js +37 -0
  59. package/src/server/middleware/redirects.js +32 -0
  60. package/src/server/middleware/static-precompressed.js +100 -0
  61. package/src/server/middleware/upstream-proxy.js +141 -0
  62. package/src/server/prewarm.js +283 -0
  63. package/src/server/render.js +356 -0
  64. package/src/server/router.js +121 -0
  65. package/src/server/status-page.js +164 -0
  66. package/src/server/upstream-tracking.js +51 -0
  67. package/src/start.mjs +7 -0
  68. package/src/templates/layout.ejs +44 -0
  69. package/src/version.mjs +17 -0
  70. package/src/views/components/loader.js +85 -0
  71. package/src/views/helpers/html.js +102 -0
  72. package/src/views/helpers/tags.js +193 -0
@@ -0,0 +1,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` | Kapsayıcı içinde dışarıdan erişim için (varsayılan) |
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)
@@ -0,0 +1,352 @@
1
+ # 11 — Next.js'ten taşıma
2
+
3
+ Bu belge Next.js App Router kullanan bir projeyi JSkelet'e taşımayı anlatır:
4
+ kavram ve API karşılıklarının tablosu, taşınamayan şeylerin açıkça listesi ve
5
+ adım adım bir plan. JSkelet'in yüzeyi bilinçli olarak Next'in fiilen kullanılan
6
+ alt kümesine benzetildi — `next.config` sözdizimi, Metadata API, `notFound()`,
7
+ `revalidate`, `cache()` gibi kavramlar tanıdık gelecek. Farkların *nedenleri*
8
+ [02-mimari.md](./02-mimari.md)'de.
9
+
10
+ ## Karşılık tablosu
11
+
12
+ ### Yapılandırma
13
+
14
+ | Next.js | JSkelet | Not |
15
+ | --- | --- | --- |
16
+ | `next.config.mjs` | `jskelet.config.mjs` | Aynı ruh, daha küçük yüzey ([07](./07-yapilandirma.md)) |
17
+ | `headers()` | `headers()` | Aynı şekil: `{ source, headers: [{ key, value }] }` |
18
+ | `redirects()` | `redirects()` | `permanent` → 308, aksi hâlde 307; `statusCode` ile ezilebilir |
19
+ | `rewrites()` | `rewrites()` | `beforeFiles` / `afterFiles` fazları var; `fallback` yok |
20
+ | `compress: true` | Otomatik | `node:zlib` ile brotli + gzip |
21
+ | `images.deviceSizes` | `images.widths` | Build zamanı webp üretimi ([08](./08-build.md)) |
22
+ | `NEXT_PUBLIC_*` | `clientEnv: [...]` | Hangi anahtarın açık olduğu isimden değil config'ten belli |
23
+ | `experimental.*` | — | Yok |
24
+
25
+ ### Routing ve render
26
+
27
+ | Next.js | JSkelet | Not |
28
+ | --- | --- | --- |
29
+ | `app/page.js` (dosya bazlı routing) | `routes/*.mjs` içinde `app.get(...)` | Sıra açık yazılır ([03](./03-routing.md)) |
30
+ | `app/[slug]/page.js` | `app.get("/:slug", route(...))` | Express desen sözdizimi |
31
+ | `params`, `searchParams` | `ctx.params`, `ctx.query` | Controller'ın tek argümanı |
32
+ | `layout.js` | `views/layout.ejs` + `hooks.layoutContext()` | Tek layout; iç içe layout yok |
33
+ | Sunucu bileşeni (RSC) | Controller + EJS şablonu + `views/components/**` | Fonksiyon HTML string döndürür |
34
+ | İstemci bileşeni (`"use client"`) | Island (`data-island` + `mount`) | Sayfanın tamamı hidre edilmez ([05](./05-islands.md)) |
35
+ | `notFound()` | `notFound()` | Aynı ad, aynı kontrol akışı |
36
+ | `redirect()` | `redirect()` (307) | Kalıcı için `permanentRedirect()` (308) |
37
+ | `not-found.js` | `hooks.notFound()` | Bir sayfa tanımı döndürür |
38
+ | `error.js` | Express hata yöneticisi | Framework 500 için minimal HTML döner |
39
+ | `loading.js` / Suspense | — | Sunucu HTML'i tam; iskelet gerekmiyor |
40
+ | Streaming SSR | — | Yanıt tek parça |
41
+ | `generateMetadata()` | Controller `metadata` + `hooks.metadata()` | Aynı alan adları ([04](./04-render-ve-sablonlar.md)) |
42
+ | `generateStaticParams()` | `hooks.prewarmPaths()` | Build zamanı değil, açılış zamanı ısıtma |
43
+ | Route Handlers (`route.js`) | Düz Express handler'ı | `app.get/post(...)` |
44
+ | Middleware (`middleware.ts`) | Express middleware + config `rewrites`/`headers`/`redirects` | `app.use(...)` |
45
+
46
+ ### Veri ve önbellek
47
+
48
+ | Next.js | JSkelet | Not |
49
+ | --- | --- | --- |
50
+ | `export const revalidate = 60` | `route(controller, { revalidate: 60 })` | Ya da `cache().html` ([06](./06-cache.md)) |
51
+ | ISR (dosyaya yazılan prerender) | Bellek içi TTL cache + stale-while-revalidate | Diske yazılmaz |
52
+ | `fetch(..., { next: { revalidate } })` | — | Önbellek sayfa düzeyinde |
53
+ | `unstable_cache` | — | İstek üstü veri önbelleği yok; sayfa önbelleği var |
54
+ | React `cache()` | `cache()` | Aynı davranış: istek içi memoizasyon |
55
+ | `revalidatePath()` | `clearHtmlCache()` | Şu anda tek tek anahtar geçersizleme yok |
56
+ | `cookies()`, `headers()` | `ctx.req.headers`, `ctx.req.cookies`* | Express nesnesine doğrudan erişim |
57
+ | `dynamic = "force-dynamic"` | `revalidate` vermemek | Önbellek kapalı demek |
58
+
59
+ \* Express 5 çerezleri kendiliğinden ayrıştırmaz; `cookie-parser` ekleyin ya da
60
+ başlığı elle okuyun.
61
+
62
+ ### Bileşenler ve yardımcılar
63
+
64
+ | Next.js | JSkelet | Not |
65
+ | --- | --- | --- |
66
+ | `next/link` | `link({ href, text })` — `jskelet/tags` | `title` otomatik, dış bağlantıya `rel`/`target` otomatik |
67
+ | `next/link` prefetch'i | `navigation: { prefetch, prerender }` | Speculation Rules; client runtime'ı yok ([07](./07-yapilandirma.md)) |
68
+ | `next/image` | `image({ src, alt, priority })` — `jskelet/tags` | `srcset` build manifest'inden |
69
+ | `next/font/google` | `fonts: [{ family, weights }]` | Self-host woff2, commit edilir |
70
+ | `@phosphor-icons/react` | `icon({ name, weight })` — `jskelet/tags` | Build zamanı SVG sprite |
71
+ | `react-dom` preconnect/preload | `preconnect: [...]` + `headHints()` | ([04](./04-render-ve-sablonlar.md)) |
72
+ | `clsx` | `cx()` — `jskelet/html` | — |
73
+ | `cn()` (clsx + tailwind-merge) | `cn()` — `jskelet/html` | Aynı davranış |
74
+ | JSX otomatik kaçışı | `esc()` — `jskelet/html` | **Elle çağırmanız gerekir** |
75
+ | React Context | `createStore()` — `jskelet/client` | Minimal pub/sub |
76
+ | `useState` / `useEffect` | Island `mount()` içinde düz JS | — |
77
+ | `useSyncExternalStore` | `store.subscribe()` | — |
78
+ | `<Script>` | Layout'ta `<script>` ya da island | — |
79
+
80
+ ### Neyin karşılığı yok
81
+
82
+ Bunları taşıma planında baştan hesaba katın:
83
+
84
+ - **React'in kendisi.** Bileşenler HTML string döndüren fonksiyonlara dönüşür.
85
+ JSX yok, hook yok, sanal DOM yok.
86
+ - **TypeScript.** Proje düz JS + JSDoc. `jsconfig.json` içinde `checkJs: true`
87
+ ile editörden tip kontrolü alırsınız.
88
+ - **İç içe layout'lar.** Tek bir layout var; ortak bölümleri EJS `include` ya da
89
+ bileşen fonksiyonlarıyla paylaşırsınız.
90
+ - **Streaming / Suspense / kısmi prerender.** Yanıt tek parça üretilir.
91
+ - **İstemci tarafı yönlendirme.** Gezinme gerçek sayfa yüklemesidir. Sunucu HTML'i
92
+ önbellekten geldiği için pratikte çok hızlıdır, ama SPA geçişleri yoktur.
93
+ Aradaki farkı kapatan şey `navigation` bölümü: prefetch/prerender tıklamadan
94
+ önce belgeyi hazırlar, `viewTransition` geçişi yumuşatır
95
+ ([07](./07-yapilandirma.md)).
96
+ - **Server Actions.** Form gönderimleri normal `app.post(...)` handler'larıdır.
97
+ - **Tek tek yol geçersizleme (`revalidatePath`).** Şimdilik tüm önbelleği
98
+ temizlemek (`clearHtmlCache()`) ya da TTL'in dolmasını beklemek var.
99
+ - **Otomatik görsel optimizasyonu (istek anında).** Optimizasyon build zamanında
100
+ yapılır ve yalnızca `public/` altındaki yerel görselleri kapsar; uzak görseller
101
+ olduğu gibi basılır.
102
+
103
+ ## Yan yana örnek
104
+
105
+ **Next.js (App Router):**
106
+
107
+ ```jsx
108
+ // app/haber/[slug]/page.jsx
109
+ import { notFound } from "next/navigation";
110
+ import Image from "next/image";
111
+ import { getArticle } from "@/lib/api";
112
+
113
+ export const revalidate = 300;
114
+
115
+ export async function generateMetadata({ params }) {
116
+ const article = await getArticle(params.slug);
117
+ return {
118
+ title: article?.title,
119
+ description: article?.summary,
120
+ alternates: { canonical: `/haber/${params.slug}` },
121
+ };
122
+ }
123
+
124
+ export default async function Page({ params }) {
125
+ const article = await getArticle(params.slug);
126
+ if (!article) notFound();
127
+
128
+ return (
129
+ <article className="wrapper">
130
+ <h1 className="text-3xl font-bold">{article.title}</h1>
131
+ <Image src={article.cover} alt={article.title} priority width={1200} height={630} />
132
+ <div dangerouslySetInnerHTML={{ __html: article.body }} />
133
+ </article>
134
+ );
135
+ }
136
+ ```
137
+
138
+ **JSkelet:**
139
+
140
+ ```js
141
+ // routes/50-haber.mjs
142
+ import { getArticle } from "@/lib/api.js";
143
+
144
+ export default function register(app, { route, notFound }) {
145
+ app.get(
146
+ "/haber/:slug",
147
+ route(
148
+ async ({ params }) => {
149
+ const article = await getArticle(params.slug);
150
+ if (!article) notFound();
151
+
152
+ return {
153
+ view: "pages/article",
154
+ data: { article },
155
+ metadata: {
156
+ title: article.title,
157
+ description: article.summary,
158
+ canonical: `/haber/${params.slug}`,
159
+ openGraph: { type: "article", image: article.cover },
160
+ },
161
+ };
162
+ },
163
+ { revalidate: 300 },
164
+ ),
165
+ );
166
+ }
167
+ ```
168
+
169
+ ```ejs
170
+ <%# views/pages/article.ejs %>
171
+ <article class="wrapper">
172
+ <h1 class="text-3xl font-bold"><%= article.title %></h1>
173
+ <%- image({ src: article.cover, alt: article.title, priority: true, width: 1200, height: 630 }) %>
174
+ <div><%- article.body %></div>
175
+ </article>
176
+ ```
177
+
178
+ `getArticle`'ın `cache()` ile sarılması, aynı render'da `hooks.layoutContext()`
179
+ de aynı yazıyı isterse tek upstream isteği yapılmasını sağlar
180
+ ([06-cache.md](./06-cache.md)).
181
+
182
+ ## Adım adım plan
183
+
184
+ ### 1. İskeleti kur (yarım gün)
185
+
186
+ Yeni bir dizinde `npx jskelet init` çalıştırın ve `jskelet dev`in açıldığını
187
+ görün. Mevcut Next projesini olduğu gibi bırakın; taşıma paralel yürüsün.
188
+
189
+ `jsconfig.json` içindeki `paths` alias'larınızı taşıyın — `@/` gibi önekler hem
190
+ sunucuda hem bundle'da aynı şekilde çalışır ([02-mimari.md](./02-mimari.md)).
191
+
192
+ ### 2. `next.config.mjs`'i çevir (1-2 saat)
193
+
194
+ `headers()`, `redirects()` ve `rewrites()` bölümleri neredeyse birebir kopyalanır.
195
+ Desen sözdizimini kontrol edin: JSkelet `:slug`, `:path*`, `/a-:b` ve
196
+ `/:path*.svg` biçimlerini destekler; daha karmaşık `path-to-regexp` ifadeleri
197
+ desteklenmez ve uyarı üretir ([07-yapilandirma.md](./07-yapilandirma.md)).
198
+
199
+ `NEXT_PUBLIC_*` değişkenlerini `clientEnv` listesine taşıyın ve adlarını
200
+ sadeleştirin (ön ek artık anlam taşımıyor).
201
+
202
+ ### 3. Veri katmanını taşı (en kolay adım)
203
+
204
+ `lib/` altındaki API istemcisi ve veri fonksiyonları genelde React'e bağımlı
205
+ değildir; olduğu gibi kopyalanır. İki değişiklik yapın:
206
+
207
+ - React `cache()` yerine `import { cache } from "jskelet"`.
208
+ - Başarısız upstream yanıtlarında `reportUpstreamFailure({ status, path })`
209
+ çağırın. Bu, eksik veriyle üretilmiş sayfaların önbelleğe yazılmasını önler
210
+ ([06-cache.md](./06-cache.md)).
211
+
212
+ ### 4. Layout'u kur (yarım gün)
213
+
214
+ `app/layout.jsx`'i `views/layout.ejs`'e çevirin. Framework'ün varsayılan
215
+ layout'unu (`node_modules/jskelet/src/templates/layout.ejs`) kopyalayıp
216
+ üzerine yazmak en hızlı yol.
217
+
218
+ `layout.jsx` içinde veri çekiyorsanız (navigasyon, site ayarları) bunu
219
+ `hooks.layoutContext()` içine taşıyın: gövde render'ıyla paralel çalışır ve
220
+ döndürdüğü her alan layout local'i olur.
221
+
222
+ Global metadata varsayılanlarını (`titleTemplate`, `siteUrl`, `description`)
223
+ `hooks.metadata()` içine koyun.
224
+
225
+ ### 5. Bileşenleri çevir (en uzun adım)
226
+
227
+ Her React bileşeni bir fonksiyona dönüşür:
228
+
229
+ ```jsx
230
+ // Önce
231
+ export function Badge({ label, tone = "neutral", className }) {
232
+ return <span className={cn("rounded px-2 py-1", TONES[tone], className)}>{label}</span>;
233
+ }
234
+ ```
235
+
236
+ ```js
237
+ // Sonra — views/components/badge.js
238
+ import { attrs, cn, esc } from "jskelet/html";
239
+
240
+ export function badge({ label, tone = "neutral", class: className }) {
241
+ return `<span${attrs({ class: cn("rounded px-2 py-1", TONES[tone], className) })}>${esc(label)}</span>`;
242
+ }
243
+ ```
244
+
245
+ Dikkat edilecekler:
246
+
247
+ - **Kaçış artık elinizde.** JSX otomatik kaçıyordu; burada dış veriyi basarken
248
+ `esc()` çağırmak zorundasınız.
249
+ - **`className` → `class`.** JS'te `class` ayrılmış sözcük olduğu için props'ta
250
+ `class: className` biçiminde yeniden adlandırın.
251
+ - **Children yerine `html` alanı.** İç içe içerik string olarak geçirilir.
252
+ - Named export olarak `views/components/**` altına koyduğunuz her fonksiyon
253
+ şablonlarda import gerektirmeden kullanılabilir
254
+ ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
255
+
256
+ Bileşenleri küçük ve saf tutun; veri çekmeyi controller'da bırakın.
257
+
258
+ ### 6. Sayfaları taşı (sayfa başına saatler)
259
+
260
+ Her `page.jsx` bir controller + bir EJS şablonuna bölünür. Sırayı düşünerek
261
+ dosyalayın:
262
+
263
+ ```
264
+ routes/
265
+ ├── 00-health.mjs sağlık kontrolü
266
+ ├── 10-pages.mjs sabit yollar: /, /hakkinda
267
+ ├── 50-haber.mjs /haber/:slug
268
+ └── 99-catch-all.mjs /:slug (varsa, en sonda)
269
+ ```
270
+
271
+ `generateStaticParams()` kullandığınız yerler `hooks.prewarmPaths()`e dönüşür.
272
+ Sitemap üreten fonksiyonunuz varsa aynısını kullanın.
273
+
274
+ `export const revalidate` değerlerini ya `route()`un ikinci argümanına ya da tek
275
+ bir yerden yönetmek için `cache().html` desenlerine taşıyın.
276
+
277
+ ### 7. İstemci bileşenlerini island'a çevir (sayfa başına saatler)
278
+
279
+ Her `"use client"` bileşeni bir island olur. Süreç:
280
+
281
+ 1. Bileşenin **statik** çıktısını sunucu şablonuna taşıyın. İlk render'da
282
+ görünen her şey HTML'de olmalı.
283
+ 2. Kalan davranışı `mount(element, props)` içine yazın: `useState` yerel
284
+ değişken, `useEffect` doğrudan çağrı, event handler'lar `on()`/`onClick()`.
285
+ 3. Props'u `data-island-props` ile JSON olarak geçirin.
286
+ 4. Entry'de `registerAll()` haritasına ekleyin.
287
+ 5. Bağlanma stratejisini seçin: varsayılan (görünürlük), `data-island-eager`
288
+ (global davranış) ya da `data-island-idle` (ağır ve kritik olmayan).
289
+
290
+ Context kullanan bileşenler için `createStore()` en yakın karşılık
291
+ ([05-islands.md](./05-islands.md)).
292
+
293
+ **Bu adımın en büyük kazancı burada:** hidre edilen alan sayfanın tamamı değil,
294
+ yalnızca gerçekten etkileşimli parçalar.
295
+
296
+ ### 8. CSS'i taşı (1-2 saat)
297
+
298
+ Tailwind yapılandırmanız v4 formatındaysa `styles/globals.css` neredeyse aynı
299
+ kalır. Tek kritik ekleme `@source` direktifleri:
300
+
301
+ ```css
302
+ @import "tailwindcss" source(none);
303
+
304
+ @source "../views";
305
+ @source "../client";
306
+ @source "../routes";
307
+ @source "../lib";
308
+ ```
309
+
310
+ Bunlar olmadan şablonlarda geçen sınıflar (özellikle
311
+ `data-[state=open]:…` gibi varyantlar) sessizce düşer
312
+ ([08-build.md](./08-build.md)).
313
+
314
+ `next/font` kullanıyorsanız `fonts: [{ family, weights }]` ekleyin ve
315
+ `@font-face` bloklarını elle yazın; üretilen dosyalar `public/fonts/` altında
316
+ sabit isimlerle durur.
317
+
318
+ ### 9. Doğrula ve ölç
319
+
320
+ - `jskelet dev` ile gezip dev overlay'inde hata olmadığını doğrulayın.
321
+ - Rapor sayfasında (`/__jskelet/dev/report`) her sayfanın Web Vitals ölçümlerine,
322
+ SSR boyutuna ve island durumuna bakın ([09-dev-araclari.md](./09-dev-araclari.md)).
323
+ - Eksik ikon uyarılarını temizleyin.
324
+ - `jskelet build` çıktısındaki boyutları eski Next bundle'ıyla karşılaştırın.
325
+ - `X-JSkelet-Cache` başlığının beklediğiniz sayfalarda `HIT` döndüğünü kontrol
326
+ edin.
327
+
328
+ ### 10. Yayına al
329
+
330
+ [10-dagitim.md](./10-dagitim.md) içindeki kontrol listesini geçin. Eski Next
331
+ kurulumunu bir süre yanında tutup trafiği kademeli çevirmek, özellikle
332
+ redirect kurallarının doğruluğunu ölçmek için işe yarar.
333
+
334
+ ## Taşıma sırasında sık yapılan hatalar
335
+
336
+ - **`esc()` unutmak.** JSX'ten gelen alışkanlıkla `${value}` yazmak XSS demektir.
337
+ Şablonlarda `<%= %>` (kaçışlı) ile `<%- %>` (ham) ayrımına dikkat edin.
338
+ - **`@source` eklemeden yeni bir dizin açmak.** Sınıflar sessizce düşer.
339
+ - **Yakalayıcı route'u yanlış sıraya koymak.** `/:slug` her zaman en sonda.
340
+ - **Sayfanın tamamını island yapmak.** Kazanç sunucu HTML'inin tam olmasından
341
+ geliyor; island'ı yalnızca gerçekten etkileşimli parçaya bağlayın.
342
+ - **`revalidate` vermeyi unutmak.** Önbellek kapalı kalır, her istek render
343
+ edilir ve `X-JSkelet-Cache: MISS` döner.
344
+ - **`reportUpstreamFailure()` çağırmamak.** Upstream düştüğünde eksik veriyle
345
+ üretilen sayfa tüm TTL boyunca servis edilir.
346
+ - **`clientEnv`e gizli anahtar koymak.** Değerler bundle'da düz metin durur.
347
+
348
+ ## Sırada ne var
349
+
350
+ - Mimari kararların gerekçeleri: [02-mimari.md](./02-mimari.md)
351
+ - Island modelinin ayrıntıları: [05-islands.md](./05-islands.md)
352
+ - Yapılandırma referansı: [07-yapilandirma.md](./07-yapilandirma.md)