jskelet 0.6.2 → 0.6.4

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 (43) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +2 -0
  3. package/docs/02-mimari.md +1 -0
  4. package/docs/04-render-ve-sablonlar.md +9 -3
  5. package/docs/06-cache.md +53 -9
  6. package/docs/07-yapilandirma.md +1208 -1191
  7. package/docs/08-build.md +2 -1
  8. package/docs/10-dagitim.md +16 -6
  9. package/docs/12-panel-ve-oturum.md +2 -1
  10. package/docs/en/02-architecture.md +2 -1
  11. package/docs/en/04-rendering.md +9 -3
  12. package/docs/en/06-caching.md +54 -9
  13. package/docs/en/07-configuration.md +24 -9
  14. package/docs/en/08-build.md +2 -1
  15. package/docs/en/10-deployment.md +17 -6
  16. package/docs/en/12-dashboards-and-sessions.md +2 -1
  17. package/package.json +1 -1
  18. package/src/config/defaults.js +541 -518
  19. package/src/config/index.js +1500 -1456
  20. package/src/init.mjs +2 -0
  21. package/src/server/cache-blob.js +70 -0
  22. package/src/server/cache-control.js +45 -0
  23. package/src/server/data-cache.js +118 -27
  24. package/src/server/disk-cache.js +233 -0
  25. package/src/server/html-cache.js +90 -16
  26. package/src/server/image-optimizer.js +95 -2
  27. package/src/server/logs/file-sink.js +159 -32
  28. package/src/server/logs/pipeline.js +10 -3
  29. package/src/server/middleware/static-precompressed.js +31 -10
  30. package/src/server/og-image.js +17 -4
  31. package/src/server/prewarm.js +25 -1
  32. package/src/server/redis.js +31 -12
  33. package/src/server/render.js +910 -910
  34. package/types/config/defaults.d.ts +21 -1
  35. package/types/config/index.d.ts +14 -0
  36. package/types/server/cache-blob.d.ts +13 -0
  37. package/types/server/cache-control.d.ts +28 -0
  38. package/types/server/data-cache.d.ts +9 -0
  39. package/types/server/disk-cache.d.ts +36 -0
  40. package/types/server/html-cache.d.ts +26 -3
  41. package/types/server/logs/file-sink.d.ts +16 -5
  42. package/types/server/og-image.d.ts +5 -0
  43. package/types/server/redis.d.ts +2 -1
package/docs/08-build.md CHANGED
@@ -343,7 +343,8 @@ orijinal dosyaya döner. Watch turunda hiç çalışmaz.
343
343
  `images.remote.allowHosts` verilirse `createApp` `/_jskelet/image` ucunu
344
344
  mount eder. CMS / CDN kapakları build'e girmediği için `image()` bu host'lardaki
345
345
  URL'leri `?url=&w=` biçiminde yeniden yazar; uç sharp ile webp üretir ve
346
- `.jskelet/image-cache/` altına yazar. Upstream fetch redirect'leri elle takip
346
+ `.jskelet/image-cache/` altına yazar. Dizin 256 MB'yi geçince en eski dosya
347
+ düşer. Upstream fetch redirect'leri elle takip
347
348
  edilir: her hop allowlist + private IP / DNS kontrolünden geçer (açık redirect
348
349
  SSRF kapalı). Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
349
350
 
