jskelet 0.6.0 → 0.6.2

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.
@@ -50,7 +50,7 @@ ayarlamayı düşünmeniz gerekenler:
50
50
  | `HOST` | `0.0.0.0` | Yalnızca IPv4 dinlemek gerekiyorsa. Varsayılan `::` zaten çift yığın dinler |
51
51
  | `PREWARM_MAX` | Site boyutuna göre | Açılışta ısıtılacak sayfa sayısı |
52
52
  | `PREWARM_INTERVAL_SECONDS` | `0` ya da uzun bir değer | Hiç ziyaret edilmeyen sayfaları sıcak tutmak isterseniz |
53
- | `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler |
53
+ | `DEV_GATE` + `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler. Token tek başına siteyi kilitlemez |
54
54
  | `JSKELET_S3_*` | Access log'u S3'e yazıyorsanız | Bucket + credential; ayrıntı [07](./07-yapilandirma.md) |
55
55
 
56
56
  Production'da dosya veya S3 sink açıldığında HTTP access log middleware
@@ -156,15 +156,11 @@ Build aşaması `npx jskelet build` ile bunları kendisi üretir.
156
156
 
157
157
  Bu depodaki örnekler jskelet'i npm'den değil `"jskelet": "file:../.."` ile
158
158
  alıyor. Coolify, Railway, Render gibi araçlarda "base directory" olarak
159
- `examples/marketing` verilirse build context yalnızca o dizin olur, `../..`
159
+ `examples/blog` verilirse build context yalnızca o dizin olur, `../..`
160
160
  context'in dışında kalır ve kurulum `npm ci`de düşer. Doğru ayar: **base
161
- directory `/`**, Dockerfile konumu `/examples/marketing/Dockerfile`. Çalışan
162
- örnek `examples/marketing/Dockerfile` içinde ve context'i depo kökü kabul eder:
163
-
164
- ```bash
165
- docker build -f examples/marketing/Dockerfile -t jskelet-marketing .
166
- docker run --rm -p 3000:3000 -e SITE_URL=https://example.com jskelet-marketing
167
- ```
161
+ directory `/`** (depo kökü) ve imajı yukarıdaki çok aşamalı Dockerfile ile
162
+ uygulama dizinine göre uyarlamak — ya da jskelet'i npm bağımlılığı olarak
163
+ kurup context'i uygulamanın kendi dizini yapmak.
168
164
 
169
165
  Kendi uygulamanızda jskelet normal bir bağımlılık olacağı için bu kısıt yoktur;
170
166
  yukarıdaki çok aşamalı imaj yeterli.
@@ -173,8 +169,8 @@ yukarıdaki çok aşamalı imaj yeterli.
173
169
 
174
170
  Framework hazır bir sağlık kontrolü ucu **eklemez**; kendi route'unuza koymanız
175
171
  gerekir. Varsayılan `devGateBypass` listesi `/api/healthcheck` yolunu içerdiği
176
- için bu adı kullanmak en az sürprizli seçenektir: `DEV_TOKEN` ayarlı bir ortamda
177
- bile erişilebilir kalır.
172
+ için bu adı kullanmak en az sürprizli seçenektir: dev gate açıkken bile
173
+ erişilebilir kalır.
178
174
 
179
175
  ```js
180
176
  // routes/00-health.mjs
@@ -331,7 +327,7 @@ istek başına iş neredeyse sıfıra iner.
331
327
  - [ ] `hooks.prewarmPaths()` en önemli sayfaları başa koyuyor
332
328
  - [ ] `headers()` içinde CSP ve güvenlik başlıkları tanımlı
333
329
  - [ ] Sağlık kontrolü ucu var ve `devGateBypass` listesinde
334
- - [ ] Staging'de `DEV_TOKEN` ayarlı, prod'da **ayarlı değil**
330
+ - [ ] Staging'de `DEV_GATE=1` ve `DEV_TOKEN` ayarlı, prod'da gate **kapalı**
335
331
  - [ ] Ters proxy `Accept-Encoding`i iletiyor ve kendi sıkıştırmasını yapmıyor
336
332
  - [ ] `clientEnv` listesinde gizli anahtar yok
337
333
 
package/docs/README.md CHANGED
@@ -47,7 +47,7 @@ eşlenik tutuluyor; birini değiştiriyorsan diğerini de değiştir.
47
47
 
48
48
  ## Çalışan örnekler
49
49
 
50
- Dördü de çalışır durumda; belgelerdeki örneklerin çoğu buralardan alınmıştır.
50
+ Üçü de çalışır durumda; belgelerdeki örneklerin çoğu buralardan alınmıştır.
51
51
 
52
52
  **`examples/minimal/`** — iki route, bir bileşen, bir island, minimal config.
53
53
  Framework'ün en küçük çalışan hâli.
@@ -68,32 +68,7 @@ npm --prefix examples/blog install
68
68
  npm --prefix examples/blog run dev
69
69
  ```
