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,285 @@
|
|
|
1
|
+
# 01 — Başlangıç
|
|
2
|
+
|
|
3
|
+
Bu belge JSkelet'i sıfırdan çalıştırmayı anlatır: paket kurulumu, `jskelet init`
|
|
4
|
+
ile iskeletin oluşturulması, ilk route ve ilk island'ın yazılması, oluşan dizin
|
|
5
|
+
yapısının ne anlama geldiği ve CLI'ın dört komutu. Sonunda tarayıcıda sunucuda
|
|
6
|
+
render edilmiş, önbelleğe alınmış ve island'ı görünürlükte hidre olan bir sayfa
|
|
7
|
+
olacak. Kararların *nedenleri* için [02-mimari.md](./02-mimari.md)'ye, buradaki
|
|
8
|
+
her config alanının tam referansı için
|
|
9
|
+
[07-yapilandirma.md](./07-yapilandirma.md)'ye bakın.
|
|
10
|
+
|
|
11
|
+
## Gereksinimler
|
|
12
|
+
|
|
13
|
+
- **Node.js 22 veya üstü.** `package.json` → `engines` bunu zorunlu tutuyor.
|
|
14
|
+
Framework `node:async_hooks`, `fs.readdirSync(..., { recursive: true })`,
|
|
15
|
+
`--env-file-if-exists` ve `module.register()` gibi yeni Node yüzeylerini
|
|
16
|
+
doğrudan kullanıyor.
|
|
17
|
+
- Tailwind CSS kullanacaksanız `postcss`, `@tailwindcss/postcss` ve
|
|
18
|
+
`tailwindcss` paketleri. Bunlar framework'ün **opsiyonel peer
|
|
19
|
+
bağımlılıkları**dır; kurulu değilse CSS adımı atlanır ve site stilsiz ama
|
|
20
|
+
çalışır durumda kalır (ayrıntı: [08-build.md](./08-build.md)).
|
|
21
|
+
|
|
22
|
+
## Kurulum
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
mkdir benim-sitem && cd benim-sitem
|
|
26
|
+
npm init -y
|
|
27
|
+
npm pkg set type=module
|
|
28
|
+
npm install jskelet
|
|
29
|
+
npm install -D postcss @tailwindcss/postcss tailwindcss lightningcss
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`type: "module"` şart: route modülleri, bileşenler ve config dosyası ESM olarak
|
|
33
|
+
yüklenir.
|
|
34
|
+
|
|
35
|
+
Ardından `package.json` içine script'leri ekleyin:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "jskelet dev",
|
|
41
|
+
"build": "jskelet build",
|
|
42
|
+
"start": "jskelet start"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## `jskelet init`
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx jskelet init
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Bu komut bulunduğunuz dizine çalışan bir minimum iskelet kurar. **Var olan
|
|
54
|
+
dosyaların üzerine yazmaz**: ikinci kez çalıştırmak yalnızca eksikleri
|
|
55
|
+
tamamlar, atlanan dosyaların sayısını uyarı olarak basar. Amaç, "kurulumu
|
|
56
|
+
yaptım ama hiçbir şey çalışmıyor" aşamasını tamamen atlamak — `jskelet dev`
|
|
57
|
+
hemen ardından çalışır.
|
|
58
|
+
|
|
59
|
+
Oluşturulan dosyalar:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
jskelet.config.mjs config: brand, preconnect, cache(), hooks
|
|
63
|
+
routes/10-pages.mjs "/" route'u
|
|
64
|
+
views/pages/home.ejs ana sayfa şablonu
|
|
65
|
+
views/pages/not-found.ejs 404 şablonu
|
|
66
|
+
views/components/button.js örnek bileşen (HTML string döndüren fonksiyon)
|
|
67
|
+
client/entries/main.js island bootstrap'ı
|
|
68
|
+
client/islands/counter.js örnek island
|
|
69
|
+
styles/globals.css Tailwind girişi + @source direktifleri
|
|
70
|
+
jsconfig.json checkJs + "@/*" alias'ı
|
|
71
|
+
.gitignore node_modules/, .jskelet/, public/assets/, .env
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Sonra:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm run dev
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Terminalde banner, hizalı build satırları ve bir `Ready` özeti görürsünüz;
|
|
81
|
+
`http://localhost:3000` sayfayı verir. Sağ altta dev overlay baloncuğu durur,
|
|
82
|
+
`Alt+D` ile açılır ([09-dev-araclari.md](./09-dev-araclari.md)).
|
|
83
|
+
|
|
84
|
+
## Dizin yapısı
|
|
85
|
+
|
|
86
|
+
Dizin adlarının hiçbiri sabit değildir; hepsi `jskelet.config.mjs` → `paths`
|
|
87
|
+
ile ezilebilir. Aşağıdaki değerler varsayılanlardır (`src/config/defaults.js`).
|
|
88
|
+
|
|
89
|
+
| Dizin | Varsayılan | İçeriği |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `views` | `views` | EJS layout, sayfalar ve bileşenler |
|
|
92
|
+
| `public` | `public` | Statik dosyalar; build çıktısı da buraya yazılır |
|
|
93
|
+
| `client` | `client` | Island runtime kaynakları ve entry'ler |
|
|
94
|
+
| `routes` | `routes` | Route modülleri |
|
|
95
|
+
| `styles` | `styles/globals.css` | Tailwind/PostCSS giriş **dosyası** |
|
|
96
|
+
| `generated` | `.jskelet` | Build ara çıktıları: `manifest.json`, `metafile.json`, `images.json` |
|
|
97
|
+
|
|
98
|
+
Bunlara ek olarak framework iki yolu her zaman türetir ve ayrı ayar kabul
|
|
99
|
+
etmez: `public/assets` (hash'li build çıktısı) ve `public/fonts` (self-host
|
|
100
|
+
fontlar).
|
|
101
|
+
|
|
102
|
+
Tipik bir proje:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
benim-sitem/
|
|
106
|
+
├── jskelet.config.mjs
|
|
107
|
+
├── jsconfig.json
|
|
108
|
+
├── routes/
|
|
109
|
+
│ ├── 10-pages.mjs
|
|
110
|
+
│ └── 90-catch-all.mjs
|
|
111
|
+
├── views/
|
|
112
|
+
│ ├── layout.ejs
|
|
113
|
+
│ ├── pages/
|
|
114
|
+
│ │ ├── home.ejs
|
|
115
|
+
│ │ └── not-found.ejs
|
|
116
|
+
│ └── components/
|
|
117
|
+
│ └── card.js
|
|
118
|
+
├── client/
|
|
119
|
+
│ ├── entries/
|
|
120
|
+
│ │ └── main.js
|
|
121
|
+
│ └── islands/
|
|
122
|
+
│ └── counter.js
|
|
123
|
+
├── styles/
|
|
124
|
+
│ └── globals.css
|
|
125
|
+
├── public/
|
|
126
|
+
│ └── (statik dosyalar; build → public/assets)
|
|
127
|
+
└── .jskelet/
|
|
128
|
+
└── manifest.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## İlk route
|
|
132
|
+
|
|
133
|
+
Route modülleri **dosya sistemine dayalı otomatik URL türetmez**; her modül
|
|
134
|
+
kendi yollarını `app.get(...)` ile açıkça yazar. Modül sözleşmesi: default
|
|
135
|
+
export ya da `register` adlı named export, `(app, api)` imzasıyla.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
// routes/10-pages.mjs
|
|
139
|
+
export default function register(app, { route }) {
|
|
140
|
+
app.get(
|
|
141
|
+
"/",
|
|
142
|
+
route(
|
|
143
|
+
async () => ({
|
|
144
|
+
view: "pages/home",
|
|
145
|
+
metadata: { title: "Ana sayfa" },
|
|
146
|
+
data: { heading: "JSkelet çalışıyor", items: ["Bir", "İki"] },
|
|
147
|
+
}),
|
|
148
|
+
{ revalidate: 60 },
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`api` nesnesi içinde `route`, `renderView`, `renderPage`, `notFound`, `redirect`
|
|
155
|
+
ve `permanentRedirect` hazır gelir; route dosyaları framework'ten tek tek import
|
|
156
|
+
yapmak zorunda kalmaz. `route()` controller'ı sarar: HTML cache'i,
|
|
157
|
+
notFound/redirect kontrol akışı, sıkıştırma ve `X-JSkelet-Cache` başlığı ondan
|
|
158
|
+
gelir. Controller'ın tek işi bir sayfa tanımı döndürmektir.
|
|
159
|
+
|
|
160
|
+
Dosya adındaki `10-` öneki yükleme sırasını belirler. `routes/` alfabetik
|
|
161
|
+
tarandığı için `/:slug` gibi yakalayıcı route'ları daha yüksek numaralı bir
|
|
162
|
+
dosyaya koymalısınız; aksi hâlde `/hakkinda` bir slug sanılır. Ayrıntı:
|
|
163
|
+
[03-routing.md](./03-routing.md).
|
|
164
|
+
|
|
165
|
+
Şablon tarafı düz EJS:
|
|
166
|
+
|
|
167
|
+
```ejs
|
|
168
|
+
<%# views/pages/home.ejs %>
|
|
169
|
+
<section class="wrapper">
|
|
170
|
+
<h1 class="text-3xl font-bold"><%= heading %></h1>
|
|
171
|
+
<%- list({ items }) %>
|
|
172
|
+
<div data-island="counter" data-island-props='{"start":5}'></div>
|
|
173
|
+
</section>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`list` burada `views/components/list.js` içinde tanımlı bir fonksiyondur ve
|
|
177
|
+
import edilmemiştir: `views/components/**` altındaki her named export otomatik
|
|
178
|
+
olarak şablon local'i olur ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
|
|
179
|
+
|
|
180
|
+
## İlk island
|
|
181
|
+
|
|
182
|
+
Island, sunucunun ürettiği HTML'e davranış ekleyen küçük bir modüldür. Sözleşme
|
|
183
|
+
iki parçadan oluşur.
|
|
184
|
+
|
|
185
|
+
**1. Şablonda işaret:** bir elemente `data-island="ad"` verin. Props JSON olarak
|
|
186
|
+
`data-island-props` içinde taşınır.
|
|
187
|
+
|
|
188
|
+
```ejs
|
|
189
|
+
<div data-island="counter" data-island-props='{"start":5}'></div>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**2. Modülde `mount`:** island `mount(element, props)` adlı bir named export
|
|
193
|
+
verir.
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
// client/islands/counter.js
|
|
197
|
+
/**
|
|
198
|
+
* @param {HTMLElement} element
|
|
199
|
+
* @param {{ start?: number }} props
|
|
200
|
+
*/
|
|
201
|
+
export function mount(element, props) {
|
|
202
|
+
let value = props.start ?? 0;
|
|
203
|
+
|
|
204
|
+
const button = document.createElement("button");
|
|
205
|
+
button.type = "button";
|
|
206
|
+
|
|
207
|
+
const paint = () => {
|
|
208
|
+
button.textContent = `Tıklama: ${value}`;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
button.addEventListener("click", () => {
|
|
212
|
+
value += 1;
|
|
213
|
+
paint();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
paint();
|
|
217
|
+
element.append(button);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**3. Kayıt:** `client/entries/main.js` island adını dinamik import'a bağlar ve
|
|
222
|
+
runtime'ı başlatır.
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
import { registerAll, start } from "jskelet/client";
|
|
226
|
+
|
|
227
|
+
registerAll({
|
|
228
|
+
counter: () => import("../islands/counter.js"),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
start();
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Değerlerin dinamik import olması kritik: modül yalnızca sayfada o island
|
|
235
|
+
gerçekten varsa **ve** element görünür hâle geldiğinde indirilir. Yani bu
|
|
236
|
+
haritayı büyütmek ilk yükü büyütmez. Hidrasyon stratejileri
|
|
237
|
+
(`data-island-eager`, `data-island-idle`) ve runtime API'sinin tamamı
|
|
238
|
+
[05-islands.md](./05-islands.md)'de.
|
|
239
|
+
|
|
240
|
+
## CLI komutları
|
|
241
|
+
|
|
242
|
+
`bin/jskelet.mjs` dört alt komut sunar. Her biri ayrı bir Node sürecinde
|
|
243
|
+
çalışır; sebebi `dev`in iki uzun ömürlü süreci yönetmesi ve sunucunun ESM
|
|
244
|
+
resolve hook'larına (`--import`) süreç başlangıcında ihtiyaç duyması.
|
|
245
|
+
|
|
246
|
+
| Komut | Ne yapar |
|
|
247
|
+
| --- | --- |
|
|
248
|
+
| `jskelet dev` | Build watch + sunucu, tek terminalde. Canlı yenileme, CSS hot-swap, dev overlay. `NODE_ENV=development`. |
|
|
249
|
+
| `jskelet build` | Tek seferlik prod build: fontlar → ikon sprite → CSS → client JS → görseller → manifest → precompress. `NODE_ENV` verilmemişse `production`. |
|
|
250
|
+
| `jskelet start` | Prod sunucu. Build çıktısı yoksa önce üretir. `NODE_ENV` verilmemişse `production`. |
|
|
251
|
+
| `jskelet init` | Bulunduğun dizine minimal iskelet kurar; var olan dosyalara dokunmaz. |
|
|
252
|
+
|
|
253
|
+
Bilinmeyen bir komut ya da argümansız çağrı kullanım metnini basar.
|
|
254
|
+
|
|
255
|
+
Her komut iki Node bayrağıyla çalışır:
|
|
256
|
+
|
|
257
|
+
- `--env-file=.env` — yalnızca dosya gerçekten varsa geçilir; yoksa hiçbir
|
|
258
|
+
bayrak eklenmez ve uyarı basılmaz.
|
|
259
|
+
- `--import <register.mjs>` — `jsconfig.json` / `tsconfig.json` içindeki
|
|
260
|
+
`compilerOptions.paths` alias'larını (`@/lib/x`) ve uzantısız göreli
|
|
261
|
+
import'ları (`./cache` → `./cache.js`) çözen ESM hook'larını kurar.
|
|
262
|
+
(`jskelet dev` bu hook'ları kendi alt süreçlerinde kurar, dış süreçte kurmaz.)
|
|
263
|
+
|
|
264
|
+
## İthal yolları
|
|
265
|
+
|
|
266
|
+
`package.json` → `exports` haritası kararlı yüzeyi tanımlar. Örneklerde
|
|
267
|
+
yalnızca bu belirteçleri kullanın:
|
|
268
|
+
|
|
269
|
+
| Belirteç | İçeriği |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| `jskelet` | Sunucu API'si: `route`, `renderPage`, `renderView`, `renderNotFound`, `createApp`, `startServer`, `notFound`, `redirect`, `permanentRedirect`, `cache`, `withRequestCache`, `reportUpstreamFailure`, `asset`, `hasAsset`, `optimizedImage`, `getSpriteIds`, `headHints`, `renderHeadMeta`, HTML cache fonksiyonları, `prewarm`, `createProxy`, `getConfig`, `loadConfig` ve html/tag yardımcıları |
|
|
272
|
+
| `jskelet/server` | `jskelet` ile aynı modül (okunurluk için takma ad) |
|
|
273
|
+
| `jskelet/client` | Tarayıcı runtime'ı: `register`, `registerAll`, `hydrate`, `observeDocument`, `start`, `createStore`, DOM yardımcıları, `startSafeImages` |
|
|
274
|
+
| `jskelet/html` | `esc`, `attrs`, `cx`, `cn`, `jsonScript` |
|
|
275
|
+
| `jskelet/tags` | `link`, `image`, `icon`, `preloadImage`, `toKebab` |
|
|
276
|
+
| `jskelet/log` | Konsol çıktısı yardımcıları (`banner`, `event`, `task`, `size`, `ms`, …) |
|
|
277
|
+
| `jskelet/register` | `node --import jskelet/register` ile alias + uzantı hook'ları |
|
|
278
|
+
| `jskelet/layout` | Framework'ün varsayılan `layout.ejs` dosyasının yolu |
|
|
279
|
+
|
|
280
|
+
## Sırada ne var
|
|
281
|
+
|
|
282
|
+
- Neden bu şekilde çalışıyor: [02-mimari.md](./02-mimari.md)
|
|
283
|
+
- Daha fazla route ve yakalayıcı desenler: [03-routing.md](./03-routing.md)
|
|
284
|
+
- Layout'u devralmak ve metadata: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
|
|
285
|
+
- Önbelleği ayarlamak: [06-cache.md](./06-cache.md)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# 02 — Mimari ve kararların gerekçeleri
|
|
2
|
+
|
|
3
|
+
Bu belge JSkelet'in nasıl çalıştığını değil, **neden böyle çalıştığını**
|
|
4
|
+
anlatır. Bir isteğin sunucudan tarayıcıya kadar izlediği yol, island modelinin
|
|
5
|
+
neden görünürlüğe bağlı olduğu, HTML'in neden tam üretildiği, önbelleğin neden
|
|
6
|
+
süreç belleğinde durduğu ve middleware sırasının neden yer değiştirmemesi
|
|
7
|
+
gerektiği burada. Gerekçelerin çoğu kaynak dosyaların başlıklarındaki ölçüm
|
|
8
|
+
notlarından geliyor; API'lerin kendisi için [03](./03-routing.md),
|
|
9
|
+
[04](./04-render-ve-sablonlar.md), [05](./05-islands.md) ve
|
|
10
|
+
[06](./06-cache.md) numaralı belgelere bakın.
|
|
11
|
+
|
|
12
|
+
## Temel önerme
|
|
13
|
+
|
|
14
|
+
Bir haber ya da içerik sitesinde ziyaretçinin gördüğü şeyin neredeyse tamamı
|
|
15
|
+
sunucuda hazırdır. Etkileşim ise nokta nokta dağılmıştır: bir arama kutusu, bir
|
|
16
|
+
drawer, bir grafik, bir yorum formu. Bu profilde tüm sayfayı istemcide yeniden
|
|
17
|
+
kurmak (hidrasyon) ödediğiniz en büyük maliyettir ve karşılığında ziyaretçi
|
|
18
|
+
hiçbir şey kazanmaz.
|
|
19
|
+
|
|
20
|
+
JSkelet bu gözlemi mimarinin merkezine alır:
|
|
21
|
+
|
|
22
|
+
1. **Sunucu HTML'i tamdır.** JS çalışmasa bile sayfa okunur, gezilebilir ve
|
|
23
|
+
indekslenebilir.
|
|
24
|
+
2. **JS yalnızca davranış ekler.** Her etkileşimli parça bağımsız bir "island"
|
|
25
|
+
olarak, kendi modülüyle, kendi zamanında bağlanır.
|
|
26
|
+
3. **Sayfa üretimi önbelleklenir.** Aynı HTML'i her istekte yeniden üretmenin
|
|
27
|
+
anlamı yok; TTL'li bir bellek önbelleği ISR'nin yerini tutar.
|
|
28
|
+
|
|
29
|
+
## Bir isteğin yolu
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
İstek
|
|
33
|
+
├─ rewrites(beforeFiles) config → proxy ya da req.url değişimi
|
|
34
|
+
├─ compression brotli/gzip pazarlığı (kalite 5)
|
|
35
|
+
├─ headers statik cache + config headers()
|
|
36
|
+
├─ devGate DEV_TOKEN varsa token yoksa 404
|
|
37
|
+
├─ redirects config redirects(), ilk eşleşen kazanır
|
|
38
|
+
├─ staticPrecompressed build'de üretilmiş .br/.gz kopyalar (kalite 11)
|
|
39
|
+
├─ express.static public/ altındaki dosyalar
|
|
40
|
+
├─ (dev) devtools yalnızca NODE_ENV=development
|
|
41
|
+
├─ body parser'lar urlencoded 64kb + json 256kb
|
|
42
|
+
├─ rewrites(afterFiles) statik denendikten sonra
|
|
43
|
+
├─ route'lar
|
|
44
|
+
│ └─ route(controller)
|
|
45
|
+
│ └─ withHtmlCache TTL + stale-while-revalidate
|
|
46
|
+
│ └─ withUpstreamTracking
|
|
47
|
+
│ └─ withRequestCache
|
|
48
|
+
│ └─ controller → renderPage → EJS
|
|
49
|
+
├─ 404 → hooks.notFound()
|
|
50
|
+
└─ hata yönetimi redirect/notFound + 500 fallback
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Middleware sırası neden bu sıra
|
|
54
|
+
|
|
55
|
+
`src/server/create-app.js` dosyasının asıl değeri sıradır; her konumun bir
|
|
56
|
+
sebebi var ve yer değiştirmek sessiz bozulmalara yol açıyor.
|
|
57
|
+
|
|
58
|
+
- **`rewrites(beforeFiles)` statik dosyalardan da önce.** Aksi hâlde
|
|
59
|
+
`/assets/x.js` yolunu başka bir yere taşıyan bir kural hiç işlemez, çünkü
|
|
60
|
+
`express.static` isteği önce yanıtlar.
|
|
61
|
+
- **`compression`, static'ten önce.** Sonra gelirse statik dosyalar hiç
|
|
62
|
+
sıkışmaz.
|
|
63
|
+
- **`headers` → `devGate` → `redirects`.** Gate'in 404'ü redirect'ten önce
|
|
64
|
+
gelmeli: yayına açılmamış bir ortam, yönlendirme kurallarını bile dışarıya
|
|
65
|
+
sızdırmamalı.
|
|
66
|
+
- **`staticPrecompressed`, `express.static`ten önce.** Build'de üretilmiş
|
|
67
|
+
`.br`/`.gz` kopyalar varsa onlar servis edilir (brotli kalite 11); yoksa
|
|
68
|
+
istek altındaki `static`e düşer ve middleware anında sıkıştırır (kalite 5).
|
|
69
|
+
Hash'li ve `immutable` bir dosyayı her istekte yeniden sıkıştırmak boşa CPU.
|
|
70
|
+
- **Body parser'lar statikten sonra.** Görsel isteklerinde gövde ayrıştırma
|
|
71
|
+
maliyeti ödenmesin.
|
|
72
|
+
- **`rewrites(afterFiles)`, statik denendikten sonra ve sayfalardan önce.**
|
|
73
|
+
Next.js'teki iki fazlı rewrite semantiğinin karşılığı.
|
|
74
|
+
- **404 ve hata yönetimi en sonda.** Hata yöneticisi `notFound`/`redirect`
|
|
75
|
+
kontrol akışını da yakalar, çünkü bunlar bir controller dışında (ör. bir
|
|
76
|
+
middleware içinde) da fırlatılabilir.
|
|
77
|
+
|
|
78
|
+
Framework `x-powered-by`'ı kapatır ve yerine markalanabilir bir başlık yazar,
|
|
79
|
+
`etag`i `strong` yapar ve `trust proxy`yi açar. `trust proxy` ters proxy
|
|
80
|
+
arkasında doğru protokol ve istemci IP'si için gerekli
|
|
81
|
+
([10-dagitim.md](./10-dagitim.md)).
|
|
82
|
+
|
|
83
|
+
## Island modeli: neden görünürlüğe bağlı hidrasyon
|
|
84
|
+
|
|
85
|
+
`src/client/registry.js` her `[data-island]` elementini bir
|
|
86
|
+
`IntersectionObserver`'a verir (`rootMargin: "200px 0px"`). Ekranda olanlar
|
|
87
|
+
zaten ilk gözlemde tetiklenir; ekran dışındakiler kaydırılana kadar **hiç
|
|
88
|
+
indirilmez**. Ana sayfadaki grafik kütüphanesi gibi ağır modüller böylece ilk
|
|
89
|
+
yükten tamamen çıkar.
|
|
90
|
+
|
|
91
|
+
Üç davranış var, hepsi HTML'den kontrol edilir:
|
|
92
|
+
|
|
93
|
+
- **Varsayılan:** görünürlüğe bağlı.
|
|
94
|
+
- **`data-island-eager`:** görünürlükten bağımsız, hemen bağlanır. Header,
|
|
95
|
+
çerez bandı gibi global davranışlar için.
|
|
96
|
+
- **`data-island-idle`:** görünür olsa bile `load` tamamlanıp ana iş parçacığı
|
|
97
|
+
boşalana kadar bekler. İlk ekranda görünen ama kritik olmayan ağır modüller
|
|
98
|
+
(ör. grafik kütüphanesi çeken mini grafik) LCP ile yarışmasın diye.
|
|
99
|
+
|
|
100
|
+
İki ek ayrıntı ölçümden geldi:
|
|
101
|
+
|
|
102
|
+
- **Bağlama işi boş zamana kaydırılır** (`requestIdleCallback`, `timeout: 500`).
|
|
103
|
+
Aynı anda görünen çok sayıda island tek bir uzun task'a dönüşürse TBT ve INP
|
|
104
|
+
bozulur.
|
|
105
|
+
- **Düzen kutusu olmayan elementler doğrudan bağlanır.** `hidden` bir
|
|
106
|
+
drawer/dialog'un düzen kutusu yoktur ve `IntersectionObserver` onu asla
|
|
107
|
+
bildirmez; bu yüzden `hydrate()` ölçümleri tek seferde okur
|
|
108
|
+
(`getClientRects().length`) ve kutusu olmayanları gözlemciye vermek yerine
|
|
109
|
+
hemen bağlar.
|
|
110
|
+
|
|
111
|
+
Buradan çıkan bir sonuç: **görsel hata yönetimi island değildir.** Görsel
|
|
112
|
+
ağırlıklı bir sayfada 80+ `<img>` olabiliyor ve her birine ayrı island bağlamak
|
|
113
|
+
(gözlemci + dinamik import + mount) sırf hata ihtimali için ciddi bir hidrasyon
|
|
114
|
+
yükü. `startSafeImages()` bunun yerine belgeye tek bir yakalama fazı
|
|
115
|
+
dinleyicisi kurar ([05-islands.md](./05-islands.md)).
|
|
116
|
+
|
|
117
|
+
## Sunucu HTML'i neden tam
|
|
118
|
+
|
|
119
|
+
Layout ve sayfa şablonu, ziyaretçinin göreceği içeriğin tamamını üretir.
|
|
120
|
+
İstemci tarafında "iskelet göster, sonra doldur" deseni yoktur. Bunun üç
|
|
121
|
+
karşılığı var:
|
|
122
|
+
|
|
123
|
+
1. **SEO:** kazıyıcı JS beklemek zorunda kalmaz.
|
|
124
|
+
2. **LCP:** en büyük içerik öğesi ilk HTML yanıtında gelir; JS'in indirilmesi,
|
|
125
|
+
ayrıştırılması ve çalıştırılması LCP yolunda değildir.
|
|
126
|
+
3. **CLS:** içerik sonradan enjekte edilmediği için düzen kaymaz.
|
|
127
|
+
|
|
128
|
+
Aynı ilke `<head>` tarafında da uygulanır. Layout kaynak ipuçlarını
|
|
129
|
+
(`preconnect`, LCP `preload`) `<head>`in **en başına** basar; bunları
|
|
130
|
+
geciktirmek doğrudan LCP'ye yazılır.
|
|
131
|
+
|
|
132
|
+
### Neden tek, render-blocking stylesheet
|
|
133
|
+
|
|
134
|
+
Ayrı bir "critical CSS" üretilmez. Ölçümde inline kritik CSS ilk ekranı tam
|
|
135
|
+
kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasında CLS
|
|
136
|
+
0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış tek
|
|
137
|
+
sheet'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci ziyarette
|
|
138
|
+
zaten `immutable` önbellekten geliyor.
|
|
139
|
+
|
|
140
|
+
Aynı mantık ikonlarda da var: her ikon için ayrı istek yerine, build zamanında
|
|
141
|
+
yalnızca kaynakta kullanılan sembollerden bir SVG sprite üretilir. Tüm Phosphor
|
|
142
|
+
setini göndermek 1500+ ikon, yani birkaç megabayt; tarama sprite'ı tipik olarak
|
|
143
|
+
10-30 sembolde tutuyor ([08-build.md](./08-build.md)).
|
|
144
|
+
|
|
145
|
+
## Cache stratejisi: ISR yerine bellek içi TTL
|
|
146
|
+
|
|
147
|
+
`src/server/html-cache.js` route + query anahtarlı, TTL'li, LRU bir HTML
|
|
148
|
+
önbelleği tutar (en fazla 500 girdi). TTL dolduğunda girdi hemen atılmaz: `stale`
|
|
149
|
+
pencerede eski HTML anında döner ve tazeleme arkada çalışır
|
|
150
|
+
(stale-while-revalidate, `STALE_FACTOR = 1`, yani stale penceresi TTL kadar).
|
|
151
|
+
|
|
152
|
+
Kazanç: ilk ısıtmadan sonra hiçbir istek render'ı beklemez. Bedel: HTML'deki
|
|
153
|
+
veri en fazla `revalidate + bir tazeleme turu` kadar geride olabilir. Bu bedel
|
|
154
|
+
kabul edilebilir, çünkü fiyat gibi canlı alanlar istemcide WebSocket'ten
|
|
155
|
+
güncelleniyor ve gecikme ekranda görünmüyor.
|
|
156
|
+
|
|
157
|
+
Diske yazmama kararı bilinçli. Next'teki build-time prerender'ın karşılığı
|
|
158
|
+
prewarm'dır ama çıktı diske yazılmaz: önbellek süreç belleğinde yaşadığı için
|
|
159
|
+
ısıtma da süreç ayağa kalkınca yapılır. Kazanç aynı — ilk ziyaretçi soğuk
|
|
160
|
+
render'ı beklemez — fakat veri dondurulmaz; her girdi route'un `revalidate`
|
|
161
|
+
süresiyle yaşlanır ([06-cache.md](./06-cache.md)).
|
|
162
|
+
|
|
163
|
+
### Sıkıştırılmış gövdenin önbellekte durması
|
|
164
|
+
|
|
165
|
+
Önbelleğe alınan her girdi, brotli/gzip çıktısını HTML ile birlikte saklar
|
|
166
|
+
(`encoded` haritası, HTML ile aynı ömrü paylaşır). Aynı sayfa her istekte
|
|
167
|
+
yeniden brotli'lenmez. `Content-Encoding` bu yolda `route()` içinde ayarlandığı
|
|
168
|
+
için sıkıştırma middleware'i devreye girmez.
|
|
169
|
+
|
|
170
|
+
### Neden geçici ve kalıcı upstream hataları farklı ele alınır
|
|
171
|
+
|
|
172
|
+
Render sırasında upstream düştüyse çıktı eksik veri içeriyor demektir ve böyle
|
|
173
|
+
bir HTML önbelleğe **yazılmaz**: sonraki istek yeniden dener.
|
|
174
|
+
|
|
175
|
+
Ancak bu yalnızca *geçici* hatalar için geçerli (ağ hatası, 408, 425, 429 ve tüm
|
|
176
|
+
5xx). 400/403/404 gibi deterministik cevaplar tekrar denemekle düzelmez; onlar
|
|
177
|
+
yüzünden önbelleği kapatmak sayfayı her ziyarette baştan render etmek olur —
|
|
178
|
+
içerik yine aynı eksik hâliyle döner, ziyaretçi sadece render süresini öder. Bu
|
|
179
|
+
yüzden kalıcı hatalar yalnızca loglanır, önbelleği engellemez.
|
|
180
|
+
|
|
181
|
+
Bu bilginin framework'e ulaşma yönü de bilinçli olarak terstir: framework veri
|
|
182
|
+
katmanını tanımaz, veri katmanı framework'e haber verir
|
|
183
|
+
(`reportUpstreamFailure()`). Hiç çağıran olmazsa maliyet boş bir dizidir.
|
|
184
|
+
|
|
185
|
+
### Üç kapsamın iç içe sırası
|
|
186
|
+
|
|
187
|
+
`route()` şu sırayı kurar:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
withHtmlCache( withUpstreamTracking( withRequestCache( controller ) ) )
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki çağrı
|
|
194
|
+
tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
|
|
195
|
+
eksik veriyle üretilen çıktı önbelleğe yazılmasın.
|
|
196
|
+
|
|
197
|
+
## Hata toleransı: hiçbir eksik siteyi indirmez
|
|
198
|
+
|
|
199
|
+
Framework boyunca tekrarlanan bir ilke var: eksik yapılandırma ya da eksik build
|
|
200
|
+
çıktısı, hata yerine bozulmuş ama çalışan bir sayfa üretir.
|
|
201
|
+
|
|
202
|
+
- **Config dosyası yoksa ya da okunamıyorsa** uyarı basılır ve sunucu
|
|
203
|
+
varsayılanlarla ayağa kalkar. Bozuk bir düzenleme siteyi açılamaz hâle
|
|
204
|
+
getirmemeli. Aynı şekilde `headers()`/`redirects()`/`rewrites()`/`cache()`
|
|
205
|
+
bölümlerinden biri hata verirse yalnızca o bölüm yok sayılır.
|
|
206
|
+
- **Hook'lar hata verirse** framework kendi varsayılanına döner ve uyarır.
|
|
207
|
+
- **Build çalışmadıysa** `asset()` `/assets/<isim>` döner, `hasAsset()` false
|
|
208
|
+
olur ve layout stylesheet/script etiketlerini hiç basmaz. `jskelet build`
|
|
209
|
+
unutulduğunda hata yerine stilsiz ama çalışan bir sayfa görürsünüz.
|
|
210
|
+
- **Dev'de bozuk bir route modülü** uyarı basıp atlanır; **üretimde fırlatır.**
|
|
211
|
+
Yarım route tablosuyla yayına çıkmak, sessizce 404 dönen sayfalar demek.
|
|
212
|
+
- **404 render'ı da patlarsa** şablonsuz, minimal bir HTML döner; ziyaretçi boş
|
|
213
|
+
yanıt görmesin.
|
|
214
|
+
- **Tek bir istek hatası süreci düşürmez:** `unhandledRejection` ve
|
|
215
|
+
`uncaughtException` loglanır ve süreç ayakta kalır. Bir haber sitesinde tek
|
|
216
|
+
sayfanın hatası tüm siteyi indirmemeli.
|
|
217
|
+
|
|
218
|
+
## Neden dosya sistemi tabanlı routing yok
|
|
219
|
+
|
|
220
|
+
Sıra önemli. `/:slug` gibi tek segmentli bir yakalayıcı `/about` rotasından önce
|
|
221
|
+
kaydedilirse "about" bir slug sanılır. Sırayı dosya adına gizlemek yerine
|
|
222
|
+
görünür kılmak teşhisi kolaylaştırıyor: ya `jskelet.config.mjs` → `routes` ile
|
|
223
|
+
açık bir liste verirsiniz, ya da `routes/` dizinini alfabetik taratıp dosya
|
|
224
|
+
adlarına sayısal önek koyarsınız (`10-pages.js`, `50-blog.js`,
|
|
225
|
+
`99-catch-all.js`). Ayrıntı: [03-routing.md](./03-routing.md).
|
|
226
|
+
|
|
227
|
+
## Neden tek bir config gerçek kaynağı
|
|
228
|
+
|
|
229
|
+
`src/config/index.js` proje kökünü, dizin yollarını, markalamayı, hook'ları ve
|
|
230
|
+
kuralları normalize eder. Diğer modüller yol hesaplamaz, `getConfig()` çağırır.
|
|
231
|
+
Sebebi somut: framework `node_modules/` içine girdiğinde `../..` sayarak kök
|
|
232
|
+
bulmaya çalışan her dosya bozulur. Aynı gerekçeyle build tarafında da tek bir
|
|
233
|
+
mutasyon noktası var (`initBuildPaths()`).
|
|
234
|
+
|
|
235
|
+
`getConfig()` `loadConfig()` çağrılmadan kullanılırsa boş bir proje kökü
|
|
236
|
+
varsaymak yerine hata verir: sessiz yanlış yol, "stylesheet neden yok" gibi
|
|
237
|
+
teşhisi zor sorunlara dönüşüyor.
|
|
238
|
+
|
|
239
|
+
## Neden bu bağımlılık listesi
|
|
240
|
+
|
|
241
|
+
Çalışma zamanı bağımlılıkları dörttür: `express`, `ejs`, `esbuild`,
|
|
242
|
+
`tailwind-merge`. Geri kalan her şey (Tailwind, PostCSS, lightningcss, sharp,
|
|
243
|
+
Phosphor ikonları) **opsiyonel peer bağımlılığıdır** ve yoksa ilgili build adımı
|
|
244
|
+
atlanır.
|
|
245
|
+
|
|
246
|
+
İki karar ayrıca açıklanmayı hak ediyor:
|
|
247
|
+
|
|
248
|
+
- **`compression` paketi yerine `node:zlib`.** Paket brotli desteklemiyor ve
|
|
249
|
+
yedi geçişli bir bağımlılık ağacı getiriyor; brotli + gzip pazarlığını elle
|
|
250
|
+
yapmak yeterli. Brotli tercih edilir: ana sayfa HTML'inde gzip'e göre ~%35
|
|
251
|
+
daha küçük.
|
|
252
|
+
- **`tailwind-merge` çalışma zamanında kalır.** Sınıf hesabı yalnızca sunucuda
|
|
253
|
+
yapılır, client bundle'a hiç girmez, dolayısıyla sayfa ağırlığına etkisi
|
|
254
|
+
yoktur. Elle yazılmış bir grup tablosu ise `border-2` + `border-transparent`
|
|
255
|
+
gibi genişlik/renk çiftlerini birbirine karıştırıp sınıf düşürdüğü için
|
|
256
|
+
görsel regresyon üretiyordu.
|
|
257
|
+
|
|
258
|
+
Opsiyonel paketler **uygulamanın** `node_modules`'ünden çözülür, framework'ün
|
|
259
|
+
kendisinden değil. Framework `file:` ya da workspace bağlantısıyla kuruluysa düz
|
|
260
|
+
bir `import "postcss"` framework'ün ağacına bakar — uygulamanınkine değil.
|
|
261
|
+
|
|
262
|
+
## Neden alias ve uzantı hook'ları
|
|
263
|
+
|
|
264
|
+
`node --import jskelet/register` iki iş yapar:
|
|
265
|
+
|
|
266
|
+
1. `jsconfig.json` / `tsconfig.json` içindeki `compilerOptions.paths`
|
|
267
|
+
alias'larını çözer (`@/lib/x` → `<root>/lib/x`). Editör ve çalışma zamanı aynı
|
|
268
|
+
dosyadan beslendiği için ikisi birbirinden ayrışmaz.
|
|
269
|
+
2. Uzantısız göreli import'lara uzantı ekler (`./cache` → `./cache.js`). Node ESM
|
|
270
|
+
bunu yapmaz ve bundler'dan taşınan kodda en sık karşılaşılan kırılma noktası
|
|
271
|
+
budur.
|
|
272
|
+
|
|
273
|
+
esbuild tarafındaki `@/` çözümü de aynı davranışı taklit eder, böylece `lib/`
|
|
274
|
+
altındaki modüller hem sunucuda hem tarayıcıda aynı import stilini kullanabilir.
|
|
275
|
+
|
|
276
|
+
`--import` bir modül **belirteci** bekler, dosya yolu değil. Windows'ta `H:\...`
|
|
277
|
+
mutlak yolu `h:` şemalı bir URL sanılıp reddediliyor; bu yüzden framework her
|
|
278
|
+
yerde `pathToFileURL(...).href` kullanır. Aynı sebeple config, route modülleri
|
|
279
|
+
ve bileşenler de `file://` URL'le import edilir.
|
|
280
|
+
|
|
281
|
+
## Sırada ne var
|
|
282
|
+
|
|
283
|
+
- Route ve controller sözleşmesi: [03-routing.md](./03-routing.md)
|
|
284
|
+
- Şablon katmanı ve metadata: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)
|
|
285
|
+
- Island runtime API'si: [05-islands.md](./05-islands.md)
|
|
286
|
+
- Önbelleğin ayarları ve prewarm: [06-cache.md](./06-cache.md)
|
|
287
|
+
- Dev akışının iç işleyişi: [09-dev-araclari.md](./09-dev-araclari.md)
|