@@ -275,16 +275,26 @@ server {
275
275
 
276
276
  ### CDN ile birlikte
277
277
 
278
- Önbelleklenebilir sayfalara yazılan başlık:
278
+ Önbelleklenebilir sayfalara yazılan başlıklar:
279
279
 
280
280
  ```
281
- Cache-Control: public, max-age=0, s-maxage=<revalidate>, stale-while-revalidate=60
281
+ Cache-Control: public, max-age=0
282
+ CDN-Cache-Control: max-age=<html ttl>, stale-while-revalidate=<staleWhileRevalidate>
282
283
  ```
283
284
 
284
- `max-age=0` tarayıcıda saklamayı kapatır, `s-maxage` CDN'e süreyi bildirir. Yani
285
- aynı tazelik modeli iki katmanda birlikte çalışır: CDN `s-maxage` boyunca kendi
286
- kopyasını verir, süresi geçtiğinde origin'e sorar ve origin de kendi
287
- önbelleğinden anında yanıtlar.
285
+ `max-age=0` tarayıcıda saklamayı kapatır. Edge süresi `CDN-Cache-Control`
286
+ üzerindeki `max-age`'dir ve mevcut HTML TTL'dir. `stale-while-revalidate`
287
+ `cache().staleWhileRevalidate` değeridir (varsayılan 60; `0` direktifi basmaz).
288
+ `s-maxage` yazılmaz: Cloudflare `max-age=0` ile birlikte onu `EXPIRED` sayar.
289
+ `must-revalidate`, `proxy-revalidate` ve `no-cache` aynı yanıtta yoktur.
290
+
291
+ CDN `max-age` boyunca kendi kopyasını verir, taze pencere bitince
292
+ `stale-while-revalidate` süresince eski HTML'i sunar ve arkada origin'e sorar.
293
+ Origin da kendi önbelleğinden anında yanıtlar.
294
+
295
+ **Kırılma.** Yalnızca `Cache-Control` / `s-maxage` okuyan bir ara katman
296
+ (nginx `proxy_cache`) bu HTML'i artık önbelleklemez. Cloudflare
297
+ `CDN-Cache-Control` okur. Süreç içi önbellek ve `X-JSkelet-Cache` aynı kalır.
288
298
 
289
299
  `X-JSkelet-Cache` başlığı hangi katmanın yanıtladığını teşhis etmeyi
290
300
  kolaylaştırır; CDN'in kendi cache başlığıyla birlikte okuyun
@@ -52,7 +52,8 @@ Bayrağın yaptığı işler:
52
52
  | --- | --- | --- |
53
53
  | HTML önbelleği | TTL varsa açık | Kapalı, açılamaz |
54
54
  | `cache.html` deseni | TTL'i ezer | Yok sayılır |
55
- | `Cache-Control` | `public, s-maxage=…` | `private, no-store` |
55
+ | `Cache-Control` | `public, max-age=0` | `private, no-store` |
56
+ | `CDN-Cache-Control` | `max-age=<ttl>, stale-while-revalidate=…` | Yazılmaz |
56
57
  | `Vary` | `Accept-Encoding` | `Cookie, Accept-Encoding` |
57
58
  | ETag | Var | Yok |
58
59
  | `X-JSkelet-Cache` | `HIT`/`STALE`/`MISS` | Yazılmaz |
@@ -81,7 +81,8 @@ position has a reason, and moving things around leads to silent breakage.
81
81
  copies produced at build time, those are served (brotli quality 11);
82
82
  otherwise the request falls through to the `static` below it and the
83
83
  middleware compresses on the fly (quality 5). Recompressing a hashed,
84
- `immutable` file on every request is wasted CPU.
84
+ `immutable` file on every request is wasted CPU. In production the `stat`
85
+ result (present or missing) stays in process memory; in development it does not.
85
86
  - **Admin panel** (when `admin().enabled` / `JSKELET_ADMIN`): after static,
86
87
  before body parsers and routes. Carries its own body parsers so the app
87
88
  cannot shadow the path. When off, the module is never loaded.
@@ -588,9 +588,15 @@ app.get("/og/custom.png", async (req, res) => {
588
588
  });
589
589
  ```
590
590
 
591
- Default `Cache-Control`:
592
- `public, max-age=0, s-maxage=86400, stale-while-revalidate=604800`.
593
- Override with `cacheControl`. Working example: `examples/blog/routes/35-og.mjs`.
591
+ Default headers (the durations are not tied to the HTML setting):
592
+
593
+ ```
594
+ Cache-Control: public, max-age=0
595
+ CDN-Cache-Control: max-age=86400, stale-while-revalidate=604800
596
+ ```
597
+
598
+ `cacheControl` overrides `Cache-Control`; in that case `CDN-Cache-Control` is
599
+ not written. Working example: `examples/blog/routes/35-og.mjs`.
594
600
 
595
601
  ## Hooks
596
602
 
@@ -90,6 +90,9 @@ export default {
90
90
  "/news/:slug": 300,
91
91
  "/tag/:slug": 120,
92
92
  },
93
+ // How long the edge serves stale HTML after the fresh window ends.
94
+ // 0 omits the directive entirely.
95
+ staleWhileRevalidate: 60,
93
96
  };
94
97
  },
95
98
  };
@@ -214,9 +217,11 @@ leadMs = min(max(produceMs * 2, 250ms), ttl / 2)
214
217
 
215
218
  So a slow page does not fall back to a cold render the moment TTL ends: fresh
216
219
  HTML is usually written before `expiresAt`. With no traffic, a sweeper
217
- soft-stales the entry in the same window and queues it for warming;
218
- `startPrewarm` (unless `PREWARM=0`) drains that queue over HTTP — even when
219
- classic `prewarmPaths` is absent.
220
+ soft-stales the entry in the same window and queues it for warming. One pass
221
+ marks at most four entries (soonest expiry first); the rest wait for later
222
+ seconds. `startPrewarm` (unless `PREWARM=0`) drains that queue over HTTP —
223
+ even when classic `prewarmPaths` is absent — but the drain does not use the
224
+ classic tour's `rps: 0`. It runs one request at a time, at most two per second.
220
225
 
221
226
  A failure of the refresh inside the stale window does not affect the request:
222
227
  the old HTML stays valid for the whole window and the error is only logged
@@ -234,7 +239,8 @@ The store is an LRU: an accessed entry is moved to the end, and once the limit
234
239
  (`cache().maxEntries`, 500 by default) is exceeded the oldest is evicted. Config
235
240
  may ask for more than 500; it **cannot exceed 800** — a higher value is clamped
236
241
  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
242
+ bodies **cannot exceed 256 MB**. The compressed copy is singular: brotli or
243
+ gzip, whichever was requested last. The raw HTML stays. A fat page or a `vary.host` copy that is still
238
244
  under the count limit is evicted by this budget too. A single page larger than
239
245
  256 MB is not stored; that response is still sent.
240
246
 
@@ -263,12 +269,32 @@ changed with `brand.cacheHeader`):
263
269
  On cacheable responses, additionally:
264
270
 
265
271
  ```
266
- Cache-Control: public, max-age=0, s-maxage=<revalidate>, stale-while-revalidate=60
272
+ Cache-Control: public, max-age=0
273
+ CDN-Cache-Control: max-age=<html ttl>, stale-while-revalidate=<staleWhileRevalidate>
267
274
  ```
268
275
 
269
- `max-age=0` turns off storage in the browser, `s-maxage` announces the duration
270
- to intermediate layers (CDN, reverse proxy). This way, when a CDN sits in
271
- front, the same freshness model works across both layers together.
276
+ `max-age=0` turns off storage in the browser. The edge duration is not written
277
+ into `Cache-Control`: `s-maxage` together with `max-age=0` produces `EXPIRED`
278
+ on Cloudflare. The duration is `max-age` on `CDN-Cache-Control`, and it is the
279
+ existing HTML TTL (`cache().html` or the route's `revalidate`). `s-maxage` is
280
+ never written, and there is no switch that puts it back.
281
+
282
+ `stale-while-revalidate` is how long the edge serves stale HTML after the fresh
283
+ window ends. It comes from `cache().staleWhileRevalidate`; the default is `60`.
284
+ `0` omits the directive entirely:
285
+
286
+ ```
287
+ CDN-Cache-Control: max-age=<html ttl>
288
+ ```
289
+
290
+ `must-revalidate`, `proxy-revalidate` and `no-cache` are not on the same
291
+ response; those directives cut the stale window.
292
+
293
+ `X-JSkelet-Cache: STALE` is the in-process HTML cache layer. It is independent
294
+ of the stale window on the edge header.
295
+
296
+ An intermediate layer that only reads `Cache-Control` / `s-maxage` (nginx
297
+ `proxy_cache`) no longer caches this HTML. Cloudflare reads `CDN-Cache-Control`.
272
298
 
273
299
  ## Storing the compressed body
274
300
 
@@ -379,6 +405,10 @@ Details:
379
405
  version or page number go into the key (`news:en:v2:${slug}`).
380
406
  - When the TTL is `0` the cache is disabled and the `producer` runs on every
381
407
  call — enough to switch a setting off temporarily.
408
+ - **Byte ceiling is 64 MB.** The entry count does not hold fat JSON; in-process
409
+ bodies cannot pass this ceiling and config cannot raise it. A single value
410
+ larger than the ceiling is not stored; the caller still receives it.
411
+ Eviction drops the oldest entry.
382
412
 
383
413
  The management surface:
384
414
 
@@ -528,7 +558,7 @@ rendering the page from scratch on every visit — the content comes back just a
528
558
  incomplete, and the visitor only pays the render time.
529
559
 
530
560
  Output produced with missing data is **not offered to shared caches** either: a
531
- `degraded` response gets `private, no-store` instead of `public, s-maxage=…`.
561
+ `degraded` response gets `private, no-store` instead of `CDN-Cache-Control`.
532
562
  Taking back the "do not store" decision at the CDN would repeat the same mistake
533
563
  one layer up. The diagnostic header (`X-JSkelet-Cache: MISS`) is still written.
534
564
 
@@ -853,6 +883,21 @@ build has not been run the id is `dev`.
853
883
  deliberately does **not** carry `buildId`: during a deploy the old and the new
854
884
  version run side by side and a purge has to reach both.
855
885
 
886
+ HTML and data entries of 1 KB or more are not stored as plain JSON. The shared
887
+ tier receives a brotli body prefixed with `JSK\x01` (quality 5, text mode — the
888
+ same settings as response compression). L1 still holds the decoded value;
889
+ compression runs only when sharing after an L1 miss, and it does not delay the
890
+ response. Smaller records stay plain JSON, and older plain JSON records are
891
+ still read. zstd is not used: `node:zlib` gained it in 22.15, while the engine
892
+ range is `>=22`.
893
+
894
+ When Redis is off, or cannot connect, the same body is written under
895
+ `.jskelet/cache/<buildId>/`. After a restart L1 is empty, but a fresh file
896
+ skips the render. This is single-machine: several instances do not share the
897
+ directory, and a cluster still wants Redis. A new `buildId` deletes the previous
898
+ directory on the next write. `clearHtmlCache()` and invalidation remove the
899
+ file too.
900
+
856
901
  ### Trade-offs worth knowing
857
902
 
858
903
  - **Personalised output is never shared.** A render marked `storable: false` (a
@@ -120,6 +120,7 @@ export default {
120
120
  async cache() {
121
121
  return {
122
122
  html: { "/": 60, "/news/:slug": 300 },
123
+ staleWhileRevalidate: 60,
123
124
  query: { "/search": ["q", "page"] },
124
125
  maxEntries: 500,
125
126
  data: { maxEntries: 10000, staleFactor: 10 },
@@ -572,7 +573,7 @@ When `remote.allowHosts` is set, also proxies remote images at runtime
572
573
  | `allowHosts` | `string[]` | `[]` | Hosts that may be fetched. Supports a `*.cdn.example.com` suffix wildcard. |
573
574
  | `path` | `string` | `/_jskelet/image` | Optimizer GET path. |
574
575
  | `maxWidth` | `number` | `1920` | Cap for `w`. |
575
- | `cacheMaxAge` | `number` | `2592000` (30 days) | Response `Cache-Control` max-age (seconds). Disk cache under `.jskelet/image-cache/`. |
576
+ | `cacheMaxAge` | `number` | `2592000` (30 days) | Response `Cache-Control` max-age (seconds). Disk cache under `.jskelet/image-cache/`; past 256 MB the oldest file is deleted. |
576
577
  | `fetchTimeoutMs` | `number` | `10000` | Upstream fetch timeout. |
577
578
  | `maxBytes` | `number` | `10485760` (10 MiB) | Upstream body size limit. |
578
579
 
@@ -695,9 +696,9 @@ Details: [03-routing.md](./03-routing.md).
695
696
  ## `cache()`
696
697
 
697
698
  **Type:**
698
- `() => { html?: Record<string, number>, query?: Record<string, string[] | true>, vary?: { host?: boolean, headers?: string[], fn?: (req) => string | null }, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, upstream?: object, redis?: object, prewarm?: object }` —
699
+ `() => { html?: Record<string, number>, staleWhileRevalidate?: number, query?: Record<string, string[] | true>, vary?: { host?: boolean, headers?: string[], fn?: (req) => string | null }, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, upstream?: object, redis?: object, prewarm?: object }` —
699
700
  **Default:**
700
- `{ html: {}, query: {}, vary: { host: false }, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300 }, upstream: { rate: 0 }, redis: { enabled: false }, prewarm: { enabled: true, max: 400, intervalSeconds: 0, origins: [] } }`
701
+ `{ html: {}, staleWhileRevalidate: 60, query: {}, vary: { host: false }, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300 }, upstream: { rate: 0 }, redis: { enabled: false }, prewarm: { enabled: true, max: 400, intervalSeconds: 0, origins: [] } }`
701
702
 
702
703
  ### `cache().html`
703
704
 
@@ -719,6 +720,15 @@ html: {
719
720
  }
720
721
  ```