70
70
 
71
- **`examples/marketing/`** — framework'ün kendi tanıtım sitesi: hero, kıyaslama
72
- tablosu, canlı gecikme ölçümü, SSS, belgeler dizini, sürüm notları ve indirme
73
- sayfası. Sayfadaki bayt sayıları `lib/payload.js` içinde sitenin **kendi** build
74
- çıktısından, sürüm künyesi ise `lib/release.js` içinde kurulu paketin
75
- `package.json`'ından okunur; gecikme sayıları `latency` island'ında tarayıcıda
76
- ölçülür. Uzun TTL (bir saat) ve tüm sayfaları ısıtan prewarm ile, cache'in en
77
- verimli çalıştığı profili gösterir.
78
-
79
- Site aynı zamanda **bu belgeleri** servis ediyor: `/docs/<bölüm>` adresleri
80
- `node_modules/jskelet/docs/` altındaki markdown dosyalarını okuyup sol gezinme,
81
- "bu sayfada" listesi ve sıralı geçişle basıyor. Çevirici `lib/markdown.js`
82
- içinde küçük bir modül — bağımlılık yok — ve kaynak paketin kendisi olduğu için
83
- site kurulu sürümden hiç ayrışmıyor.
84
-
85
- Site aynı zamanda **iki dilli**: varsayılan İngilizce kökte, Türkçe `/tr`
86
- altında ve route adları iki dilde de aynı. Framework'te i18n yok; dil
87
- çözümlemesi `lib/i18n.js` içinde uygulamanın kendi sözleşmesi olarak duruyor ve
88
- `hooks.layoutContext` ile bir sözlüğe bağlanıyor. Çok dilli bir siteyi bu
89
- yüzeyle nasıl kurabileceğinizi görmek için bakılacak yer burası.
90
-
91
- ```bash
92
- npm --prefix examples/marketing install
93
- npm --prefix examples/marketing run dev
94
- ```
95
-
96
- **`examples/dashboard/`** — diğer üçünün tersi eksen: kişiye özel sayfalar.
71
+ **`examples/dashboard/`** — diğer ikisinin tersi eksen: kişiye özel sayfalar.
97
72
  İmzalı cookie ile giriş, `private: true` korumalı panel, sayfalı tablo
98
73
  fragment'i, CSRF'li mutasyon formu ve temizlik fonksiyonu döndüren bir island.
99
74
  Public bir tanıtım sayfası da var, böylece aynı uygulamada önbelleklenen ve
@@ -104,5 +79,5 @@ npm --prefix examples/dashboard install
104
79
  npm --prefix examples/dashboard run dev
