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
package/AGENTS.md CHANGED
@@ -24,6 +24,11 @@ zaten tartışılmış:
24
24
  | `src/config/**` | [07-yapilandirma.md](./docs/07-yapilandirma.md) |
25
25
  | `src/dev-server.mjs`, `src/server/dev/**` | [09-dev-araclari.md](./docs/09-dev-araclari.md) |
26
26
 
27
+ Belgeler iki dilde: Türkçesi `docs/`, İngilizcesi `docs/en/` altında ve
28
+ dosyalar birebir eşlenik. Bir belgeyi değiştirdiysen karşılığını da güncelle;
29
+ pazarlama sitesi (`examples/marketing`) bu dosyaları doğrudan `/docs` altında
30
+ servis ettiği için eksik kalan çeviri kullanıcıya görünür.
31
+
27
32
  ## Doğrulama
28
33
 
29
34
  **Lint yeterli, tam build zorunlu değil.**
package/CHANGELOG.md CHANGED
@@ -10,10 +10,135 @@ one is listed under a **Breaking** heading.
10
10
 
11
11
  ### Added
12
12
 
13
+ - An upstream data cache: `withDataCache(key, ttlSeconds, producer)` and the
14
+ `dataCache(fn, { key, revalidate })` wrapper, with `clearDataCache(prefix?)`,
15
+ `getDataCacheSize()` and `getDataCacheEntries()` alongside them. It keeps JSON
16
+ rather than HTML, so its default limit is 10,000 entries: a long-tail page that
17
+ was never prewarmed still renders without touching the API. Concurrent reads of
18
+ the same key collapse into one upstream request, an expired entry is served
19
+ immediately while it refreshes in the background, a failing producer falls back
20
+ to the stale value, and empty answers (`null`/`undefined`) are not stored
21
+ unless `storeEmpty: true` is passed.
22
+ - `cache().prewarm.priority` decides the warm-up order and accepts both the
23
+ config pattern syntax (`/news/:slug`) and plain regular expressions. Matching
24
+ paths are warmed on every pass.
25
+ - Drip prewarming for large sites: `cache().prewarm.rps` (also `PREWARM_RPS`)
26
+ caps requests per second regardless of parallelism, and `rotate` (on by
27
+ default) makes periodic passes continue through the queue where the previous
28
+ one stopped instead of re-warming the same first slice. A pass is skipped while
29
+ the previous one is still running.
30
+ - `cache().prewarm.retryDelayMs` (also `PREWARM_RETRY_DELAY_MS`) waits before the
31
+ retry pass, since rate limit windows are measured in seconds.
32
+ - `cache().maxEntries` configures the HTML cache limit, which used to be a fixed
33
+ 500.
34
+ - The dev report now includes the data cache entry count under `cache.data`.
35
+
36
+ ### Changed
37
+
38
+ - `notFound()` is no longer served as a 404 when a transient upstream failure
39
+ (`429`, `5xx`, network error) was reported during the same render. Those pages
40
+ now respond with an uncached `503` and `Retry-After`, so a temporary rate limit
41
+ is not frozen into "this page does not exist" for the whole TTL.
42
+ - Responses produced with missing data are no longer offered to shared caches:
43
+ a `degraded` render is sent with `private, no-store` instead of
44
+ `public, s-maxage=…`. The `X-JSkelet-Cache` diagnostic header is still written.
45
+ - The prewarm summary distinguishes paths left for the next pass
46
+ (`700 deferred to the next pass`) from paths dropped entirely
47
+ (`700 over the limit`).
48
+ - The changelog page of the marketing example is generated from the installed
49
+ package's `CHANGELOG.md` instead of a hand-written list, and shows the version
50
+ published on npm next to the installed one.
51
+
52
+ ## [0.1.2] - 2026-08-30
53
+
54
+ ### Added
55
+
56
+ - `route(fn, { private: true })` for pages that depend on the visitor. The HTML
57
+ cache is bypassed, `cache.html` patterns can no longer turn caching on for
58
+ that route, and the response is sent with `private, no-store`, `Vary: Cookie`
59
+ and no ETag.
60
+ - A runtime guard against identity leaks: when a cacheable route reads
61
+ `Cookie`, `Authorization` or a session field, the rendered HTML is never
62
+ stored. In development the request fails with an explanation, in production it
63
+ is served with `no-store` and logged.
64
+ - `fragment()` for layout-less partial responses, with `no-store` and cache
65
+ bypass built in.
66
+ - CSRF protection. Cross-site state-changing requests are rejected based on
67
+ `Origin` and `Sec-Fetch-Site`; requests carrying neither header still pass, so
68
+ webhooks keep working. An optional double-submit token layer is enabled with
69
+ `security.csrf.token` and rendered into forms by the new `csrfField()` helper.
70
+ - Signed cookie helpers under `jskelet/cookies`: `parseCookies()`,
71
+ `setCookie()`, `clearCookie()`, `setSignedCookie()`, `getSignedCookie()`,
72
+ `randomToken()` and `safeEqual()`. Defaults are `HttpOnly`, `SameSite=Lax` and
73
+ `Secure` outside development.
74
+ - A `security` configuration section: `trustProxy`, `cookieSecret` and `csrf`.
75
+ - `seeOther()` for the post/redirect/get flow, which needs 303 rather than the
76
+ method-preserving 307 that `redirect()` sends.
77
+ - Island cleanup. A `mount()` function may return a teardown callback; it is now
78
+ stored and called by the new `unmount(root)` export when the subtree leaves
79
+ the DOM.
80
+ - Client helpers for partial updates: `swap()` and `startSwapLinks()` for
81
+ fetching and replacing a region, `enhanceForm()` and `startForms()` for
82
+ submitting forms without a full page load while keeping the no-JavaScript
83
+ path working.
84
+ - A fourth example, `examples/dashboard`: sign-in with a signed cookie session,
85
+ a private page, a paginated table fragment, a CSRF-protected mutation and an
86
+ island with cleanup, covered by its own `smoke.mjs`.
87
+ - An npm version badge in the `README`, linking to the package page.
88
+ - An English edition of the documentation under `docs/en/`, mirroring every
89
+ chapter of the Turkish `docs/`.
90
+ - The dev overlay now compares the installed version against the `latest` tag on
91
+ npm: the Server tab shows the version, marks an `update` chip when a newer
92
+ release exists and offers the upgrade command. The lookup is cached for six
93
+ hours, never blocks the server and can be turned off with
94
+ `JSKELET_VERSION_CHECK=0`.
95
+
96
+ ### Changed
97
+
98
+ - `trust proxy` is now configurable through `security.trustProxy` instead of
99
+ being always on. The default is unchanged, but a server exposed directly to
100
+ the internet should turn it off: while it is on, a client can forge its own
101
+ `X-Forwarded-For` and rate limiting or audit logs see the wrong address.
102
+ - Every message the framework prints is now English: config, router, render,
103
+ cache, prewarm, asset and build warnings, CLI output, the project `jskelet
104
+ init` scaffolds, and the devtools overlay and report interfaces. Visitor-facing
105
+ status pages still follow `brand.lang` and keep their Turkish translations.
106
+ - The dev overlay and report now show the current JSkelet logo, served with a
107
+ cacheable response instead of being re-fetched on every navigation.
108
+
109
+ ### Fixed
110
+
111
+ - A page rendered without `revalidate` used to be sent with no `Cache-Control`
112
+ at all, while still carrying a strong ETag. HTTP treats such a response as
113
+ heuristically cacheable, so an intermediate proxy or the browser's back button
114
+ could store a response meant for a single visitor. Dynamic pages now send
115
+ `private, no-store` and no ETag.
116
+ - A redirect thrown from a route that reads the session is no longer cacheable
117
+ either; a stored "you need to sign in" redirect used to follow the visitor
118
+ even after signing in.
119
+
120
+ ## [0.1.1] - 2026-08-30
121
+
122
+ ### Added
123
+
13
124
  - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