721
722
 
723
+ ### `cache().staleWhileRevalidate`
724
+
725
+ **Type:** `number` — **Default:** `60`
726
+
727
+ How long the edge serves stale HTML after the fresh window (`cache().html` /
728
+ `revalidate`) ends, in seconds. Written as `stale-while-revalidate` on
729
+ `CDN-Cache-Control`. `0` omits the directive. It does not change the in-process
730
+ HTML cache's stale window. Details: [06-caching.md](./06-caching.md).
731
+
722
732
  ### `cache().query`
723
733
 
724
734
  A pattern → list of query parameters allowed into the cache key.
@@ -788,7 +798,7 @@ tens of thousands of paths from here is the wrong layer — the right place is
788
798
 
789
799
  **Ceiling 800.** A higher value is clamped to 800 with a warning at load.
790
800
  In-process HTML plus compressed bodies also cannot exceed 256 MB; config
791
- cannot raise that budget.
801
+ cannot raise that budget. Only one compressed copy is kept (brotli or gzip).
792
802
 
793
803
  ### `cache().data`
794
804
 
@@ -797,7 +807,7 @@ The upstream data cache (`withDataCache`). Details:
797
807
 
798
808
  | Field | Type | Default | Meaning |
799
809
  | --- | --- | --- | --- |
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. |
810
+ | `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. In-process JSON also cannot exceed **64 MB**; config cannot raise that budget. |
801
811
  | `staleFactor` | `number` | `10` | For how many TTLs an entry stays usable after the TTL expired. `0` → no stale serving. |
802
812
 
803
813
  ### `cache().trackUpstream`
@@ -901,15 +911,17 @@ redis: {
901
911
 
902
912
  Persistent log sinks. Everything is off by default: stdout and the admin panel
903
913
  ring keep their current behaviour. When enabled, HTTP access logs and framework
904
- events (`event` / `error`) are written as NDJSON lines to a file and/or S3.
914
+ events (`event` / `error`) go out as NDJSON to a file, `drainLog`, and/or S3.
915
+ File chunks are zstd and stay at most 5 minutes; the oldest expired chunk is
916
+ deleted.
905
917
 
906
918
  | Field | Type | Default | Meaning |
907
919
  | --- | --- | --- | --- |
908
920
  | `console` | `boolean` | `true` | Whether runtime `http` / `event` / `error` lines go to stdout (banner/build lines are unaffected) |
909
921
  | `kinds` | `("http" \| "event" \| "error")[]` | all | Which kinds reach the sinks |
910
- | `file.enabled` | `boolean` | `false` | Daily file sink |
911
- | `file.dir` | `string` | `"logs"` | Directory relative to the project root; `jskelet-YYYY-MM-DD.log` |
912
- | `file.rotate` | `"daily"` | `"daily"` | Daily rotation only |
922
+ | `file.enabled` | `boolean` | `false` | File spool. Lines become `jskelet-<time>-<n>.ndjson.zst` about every 1s or 32 lines. Kept at most 5 minutes; the oldest chunk is deleted. |
923
+ | `file.dir` | `string` | `"logs"` | Directory relative to the project root |
924
+ | `drainLog` | `(chunk) => void \| Promise<void>` | `null` | Forwards each sealed zstd chunk (`{ body, encoding, bytes, lines, at }`) wherever the app wants. A throw warns and does not take the site down. With the file sink off, nothing is written to disk. |
913
925
  | `s3.enabled` | `boolean` | `false` | S3 batch PutObject sink |
914
926
  | `s3.bucket` | `string \| null` | `null` | Bucket or a `bucket/prefix/…` path; `JSKELET_LOG_BUCKET` overrides |
915
927
  | `s3.prefix` | `string` | `"jskelet/logs/"` | Object key prefix (when not given in the path) |
@@ -928,6 +940,9 @@ logs: {
928
940
  console: true,
929
941
  kinds: ["http", "error"],
930
942
  file: { enabled: true, dir: "logs" },
943
+ async drainLog(chunk) {
944
+ // chunk.body is zstd NDJSON. The file is deleted after 5 minutes; keep a copy here.
945
+ },
931
946
  s3: {
932
947
  enabled: process.env.NODE_ENV === "production",
933
948
  bucket: process.env.JSKELET_LOG_BUCKET,
@@ -359,7 +359,8 @@ and `image()` falls back to the original file. It never runs on a watch pass.
359
359
  When `images.remote.allowHosts` is set, `createApp` mounts `/_jskelet/image`.
360
360
  CMS / CDN covers never enter the build, so `image()` rewrites those host URLs to
361
361
  `?url=&w=`; the endpoint encodes webp with sharp and stores files under
362
- `.jskelet/image-cache/`. Upstream fetch follows redirects manually: every hop is
362
+ `.jskelet/image-cache/`. When the directory passes 256 MB the oldest file is
363
+ deleted. Upstream fetch follows redirects manually: every hop is
363
364
  re-checked against the allowlist and private IP / DNS rules (open-redirect SSRF
364
365
  is closed). Details: [07-configuration.md](./07-configuration.md).
365
366
 
@@ -276,16 +276,27 @@ Key points:
276
276
 
277
277
  ### Together with a CDN
278
278
 
279
- The header written on cacheable pages:
279
+ The headers written on cacheable pages:
280
280
 
281
281
  ```