105
80
  ```
106
81
 
107
- Her dört örnekte `node smoke.mjs` sunucu ayaktayken uçların beklendiği gibi
82
+ Her üç örnekte `node smoke.mjs` sunucu ayaktayken uçların beklendiği gibi
108
83
  yanıt verdiğini doğrular.
@@ -36,9 +36,10 @@ Request
36
36
  ├─ rewrites(beforeFiles) config → proxy or a change to req.url
37
37
  ├─ compression brotli/gzip negotiation (quality 5)
38
38
  ├─ headers static cache + config headers()
39
- ├─ devGate if DEV_TOKEN is set, 404 without a token
39
+ ├─ devGate if the gate is on, 404 without a token
40
40
  ├─ redirects config redirects(), first match wins
41
41
  ├─ trailingSlash 308 when config trailingSlash is true
42
+ ├─ robots.txt appends framework Disallow rules to the user's body
42
43
  ├─ staticPrecompressed .br/.gz copies produced at build (quality 11)
43
44
  ├─ express.static files under public/
44
45
  ├─ (dev) devtools only when NODE_ENV=development
@@ -71,6 +72,11 @@ position has a reason, and moving things around leads to silent breakage.
71
72
  leak even its redirect rules to the outside. `trailingSlash` sits after config
72
73
  redirects so explicit rules see the requested path first; the canonical slash
73
74
  form is enforced as a second step.
75
+ - **`robots.txt` before static, and inside compression.** The body the
76
+ application wrote is left intact; the framework appends `Disallow` rules
77
+ for its own endpoints. A response that already has `Content-Encoding` is
78
+ not rewritten — the block is added to plain text, and compression stays
79
+ outside.
74
80
  - **`staticPrecompressed` before `express.static`.** If there are `.br`/`.gz`
75
81
  copies produced at build time, those are served (brotli quality 11);
76
82
  otherwise the request falls through to the `static` below it and the
@@ -158,7 +158,9 @@ stored), but the flag is the right place. Details in
158
158
  ## `fragment()` — a partial without the layout
159
159
 
160
160
  For endpoints that refresh a region. No layout is printed, the response is sent
161
- with `private, no-store` and no ETag, and it never touches the HTML cache.
161
+ with `private, no-store` and no ETag, and it never touches the HTML cache. The
162
+ `/_fragment/` prefix is appended to `robots.txt`, so partial endpoints are not
163
+ indexed ([04](./04-rendering.md#robotstxt)).
162
164
 
163
165
  ```js
