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.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -0
- package/docs/02-mimari.md +1 -0
- package/docs/04-render-ve-sablonlar.md +9 -3
- package/docs/06-cache.md +53 -9
- package/docs/07-yapilandirma.md +1208 -1191
- package/docs/08-build.md +2 -1
- package/docs/10-dagitim.md +16 -6
- package/docs/12-panel-ve-oturum.md +2 -1
- package/docs/en/02-architecture.md +2 -1
- package/docs/en/04-rendering.md +9 -3
- package/docs/en/06-caching.md +54 -9
- package/docs/en/07-configuration.md +24 -9
- package/docs/en/08-build.md +2 -1
- package/docs/en/10-deployment.md +17 -6
- package/docs/en/12-dashboards-and-sessions.md +2 -1
- package/package.json +1 -1
- package/src/config/defaults.js +541 -518
- package/src/config/index.js +1500 -1456
- package/src/init.mjs +2 -0
- package/src/server/cache-blob.js +70 -0
- package/src/server/cache-control.js +45 -0
- package/src/server/data-cache.js +118 -27
- package/src/server/disk-cache.js +233 -0
- package/src/server/html-cache.js +90 -16
- package/src/server/image-optimizer.js +95 -2
- package/src/server/logs/file-sink.js +159 -32
- package/src/server/logs/pipeline.js +10 -3
- package/src/server/middleware/static-precompressed.js +31 -10
- package/src/server/og-image.js +17 -4
- package/src/server/prewarm.js +25 -1
- package/src/server/redis.js +31 -12
- package/src/server/render.js +910 -910
- package/types/config/defaults.d.ts +21 -1
- package/types/config/index.d.ts +14 -0
- package/types/server/cache-blob.d.ts +13 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/data-cache.d.ts +9 -0
- package/types/server/disk-cache.d.ts +36 -0
- package/types/server/html-cache.d.ts +26 -3
- package/types/server/logs/file-sink.d.ts +16 -5
- package/types/server/og-image.d.ts +5 -0
- 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.
|
|
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
|
|
package/docs/10-dagitim.md
CHANGED
|
@@ -275,16 +275,26 @@ server {
|
|
|
275
275
|
|
|
276
276
|
### CDN ile birlikte
|
|
277
277
|
|
|
278
|
-
Önbelleklenebilir sayfalara yazılan
|
|
278
|
+
Önbelleklenebilir sayfalara yazılan başlıklar:
|
|
279
279
|
|
|
280
280
|
```
|
|
281
|
-
Cache-Control: public, max-age=0
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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,
|
|
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.
|
package/docs/en/04-rendering.md
CHANGED
|
@@ -588,9 +588,15 @@ app.get("/og/custom.png", async (req, res) => {
|
|
|
588
588
|
});
|
|
589
589
|
```
|
|
590
590
|
|
|
591
|
-
Default
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
package/docs/en/06-caching.md
CHANGED
|
@@ -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
|
-
|
|
219
|
-
|
|
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**.
|
|
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
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
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 `
|
|
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`)
|
|
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` |
|
|
911
|
-
| `file.dir` | `string` | `"logs"` | Directory relative to the project root
|
|
912
|
-
| `
|
|
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,
|
package/docs/en/08-build.md
CHANGED
|
@@ -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/`.
|
|
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
|
|
package/docs/en/10-deployment.md
CHANGED
|
@@ -276,16 +276,27 @@ Key points:
|
|
|
276
276
|
|
|
277
277
|
### Together with a CDN
|
|
278
278
|
|
|
279
|
-
The
|
|
279
|
+
The headers written on cacheable pages:
|
|
280
280
|
|
|
281
281
|
```
|
|
282
|
-
Cache-Control: public, max-age=0
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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,
|
|
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.
|
|
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",
|