282
- Cache-Control: public, max-age=0, s-maxage=<revalidate>, stale-while-revalidate=60
282
+ Cache-Control: public, max-age=0
283
+ CDN-Cache-Control: max-age=<html ttl>, stale-while-revalidate=<staleWhileRevalidate>
283
284
  ```
284
285
 
285
- `max-age=0` disables browser storage, `s-maxage` tells the CDN the duration. So
286
- the same freshness model works across two layers together: the CDN serves its
287
- own copy for the duration of `s-maxage`, asks the origin when it expires, and
288
- the origin answers instantly from its own cache.
286
+ `max-age=0` disables browser storage. The edge duration is `max-age` on
287
+ `CDN-Cache-Control`, and it is the existing HTML TTL. `stale-while-revalidate`
288
+ is `cache().staleWhileRevalidate` (default 60; `0` omits the directive).
289
+ `s-maxage` is not written: Cloudflare treats it as `EXPIRED` together with
290
+ `max-age=0`. `must-revalidate`, `proxy-revalidate` and `no-cache` are not on
291
+ the same response.
292
+
293
+ The CDN serves its own copy for `max-age`, then serves the stale HTML for
294
+ `stale-while-revalidate` while it asks the origin. The origin answers instantly
295
+ from its own cache.
296
+
297
+ **Breaking.** An intermediate layer that only reads `Cache-Control` /
298
+ `s-maxage` (nginx `proxy_cache`) no longer caches this HTML. Cloudflare reads
299
+ `CDN-Cache-Control`. The in-process cache and `X-JSkelet-Cache` stay the same.
289
300
 
290
301
  The `X-JSkelet-Cache` header makes it easier to diagnose which layer answered;
291
302
  read it together with the CDN's own cache header
@@ -53,7 +53,8 @@ What the flag does:
53
53
  | --- | --- | --- |
54
54
  | HTML cache | On when a TTL exists | Off, cannot be turned on |
55
55
  | `cache.html` pattern | Overrides the TTL | Ignored |
56
- | `Cache-Control` | `public, s-maxage=…` | `private, no-store` |
56
+ | `Cache-Control` | `public, max-age=0` | `private, no-store` |
57
+ | `CDN-Cache-Control` | `max-age=<ttl>, stale-while-revalidate=…` | Not written |
57
58
  | `Vary` | `Accept-Encoding` | `Cookie, Accept-Encoding` |
58
59
  | ETag | Present | Absent |
59
60
  | `X-JSkelet-Cache` | `HIT`/`STALE`/`MISS` | Not written |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",