jskelet 0.2.1 → 0.2.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.
- package/CHANGELOG.md +368 -261
- package/docs/03-routing.md +4 -0
- package/docs/06-cache.md +340 -12
- package/docs/07-yapilandirma.md +131 -2
- package/docs/en/03-routing.md +4 -0
- package/docs/en/06-caching.md +347 -14
- package/docs/en/07-configuration.md +135 -2
- package/package.json +1 -1
- package/src/client/cache-panel/login.html +71 -0
- package/src/client/cache-panel/panel.css +740 -0
- package/src/client/cache-panel/panel.html +307 -0
- package/src/client/cache-panel/panel.js +808 -0
- package/src/client/devtools/overlay.js +85 -0
- package/src/client/devtools/report.js +13 -0
- package/src/config/defaults.js +98 -2
- package/src/config/index.js +185 -3
- package/src/index.js +21 -1
- package/src/log.mjs +25 -0
- package/src/server/cache-panel.js +738 -0
- package/src/server/cloudflare.js +595 -0
- package/src/server/create-app.js +16 -0
- package/src/server/data-cache.js +92 -2
- package/src/server/dev/report.js +8 -1
- package/src/server/html-cache.js +36 -0
- package/src/server/prewarm.js +94 -15
- package/src/server/redis.js +108 -0
- package/src/server/render.js +74 -2
- package/src/server/upstream-limiter.js +376 -0
- package/src/server/upstream-tracking.js +25 -0
- package/src/version.mjs +9 -0
package/docs/03-routing.md
CHANGED
|
@@ -123,6 +123,10 @@ app.get(
|
|
|
123
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
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
125
|
|
|
126
|
+
`revalidate` verilse bile **query parametresi taşıyan istek varsayılan olarak
|
|
127
|
+
dinamiktir**; o yol için `cache().query` altında bir izin listesi tanımlamak
|
|
128
|
+
gerekir ([06-cache.md](./06-cache.md)).
|
|
129
|
+
|
|
126
130
|
Oturuma bağlı her sayfa `private: true` almalı; önbellek anahtarında kimlik
|
|
127
131
|
olmadığı için bayrak olmadan bir kullanıcının HTML'i bir başkasına servis
|
|
128
132
|
edilir. Framework bu hatayı çalışma zamanında da yakalıyor (controller cookie
|
package/docs/06-cache.md
CHANGED
|
@@ -110,17 +110,30 @@ saklayabiliyordu.
|
|
|
110
110
|
## Cache anahtarı
|
|
111
111
|
|
|
112
112
|
```
|
|
113
|
-
`${
|
|
113
|
+
`${yol}?${izin verilen query parametreleri, sıralı}`
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
Query'siz bir istek için anahtar yalnızca yoldur. **Query parametresi taşıyan
|
|
117
|
+
istek varsayılan olarak dinamiktir**: önbelleğe hiç girmez ve `private,
|
|
118
|
+
no-store` ile gider. Bir yolun bütün varyantlarını cache'lemek `?utm_source=…`
|
|
119
|
+
gibi sonsuz sayıda anahtar üretiyor ve 500 girdilik store'da LRU, gerçek
|
|
120
|
+
sayfaları kampanya varyantları için dışarı atıyor.
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
Hangi parametrenin çıktıyı gerçekten değiştirdiğini uygulama bildirir —
|
|
123
|
+
`jskelet.config.mjs` → `cache().query`:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
cache: () => ({
|
|
127
|
+
html: { "/liste": 60 },
|
|
128
|
+
query: { "/liste": ["sayfa"] },
|
|
129
|
+
}),
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Artık `/liste?sayfa=2` ile `/liste?sayfa=3` ayrı girdiler, `/liste?sayfa=2&utm_source=x`
|
|
133
|
+
ise `?sayfa=2` kopyasını paylaşır: listede olmayan parametre anahtara girmez.
|
|
134
|
+
Bir desen `true` ile eşlenirse bütün parametreler anahtara girer (dikkat: girdi
|
|
135
|
+
sayısını sınırlayan tek şey `maxEntries` olur), `[]` ile eşlenirse query tamamen
|
|
136
|
+
yok sayılır. Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
|
|
124
137
|
|
|
125
138
|
## Stale-while-revalidate
|
|
126
139
|
|
|
@@ -431,6 +444,97 @@ cache: {
|
|
|
431
444
|
`transientRetry: false` (ya da `attempts: 0`) tekrarı kapatır ve doğrudan 503'e
|
|
432
445
|
düşer.
|
|
433
446
|
|
|
447
|
+
## Upstream hız freni: `cache().upstream`
|
|
448
|
+
|
|
449
|
+
Buraya kadarki her şey 429 **geldikten sonra** ne olacağını anlatıyor. Bu bölüm
|
|
450
|
+
429'u en baştan almamakla ilgili.
|
|
451
|
+
|
|
452
|
+
Fren `trackUpstreamFetch()` sarmalayıcısının içinde, yani gerçek `fetch`
|
|
453
|
+
çağrısının geçtiği yerde duruyor. Isıtma turundaki `prewarm.rps` bu işi
|
|
454
|
+
yapamaz: o, kendi sunucumuza atılan **sayfa** isteklerini sayıyor, ama bir
|
|
455
|
+
sayfa render'ı bir API çağrısı da yapabilir yirmi tane de. Kotayı bağlayan şey
|
|
456
|
+
sayfa sayısı değil, çağrı sayısı — ve fren buraya konduğunda ısıtma da gerçek
|
|
457
|
+
trafik de aynı bütçeden harcar.
|
|
458
|
+
|
|
459
|
+
Varsayılan **kapalı**: `rate` verilmedikçe hiçbir istek beklemez ve maliyet bir
|
|
460
|
+
daldan ibarettir.
|
|
461
|
+
|
|
462
|
+
```js
|
|
463
|
+
// jskelet.config.mjs
|
|
464
|
+
cache: () => ({
|
|
465
|
+
upstream: {
|
|
466
|
+
rate: 10, // saniyedeki tavan (host başına)
|
|
467
|
+
burst: 20, // kısa patlama toleransı
|
|
468
|
+
concurrency: 8, // aynı anda uçan çağrı
|
|
469
|
+
hosts: {
|
|
470
|
+
// Kotası farklı olan uçlar ayrı ayarlanır.
|
|
471
|
+
"api.example.com": { rate: 3, concurrency: 2 },
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
}),
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Üç mekanizma, üç farklı sınır
|
|
478
|
+
|
|
479
|
+
| Mekanizma | Neyi sınırlar | Ayar |
|
|
480
|
+
| --- | --- | --- |
|
|
481
|
+
| Token bucket | Ortalama hız (çağrı/saniye) | `rate`, `burst` |
|
|
482
|
+
| Eşzamanlılık | Anlık baskı (aynı anda uçan çağrı) | `concurrency` |
|
|
483
|
+
| AIMD | Doğru hızın ne olduğu | `minRate`, `increaseStep`, `increaseIntervalMs`, `decreaseIntervalMs` |
|
|
484
|
+
|
|
485
|
+
Üçüncüsü asıl fikir. Sabit bir hız her zaman ya çok yavaş ya çok hızlıdır:
|
|
486
|
+
kotanın gerçek sınırını kimse config'e doğru yazamaz, üstelik gün içinde
|
|
487
|
+
değişir. Bu yüzden `rate` bir **tavan** olarak alınır ve gerçek hız upstream'in
|
|
488
|
+
cevabına göre oynar:
|
|
489
|
+
|
|
490
|
+
- **429 ya da 503** → hız yarıya iner (çarpımsal azalma). Yanıt `Retry-After`
|
|
491
|
+
taşıyorsa kova o süre boyunca tamamen durur — upstream sana ne kadar
|
|
492
|
+
bekleyeceğini zaten söylüyor.
|
|
493
|
+
- **Temiz geçen her pencere** → hız `increaseStep` kadar yukarı çıkar
|
|
494
|
+
(toplamsal artış), `rate` tavanına kadar.
|
|
495
|
+
|
|
496
|
+
Azalmanın çarpımsal, artışın toplamsal olması bilinçli. Tersi olsaydı her
|
|
497
|
+
pencerede yeniden 429 yenirdi.
|
|
498
|
+
|
|
499
|
+
### Devre kesici
|
|
500
|
+
|
|
501
|
+
Art arda `breakerFailures` (varsayılan 5) tane 429 alan bir host
|
|
502
|
+
`breakerCooldownMs` süresince tamamen baypas edilir: çağrı hiç yapılmaz,
|
|
503
|
+
doğrudan geçici hata olarak bildirilir.
|
|
504
|
+
|
|
505
|
+
Sert görünüyor ama asimetri bunu gerektiriyor: 429 geçici sayıldığı için o
|
|
506
|
+
çağrıyla üretilen HTML **önbelleğe yazılmaz**. Yani rate limit'e girmiş bir
|
|
507
|
+
turda kota harcanır ve karşılığında hiçbir şey saklanmaz. Üstüne bir sonraki
|
|
508
|
+
tur aynı sayfayı yine soğuk bulup yine dener. Kesici bu boşa yanmayı kesiyor.
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
[upstream] api.example.com: 5 consecutive rate limits — bypassing for 10000ms (rate is now 1.2/s)
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Yalnızca 429 ve 503 sayılır. `400`/`404` bir kota sorunu değil, `500` de öyle:
|
|
515
|
+
onlar için yavaşlamak arızayı düzeltmez, sadece siteyi yavaşlatır.
|
|
516
|
+
|
|
517
|
+
### Durumu görmek
|
|
518
|
+
|
|
519
|
+
`getUpstreamLimiterStatus()` host başına o anki hızı, uçuştaki çağrıyı ve
|
|
520
|
+
sayaçları döner; dev panelinin **Server** sekmesi de aynı bilgiyi basıyor.
|
|
521
|
+
429 fırtınasında "şu an saniyede kaça indi" bilgisi olmadan ayar yapmak
|
|
522
|
+
körlemesine olur.
|
|
523
|
+
|
|
524
|
+
```js
|
|
525
|
+
import { getUpstreamLimiterStatus } from "jskelet";
|
|
526
|
+
|
|
527
|
+
// [{ host: "api.example.com", rate: 2.5, maxRate: 10, concurrency: 8,
|
|
528
|
+
// active: 3, throttled: 12, rejected: 40, bypassed: false, blockedMs: 0 }]
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Freni açmadan önce
|
|
532
|
+
|
|
533
|
+
Hız freni son çare. Aynı upstream yanıtını yüzlerce sayfa çekiyorsa asıl çözüm
|
|
534
|
+
[`withDataCache`](#istekler-arası-veri-önbelleği-withdatacache) TTL'ini tur
|
|
535
|
+
aralığından uzun tutmak: 400 sayfalık bir tur, ortak bir uç için tek çağrı
|
|
536
|
+
yapar. Fren o çağrıları yavaşlatır, sayısını azaltmaz.
|
|
537
|
+
|
|
434
538
|
## Önbelleği yönetmek
|
|
435
539
|
|
|
436
540
|
`jskelet` şu fonksiyonları dışa açar:
|
|
@@ -647,8 +751,216 @@ Bağlantı kurulmamışken de güvenle çağrılır. Dönen nesne
|
|
|
647
751
|
kesicinin açık olduğunu, `errors` toplam komut hatasını gösterir. Aynı özet dev
|
|
648
752
|
panelinin raporunda da var ([09-dev-araclari.md](./09-dev-araclari.md)).
|
|
649
753
|
|
|
754
|
+
İki teşhis yüzeyi daha var:
|
|
755
|
+
|
|
756
|
+
| Çağrı | Ne der |
|
|
757
|
+
| --- | --- |
|
|
758
|
+
| `getRedisDetails()` | Bağlantının **nereye** kurulduğu: adres, TLS, veritabanı, `namespace`, hangi türlerin paylaşıldığı, purge yayınına abone olunup olunmadığı. Şifre asla dönmez — bağlantı URL'i sır taşıyor olabilir. |
|
|
759
|
+
| `inspectRedis()` | Paylaşımlı kademede gerçekten ne durduğu: tür başına anahtar sayısı, `DBSIZE` ve `used_memory`. Bir `SCAN` turu olduğu için **istek yolunda çağrılmaz**; yönetim panelinde de ayrı bir düğmeye bağlı. |
|
|
760
|
+
|
|
650
761
|
Ayarların tam listesi: [07-yapilandirma.md](./07-yapilandirma.md).
|
|
651
762
|
|
|
763
|
+
## Yönetim paneli
|
|
764
|
+
|
|
765
|
+
Yukarıdaki `getHtmlCacheEntries()` / `getRedisStatus()` uçlarını elle yazmak
|
|
766
|
+
yerine framework hazır bir panel taşıyor. Dev overlay'den ayrı bir şey: overlay
|
|
767
|
+
yalnızca `NODE_ENV=development` iken var, panel ise ortama bakmaz — "bu sayfa
|
|
768
|
+
neden bayat", "webhook purge'ü geçti mi", "Redis gerçekten bağlı mı" soruları
|
|
769
|
+
üretimde soruluyor.
|
|
770
|
+
|
|
771
|
+
```js
|
|
772
|
+
// jskelet.config.mjs
|
|
773
|
+
export default {
|
|
774
|
+
cache() {
|
|
775
|
+
return {
|
|
776
|
+
html: { "/haber/:slug": 300 },
|
|
777
|
+
panel: { enabled: process.env.CACHE_PANEL === "1" },
|
|
778
|
+
};
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
`enabled` verilmedikçe **hiçbir şey mount edilmez**: yol yoktur, modül
|
|
784
|
+
yüklenmez, üretim sürecinde hiçbir maliyeti olmaz. Ortam değişkeni
|
|
785
|
+
(`JSKELET_CACHE_PANEL=1`) config'i ezer; panel genelde bir arıza sırasında tek
|
|
786
|
+
seferlik açılıyor ve o an config dosyasını değiştirip yeniden dağıtmak
|
|
787
|
+
istenmiyor.
|
|
788
|
+
|
|
789
|
+
Panel açıldığında sunucu logu şifreyi basar:
|
|
790
|
+
|
|
791
|
+
```
|
|
792
|
+
[cache-panel] http://localhost:3000/_jskelet/cache — password for this run: 3f9c…
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### Erişim ve güvenlik
|
|
796
|
+
|
|
797
|
+
- **Şifre her süreç başlangıcında yeniden üretilir** (32 haneli, onaltılık) ve
|
|
798
|
+
yalnızca logda görünür. Kalıcı bir sır tutulmuyor: sızması önbelleği boşaltma
|
|
799
|
+
yetkisi demek ve bir deploy eski erişimi kendiliğinden iptal etmeli.
|
|
800
|
+
- **Şifre query string ile kabul edilmez.** Erişim logları, tarayıcı geçmişi ve
|
|
801
|
+
`Referer` başlığı sırrı taşımasın; giriş yalnızca form üzerinden.
|
|
802
|
+
- **Üç başarısız denemede IP 24 saat yasaklanır** (`banAttempts`, `banHours`).
|
|
803
|
+
Yanlış şifre kadar oturumsuz istek de sayılır; başarılı giriş sayacı sıfırlar.
|
|
804
|
+
- **Yasaklı ve yetkisiz her cevap `404`.** 401/403 panelin var olduğunu
|
|
805
|
+
doğrular, 404 hiç yokmuş gibi davranır. Panelin dışındaki site etkilenmez.
|
|
806
|
+
- **Hiçbir yeri indekslenmez:** her cevapta `X-Robots-Tag: noindex, nofollow,
|
|
807
|
+
noarchive, nosnippet`, `Cache-Control: no-store` ve `Referrer-Policy:
|
|
808
|
+
no-referrer`. Yol ayrıca ısıtma listesinden ve gezinme ipuçlarından muaftır.
|
|
809
|
+
- Aksiyonlar `X-JSkelet-Cache-Panel` başlığı ister — çapraz siteden
|
|
810
|
+
gönderilemeyen bir başlık, yani panelin kendi CSRF freni.
|
|
811
|
+
- Oturumlar ve yasak sayaçları süreç belleğinde durur; şifresi zaten her
|
|
812
|
+
restart'ta değişen bir panel için diske yazmak yanlış takas olurdu.
|
|
813
|
+
|
|
814
|
+
### Panelde ne var
|
|
815
|
+
|
|
816
|
+
| Bölüm | Gösterdiği |
|
|
817
|
+
| --- | --- |
|
|
818
|
+
| Üst satır | Sürüm, ortam, pid, uptime, RSS |
|
|
819
|
+
| Kartlar | HTML girdi sayısı ve sınırı, bellekteki HTML boyutu, bayat girdi sayısı, veri girdisi sayısı, Redis durumu (`connected` / `bypassed` / `off`), ısıtma turunun ilerlemesi |
|
|
820
|
+
| Paylaşımlı kademe | Bağlantının **nereye** kurulduğu (adres, TLS, veritabanı), anahtar öneki ve `namespace`, `buildId`, hangi türlerin paylaşıldığı, sıkıştırılmış gövde ve purge yayını durumu, komut zaman aşımı ve hata sayısı. Kapalıysa yerine Redis önerisi ve kurulum parçacığı çıkar. |
|
|
821
|
+
| Cloudflare | Zone, plan, cache ile ilgili zone ayarları, development mode'un kalan süresi, Tiered Cache / Cache Reserve durumu ve cache isabet oranı. Bağlı değilse kurulum parçacığı çıkar. |
|
|
822
|
+
| Host | Makinenin RAM kullanımı ve projenin bulunduğu diskin doluluğu |
|
|
823
|
+
| Girdi listesi | HTML: yol (yeni sekmede açılır), taze/bayat, boyut, durum kodu, kalan TTL, bağımlılık sayısı, hazır sıkıştırılmış gövdeler. Veri: anahtar (tıklayınca panoya kopyalanır), taze/bayat, kalan TTL |
|
|
824
|
+
|
|
825
|
+
Liste **anahtar bazında filtrelenir** ve filtre sunucuda uygulanır: veri
|
|
826
|
+
önbelleğinde on binlerce anahtar olabiliyor. Her istekte en fazla 500 satır
|
|
827
|
+
döner; başlıktaki sayaç kaç eşleşmenin kesildiğini söyler. HTML gövdesi ve veri
|
|
828
|
+
değerleri **hiç dönmez** — panelin işi durumu göstermek, içeriği dışa vermek
|
|
829
|
+
değil.
|
|
830
|
+
|
|
831
|
+
### Panelden yapılabilenler
|
|
832
|
+
|
|
833
|
+
| İşlem | Karşılığı |
|
|
834
|
+
| --- | --- |
|
|
835
|
+
| Invalidate (hedef + `hard`) | `invalidateHtmlCache(target, { hard })` |
|
|
836
|
+
| Tek satırı `drop` | `dropHtmlCacheKey(key)` / `dropDataCacheKey(key)` |
|
|
837
|
+
| Clear HTML cache | `clearHtmlCache()` |
|
|
838
|
+
| Clear data cache (önek opsiyonel) | `clearDataCache(prefix)` |
|
|
839
|
+
| Drop shared keys | Redis'teki `html` ya da `data` isim alanını tarar ve düşürür |
|
|
840
|
+
| Count keys in Redis | `inspectRedis()` — tür başına anahtar sayısı, `DBSIZE` ve `used_memory` |
|
|
841
|
+
| Prewarm | `prewarm()` — tur arkada koşar, ilerleme kartta görünür |
|
|
842
|
+
| Cloudflare purge (her şey / bellekteki URL'ler / prefix / host / tag) | `purgeCloudflare()` |
|
|
843
|
+
| Cloudflare ayarı ya da özelliği değiştirmek | Zone ayarları ve Tiered Cache / Cache Reserve |
|
|
844
|
+
|
|
845
|
+
Hepsi paylaşımlı kademeye de yayılır: tek kopyanın önbelleğini boşaltmak,
|
|
846
|
+
kümede çalışan bir kurulumda "temizledim ama hâlâ eski" sorusunu üretir.
|
|
847
|
+
|
|
848
|
+
Listedeki tek satırı silmek `invalidateHtmlCache()`ten farklıdır: o yol
|
|
849
|
+
desenine bakar ve bir yolun **bütün** query varyantlarını düşürür,
|
|
850
|
+
`dropHtmlCacheKey()` ise tam anahtarı alır — `/liste?sayfa=2` düşerken
|
|
851
|
+
`/liste?sayfa=3` sıcak kalır.
|
|
852
|
+
|
|
853
|
+
## CDN kademesi: Cloudflare
|
|
854
|
+
|
|
855
|
+
Buraya kadar anlatılan her şey **origin** önbelleği. Önünde Cloudflare varsa
|
|
856
|
+
ziyaretçinin gördüğü HTML çoğu zaman hiç size ulaşmıyor: edge'deki kopya
|
|
857
|
+
TTL'ini doldurana kadar servis edilir. Bu yüzden `invalidateHtmlCache()`
|
|
858
|
+
tek başına "sayfayı güncelledim ama eski hâli görünüyor" sorununu çözmez —
|
|
859
|
+
origin tazelenir, edge beklemeye devam eder.
|
|
860
|
+
|
|
861
|
+
JSkelet iki kademeyi aynı yerden yönetilebilir kılar.
|
|
862
|
+
|
|
863
|
+
### Kurulum
|
|
864
|
+
|
|
865
|
+
Token bir sır; config dosyasına değil ortama yazılır:
|
|
866
|
+
|
|
867
|
+
```bash
|
|
868
|
+
JSKELET_CLOUDFLARE_KEY=... # API token
|
|
869
|
+
JSKELET_CLOUDFLARE_ZONE_ID=... # zone kimliği
|
|
870
|
+
JSKELET_CLOUDFLARE_HOSTNAME=example.com # opsiyonel
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
Token'a gereken izinler, yapmak istediğinize göre: purge için `Zone.Cache
|
|
874
|
+
Purge`, ayarları değiştirmek için `Zone.Zone Settings`, isabet oranı ve edge
|
|
875
|
+
kırılımı için `Zone.Analytics` (salt okunur). Yalnızca purge izni verilen bir
|
|
876
|
+
token'la panel açılır, ayar bölümleri hata yazar.
|
|
877
|
+
|
|
878
|
+
Zone kimliği ve site adı sır olmadığı için `jskelet.config.mjs` içinden de
|
|
879
|
+
verilebilir; env her zaman önceliklidir:
|
|
880
|
+
|
|
881
|
+
```js
|
|
882
|
+
cache: {
|
|
883
|
+
cloudflare: {
|
|
884
|
+
zoneId: "…",
|
|
885
|
+
hostname: "example.com", // purge tam URL ister; yol → URL çevrimi için
|
|
886
|
+
analyticsHours: 24,
|
|
887
|
+
},
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
`hostname` verilmezse purge URL'leri panelin açıldığı origin'den türetilir.
|
|
892
|
+
Paneli iç bir adresten (`http://10.0.0.4:3000`) açıyorsanız bu adresin
|
|
893
|
+
Cloudflare'de karşılığı yok; o kurulumda `hostname` zorunlu.
|
|
894
|
+
|
|
895
|
+
### Ne yapılabilir
|
|
896
|
+
|
|
897
|
+
Cloudflare'in cache yüzeyinde ne varsa panelde de var:
|
|
898
|
+
|
|
899
|
+
| İşlem | Not |
|
|
900
|
+
| --- | --- |
|
|
901
|
+
| Purge everything | Zone'un tamamı. En kaba araç; ısınma maliyeti yüksek |
|
|
902
|
+
| Purge by URL | Panelin o an bellekte tuttuğu sayfalar tek düğmeyle; ya da satır başına `cf purge` |
|
|
903
|
+
| Purge by prefix / host / tag | Artık her planda çalışıyor; istek başına 100 anahtar |
|
|
904
|
+
| Development mode | Üç saat boyunca edge önbelleğini baypas eder, sonra kendiliğinden kapanır |
|
|
905
|
+
| Cache level, browser cache TTL, query string sıralaması, Always Online | Zone ayarları |
|
|
906
|
+
| Tiered Cache, Regional Tiered Cache, Cache Reserve | Plana bağlı; kapalı planda "unavailable" görünür |
|
|
907
|
+
| Clear Cache Reserve | Purge'den ayrı: `purge_everything` edge'i düşürür, R2'deki kalıcı kopya kalır |
|
|
908
|
+
|
|
909
|
+
Uzun URL listeleri istek başına 100 anahtarlık partilere bölünür ve **sırayla**
|
|
910
|
+
gönderilir. Paralel göndermek Free planda dakikada beş isteklik hız freni
|
|
911
|
+
yüzünden yarısı reddedilen bir tur demek.
|
|
912
|
+
|
|
913
|
+
Kod tarafında aynı yüzey:
|
|
914
|
+
|
|
915
|
+
```js
|
|
916
|
+
import { invalidateHtmlCache, purgeCloudflare, toCloudflareUrls } from "jskelet";
|
|
917
|
+
|
|
918
|
+
export async function onPostPublished(slug) {
|
|
919
|
+
const paths = ["/", `/blog/${slug}`];
|
|
920
|
+
|
|
921
|
+
invalidateHtmlCache(paths); // origin
|
|
922
|
+
await purgeCloudflare({ files: toCloudflareUrls(paths) }); // edge
|
|
923
|
+
}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
Bu modüldeki hiçbir fonksiyon fırlatmaz: token yoksa, Cloudflare 403 dönerse
|
|
927
|
+
ya da ağ düşerse sonuç `{ ok: false, error }` olur. Bir CDN arızası içerik
|
|
928
|
+
yayınlama akışını kesmemeli.
|
|
929
|
+
|
|
930
|
+
### "Bu sayfa kaç edge'de cache'li?" — sorulabilen ve sorulamayan
|
|
931
|
+
|
|
932
|
+
Cloudflare API'sinde bir objenin **envanterini** veren uç yok. Yüzlerce şehirde
|
|
933
|
+
birbirinden bağımsız önbellekler var ve hiçbiri "şu an bende bu URL'in kopyası
|
|
934
|
+
var mı" sorusuna cevap vermiyor. Panel bu yüzden envanter değil **gözlem**
|
|
935
|
+
gösterir: bir yol girip sorguladığınızda GraphQL analitiğinden son N saatte
|
|
936
|
+
hangi kolonun (IST, FRA, AMS…) o yolu kaç kez cache'ten, kaç kez origin'den
|
|
937
|
+
servis ettiği gelir.
|
|
938
|
+
|
|
939
|
+
```js
|
|
940
|
+
const report = await fetchPathEdges({ path: "/blog", hours: 24 });
|
|
941
|
+
// → { colos: [{ colo: "IST", hits: 7, misses: 2 }, …], hits, misses }
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
Okurken iki sınırı akılda tutun: hiç istek almamış bir edge listede görünmez,
|
|
945
|
+
kopyası olsa da; ve veri kümesi örneklemeli, yani oranlar güvenilir ama mutlak
|
|
946
|
+
sayılar yaklaşıktır.
|
|
947
|
+
|
|
948
|
+
Seçtiğiniz bir edge'i **ısıtmanın** da yolu yok. Bir obje ancak o koloya
|
|
949
|
+
yönlenen gerçek bir istekle o edge'in önbelleğine giriyor; sunucudan
|
|
950
|
+
"Frankfurt'a bunu cache'let" diyemezsiniz. Pratikte yapılabilen üç şey var:
|
|
951
|
+
|
|
952
|
+
- **Origin'i ısıtmak** (`prewarm`): ilk isteği alan edge cevabı hazır bulur,
|
|
953
|
+
o istek yavaşlamaz.
|
|
954
|
+
- **Tiered Cache**: edge'ler origin'e doğrudan gitmez, aradaki üst katmandan
|
|
955
|
+
besleniyor; bir şehirdeki ilk istek diğer şehirler için de ısıtma sayılır.
|
|
956
|
+
- **Cache Reserve**: uzun kuyruklu içerik için R2'de kalıcı kopya; edge
|
|
957
|
+
düşünce istek origin'e kadar inmiyor.
|
|
958
|
+
|
|
959
|
+
`hit` oranınız düşükse önce cevabın cache'lenebilir olup olmadığına bakın:
|
|
960
|
+
`Cache-Control: private`, `Set-Cookie` ve query string ayarları edge'in
|
|
961
|
+
cache'lememe kararının en sık sebepleri, ve bu panelde `dynamic` olarak
|
|
962
|
+
görünür.
|
|
963
|
+
|
|
652
964
|
## Prewarm — açılışta ısıtma
|
|
653
965
|
|
|
654
966
|
Next'teki build-time prerender'ın karşılığı, ama çıktı diske yazılmaz: önbellek
|
|
@@ -700,12 +1012,28 @@ Kurallar:
|
|
|
700
1012
|
Dev'de varsayılan olarak saniyede 4 istek uygulanır: render tek bir olay
|
|
701
1013
|
döngüsünde çalıştığı için aralıksız bir tur, sayfa isteklerini ve dev
|
|
702
1014
|
panelinin canlı kanalını arkasında bekletiyor.
|
|
703
|
-
4.
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1015
|
+
4. **Geçici** hata alan yollar için **tek seri tekrar turu** yapılır
|
|
1016
|
+
(`concurrency: 1`). `400`/`403`/`404` gibi kalıcı cevaplar tekrar turuna
|
|
1017
|
+
girmez: deterministik bir hata tekrar denemekle düzelmez ve o çağrılar
|
|
1018
|
+
kotadan karşılıksız yer. Özette `N not retried (permanent)` olarak görünür.
|
|
1019
|
+
5. Bekleme süresi `retryDelayMs`, ama hız freni açıkken **freni bekleten süre**
|
|
1020
|
+
varsa o kazanır: 10 saniye kapalı kalacak bir devre kesiciden 2 saniye sonra
|
|
1021
|
+
tekrar denemek, aynı 429'u peşin peşin almak olurdu.
|
|
1022
|
+
6. Özet loglanır:
|
|
707
1023
|
`[prewarm] warmed 128/130 pages, 2 failed, 5 recovered on the retry pass (12.4s)`
|
|
708
1024
|
|
|
1025
|
+
Ardından turun upstream'e ne kadar dokunduğu basılır:
|
|
1026
|
+
|
|
1027
|
+
```text
|
|
1028
|
+
[prewarm] 12 upstream calls for 430 data reads (97% from the data cache, 38 coalesced)
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
Bu satır ayarın hangi yönde değişmesi gerektiğini söyleyen tek satır. Oran
|
|
1032
|
+
düşükse çözüm hız freni değil, `withDataCache` TTL'ini tur aralığından uzun
|
|
1033
|
+
tutmak: fren çağrıları yavaşlatır, sayısını azaltmaz. Aynı sayaçlara
|
|
1034
|
+
`getDataCacheStats()` ile ya da dev raporundaki **Data cache** kartından da
|
|
1035
|
+
bakılabilir.
|
|
1036
|
+
|
|
709
1037
|
Tur sırasında oluşan istek hataları ve sayfa başına basılan render uyarıları
|
|
710
1038
|
(`was produced with missing data`, `returned notFound() while upstream is
|
|
711
1039
|
failing`, `could not be produced`) tek tek loglanmaz; sayılır ve tur bitince
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -115,6 +115,7 @@ export default {
|
|
|
115
115
|
async cache() {
|
|
116
116
|
return {
|
|
117
117
|
html: { "/": 60, "/haber/:slug": 300 },
|
|
118
|
+
query: { "/arama": ["q", "page"] },
|
|
118
119
|
maxEntries: 500,
|
|
119
120
|
data: { maxEntries: 10000, staleFactor: 10 },
|
|
120
121
|
prewarm: {
|
|
@@ -560,9 +561,9 @@ Ayrıntı: [03-routing.md](./03-routing.md).
|
|
|
560
561
|
## `cache()`
|
|
561
562
|
|
|
562
563
|
**Tip:**
|
|
563
|
-
`() => { html?: Record<string, number>, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, redis?: object, prewarm?: object }` —
|
|
564
|
+
`() => { html?: Record<string, number>, query?: Record<string, string[] | true>, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, upstream?: object, redis?: object, prewarm?: object }` —
|
|
564
565
|
**Varsayılan:**
|
|
565
|
-
`{ html: {}, maxEntries: 500, data: { maxEntries: 10000, staleFactor: 10 }, trackUpstream: true, trackDependencies: true, transientRetry: { attempts: 1, delayMs: 300 }, redis: { enabled: false }, prewarm: { enabled: true, max: 400, intervalSeconds: 0 } }`
|
|
566
|
+
`{ html: {}, query: {}, 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 } }`
|
|
566
567
|
|
|
567
568
|
### `cache().html`
|
|
568
569
|
|
|
@@ -582,6 +583,39 @@ Tek istisna `route(fn, { private: true })`: bu route'ta desen eşleşse bile yok
|
|
|
582
583
|
sayılır. Kilidin tek yönlü olması bilinçli — ters yönde bir hata, bir
|
|
583
584
|
kullanıcının HTML'inin bir başkasına servis edilmesi anlamına geliyor.
|
|
584
585
|
|
|
586
|
+
### `cache().query`
|
|
587
|
+
|
|
588
|
+
Desen → cache anahtarına girmesine izin verilen query parametreleri.
|
|
589
|
+
|
|
590
|
+
**Varsayılan olarak query parametresi taşıyan istek dinamiktir**: `cache().html`
|
|
591
|
+
o yolu kapsıyor olsa bile HTML önbelleğine hiç girmez, `private, no-store` ile
|
|
592
|
+
gider. Sebebi basit — bir yolun bütün varyantlarını cache'lemek `?utm_source=…`
|
|
593
|
+
gibi sonsuz sayıda anahtar üretiyor ve `maxEntries` sınırına dayandığında
|
|
594
|
+
LRU'daki gerçek sayfaları dışarı atıyor. Hangi parametrenin çıktıyı gerçekten
|
|
595
|
+
değiştirdiğini yalnızca uygulama bilir.
|
|
596
|
+
|
|
597
|
+
```js
|
|
598
|
+
query: {
|
|
599
|
+
"/arama": ["q", "page"], // yalnızca bu ikisi anahtara girer
|
|
600
|
+
"/urunler": ["kategori"],
|
|
601
|
+
"/rapor/:id": true, // bütün parametreler anahtara girer
|
|
602
|
+
"/kampanya": [], // query tamamen yok sayılır
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
- **İzin listesi** (`string[]`): listedeki parametreler anahtara girer, her
|
|
607
|
+
farklı değer kendi girdisini alır. Listede olmayan parametreler **yok
|
|
608
|
+
sayılır** — sayfa yine cache'lenir ve bütün kampanya varyantları tek kopyayı
|
|
609
|
+
paylaşır.
|
|
610
|
+
- **`true`**: bütün parametreler anahtara girer. Anahtar sayısını sınırlayan
|
|
611
|
+
tek şey `maxEntries` olur; yalnızca değer kümesi kapalı olan yollarda kullan.
|
|
612
|
+
- **`[]`**: query hiç dikkate alınmaz, bütün varyantlar query'siz sürümün
|
|
613
|
+
HTML'ini alır.
|
|
614
|
+
|
|
615
|
+
Parametreler anahtara **sıralı** yazılır: `?a=1&b=2` ile `?b=2&a=1` aynı girdiyi
|
|
616
|
+
paylaşır. `route(fn, { private: true })` bu bölümden etkilenmez; private route
|
|
617
|
+
hiçbir koşulda cache'lenmez.
|
|
618
|
+
|
|
585
619
|
### `cache().maxEntries`
|
|
586
620
|
|
|
587
621
|
**Tip:** `number` — **Varsayılan:** `500`
|
|
@@ -627,6 +661,39 @@ denenir. Amaç var olan bir sayfanın 404'e dönüşmemesi; denemeler tükenirse
|
|
|
627
661
|
önbelleğe girmeyen bir 503 olur. `false` ya da `attempts: 0` tekrarı kapatır.
|
|
628
662
|
Ayrıntı: [06-cache.md](./06-cache.md).
|
|
629
663
|
|
|
664
|
+
### `cache().upstream`
|
|
665
|
+
|
|
666
|
+
Upstream API'ye giden `fetch` çağrılarının host başına hız freni. Varsayılan
|
|
667
|
+
**kapalı**: `rate` verilmedikçe hiçbir istek beklemez. `rate` bir tavandır;
|
|
668
|
+
gerçek hız 429 cevaplarına göre kendini aşağı çeker ve temiz geçen pencerelerde
|
|
669
|
+
kademe kademe geri çıkar.
|
|
670
|
+
|
|
671
|
+
| Alan | Tip | Varsayılan | Anlamı |
|
|
672
|
+
| --- | --- | --- | --- |
|
|
673
|
+
| `rate` | `number` | `0` | Saniyedeki en fazla çağrı. `0` → fren kapalı |
|
|
674
|
+
| `burst` | `number` | `0` | Kova boyu; `0` → bir saniyelik bütçe kadar patlama |
|
|
675
|
+
| `concurrency` | `number` | `8` | Aynı anda uçabilecek çağrı |
|
|
676
|
+
| `minRate` | `number` | `0.5` | Azalmanın dibi; hız buranın altına inmez |
|
|
677
|
+
| `increaseStep` | `number` | `1` | Toplamsal artışın adımı (çağrı/saniye) |
|
|
678
|
+
| `increaseIntervalMs` | `number` | `5000` | Artış periyodu |
|
|
679
|
+
| `decreaseIntervalMs` | `number` | `1000` | İki azalma arasındaki en kısa süre |
|
|
680
|
+
| `breakerFailures` | `number` | `5` | Art arda kaç 429'dan sonra host baypas edilir |
|
|
681
|
+
| `breakerCooldownMs` | `number` | `10000` | Baypasın süresi |
|
|
682
|
+
| `hosts` | `Record<string, object>` | `{}` | Host bazlı override; aynı alanlar geçerli |
|
|
683
|
+
|
|
684
|
+
Yalnızca `429` ve `503` hızı cezalandırır: `400`/`404`/`500` bir kota sorunu
|
|
685
|
+
değil. Durumu `getUpstreamLimiterStatus()` ile ya da dev panelinin **Server**
|
|
686
|
+
sekmesinden okuyabilirsin. Ayrıntı ve freni açmadan önce bakılacak yer:
|
|
687
|
+
[06-cache.md](./06-cache.md).
|
|
688
|
+
|
|
689
|
+
```js
|
|
690
|
+
upstream: {
|
|
691
|
+
rate: 10,
|
|
692
|
+
concurrency: 4,
|
|
693
|
+
hosts: { "api.example.com": { rate: 3 } },
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
630
697
|
### `cache().redis`
|
|
631
698
|
|
|
632
699
|
Opsiyonel Redis ikinci kademesi (L2). Bellek içi önbellek birincil kalır; Redis
|
|
@@ -661,6 +728,64 @@ redis: {
|
|
|
661
728
|
}
|
|
662
729
|
```
|
|
663
730
|
|
|
731
|
+
### `cache().panel`
|
|
732
|
+
|
|
733
|
+
Önbellek yönetim paneli. Bellek içi kademenin ve Redis kademesinin durumunu
|
|
734
|
+
gösterir; hedefli invalidation, tek girdi silme ve ısıtma tetikler.
|
|
735
|
+
|
|
736
|
+
Ortama bakmaz: `enabled` verilmedikçe **hiç mount edilmez** ve yol da yoktur.
|
|
737
|
+
Açıkken production'da da çalışır — asıl sorular ("bu sayfa neden bayat",
|
|
738
|
+
"webhook purge'ü geçti mi") orada soruluyor.
|
|
739
|
+
|
|
740
|
+
| Alan | Tip | Varsayılan | Anlamı |
|
|
741
|
+
| --- | --- | --- | --- |
|
|
742
|
+
| `enabled` | `boolean` | `false` | Yalnızca açıkça `true` verildiğinde açılır (`JSKELET_CACHE_PANEL` ezer) |
|
|
743
|
+
| `basePath` | `string` | `"/_jskelet/cache"` | Panelin kökü |
|
|
744
|
+
| `banAttempts` | `number` | `3` | Kaç başarısız denemeden sonra IP yasaklanır |
|
|
745
|
+
| `banHours` | `number` | `24` | Yasağın süresi |
|
|
746
|
+
| `sessionHours` | `number` | `12` | Oturum çerezinin ömrü |
|
|
747
|
+
|
|
748
|
+
Şifre **her süreç başlangıcında** üretilir ve yalnızca sunucu logunda görünür:
|
|
749
|
+
|
|
750
|
+
```
|
|
751
|
+
[cache-panel] http://localhost:3000/_jskelet/cache — password for this run: 3f9c…
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
Kalıcı bir sır (config alanı ya da env) tutulmaz; sızması önbelleği boşaltma
|
|
755
|
+
yetkisi demek ve her deploy eski erişimi kendiliğinden iptal etmeli. Yasaklı ve
|
|
756
|
+
yetkisiz her cevap `404`'tür. Kullanım ve ekran ayrıntıları:
|
|
757
|
+
[06-cache.md](./06-cache.md).
|
|
758
|
+
|
|
759
|
+
### `cache().cloudflare`
|
|
760
|
+
|
|
761
|
+
CDN kademesi. JSkelet'in önbelleği origin önbelleği; ziyaretçinin gördüğü kopya
|
|
762
|
+
edge'de duruyor. Bu bölüm bağlıysa panelden edge purge'ü, cache ile ilgili zone
|
|
763
|
+
ayarları ve cache isabet oranı yönetilebilir.
|
|
764
|
+
|
|
765
|
+
| Alan | Tip | Varsayılan | Anlamı |
|
|
766
|
+
| --- | --- | --- | --- |
|
|
767
|
+
| `enabled` | `boolean` | `true` | `false` verilirse env'de token olsa bile yüzey kapalı kalır |
|
|
768
|
+
| `zoneId` | `string \| null` | `null` | Zone kimliği (`JSKELET_CLOUDFLARE_ZONE_ID` ezer) |
|
|
769
|
+
| `apiToken` | `string \| null` | `null` | Token; **env tercih edilir**, config'e yazmak sırrı repoya sokar |
|
|
770
|
+
| `hostname` | `string \| null` | `null` | Purge tam URL ister; yol → URL çevrimi bu ad üzerinden yapılır. Verilmezse panelin açıldığı origin kullanılır |
|
|
771
|
+
| `analyticsHours` | `number` | `24` | Analitik penceresi, en çok `72` |
|
|
772
|
+
|
|
773
|
+
Token yalnızca `JSKELET_CLOUDFLARE_KEY` ile verildiğinde config dosyası temiz
|
|
774
|
+
kalır; izinler yapılacak işe göre: purge için `Zone.Cache Purge`, ayarlar için
|
|
775
|
+
`Zone.Zone Settings`, isabet oranı için `Zone.Analytics` (salt okunur). Token
|
|
776
|
+
hiçbir panel cevabında dönmez, yalnızca "env'den geldi" bilgisi görünür.
|
|
777
|
+
|
|
778
|
+
Zone bağlı değilse panel bir uyarı değil kurulum önerisi gösterir; Cloudflare
|
|
779
|
+
hata dönerse ilgili bölüm hatayı yazar ve panelin kalanı çalışmaya devam eder.
|
|
780
|
+
Neyin sorulabildiği — özellikle "bu sayfa kaç edge'de cache'li" sorusunun neden
|
|
781
|
+
tam cevabı olmadığı — [06-cache.md](./06-cache.md) içinde.
|
|
782
|
+
|
|
783
|
+
```js
|
|
784
|
+
panel: {
|
|
785
|
+
enabled: process.env.CACHE_PANEL === "1",
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
664
789
|
### `cache().prewarm`
|
|
665
790
|
|
|
666
791
|
| Alan | Tip | Varsayılan | Anlamı |
|
|
@@ -784,6 +909,10 @@ basılmaz.
|
|
|
784
909
|
| `HOST` | `startServer` | `::` | Bağlanılacak arayüz. Varsayılan çift yığın dinler (IPv6 + IPv4); IPv6 yoksa `0.0.0.0`'a düşer |
|
|
785
910
|
| `JSKELET_SECRET` | `jskelet/cookies` | — | İmzalı cookie sırrı. `security.cookieSecret` verilmediğinde buradan okunur; ikisi de yoksa imzalı cookie API'si hata verir. [12](./12-panel-ve-oturum.md) |
|
|
786
911
|
| `DEV_TOKEN` | `devGate`, `prewarm` | — | Ayarlıysa token taşımayan her isteğe 404 döner. Isıtma token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
|
|
912
|
+
| `JSKELET_CACHE_PANEL` | `createApp` | — | Ayarlıysa önbellek panelini açar; `0` config'te açık olan paneli kapatır. Env config'i ezer, çünkü panel genelde bir arıza sırasında tek seferlik açılır. [06](./06-cache.md) |
|
|
913
|
+
| `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache yüzeyi | — | API token. Verilene kadar CDN purge'ü ve edge analitiği kapalıdır; config'teki `apiToken`'ı ezer. Token hiçbir cevapta dönmez. [06](./06-cache.md) |
|
|
914
|
+
| `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache yüzeyi | — | Zone kimliği. Token'la birlikte verilmedikçe hiçbir Cloudflare ucu çağrılmaz |
|
|
915
|
+
| `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache yüzeyi | — | Purge URL'lerinin kökü. Panel iç bir adresten açılıyorsa gerekir |
|
|
787
916
|
| `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
|
|
788
917
|
| `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
|
|
789
918
|
| `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 1 | Paralel işçi sayısı |
|
package/docs/en/03-routing.md
CHANGED
|
@@ -126,6 +126,10 @@ app.get(
|
|
|
126
126
|
| `revalidate` | `number` (seconds) | The HTML cache TTL. If it is not given, or is 0, this route is not cached. A matching rule in `jskelet.config.mjs` → `cache().html` **overrides** this value. |
|
|
127
127
|
| `private` | `boolean` | The page depends on the visitor. The cache is disabled, a `cache().html` pattern **cannot** override that, and the response is sent with `private, no-store` and `Vary: Cookie`, without an ETag. |
|
|
128
128
|
|
|
129
|
+
Even with `revalidate` given, **a request that carries a query parameter is
|
|
130
|
+
dynamic by default**; that path needs an allowlist under `cache().query`
|
|
131
|
+
([06-caching.md](./06-caching.md)).
|
|
132
|
+
|
|
129
133
|
Every session-dependent page needs `private: true`; because identity is not part
|
|
130
134
|
of the cache key, without the flag one user's HTML is served to another. The
|
|
131
135
|
framework also catches this at runtime (a render that reads cookies is never
|