164
166
  app.get(
@@ -510,6 +510,30 @@ The `renderHeadMeta(metadata)` function is exported; it can be used when you
510
510
  need to produce the same tags outside the layout (for example in a fragment or
511
511
  an email).
512
512
 
513
+ ## robots.txt
514
+
515
+ The application writes `robots.txt`: `public/robots.txt` or a plain route.
516
+ The framework does not change that body; it appends a JSkelet note and
517
+ `Disallow` rules **under** a successful text response. If there is no file
518
+ and no route, the framework does not invent a `robots.txt`.
519
+
520
+ Paths added:
521
+
522
+ - `/_jskelet/` — admin panel, remote image proxy, auth handoff
523
+ - `/__jskelet/` — development tools
524
+ - `/_fragment/` — partial responses without a layout
525
+
526
+ An endpoint moved off those prefixes is added too, but only when it is
527
+ actually mounted: `admin.basePath`, `images.remote.path`,
528
+ `auth.crossSubdomainHandoff.path`. `brand.devBasePath` is written only in
529
+ development; in production that path may be the application's own page.
530
+
531
+ The note starts with the configured brand name (`brand.name`, default
532
+ `JSkelet`). The trailing group repeats `User-agent: *` together with every
533
+ other agent already named in the file. Google does not merge a
534
+ crawler-specific group with `*`; it does merge a second group for the same
535
+ agent. If the note is already in the file, it is not appended again.
536
+
513
537
  ## Dynamic OG images
514
538
 
515
539
  Counterpart to Next.js `ImageResponse` / `opengraph-image.tsx`. There is no JSX:
@@ -231,7 +231,12 @@ price is acceptable, because live fields such as prices are updated on the
231
231
  client over WebSocket.
232
232
 
233
233
  The store is an LRU: an accessed entry is moved to the end, and once the limit
234
- (`cache().maxEntries`, 500 by default) is exceeded the oldest is evicted.
234
+ (`cache().maxEntries`, 500 by default) is exceeded the oldest is evicted. Config
235
+ may ask for more than 500; it **cannot exceed 800** — a higher value is clamped
236
+ to 800 with a warning. Separately, in-process HTML strings plus compressed
237
+ bodies **cannot exceed 256 MB**. A fat page or a `vary.host` copy that is still
238
+ under the count limit is evicted by this budget too. A single page larger than
239
+ 256 MB is not stored; that response is still sent.
235
240
 
236
241
  ## What gets written to the cache
237
242
 
@@ -1145,9 +1150,9 @@ export default {
1145
1150
  html: { "/": 60, "/news/:slug": 300 },
1146
1151
  prewarm: {
1147
1152
  onVisit: {
1148
- perPage: 20, // at most this many links per page
1149
- concurrency: 2, // optional
1150
- rps: 4, // optional; 0 = unlimited
1153
+ perPage: 20, // at most this many links per page; ceiling 20
1154
+ concurrency: 2, // ceiling 2
1155
+ rps: 2, // ceiling 2; 0 is clamped to 2 as well
1151
1156
  },
1152
1157
  },
1153
1158
  };
@@ -1163,7 +1168,15 @@ Rules:
1163
1168
  degraded or `no-store` responses do not extract links.
1164
1169
  - The warmer's own UA (`brand.prewarmUserAgent`) does not trigger — no crawl
1165
1170
  loop.
1166
- - Paths that are already fresh are not enqueued.
1171
+ - Paths that are already fresh are not enqueued. Real keys look like
1172
+ `h=host|/path?`; the check sees the vary prefix and the trailing `?`.
1173
+ With `vary.host`, only this request's host counts as fresh.
1174
+ - The pending queue holds at most 64 paths; links beyond that are left for a
1175
+ later response.
1176
+ - `perPage` 20, `rps` 2 and `concurrency` 2 are ceilings. A higher value (and
1177
+ `rps: 0`) is clamped with a warning. Warm requests stay on loopback; when
1178
+ `vary.host` is on, the public host is sent as `x-forwarded-host`, so a second
1179
+ `h=127.0.0.1` entry is not created.
1167
1180
  - `nofollow`, `target="_blank"`, `data-no-prefetch`, `prewarmSkip` and
1168
1181
  `navigation.exclude` share the same exemptions as Speculation Rules.
1169
1182
  - Query strings are not warmed (default cache policy treats query as dynamic).
@@ -1308,8 +1321,9 @@ The requests go out with the headers `user-agent: jskelet-prewarm`
1308
1321
  (`brand.prewarmUserAgent`) and `accept-encoding: br, gzip`; the second one so
1309
1322
  that the compressed body enters the cache too.
1310
1323
 
1311
- If `DEV_TOKEN` is set, the warm-up carries the token as a cookie; otherwise the
1312
- dev gate returns 404 for all pages and the cache never fills.
1324
+ If the dev gate is on, the warm-up carries the token as a cookie; otherwise the
1325
+ gate returns 404 for all pages and the cache never fills. `DEV_TOKEN` alone
1326
+ does not turn the gate on.
1313
1327
 
1314
1328
  The request list in the dev panel and the terminal filter out requests carrying
1315
1329
  `prewarmUserAgent`: so that hundreds of warm-up requests do not flood the view.
@@ -298,6 +298,21 @@ static: {
298
298
  }
299
299
  ```
300
300
 
301
+ ## `devGate`
302
+
303
+ **Type:** `boolean` — **Default:** `false`
304
+
305
+ Hides an environment that is not public yet. **`DEV_TOKEN` alone does not lock
306
+ the site.** A shared task definition can carry the same variable into
307
+ production; visitors are not required to present a token, and the site stays
308
+ open.
309
+
310
+ Turn the gate on with `devGate: true` or `DEV_GATE=1`. Then a request without
311
+ the token gets a 404. `DEV_GATE=0` also turns off a gate the config enabled.
312
+ If the token is empty, requests still pass even when the gate is on.
313
+
314
+ Details: [09-dev-tools.md](./09-dev-tools.md).
315
+
301
316
  ## `devGateBypass`
302
317
 
303
318
  **Type:** `string[]` — **Default:**
@@ -305,8 +320,7 @@ static: {
305
320
 
306
321
  **Exact** paths the dev gate never closes off under any circumstances (not a
307
322
  prefix, an exact match). This is so that the health check and the robots files
308
- stay reachable in an environment where `DEV_TOKEN` is set. If provided, it
309
- replaces the default.
323
+ stay reachable while the gate is on. If provided, it replaces the default.
310
324
 
311
325
  Details: [09-dev-tools.md](./09-dev-tools.md).
312
326
 
@@ -441,7 +455,8 @@ body > footer { view-transition-name: site-footer; }
441
455
  ::view-transition-new(root) { animation-duration: 180ms; }
442
456
  ```
443
457
 
444
- A working version lives in `examples/marketing/styles/globals.css`.
458
+ Copy the Tailwind `@source` directives and view-transition CSS into your own
459
+ app's `styles/globals.css`; the blocks above are a starting point.
445
460
 
446
461
  **If you use CSP**, the rules are emitted as an inline
447
462
  `<script type="speculationrules">`; your `script-src` policy needs to allow it.
@@ -771,6 +786,10 @@ raising this number burns through memory quickly; trying to solve a site with
771
786
  tens of thousands of paths from here is the wrong layer — the right place is
772
787
  `cache().data`.
773
788
 
789
+ **Ceiling 800.** A higher value is clamped to 800 with a warning at load.
790
+ In-process HTML plus compressed bodies also cannot exceed 256 MB; config
791
+ cannot raise that budget.
792
+
774
793
  ### `cache().data`
775
794
 
776
795
  The upstream data cache (`withDataCache`). Details:
@@ -778,7 +797,7 @@ The upstream data cache (`withDataCache`). Details:
778
797
 
779
798
  | Field | Type | Default | Meaning |
780
799
  | --- | --- | --- | --- |
781
- | `maxEntries` | `number` | `10000` | The LRU entry limit. The limit is high because JSON is tens of times smaller than HTML. |
800
+ | `maxEntries` | `number` | `10000` | The LRU entry limit. The limit is high because JSON is tens of times smaller than HTML. **Ceiling 20,000**; a higher value is clamped with a warning. |
782
801
  | `staleFactor` | `number` | `10` | For how many TTLs an entry stays usable after the TTL expired. `0` → no stale serving. |
783
802
 
784
803
  ### `cache().trackUpstream`
@@ -1023,13 +1042,13 @@ prewarm: {
1023
1042
  | Field | Type | Default | Meaning |
1024
1043
  | --- | --- | --- | --- |
1025
1044
  | `onVisit` | `true \| false \| object` | off | Visit-driven warming |
1026
- | `onVisit.perPage` | `number` | `20` | At most how many links per page (top to bottom) |
1027
- | `onVisit.concurrency` | `number` | same as classic | Parallel workers |
1028
- | `onVisit.rps` | `number` | same as classic | Requests per second cap; `0` unlimited |
1045
+ | `onVisit.perPage` | `number` | `20` | At most how many links per page (top to bottom). **Ceiling 20** |
1046
+ | `onVisit.concurrency` | `number` | `2` | Parallel workers. **Ceiling 2** |
1047
+ | `onVisit.rps` | `number` | `2` | Requests per second cap. **Ceiling 2**; `0` is clamped to 2 as well |
1029
1048
 
1030
1049
  ```js
1031
1050
  prewarm: {
1032
- onVisit: { perPage: 20, rps: 4 },
1051
+ onVisit: { perPage: 20, rps: 2 },
1033
1052
  }
1034
1053
  ```
1035
1054
 
@@ -1129,7 +1148,8 @@ and no warning is printed.
1129
1148
  | `PORT` | `startServer` | `3000` | Port to listen on. If busy, the process refuses to start; `jskelet start|dev --murder` kills the listener |
1130
1149
  | `HOST` | `startServer` | `::` | Interface to bind to. The default listens dual-stack (IPv6 + IPv4); it falls back to `0.0.0.0` where IPv6 is unavailable |
1131
1150
  | `JSKELET_SECRET` | `jskelet/cookies` | — | The signed cookie secret. Read when `security.cookieSecret` is not set; if neither exists, the signed cookie API throws. [12](./12-dashboards-and-sessions.md) |
1132
- | `DEV_TOKEN` | `devGate`, `prewarm` | — | If set, every request without a token gets a 404. Prewarming carries the token as a cookie. [09](./09-dev-tools.md) |
1151
+ | `DEV_GATE` | `devGate` | off | `1` turns the gate on, `0` turns it off even when config enabled it. `DEV_TOKEN` alone does not turn it on. [09](./09-dev-tools.md) |
1152
+ | `DEV_TOKEN` | `devGate`, `prewarm` | — | The secret expected while the gate is on. If it is missing, or the gate is off, the site stays public. Prewarming carries the token as a cookie only while the gate is on. [09](./09-dev-tools.md) |
1133
1153
  | `JSKELET_ADMIN` | `createApp` | — | When set, turns the admin panel on; `0` turns off a panel enabled in the config. The env wins because the panel is usually opened once during an incident. [06](./06-caching.md) |
1134
1154
  | `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log target: bucket or `bucket/prefix` path. With credentials, the sink turns on automatically |
1135
1155
  | `JSKELET_S3_BUCKET` | `logs.s3` | — | Bucket when `JSKELET_LOG_BUCKET` is unset; joins with `JSKELET_S3_KEY_PREFIX` |
@@ -303,11 +303,20 @@ production process.
303
303
 
304
304
  ## Dev gate — `DEV_TOKEN`
305
305
 
306
- To hide an environment that is not public yet: while `DEV_TOKEN` is set,
307
- **every** request without the token gets a 404.
306
+ To hide an environment that is not public yet. **The framework does not require
307
+ the token:** a `DEV_TOKEN` sitting in the environment does not lock the site.
308
+ You turn the gate on.
308
309
 
309
310
  ```bash
310
- DEV_TOKEN=a-long-random-string npm start
311
+ DEV_GATE=1 DEV_TOKEN=a-long-random-string npm start
312
+ ```
313
+
314
+ The same thing from config:
315
+
316
+ ```js
317
+ export default {
318
+ devGate: true,
319
+ };
311
320
  ```
312
321
 
313
322
  Access:
@@ -328,10 +337,12 @@ Behavior:
328
337
  `/site.webmanifest`, `/favicon.ico`. If your health check lives at a different
329
338
  path, remember to add it to this list, otherwise your orchestrator will see a
330
339
  404.
331
- - Without `DEV_TOKEN` the middleware is entirely disabled and costs nothing in
332
- production.
333
- - Since warming makes requests to its own server, it carries the token as a
334
- cookie; without it every page gets a 404 and the cache never fills up
340
+ - While `devGate` is off (the default) or `DEV_TOKEN` is empty, the middleware
341
+ passes the request through. A `DEV_TOKEN` that leaked into a production task
342
+ does not ask visitors for a token; startup prints a warning.
343
+ - `DEV_GATE=0` turns the gate off even when config says `devGate: true`.
344
+ - While the gate is on, warming carries the token as a cookie; without it every
345
+ page gets a 404 and the cache never fills up
335
346
  ([06-caching.md](./06-caching.md)).
336
347
 
337
348
  In the middleware chain the gate sits after `headers` and **before**
@@ -51,7 +51,7 @@ considering in production:
51
51
  | `HOST` | `0.0.0.0` | Only if you need to listen on IPv4 alone; the `::` default already listens dual-stack |
52
52
  | `PREWARM_MAX` | Depends on site size | Number of pages warmed at startup |
53
53
  | `PREWARM_INTERVAL_SECONDS` | `0` or a long value | If you want to keep never-visited pages warm |
54
- | `DEV_TOKEN` | Staging only | Hides an environment that is not public yet |
54
+ | `DEV_GATE` + `DEV_TOKEN` | Staging only | Hides an environment that is not public yet. The token alone does not lock the site |
55
55
  | `JSKELET_S3_*` | If you write access logs to S3 | Bucket + credentials; details in [07](./07-configuration.md) |
56
56
 
57
57
  When a file or S3 sink is enabled in production, the HTTP access log middleware
@@ -157,16 +157,11 @@ The build stage produces these itself with `npx jskelet build`.
157
157
 
158
158
  The examples in this repo pull jskelet with `"jskelet": "file:../.."` rather
159
159
  than from npm. In tools like Coolify, Railway or Render, if you set the "base
160
- directory" to `examples/marketing`, the build context becomes only that
161
- directory, `../..` falls outside the context, and installation fails at
162
- `npm ci`. The correct setting: **base directory `/`**, Dockerfile location
163
- `/examples/marketing/Dockerfile`. The working example is in
164
- `examples/marketing/Dockerfile` and assumes the repo root as its context:
165
-
166
- ```bash
167
- docker build -f examples/marketing/Dockerfile -t jskelet-marketing .
168
- docker run --rm -p 3000:3000 -e SITE_URL=https://example.com jskelet-marketing
169
- ```
160
+ directory" to `examples/blog`, the build context becomes only that directory,
161
+ `../..` falls outside the context, and installation fails at `npm ci`. The
162
+ correct setting: **base directory `/`** (the repo root) and adapt the
163
+ multi-stage Dockerfile above to the app directory — or install jskelet as a
164
+ normal npm dependency and use the app directory as the context.
170
165
 
171
166
  In your own application jskelet will be an ordinary dependency, so this
172
167
  constraint does not apply; the multi-stage image above is enough.
@@ -176,7 +171,7 @@ constraint does not apply; the multi-stage image above is enough.
176
171
  The framework does **not** add a ready-made health check endpoint; you have to
177
172
  put it in your own route. Since the default `devGateBypass` list contains
178
173
  `/api/healthcheck`, using that name is the least surprising option: it stays
179
- reachable even in an environment with `DEV_TOKEN` set.
174
+ reachable even while the dev gate is on.
180
175
 
181
176
  ```js
182
177
  // routes/00-health.mjs
@@ -333,7 +328,7 @@ goes up, the work per request drops to almost zero.
333
328
  - [ ] `hooks.prewarmPaths()` puts the most important pages first
334
329
  - [ ] CSP and security headers are defined in `headers()`
335
330
  - [ ] A health check endpoint exists and is in the `devGateBypass` list
336
- - [ ] `DEV_TOKEN` is set on staging and **not set** in production
331
+ - [ ] Staging has `DEV_GATE=1` and `DEV_TOKEN`; production leaves the gate **off**
337
332
  - [ ] The reverse proxy forwards `Accept-Encoding` and does not do its own
338
333
  compression
339
334
  - [ ] There are no secret keys in the `clientEnv` list
package/docs/en/README.md CHANGED
@@ -50,7 +50,7 @@ change one, change the other.
50
50
 
51
51
  ## Runnable examples
52
52
 
53
- All four are in working order; most of the examples in the docs were taken from
53
+ All three are in working order; most of the examples in the docs were taken from
54
54
  them.
55
55
 
56
56
  **`examples/minimal/`** — two routes, one component, one island, minimal config.
@@ -72,34 +72,7 @@ npm --prefix examples/blog install
72
72
  npm --prefix examples/blog run dev
73
73
  ```
74
74
 
75
- **`examples/marketing/`** — the framework's own marketing site: hero, comparison
76
- table, live latency measurement, FAQ, docs index, release notes and a download
77
- page. The byte counts on the page are read in `lib/payload.js` from the site's
78
- **own** build output, and the release info in `lib/release.js` from the
79
- installed package's `package.json`; the latency numbers are measured in the
80
- browser by the `latency` island. With a long TTL (one hour) and a prewarm that
81
- warms every page, it shows the profile in which the cache works most
82
- efficiently.
83
-
84
- It also serves **these documents**: `/docs/<chapter>` reads the markdown files
85
- in `node_modules/jskelet/docs/` and renders them with a sidebar, an "on this
86
- page" list and sequential navigation. The renderer is a small module in
87
- `lib/markdown.js` — no dependency — and the source of truth stays the package,
88
- so the site never drifts from the installed version.
89
-
90
- The site is also **bilingual**: English by default at the root, Turkish under
91
- `/tr`, with the same route names in both languages. There is no i18n in the
92
- framework; language resolution lives in `lib/i18n.js` as the application's own
93
- contract and is wired to a dictionary via `hooks.layoutContext`. This is the
94
- place to look if you want to see how to build a multilingual site with this
95
- surface.
96
-
97
- ```bash
98
- npm --prefix examples/marketing install
99
- npm --prefix examples/marketing run dev
100
- ```
101
-
102
- **`examples/dashboard/`** — the opposite axis from the other three: per-visitor
75
+ **`examples/dashboard/`** — the opposite axis from the other two: per-visitor
103
76
  pages. Sign-in with a signed cookie session, a `private: true` protected panel,
104
77
  a paginated table fragment, a CSRF-protected mutation form and an island that
105
78
  returns a cleanup function. It also has a public landing page, so a cached
@@ -110,5 +83,5 @@ npm --prefix examples/dashboard install
110
83
  npm --prefix examples/dashboard run dev
111
84
  ```
112
85
 
113
- In all four examples, `node smoke.mjs` verifies that the endpoints respond as
86
+ In all three examples, `node smoke.mjs` verifies that the endpoints respond as
114
87
  expected while the server is up.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "A framework that feels like no framework: Express 5 + build-time .jsk SSR (optional EJS peer), vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -135,4 +135,4 @@
135
135
  "globals": "^16",
136
136
  "typescript": "^7.0.2"
137
137
  }
138
- }
138
+ }
@@ -106,18 +106,34 @@ export const DEFAULT_PREWARM_ON_VISIT = {
106
106
  /** Sayfa başına üstten alta en fazla kaç link kuyruğa alınır. */
107
107
  perPage: 20,
108
108
  /**
109
- * Paralel işçi; `null` → klasik prewarm ile aynı varsayılan
110
- * (prod 4 / dev 1) `startPrewarm` içinde çözülür.
109
+ * Paralel işçi. `null` yalnızca kapalı modda durur; açıkken tavan
110
+ * (`ON_VISIT_CONCURRENCY_CEILING`) uygulanır. Klasik prewarm'daki
111
+ * prod 4 burada geçerli değil — onVisit sürekli çalışır.
111
112
  * @type {number | null}
112
113
  */
113
114
  concurrency: null,
114
115
  /**
115
- * Saniyedeki istek tavanı; `null` → klasik ile aynı (prod 0 / dev 4).
116
+ * Saniyedeki istek tavanı. `null` yalnızca kapalı modda durur; açıkken
117
+ * `0` (sınırsız) dahil her şey `ON_VISIT_RPS_CEILING` ile kesilir.
116
118
  * @type {number | null}
117
119
  */
118
120
  rps: null,
119
121
  };