14
125
  a `LICENSE` file, issue and pull request templates, and a CI workflow.
126
+ - An English-first `examples/marketing` with a Turkish translation, serving the
127
+ package documentation under `/docs` and reading its version, dependencies and
128
+ bundle sizes from the installed package.
129
+
130
+ ### Changed
131
+
132
+ - The install instructions point at the npm package instead of the git
133
+ repository.
134
+
135
+ ### Fixed
136
+
137
+ - No more white flash between pages: the page background moved onto the root
138
+ element, so it applies before the body paints. Reduced-motion preferences now
139
+ switch off the decorative animations as well, not just page transitions.
15
140
 
16
- ## [0.1.0]
141
+ ## [0.1.0] - 2026-08-30
17
142
 
18
143
  Initial release.
19
144
 
@@ -36,5 +161,7 @@ Initial release.
36
161
  - Documentation under `docs/` and three examples: `minimal`, `blog`,
37
162
  `marketing`.
38
163
 
39
- [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...HEAD
164
+ [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.2...HEAD
165
+ [0.1.2]: https://github.com/ayberkenis/jskelet/compare/v0.1.1...v0.1.2
166
+ [0.1.1]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...v0.1.1
40
167
  [0.1.0]: https://github.com/ayberkenis/jskelet/releases/tag/v0.1.0
package/README.md CHANGED
@@ -9,6 +9,7 @@ Tailwind v4 stylesheet**, and instead of ISR keeps an in-process **HTML TTL
9
9
  cache** with stale-while-revalidate. No React, no TypeScript — plain JavaScript
10
10
  with JSDoc.
11
11
 
12
+ [![npm version](https://img.shields.io/npm/v/jskelet)](https://www.npmjs.com/package/jskelet)
12
13
  [![Node.js 22+](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
13
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
14
15
 
@@ -171,10 +172,15 @@ client.
171
172
  everything".
172
173
  - **No global state management** beyond the small island store.
173
174
 
174
- If you are building an app-shaped interface behind a login — a dashboard, an
175
- editor, an admin panel — page HTML cannot be cached and this framework is the
176
- wrong tool. The reasoning and a feature-by-feature comparison with Next.js live
177
- in [docs/11-tasima.md](./docs/11-tasima.md).
175
+ An app-shaped interface behind a login — a dashboard, an editor, an admin panel
176
+ — cannot benefit from the HTML cache, which is the main reason to pick this
177
+ framework. It is supported rather than recommended: `route(fn, { private: true })`
178
+ keeps per-visitor pages out of the cache, and signed cookies, CSRF, fragment
179
+ endpoints and region swapping cover the rest
180
+ ([docs/12-panel-ve-oturum.md](./docs/12-panel-ve-oturum.md)). Live data
181
+ transport is deliberately left to you; pick SSE, WebSocket or polling yourself.
182
+ A feature-by-feature comparison with Next.js is in
183
+ [docs/11-tasima.md](./docs/11-tasima.md).
178
184
 
179
185
  ## Project layout
180
186
 
@@ -260,10 +266,11 @@ Only the specifiers in the `exports` map are supported:
260
266
 
261
267
  | Specifier | Contents |
262
268
  | --- | --- |
263
- | `jskelet` | `route`, `createApp`, `startServer`, `notFound`, `redirect`, `cache`, `asset`, `getConfig`, HTML cache and prewarm helpers |
264
- | `jskelet/client` | `register`, `registerAll`, `hydrate`, `start`, `createStore`, DOM helpers |
269
+ | `jskelet` | `route`, `fragment`, `createApp`, `startServer`, `notFound`, `redirect`, `seeOther`, `cache`, `asset`, `getConfig`, cookie helpers, HTML cache and prewarm helpers |
270
+ | `jskelet/client` | `register`, `registerAll`, `hydrate`, `unmount`, `start`, `swap`, `startForms`, `createStore`, DOM helpers |
265
271
  | `jskelet/html` | `attrs`, `cn`, `cx`, `esc`, `jsonScript` |
266
- | `jskelet/tags` | `icon`, `image`, `link`, `preloadImage` |
272
+ | `jskelet/tags` | `icon`, `image`, `link`, `preloadImage`, `csrfField` |
273
+ | `jskelet/cookies` | `parseCookies`, `setCookie`, `clearCookie`, `setSignedCookie`, `getSignedCookie`, `randomToken`, `safeEqual` |
267
274
 
268
275
  Anything reachable by a deeper path is internal and may change without notice.
269
276
 
@@ -295,6 +302,7 @@ written in Turkish; translations are a welcome contribution.
295
302
  | [09-dev-araclari](./docs/09-dev-araclari.md) | Dev workflow, overlay, report page, dev gate |
296
303
  | [10-dagitim](./docs/10-dagitim.md) | Production, Docker, reverse proxy, health checks |
297
304
  | [11-tasima](./docs/11-tasima.md) | Migrating from Next.js: mapping table and plan |
305
+ | [12-panel-ve-oturum](./docs/12-panel-ve-oturum.md) | Per-visitor pages: `private: true`, sessions, CSRF, fragments, swapping |
298
306
 
299
307
  If you work with AI agents, [AGENTS.md](./AGENTS.md) summarizes the rules that
300
308
  apply to this repository.
@@ -305,6 +313,7 @@ apply to this repository.
305
313
  npm --prefix examples/minimal install && npm --prefix examples/minimal run dev
306
314
  npm --prefix examples/blog install && npm --prefix examples/blog run dev
307
315
  npm --prefix examples/marketing install && npm --prefix examples/marketing run dev
316
+ npm --prefix examples/dashboard install && npm --prefix examples/dashboard run dev
308
317
  ```
309
318
 
310
319
  - **`examples/minimal`** — two routes, one component, one island. The smallest
@@ -319,6 +328,11 @@ npm --prefix examples/marketing install && npm --prefix examples/marketing run d
319
328
  are measured in the browser; there are no invented benchmarks. It is also
320
329
  bilingual — English at the root, Turkish under `/tr` — which shows how to build
321
330
  a multi-language site on a framework that ships no i18n of its own.
331
+ - **`examples/dashboard`** — the opposite axis: per-visitor pages. A signed
332
+ cookie session, a `private: true` page that never enters the HTML cache, a
333
+ paginated table fragment, a CSRF-protected form that still works without
334
+ JavaScript, and an island with cleanup. A public landing page sits next to it,
335
+ so a cached response and a `no-store` one are visible side by side.
322
336
 
323
337
  With a server running, `node smoke.mjs` inside an example verifies that its
324
338
  endpoints respond as expected.
package/bin/jskelet.mjs CHANGED
@@ -91,13 +91,13 @@ switch (command) {
91
91
  }
92
92
 
93
93
  default: {
94
- const known = command ? `bilinmeyen komut: ${command}\n\n` : "";
94
+ const known = command ? `unknown command: ${command}\n\n` : "";
95
95
  process.stderr.write(
96
- `${known}kullanım: jskelet <dev|build|start|init>\n\n` +
97
- " dev build watch + sunucu (canlı yenileme, dev overlay)\n" +
98
- " build prod build\n" +
99
- " start prod sunucu\n" +
100
- " init bulunduğun dizine minimal iskelet kurar\n",
96
+ `${known}usage: jskelet <dev|build|start|init>\n\n` +
97
+ " dev build watch + server (live reload, dev overlay)\n" +
98
+ " build production build\n" +
99
+ " start production server\n" +
100
+ " init scaffold a minimal skeleton in the current directory\n",
101
101
  );
102
102
  process.exit(command ? 1 : 0);
103
103
  }
@@ -28,11 +28,13 @@ gerekmez:
28
28
  | Alan | Karşılığı |
29
29
  | --- | --- |
30
30
  | `route` | `jskelet` → `route` |
31
+ | `fragment` | `jskelet` → `fragment` |
31
32
  | `renderView` | `jskelet` → `renderView` |
32
33
  | `renderPage` | `jskelet` → `renderPage` |
33
34
  | `notFound` | `jskelet` → `notFound` |
34
35
  | `redirect` | `jskelet` → `redirect` |
35
36
  | `permanentRedirect` | `jskelet` → `permanentRedirect` |
37
+ | `seeOther` | `jskelet` → `seeOther` |
36
38
 
37
39
  İstersen doğrudan import da edebilirsin; `api` yalnızca kolaylık:
38
40
 
@@ -45,7 +47,7 @@ export function register(app) {
45
47
  ```
46
48
 
47
49
  Modül geçerli bir fonksiyon açmazsa uyarı basılır ve atlanır:
48
- `[router] <dosya> default ya da 'register' fonksiyonu dışa açmıyor, atlandı`.
50
+ `[router] <file> exports neither a default nor a 'register' function, skipped`.
49
51
 
50
52
  ## Yükleme sırası
51
53
 
@@ -97,8 +99,8 @@ Hiç route modülü bulunamazsa uyarı basılır ve sunucu yalnızca statik dosy
97
99
  - `ctx` nesnesini kurar ve controller'ı çağırır.
98
100
  - HTML TTL cache'ini uygular (`revalidate` varsa ve metot `GET` ise).
99
101
  - `notFound()` / `redirect()` kontrol akışını yakalar.
100
- - Yanıt başlıklarını yazar: `Content-Type`, cache'lenebilir yanıtlarda
101
- `Cache-Control`, ve her zaman `X-JSkelet-Cache`.
102
+ - Yanıt başlıklarını yazar: `Content-Type` ve cache durumuna göre
103
+ `Cache-Control` (+ önbelleklenebilir yanıtlarda `X-JSkelet-Cache`).
102
104
  - Önbellekte saklanan sıkıştırılmış gövdeyi kullanarak yanıtı gönderir.
103
105
 
104
106
  ```js
@@ -114,11 +116,42 @@ app.get(
114
116
  );
115
117
  ```
116
118
 
117
- `options` tek bir alan kabul eder:
119
+ `options` iki alan kabul eder:
118
120
 
119
121
  | Alan | Tip | Anlamı |
120
122
  | --- | --- | --- |
121
123
  | `revalidate` | `number` (saniye) | HTML önbellek TTL'i. Verilmezse ya da 0 ise bu route önbelleklenmez. `jskelet.config.mjs` → `cache().html` içindeki eşleşen bir kural bu değeri **ezer**. |
124
+ | `private` | `boolean` | Sayfa ziyaretçiye bağlı. Önbellek devre dışı kalır, `cache().html` deseni bunu **ezemez**, yanıt `private, no-store` ve `Vary: Cookie` ile ETag'siz gider. |
125
+
126
+ Oturuma bağlı her sayfa `private: true` almalı; önbellek anahtarında kimlik
127
+ olmadığı için bayrak olmadan bir kullanıcının HTML'i bir başkasına servis
128
+ edilir. Framework bu hatayı çalışma zamanında da yakalıyor (controller cookie
129
+ okuduğunda render önbelleğe yazılmaz), ama doğru yer bayrak. Ayrıntılar
130
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
131
+
132
+ ## `fragment()` — layout'suz parça
133
+
134
+ Bir bölgeyi tazeleyen uçlar için. Layout basılmaz, yanıt `private, no-store` ve
135
+ ETag'siz gider, HTML önbelleğine hiç uğramaz.
136
+
137
+ ```js
138
+ app.get(
139
+ "/_fragment/satirlar",
140
+ fragment(async ({ query }) => ({
141
+ view: "partials/rows",
142
+ data: { rows: getRows(Number(query.sayfa ?? 1)) },
143
+ })),
144
+ );
145
+ ```
146
+
147
+ Controller `{ view, data?, status? }` ya da doğrudan bir HTML string döner.
148
+ Hata durumunda tüm sayfa yerine küçük bir uyarı parçası döner
149
+ (`<div role="alert" data-fragment-error>`), çünkü takas edilen bölge bir hata
150
+ sayfasının tamamını içine almamalı.
151
+
152
+ `fragment()` POST için de kullanılabilir: form gönderiminin cevabı olarak
153
+ güncellenmiş parçayı döndürmenin yolu bu, ve şablonda `csrfField()`
154
+ çalışabilmesi için gereken istek bağlamını da kuruyor.
122
155
 
123
156
  ## `ctx` — controller bağlamı
124
157
 
@@ -200,18 +233,24 @@ fonksiyon `throw` eder, framework yakalar. Böylece veri katmanındaki bir
200
233
  fonksiyon, controller'a dönüş değeri taşımak zorunda kalmadan 404 üretebilir.
201
234
 
202
235
  ```js
203
- import { notFound, redirect, permanentRedirect } from "jskelet";
236
+ import { notFound, redirect, permanentRedirect, seeOther } from "jskelet";
204
237
 
205
238
  notFound(); // 404 → hooks.notFound() sayfası
206
- redirect("/yeni-adres"); // 307 (geçici)
207
- permanentRedirect("/yeni"); // 308 (kalıcı)
239
+ redirect("/yeni-adres"); // 307 (geçici, metodu korur)
240
+ permanentRedirect("/yeni"); // 308 (kalıcı, metodu korur)
241
+ seeOther("/panel"); // 303 (POST sonrası)
208
242
  ```
209
243
 
210
- Üçü de `never` döner (her zaman fırlatır). Ayrıntı:
244
+ Dördü de `never` döner (her zaman fırlatır). Ayrıntı:
211
245
 
212
246
  - `notFound()` → `NotFoundError` (`statusCode: 404`)
213
247
  - `redirect(location)` → `RedirectError` (`statusCode: 307`)
214
248
  - `permanentRedirect(location)` → `RedirectError` (`statusCode: 308`)
249
+ - `seeOther(location)` → `RedirectError` (`statusCode: 303`)
250
+
251
+ Bir POST handler'ında `redirect()` değil `seeOther()` kullanılır: 307 metodu
252
+ koruyor, yani tarayıcı hedefe yeniden POST ediyor. "Post/redirect/get" akışı —
253
+ geri tuşunun formu yeniden göndermediği akış — 303 gerektiriyor.
215
254
 
216
255
  Özel bir durum kodu gerekiyorsa sınıfı doğrudan kullanabilirsin:
217
256
 
@@ -424,7 +463,7 @@ Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
424
463
  Parametre adı `[A-Za-z_][A-Za-z0-9_]*` kalıbına uymalıdır.
425
464
 
426
465
  `source` mutlaka `/` ile başlamalı; başlamazsa kural yok sayılır ve uyarı
427
- basılır (`[config] geçersiz source (\`/\` ile başlamalı): …`). Tanınmayan bir
466
+ basılır (``[config] invalid source (must start with `/`): …``). Tanınmayan bir
428
467
  sözdizimi sessizce literal kabul edilmez.
429
468
 
430
469
  Tam desen listesi ve config referansı: [07-yapilandirma.md](./07-yapilandirma.md).
@@ -194,8 +194,8 @@ Kurallar:
194
194
  önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
195
195
  bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
196
196
  - Aynı ad iki farklı bileşen dosyasında tanımlıysa uyarı basılır ve **ikincisi
197
- kazanır**: `[components] 'card' iki kez tanımlı: a.js ve b.js — ikincisi
198
- kazanıyor.`
197
+ kazanır**: `[components] 'card' is defined twice: a.js and b.js — the second
198
+ one wins.`
199
199
  - `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
200
200
  bir proje de çalışır.
201
201
 
@@ -205,14 +205,67 @@ start();
205
205
  - Bir element aynı island adıyla **iki kez bağlanmaz**; kayıt element bazında
206
206
  `WeakMap` içinde tutulur.
207
207
  - Kayıtlı olmayan bir ad için konsola uyarı basılır:
208
- `[island] kayıtlı değil: <ad>`.
208
+ `[island] not registered: <name>`.
209
209
  - Modül import'u ya da `mount()` hata verirse konsola hata basılır
210
- (`[island] <ad> yüklenemedi`) ve **sayfanın kalanı etkilenmez**.
210
+ (`[island] <name> failed to load`) ve **sayfanın kalanı etkilenmez**.
211
211
  - `mount()` başarıyla dönerse elemente `data-island-ready="true"` yazılır.
212
- - `mount()` bir fonksiyon döndürebilir; sözleşmede temizlik için ayrılmıştır.
213
- Framework bu fonksiyonu şu anda kendiliğinden çağırmaz — island'ın ömrünü
214
- kendisi yönetiyorsa (ör. bir fragment'ı değiştirirken) dönen fonksiyonu
215
- saklayıp kendisi çağırmalıdır.
212
+ - `mount()` bir temizlik fonksiyonu döndürebilir; framework onu saklar ve
213
+ `unmount()` çağrıldığında işletir (aşağıya bakın).
214
+
215
+ ### `unmount(root?)`
216
+
217
+ `root` altındaki island'ları söker: saklanan temizlik fonksiyonlarını çağırır,
218
+ `data-island-ready` işaretini kaldırır ve kaydı siler, böylece aynı düğüm
219
+ tekrar DOM'a girerse yeniden bağlanabilir. `root`'un kendisi de island olabilir.
220
+
221
+ DOM'un bir bölgesini değiştirirken çağrılması **zorunlu**:
222
+
223
+ ```js
224
+ import { hydrate, unmount } from "jskelet/client";
225
+
226
+ unmount(container);
227
+ container.innerHTML = html;
228
+ hydrate(container);
229
+ ```
230
+
231
+ Atlanması en kolay gözden kaçan sızıntı biçimini üretiyor. `innerHTML` ile
232
+ değiştirilen bir bölgenin island'ları DOM'dan çıkar, ama `document`/`window`
233
+ üzerine kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder;
234
+ birkaç takastan sonra aynı iş onlarca kez çalışır.
235
+
236
+ ```js
237
+ export function mount(element) {
238
+ const timer = setInterval(() => tick(element), 1000);
239
+ const onResize = () => layout(element);
240
+ window.addEventListener("resize", onResize);
241
+
242
+ return () => {
243
+ clearInterval(timer);
244
+ window.removeEventListener("resize", onResize);
245
+ };
246
+ }
247
+ ```
248
+
249
+ `swap()` ve form yardımcıları `unmount()`u kendileri çağırıyor; elle DOM
250
+ değiştirdiğiniz yerlerde siz çağırıyorsunuz.
251
+
252
+ ### `swap(target, url, options?)` ve `startSwapLinks(root?)`
253
+
254
+ Bir bölgeyi sunucudan gelen parçayla değiştirir: eski alt ağacı söker, içeriği
255
+ yazar, yeniden hidre eder ve odağı kaybolmuşsa geri getirir.
256
+
257
+ ```html
258
+ <a href="/_fragment/satirlar?sayfa=2" data-swap="#satirlar">Sonraki</a>
259
+ ```
260
+
261
+ Sunucu tarafı ve tüm seçenekler
262
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
263
+
264
+ ### `enhanceForm(form)` ve `startForms(root?)`
265
+
266
+ `data-enhance` taşıyan formları sayfa yenilemeden gönderir; JS kapalıyken
267
+ normal POST + yönlendirme akışı çalışmaya devam eder. Sözleşmenin tamamı
268
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
216
269
 
217
270
  ## Durum paylaşımı: `createStore`
218
271