jskelet 0.1.1 → 0.1.3

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 (66) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +129 -2
  3. package/README.md +21 -7
  4. package/bin/jskelet.mjs +6 -6
  5. package/docs/03-routing.md +48 -9
  6. package/docs/04-render-ve-sablonlar.md +2 -2
  7. package/docs/05-islands.md +59 -6
  8. package/docs/06-cache.md +240 -26
  9. package/docs/07-yapilandirma.md +108 -7
  10. package/docs/08-build.md +4 -4
  11. package/docs/09-dev-araclari.md +5 -0
  12. package/docs/12-panel-ve-oturum.md +384 -0
  13. package/docs/README.md +25 -2
  14. package/docs/en/01-getting-started.md +292 -0
  15. package/docs/en/02-architecture.md +305 -0
  16. package/docs/en/03-routing.md +493 -0
  17. package/docs/en/04-rendering.md +504 -0
  18. package/docs/en/05-islands.md +492 -0
  19. package/docs/en/06-caching.md +640 -0
  20. package/docs/en/07-configuration.md +789 -0
  21. package/docs/en/08-build.md +383 -0
  22. package/docs/en/09-dev-tools.md +314 -0
  23. package/docs/en/10-deployment.md +332 -0
  24. package/docs/en/11-migration.md +360 -0
  25. package/docs/en/12-dashboards-and-sessions.md +392 -0
  26. package/docs/en/README.md +112 -0
  27. package/package.json +4 -2
  28. package/src/build/build.mjs +1 -1
  29. package/src/build/tasks/client.mjs +2 -2
  30. package/src/build/tasks/fonts.mjs +3 -3
  31. package/src/build/tasks/icons.mjs +1 -1
  32. package/src/build/tasks/images.mjs +2 -2
  33. package/src/client/devtools/overlay.js +196 -164
  34. package/src/client/devtools/report.js +96 -96
  35. package/src/client/form.js +192 -0
  36. package/src/client/index.js +10 -1
  37. package/src/client/registry.js +78 -4
  38. package/src/client/swap.js +188 -0
  39. package/src/config/defaults.js +83 -0
  40. package/src/config/index.js +129 -18
  41. package/src/config/pattern.js +1 -1
  42. package/src/dev-server.mjs +1 -1
  43. package/src/http/control-flow.js +16 -1
  44. package/src/http/cookies.js +257 -0
  45. package/src/http/request-context.js +162 -0
  46. package/src/index.js +26 -2
  47. package/src/init.mjs +32 -31
  48. package/src/log.mjs +8 -2
  49. package/src/logo.png +0 -0
  50. package/src/runtime/alias-hooks.mjs +1 -1
  51. package/src/server/assets.js +1 -1
  52. package/src/server/create-app.js +12 -4
  53. package/src/server/data-cache.js +244 -0
  54. package/src/server/dev/devtools.js +6 -2
  55. package/src/server/dev/report.js +8 -1
  56. package/src/server/dev/version-check.mjs +139 -0
  57. package/src/server/head-hints.js +1 -1
  58. package/src/server/html-cache.js +32 -6
  59. package/src/server/middleware/csrf.js +134 -0
  60. package/src/server/prewarm.js +164 -19
  61. package/src/server/render.js +256 -20
  62. package/src/server/router.js +14 -7
  63. package/src/server/status-page.js +1 -1
  64. package/src/version.mjs +9 -4
  65. package/src/views/components/loader.js +1 -1
  66. package/src/views/helpers/tags.js +53 -1
