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.
- package/AGENTS.md +127 -0
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +342 -0
- package/bin/jskelet.mjs +104 -0
- package/docs/01-baslangic.md +285 -0
- package/docs/02-mimari.md +287 -0
- package/docs/03-routing.md +437 -0
- package/docs/04-render-ve-sablonlar.md +490 -0
- package/docs/05-islands.md +429 -0
- package/docs/06-cache.md +409 -0
- package/docs/07-yapilandirma.md +673 -0
- package/docs/08-build.md +366 -0
- package/docs/09-dev-araclari.md +302 -0
- package/docs/10-dagitim.md +329 -0
- package/docs/11-tasima.md +352 -0
- package/docs/README.md +82 -0
- package/package.json +97 -0
- package/src/build/build.mjs +138 -0
- package/src/build/ensure-build.mjs +15 -0
- package/src/build/paths.mjs +118 -0
- package/src/build/resolve-peer.mjs +36 -0
- package/src/build/tasks/client.mjs +268 -0
- package/src/build/tasks/css.mjs +124 -0
- package/src/build/tasks/fonts.mjs +146 -0
- package/src/build/tasks/icons.mjs +224 -0
- package/src/build/tasks/images.mjs +244 -0
- package/src/build/tasks/precompress.mjs +78 -0
- package/src/client/devtools/overlay.js +1763 -0
- package/src/client/devtools/report.html +185 -0
- package/src/client/devtools/report.js +712 -0
- package/src/client/dom.js +95 -0
- package/src/client/index.js +26 -0
- package/src/client/registry.js +223 -0
- package/src/client/safe-image.js +91 -0
- package/src/client/store.js +36 -0
- package/src/config/defaults.js +102 -0
- package/src/config/index.js +433 -0
- package/src/config/pattern.js +107 -0
- package/src/dev-server.mjs +383 -0
- package/src/http/control-flow.js +56 -0
- package/src/http/request-cache.js +46 -0
- package/src/index.js +35 -0
- package/src/init.mjs +220 -0
- package/src/log.mjs +332 -0
- package/src/logo.png +0 -0
- package/src/runtime/alias-hooks.mjs +119 -0
- package/src/runtime/register.mjs +4 -0
- package/src/server/assets.js +119 -0
- package/src/server/create-app.js +167 -0
- package/src/server/dev/devtools.js +383 -0
- package/src/server/dev/report.js +351 -0
- package/src/server/head-hints.js +132 -0
- package/src/server/html-cache.js +166 -0
- package/src/server/metadata.js +102 -0
- package/src/server/middleware/compression.js +205 -0
- package/src/server/middleware/dev-gate.js +62 -0
- package/src/server/middleware/headers.js +37 -0
- package/src/server/middleware/redirects.js +32 -0
- package/src/server/middleware/static-precompressed.js +100 -0
- package/src/server/middleware/upstream-proxy.js +141 -0
- package/src/server/prewarm.js +283 -0
- package/src/server/render.js +356 -0
- package/src/server/router.js +121 -0
- package/src/server/status-page.js +164 -0
- package/src/server/upstream-tracking.js +51 -0
- package/src/start.mjs +7 -0
- package/src/templates/layout.ejs +44 -0
- package/src/version.mjs +17 -0
- package/src/views/components/loader.js +85 -0
- package/src/views/helpers/html.js +102 -0
- 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)
|