120
122
 
123
+ /**
124
+ * onVisit sürekli tur olduğu için klasik prewarm'daki "sınırsız" burada yok.
125
+ * Config daha yükseğini yazsa da çözümlenen değer bu tavanları geçemez.
126
+ */
127
+ export const ON_VISIT_PER_PAGE_CEILING = 20;
128
+ export const ON_VISIT_RPS_CEILING = 2;
129
+ export const ON_VISIT_CONCURRENCY_CEILING = 2;
130
+
131
+ /**
132
+ * Bekleyen onVisit yolları. Bir sayfa düzinelerce link basınca kuyruk
133
+ * birikmesin; taşan link bu turda alınmaz.
134
+ */
135
+ export const ON_VISIT_QUEUE_MAX = 64;
136
+
121
137
  /** `onVisit` açıkken `cache().prewarm` kökünde yasak olan klasik alanlar. */
122
138
  export const CLASSIC_PREWARM_KEYS = [
123
139
  "enabled",
@@ -136,9 +152,26 @@ export const CLASSIC_PREWARM_KEYS = [
136
152
  * HTML önbelleğinin girdi sınırı. 500 girdi ortalama bir sayfa boyutunda
137
153
  * yaklaşık 100-200 MB tutar; uzun kuyruklu siteler bunu yükseltmek yerine
138
154
  * veri önbelleğine yaslanmalı (bkz. `DEFAULT_DATA_CACHE`).
155
+ *
156
+ * Config daha yükseğini istese de girdi sayısı `HTML_CACHE_MAX_ENTRIES_CEILING`
157
+ * değerini geçemez. Asıl bellek freni bayt bütçesidir: şişman sayfa ve
158
+ * `vary.host` kopyası sayı tavanının altında da RSS'i şişirir.
139
159
  */
140
160
  export const DEFAULT_HTML_CACHE_MAX_ENTRIES = 500;
141
161
 
162
+ /** `cache().maxEntries` için sert tavan. Üstü uyarıyla bu değere çekilir. */
163
+ export const HTML_CACHE_MAX_ENTRIES_CEILING = 800;
164
+
165
+ /**
166
+ * Süreç içi HTML string + sıkıştırılmış gövde tavanı (256 MB).
167
+ * `install()` bunu uygular; config yükseltemez. Tek sayfa bütçeden büyükse
168
+ * saklanmaz, yanıt yine gider.
169
+ */
170
+ export const HTML_CACHE_BYTE_BUDGET = 256 * 1024 * 1024;
171
+
172
+ /** `cache().data.maxEntries` için sert tavan. Uzun kuyruk burada durur, HTML'de değil. */
173
+ export const DATA_CACHE_MAX_ENTRIES_CEILING = 20_000;
174
+
142
175
  /**
143
176
  * `notFound()` geçici bir upstream hatasına denk geldiğinde sayfanın kaç kez
144
177
  * daha denenmesi gerektiği.