@@ -0,0 +1,384 @@
1
+ # 12 — Panel, oturum ve kişiye özel sayfalar
2
+
3
+ JSkelet'in ağırlık merkezi public, önbelleklenebilir sayfalar. Bir panel ise
4
+ tam ters eksende durur: her ziyaretçiye farklı HTML, önbellek yok, yoğun
5
+ etkileşim. Bu belge o ekseni anlatır — `private: true`, oturum cookie'leri,
6
+ CSRF, fragment uçları ve parça takası.
7
+
8
+ Kapsam dışında bırakılan bir şey var ve bilinçli: **canlı veri taşıması**.
9
+ SSE, WebSocket ya da aralıklı sorgu arasındaki seçim uygulamanın; framework
10
+ yalnızca "şu parçayı sunucudan tazele" adımını veriyor. Sıkıştırma katmanı
11
+ `text/event-stream` yanıtlarını ve başlıklarını kendisi yazan akışları
12
+ atladığı için SSE'yi elle kurmak da bir şeyi bozmuyor.
13
+
14
+ Çalışan karşılığı `examples/dashboard/`; bu belgedeki kod parçalarının kaynağı
15
+ orası.
16
+
17
+ ## Neden ayrı bir yol gerekiyor
18
+
19
+ HTML önbelleğinin anahtarı yalnızca yol ve query:
20
+
21
+ ```
22
+ `${req.path}?${new URLSearchParams(query).toString()}`
23
+ ```
24
+
25
+ Kimlik anahtarın parçası değil. Yani oturuma bağlı bir sayfa normal `route()`
26
+ ile kaydedilirse, ilk isteyenin HTML'i o TTL boyunca **herkese** servis edilir.
27
+ Bu hata hiçbir yerde patlamaz: sayfa çalışır, testler geçer, sorun yalnızca
28
+ ikinci kullanıcı geldiğinde ve genelde üretimde görünür.
29
+
30
+ ## `private: true`
31
+
32
+ ```js
33
+ export default function register(app, { route, redirect }) {
34
+ app.get(
35
+ "/panel",
36
+ route(
37
+ async ({ req }) => {
38
+ const user = currentUser(req);
39
+ if (!user) redirect("/giris?next=%2Fpanel");
40
+
41
+ return { view: "pages/overview", data: { user } };
42
+ },
43
+ { private: true },
44
+ ),
45
+ );
46
+ }
47
+ ```
48
+
49
+ Bayrağın yaptığı işler:
50
+
51
+ | Davranış | Public `route()` | `private: true` |
52
+ | --- | --- | --- |
53
+ | HTML önbelleği | TTL varsa açık | Kapalı, açılamaz |
54
+ | `cache.html` deseni | TTL'i ezer | Yok sayılır |
55
+ | `Cache-Control` | `public, s-maxage=…` | `private, no-store` |
56
+ | `Vary` | `Accept-Encoding` | `Cookie, Accept-Encoding` |
57
+ | ETag | Var | Yok |
58
+ | `X-JSkelet-Cache` | `HIT`/`STALE`/`MISS` | Yazılmaz |
59
+
60
+ ETag'in düşmesi ayrıntı gibi görünüyor ama değil: kullanıcıya özel bir gövdenin
61
+ güçlü ETag'i, o kullanıcıya özgü bir parmak izidir ve `no-store`'a uymayan bir
62
+ katmanda kimlik ayrımı için kullanılabilir.
63
+
64
+ Kişiye özel bir sayfadan atılan yönlendirme de önbelleklenmez. "Giriş yapmalısın"
65
+ kararı oturuma bağlı; saklanması, giriş yapmış kullanıcının da login sayfasına
66
+ atılması demek.
67
+
68
+ ## Bayrağı unutursanız
69
+
70
+ Framework kimliğe dokunan erişimleri izliyor. Controller'a giden `req`, şu
71
+ okumaları işaretleyen ince bir Proxy ile sarılı:
72
+
73
+ - `req.headers.cookie`, `req.headers.authorization`,
74
+ `req.headers["proxy-authorization"]`
75
+ - `req.get("Cookie")` / `req.header("Authorization")`
76
+ - `req.cookies`, `req.signedCookies`, `req.session`, `req.user`
77
+ - `parseCookies(req)` ve `getSignedCookie(req, …)` (doğrudan bildiriyorlar)
78
+
79
+ İşaretlenen bir render önbelleğe **yazılmaz**. Üretimde yanıt `no-store` ile
80
+ gider ve şu satır loglanır:
81
+
82
+ ```
83
+ [render] /panel kimliğe bağlı veri okudu (req.headers.cookie), önbelleğe
84
+ alınmadı. Route 'private: true' ile kaydedilmeli.
85
+ ```
86
+
87
+ Development'ta aynı durum isteği bir hatayla düşürür. Sessiz kalmamasının
88
+ sebebi basit: bu hata çalışan bir sayfa üretiyor, yani kendi başına asla fark
89
+ edilmiyor.
90
+
91
+ `csrfField()` de aynı işaretlemeyi yapıyor. Token basan bir sayfa önbellekten
92
+ dönemez — dönerse tüm ziyaretçiler aynı token'ı paylaşır ve çift gönderim
93
+ kontrolü hiçbir şey doğrulamaz.
94
+
95
+ ## Oturum: imzalı cookie
96
+
97
+ Framework kimlik sağlamıyor. Verdiği tek şey "bu değeri ben yazdım,
98
+ kurcalanmamış" garantisi:
99
+
100
+ ```js
101
+ import { clearCookie, getSignedCookie, setSignedCookie } from "jskelet/cookies";
102
+
103
+ export function startSession(res, username) {
104
+ setSignedCookie(res, "dash_session", username, { maxAge: 60 * 60 * 8 });
105
+ }
106
+
107
+ export function currentUser(req) {
108
+ const username = getSignedCookie(req, "dash_session");
109
+ return username ? findUser(username) : null;
110
+ }
111
+
112
+ export function endSession(res) {
113
+ clearCookie(res, "dash_session");
114
+ }
115
+ ```
116
+
117
+ İmza HMAC-SHA256, karşılaştırma sabit zamanlı. İmza uymuyorsa `getSignedCookie`
118
+ `null` döner — kurcalanmış bir değer "belki geçerlidir" diye kullanılmaz.
119
+
120
+ Sır `security.cookieSecret` ya da `JSKELET_SECRET` ortam değişkeninden gelir.
121
+ Sır yoksa imzalı API **hata verir**; "yapılandırma hatası siteyi düşürmez"
122
+ kuralı burada geçerli değil, çünkü sessiz alternatif imzasız bir cookie'ye
123
+ güvenmek olurdu.
124
+
125
+ Varsayılanlar kısıtlayıcı tarafta: `HttpOnly`, `SameSite=Lax`, development
126
+ dışında `Secure`, `Path=/`. `SameSite=Lax` tek başına CSRF'in büyük kısmını
127
+ kapatıyor — cookie çapraz site POST'larında hiç gönderilmiyor.
128
+
129
+ Cookie **şifrelenmiyor**, imzalanıyor. Değer okunabilir; gizli kalması gereken
130
+ veriyi değil, onun kimliğini koyun.
131
+
132
+ ## CSRF
133
+
134
+ Gövdeyi framework ayrıştırıyor (`express.urlencoded` + `express.json`), yani
135
+ state değiştiren istekleri kabul eden katman o. Koruma iki katmanlı.
136
+
137
+ ### Katman 1 — origin kontrolü (varsayılan açık)
138
+
139
+ `Origin` kendi host'umuzla uyuşmuyorsa ya da `Sec-Fetch-Site: cross-site`
140
+ geldiyse güvenli olmayan metotlar 403 alır. **Başlıkların hiçbiri yoksa istek
141
+ geçer**: tarayıcılar çapraz origin bir POST'ta `Origin`'i her zaman gönderir,
142
+ webhook'lar ve sunucudan sunucuya çağrılar hiç göndermez. Bu ayrım korumayı
143
+ açık bırakırken entegrasyonları bozmuyor.
144
+
145
+ ```js
146
+ security: {
147
+ csrf: {
148
+ // Ayrı bir alan adından gelen panel gibi meşru istisnalar.
149
+ allowedOrigins: ["https://admin.example.com"],
150
+ // Tarayıcıdan gelmeyen uçlar.
151
+ exclude: ["/webhook/:path*"],
152
+ },
153
+ }
154
+ ```
155
+
156
+ ### Katman 2 — çift gönderim token'ı (opsiyonel)
157
+
158
+ `security.csrf.token: true` ile açılır. Formlar token'ı `csrfField()` ile basar:
159
+
160
+ ```ejs
161
+ <form method="post" action="/panel/notlar">
162
+ <%- csrfField() %>
163
+ …
164
+ </form>
165
+ ```
166
+
167
+ Token'ı **middleware üretmiyor**, `csrfField()` üretiyor — yani gerçekten bir
168
+ forma basıldığı anda. Sebebi somut: token her yanıtta yazılsaydı public ve
169
+ önbelleklenebilir bir sayfa da `Set-Cookie` taşırdı, bir CDN o yanıtı saklardı
170
+ ve tüm ziyaretçiler aynı token'ı paylaşırdı.
171
+
172
+ Sunucu tarafında token, imzalı cookie ile gönderilen alanın eşleşmesini
173
+ istiyor; alan yerine `X-CSRF-Token` başlığı da kabul ediliyor.
174
+
175
+ `csrfField()` `security.csrf.token` kapalıyken boş string döner, yani şablon
176
+ her koşulda render edilebilir.
177
+
178
+ ## Fragment uçları
179
+
180
+ `fragment()` politikası sabit bir parça yanıtı üretir: layout basılmaz, yanıt
181
+ `private, no-store` ve ETag'siz gider, HTML önbelleğine hiç uğramaz.
182
+
183
+ ```js
184
+ export default function register(app, { fragment }) {
185
+ app.get(
186
+ "/_fragment/siparisler",
187
+ fragment(async ({ req, query }) => {
188
+ const user = currentUser(req);
189
+ if (!user) return { view: "partials/session-expired", status: 401 };
190
+
191
+ return {
192
+ view: "partials/order-table",
193
+ data: { siparisler: getOrders(user.username, Number(query.sayfa ?? 1)) },
194
+ };
195
+ }),
196
+ );
197
+ }
198
+ ```
199
+
200
+ Controller ya `{ view, data?, status? }` ya doğrudan bir HTML string döner.
201
+ Hata durumunda tüm sayfa yerine küçük bir parça döner
202
+ (`<div role="alert" data-fragment-error>`): takas edilen bölge bir hata
203
+ sayfasının tamamını içine almamalı.
204
+
205
+ Aynı şablonun sayfanın içinde de fragment ucunda da kullanılması esas fikir —
206
+ işaretlemenin tek kaynağı sunucuda kalır, istemci ikinci bir şablon taşımaz.
207
+
208
+ ## İstemci: parça takası
209
+
210
+ ```js
211
+ import { registerAll, start, startForms, startSwapLinks } from "jskelet/client";
212
+
213
+ registerAll({ "live-clock": () => import("../islands/live-clock.js") });
214
+
215
+ start();
216
+ startSwapLinks();
217
+ startForms();
218
+ ```
219
+
220
+ `startSwapLinks()` `data-swap` taşıyan bağlantıları bağlar:
221
+
222
+ ```html
223
+ <a href="/_fragment/siparisler?sayfa=2" data-swap="#siparisler">Sonraki</a>
224
+ ```
225
+
226
+ `href` gerçek bir URL olduğu için JS yoksa bağlantı normal gezinmeye düşer.
227
+ Programatik kullanım için `swap()`:
228
+
229
+ ```js
230
+ import { swap } from "jskelet/client";
231
+
232
+ await swap("#siparisler", "/_fragment/siparisler?sayfa=2", { history: true });
233
+ ```
234
+
235
+ `swap()` sırayla: eski alt ağacın island'larını söker, içeriği değiştirir, yeni
236
+ alt ağacı hidre eder ve odağı kaybolmuşsa geri getirir. İstek süresince bölgeye
237
+ `aria-busy="true"` yazılır — bekleme göstergesini ayrı bir sınıfa bağlamak
238
+ yerine erişilebilirlik durumuna bağlamak ikisinin birbirinden ayrı düşmesini de
239
+ engelliyor.
240
+
241
+ Yönlendirmeyle karşılaşırsa (oturum düştü, login'e gidiliyor) parçayı takmak
242
+ yerine sayfayı o adrese götürür.
243
+
244
+ ### Island sökme
245
+
246
+ Bu, takasın en kolay atlanan yarısı. `mount()` bir temizlik fonksiyonu
247
+ döndürebiliyor:
248
+
249
+ ```js
250
+ export function mount(element) {
251
+ const timer = setInterval(() => tick(element), 1000);
252
+ return () => clearInterval(timer);
253
+ }
254
+ ```
255
+
256
+ `innerHTML` ile değiştirilen bir bölgenin island'ları DOM'dan çıkar ama
257
+ `document`/`window` üzerine kurdukları dinleyiciler ve `setInterval`'ları
258
+ yaşamaya devam eder; birkaç takastan sonra aynı iş onlarca kez çalışır.
259
+ `swap()` ve form yardımcıları `unmount()` çağırıyor, elle DOM değiştiriyorsanız
260
+ sizin çağırmanız gerekiyor:
261
+
262
+ ```js
263
+ import { hydrate, unmount } from "jskelet/client";
264
+
265
+ unmount(bolge);
266
+ bolge.innerHTML = html;
267
+ hydrate(bolge);
268
+ ```
269
+
270
+ ## Formlar
271
+
272
+ `startForms()` `data-enhance` taşıyan formları bağlar. Sözleşme progressive
273
+ enhancement: form normal bir `<form method="post" action="…">`, JS yalnızca
274
+ aradaki tam sayfa turunu kaldırıyor.
275
+
276
+ ```html
277
+ <form method="post" action="/panel/notlar" data-enhance data-target="#notlar">
278
+ <%- csrfField() %>
279
+ <textarea name="metin" required minlength="3"></textarea>
280
+ <button type="submit">Kaydet</button>
281
+ </form>
282
+ ```
283
+
284
+ Sunucu üç cevaptan birini verir:
285
+
286
+ - **yönlendirme** → `location.assign` ile izlenir (başarılı mutasyon, JS'siz yol)
287
+ - **4xx + parça** → formun yerine takılır (doğrulama hataları)
288
+ - **2xx + parça** → `data-target` bölgesine takılır ve form sıfırlanır
289
+
290
+ Sunucu tarafı ikisini de karşılıyor:
291
+
292
+ ```js
293
+ app.post(
294
+ "/panel/notlar",
295
+ fragment(async ({ req }) => {
296
+ const user = currentUser(req);
297
+ if (!user) seeOther("/giris?next=%2Fpanel");
298
+
299
+ const result = addNote(user.username, req.body?.metin);
300
+
301
+ // JS kapalıysa istemci `X-Requested-With` göndermez: tam sayfa turu.
302
+ if (req.get("X-Requested-With") !== "fragment") {
303
+ seeOther(result.ok ? "/panel" : "/panel?not=hata");
304
+ }
305
+
306
+ return result.ok
307
+ ? { view: "partials/note-list", data: { notlar: getNotes(user.username) } }
308
+ : { view: "partials/note-form", data: { hata: result.error }, status: 422 };
309
+ }),
310
+ );
311
+ ```
312
+
313
+ `seeOther()` yerine `redirect()` kullanmak burada bir hata olurdu: `redirect()`
314
+ 307 yazıyor ve 307 metodu koruyor, yani tarayıcı hedefe yeniden POST ediyor.
315
+ Form sonrası akış 303 gerektiriyor.
316
+
317
+ POST handler'ının `fragment()` içinden geçmesinin sebebi de var: layout'suz
318
+ render ve `no-store`'un yanında **istek bağlamı** kazandırıyor. Bağlam olmadan
319
+ hata durumunda yeniden basılan formun `csrfField()`i boş çıkar ve kullanıcının
320
+ ikinci denemesi 403 alır.
321
+
322
+ Doğrulama hatasında odak ilk hatalı alana taşınıyor; işaret olarak
323
+ `aria-invalid="true"` ya da `data-field-error` aranıyor.
324
+
325
+ ## Yapılandırma
326
+
327
+ ```js
328
+ export default {
329
+ security: {
330
+ /**
331
+ * Ters proxy arkasında değilsen kapat: açıkken istemci kendi
332
+ * `X-Forwarded-For` başlığını uydurabilir.
333
+ */
334
+ trustProxy: true,
335
+
336
+ cookieSecret: process.env.JSKELET_SECRET,
337
+
338
+ csrf: {
339
+ enabled: true,
340
+ token: false,
341
+ allowedOrigins: [],
342
+ exclude: [],
343
+ cookieName: "csrf_token",
344
+ fieldName: "_csrf",
345
+ headerName: "x-csrf-token",
346
+ },
347
+ },
348
+ };
349
+ ```
350
+
351
+ Panel yolları için iki ek ayar işe yarıyor:
352
+
353
+ ```js
354
+ // Yan etkili bir bağlantının önden getirilmesi kullanıcıyı oturumdan atabilir.
355
+ navigation: { exclude: ["/panel/:path*", "/cikis"] },
356
+
357
+ // Isıtıcının oturumu yok; korumalı sayfalar ısıtılamaz.
358
+ prewarmSkip: ["/api/", "/_fragment/", "/panel", "/cikis"],
359
+ ```
360
+
361
+ Ve `headers()` ile indeksleme kapatılır — `no-store` önbelleği engelliyor, ama
362
+ indekslemeyi ayrıca söylemek gerekiyor:
363
+
364
+ ```js
365
+ {
366
+ source: "/panel/:path*",
367
+ headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow" }],
368
+ }
369
+ ```
370
+
371
+ ## Kontrol listesi
372
+
373
+ Kişiye özel bir bölüm eklerken:
374
+
375
+ - [ ] Sayfalar `route(fn, { private: true })` ile kayıtlı.
376
+ - [ ] Fragment uçları `fragment()` ile kayıtlı.
377
+ - [ ] `security.cookieSecret` ortam değişkeninden geliyor, kodda sabit değil.
378
+ - [ ] Mutasyon formlarında `csrfField()` var; `security.csrf.token` açık.
379
+ - [ ] Çıkış bir POST; GET değil.
380
+ - [ ] Girişten sonraki `next` parametresi yalnızca site içi yolları kabul ediyor.
381
+ - [ ] `prewarmSkip` ve `navigation.exclude` korumalı öneki dışlıyor.
382
+ - [ ] `headers()` altında `X-Robots-Tag: noindex`.
383
+ - [ ] Duman testi `no-store` başlığını ve token'sız POST'un reddini kontrol
384
+ ediyor — bunlar bozulduğunda ekranda hiçbir şey değişmiyor.
package/docs/README.md CHANGED
@@ -9,6 +9,9 @@ kullanır. React yok, TypeScript yok; düz JavaScript ve JSDoc.
9
9
  Bu dizin framework'ün tam referansıdır. Sıralı okumak için baştan başlayın;
10
10
  belirli bir konuyu arıyorsanız doğrudan ilgili başlığa gidin.
11
11
 
12
+ Aynı belgelerin İngilizcesi [`docs/en/`](./en/README.md) altında. İki sürüm elle
13
+ eşlenik tutuluyor; birini değiştiriyorsan diğerini de değiştir.
14
+
12
15
  ## Sıralı okuma
13
16
 
14
17
  | Belge | Konu |
@@ -24,6 +27,7 @@ belirli bir konuyu arıyorsanız doğrudan ilgili başlığa gidin.
24
27
  | [09-dev-araclari.md](./09-dev-araclari.md) | `jskelet dev` akışı, watch dizinleri, CSS hot-swap, devtools overlay (Alt+D), rapor sayfası, dev gate |
25
28
  | [10-dagitim.md](./10-dagitim.md) | Prod build + start, ortam değişkenleri, Docker, ters proxy, sağlık kontrolü |
26
29
  | [11-tasima.md](./11-tasima.md) | Next.js'ten taşıma: karşılık tablosu ve adım adım plan |
30
+ | [12-panel-ve-oturum.md](./12-panel-ve-oturum.md) | Kişiye özel sayfalar: `private: true`, imzalı cookie oturumu, CSRF, `fragment()`, parça takası ve form döngüsü |
27
31
 
28
32
  ## Konuya göre hızlı erişim
29
33
 
@@ -33,13 +37,15 @@ belirli bir konuyu arıyorsanız doğrudan ilgili başlığa gidin.
33
37
  [05-islands.md](./05-islands.md)
34
38
  - **Sayfa neden `MISS` dönüyor / neden eski veri görüyorum?** →
35
39
  [06-cache.md](./06-cache.md)
40
+ - **Oturuma bağlı bir sayfa nasıl yazılır?** →
41
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)
36
42
  - **Hangi config alanı ne yapıyor?** → [07-yapilandirma.md](./07-yapilandirma.md)
37
43
  - **Stil çıkmıyor / ikon görünmüyor** → [08-build.md](./08-build.md)
38
44
  - **Yayına alma** → [10-dagitim.md](./10-dagitim.md)
39
45
 
40
46
  ## Çalışan örnekler
41
47
 
42
- Üçü de çalışır durumda; belgelerdeki örneklerin çoğu buralardan alınmıştır.
48
+ Dördü de çalışır durumda; belgelerdeki örneklerin çoğu buralardan alınmıştır.
43
49
 
44
50
  **`examples/minimal/`** — iki route, bir bileşen, bir island, minimal config.
45
51
  Framework'ün en küçük çalışan hâli.
@@ -67,6 +73,12 @@ sayfası. Sayfadaki bayt sayıları `lib/payload.js` içinde sitenin **kendi** b
67
73
  ölçülür. Uzun TTL (bir saat) ve tüm sayfaları ısıtan prewarm ile, cache'in en
68
74
  verimli çalıştığı profili gösterir.
69
75
 
76
+ Site aynı zamanda **bu belgeleri** servis ediyor: `/docs/<bölüm>` adresleri
77
+ `node_modules/jskelet/docs/` altındaki markdown dosyalarını okuyup sol gezinme,
78
+ "bu sayfada" listesi ve sıralı geçişle basıyor. Çevirici `lib/markdown.js`
79
+ içinde küçük bir modül — bağımlılık yok — ve kaynak paketin kendisi olduğu için
80
+ site kurulu sürümden hiç ayrışmıyor.
81
+
70
82
  Site aynı zamanda **iki dilli**: varsayılan İngilizce kökte, Türkçe `/tr`
71
83
  altında ve route adları iki dilde de aynı. Framework'te i18n yok; dil
72
84
  çözümlemesi `lib/i18n.js` içinde uygulamanın kendi sözleşmesi olarak duruyor ve
@@ -78,5 +90,16 @@ npm --prefix examples/marketing install
78
90
  npm --prefix examples/marketing run dev
79
91
  ```
80
92
 
81
- Her üç örnekte `node smoke.mjs` sunucu ayaktayken uçların beklendiği gibi
93
+ **`examples/dashboard/`** — diğer üçünün tersi eksen: kişiye özel sayfalar.
94
+ İmzalı cookie ile giriş, `private: true` korumalı panel, sayfalı tablo
95
+ fragment'i, CSRF'li mutasyon formu ve temizlik fonksiyonu döndüren bir island.
96
+ Public bir tanıtım sayfası da var, böylece aynı uygulamada önbelleklenen ve
97
+ `no-store` dönen iki yanıt yan yana görülüyor.
98
+
99
+ ```bash
100
+ npm --prefix examples/dashboard install
101
+ npm --prefix examples/dashboard run dev
102
+ ```
103
+
104
+ Her dört örnekte `node smoke.mjs` sunucu ayaktayken uçların beklendiği gibi
82
105
  yanıt verdiğini doğrular.