jskelet 0.2.2 → 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 -315
- package/docs/03-routing.md +4 -0
- package/docs/06-cache.md +147 -10
- package/docs/07-yapilandirma.md +63 -2
- package/docs/en/03-routing.md +4 -0
- package/docs/en/06-caching.md +147 -11
- package/docs/en/07-configuration.md +65 -2
- package/package.json +1 -1
- package/src/client/cache-panel/login.html +9 -1
- package/src/client/cache-panel/panel.css +298 -6
- package/src/client/cache-panel/panel.html +179 -5
- package/src/client/cache-panel/panel.js +472 -3
- package/src/config/defaults.js +31 -0
- package/src/config/index.js +87 -2
- package/src/index.js +16 -1
- package/src/log.mjs +25 -0
- package/src/server/cache-panel.js +246 -13
- package/src/server/cloudflare.js +595 -0
- package/src/server/redis.js +108 -0
- package/src/server/render.js +74 -2
- 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
|
|
|
@@ -738,6 +751,13 @@ Bağlantı kurulmamışken de güvenle çağrılır. Dönen nesne
|
|
|
738
751
|
kesicinin açık olduğunu, `errors` toplam komut hatasını gösterir. Aynı özet dev
|
|
739
752
|
panelinin raporunda da var ([09-dev-araclari.md](./09-dev-araclari.md)).
|
|
740
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
|
+
|
|
741
761
|
Ayarların tam listesi: [07-yapilandirma.md](./07-yapilandirma.md).
|
|
742
762
|
|
|
743
763
|
## Yönetim paneli
|
|
@@ -795,9 +815,12 @@ Panel açıldığında sunucu logu şifreyi basar:
|
|
|
795
815
|
|
|
796
816
|
| Bölüm | Gösterdiği |
|
|
797
817
|
| --- | --- |
|
|
798
|
-
| Üst satır |
|
|
818
|
+
| Üst satır | Sürüm, ortam, pid, uptime, RSS |
|
|
799
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 |
|
|
800
|
-
|
|
|
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 |
|
|
801
824
|
|
|
802
825
|
Liste **anahtar bazında filtrelenir** ve filtre sunucuda uygulanır: veri
|
|
803
826
|
önbelleğinde on binlerce anahtar olabiliyor. Her istekte en fazla 500 satır
|
|
@@ -814,7 +837,10 @@ değil.
|
|
|
814
837
|
| Clear HTML cache | `clearHtmlCache()` |
|
|
815
838
|
| Clear data cache (önek opsiyonel) | `clearDataCache(prefix)` |
|
|
816
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` |
|
|
817
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 |
|
|
818
844
|
|
|
819
845
|
Hepsi paylaşımlı kademeye de yayılır: tek kopyanın önbelleğini boşaltmak,
|
|
820
846
|
kümede çalışan bir kurulumda "temizledim ama hâlâ eski" sorusunu üretir.
|
|
@@ -824,6 +850,117 @@ desenine bakar ve bir yolun **bütün** query varyantlarını düşürür,
|
|
|
824
850
|
`dropHtmlCacheKey()` ise tam anahtarı alır — `/liste?sayfa=2` düşerken
|
|
825
851
|
`/liste?sayfa=3` sıcak kalır.
|
|
826
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
|
+
|
|
827
964
|
## Prewarm — açılışta ısıtma
|
|
828
965
|
|
|
829
966
|
Next'teki build-time prerender'ın karşılığı, ama çıktı diske yazılmaz: önbellek
|
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, upstream?: object, 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 }, upstream: { rate: 0 }, 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`
|
|
@@ -722,6 +756,30 @@ yetkisi demek ve her deploy eski erişimi kendiliğinden iptal etmeli. Yasaklı
|
|
|
722
756
|
yetkisiz her cevap `404`'tür. Kullanım ve ekran ayrıntıları:
|
|
723
757
|
[06-cache.md](./06-cache.md).
|
|
724
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
|
+
|
|
725
783
|
```js
|
|
726
784
|
panel: {
|
|
727
785
|
enabled: process.env.CACHE_PANEL === "1",
|
|
@@ -852,6 +910,9 @@ basılmaz.
|
|
|
852
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) |
|
|
853
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) |
|
|
854
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 |
|
|
855
916
|
| `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
|
|
856
917
|
| `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
|
|
857
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
|
package/docs/en/06-caching.md
CHANGED
|
@@ -117,18 +117,31 @@ The cache also only kicks in for `GET` requests.
|
|
|
117
117
|
## The cache key
|
|
118
118
|
|
|
119
119
|
```
|
|
120
|
-
`${
|
|
120
|
+
`${path}?${the allowed query parameters, sorted}`
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
For a request without a query the key is just the path. **A request that carries
|
|
124
|
+
a query parameter is dynamic by default**: it never enters the cache and is sent
|
|
125
|
+
with `private, no-store`. Caching every variant of a path mints an unbounded
|
|
126
|
+
number of keys (`?utm_source=…` and friends), and in a 500-entry store LRU then
|
|
127
|
+
evicts the real pages in favour of campaign variants.
|
|
125
128
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
Which parameter actually changes the output is declared by the application, in
|
|
130
|
+
`jskelet.config.mjs` → `cache().query`:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
cache: () => ({
|
|
134
|
+
html: { "/list": 60 },
|
|
135
|
+
query: { "/list": ["page"] },
|
|
136
|
+
}),
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Now `/list?page=2` and `/list?page=3` are separate entries, while
|
|
140
|
+
`/list?page=2&utm_source=x` shares the `?page=2` copy: a parameter outside the
|
|
141
|
+
list never reaches the key. A pattern mapped to `true` puts every parameter in
|
|
142
|
+
the key (careful: nothing but `maxEntries` then bounds the entry count), and one
|
|
143
|
+
mapped to `[]` ignores the query entirely. Details:
|
|
144
|
+
[07-configuration.md](./07-configuration.md).
|
|
132
145
|
|
|
133
146
|
## Stale-while-revalidate
|
|
134
147
|
|
|
@@ -756,6 +769,13 @@ you the circuit breaker is open and `errors` is the total command failure count.
|
|
|
756
769
|
The same summary is in the dev panel report
|
|
757
770
|
([09-dev-tools.md](./09-dev-tools.md)).
|
|
758
771
|
|
|
772
|
+
Two more diagnostic surfaces:
|
|
773
|
+
|
|
774
|
+
| Call | What it tells you |
|
|
775
|
+
| --- | --- |
|
|
776
|
+
| `getRedisDetails()` | **Where** the connection points: address, TLS, database, `namespace`, which kinds are shared, whether the purge channel is subscribed. The password is never returned — a connection URL may carry one. |
|
|
777
|
+
| `inspectRedis()` | What is actually in the shared tier: keys per kind, `DBSIZE` and `used_memory`. It runs a `SCAN`, so **never call it on the request path**; in the admin panel it sits behind its own button. |
|
|
778
|
+
|
|
759
779
|
The full list of settings: [07-configuration.md](./07-configuration.md).
|
|
760
780
|
|
|
761
781
|
## The admin panel
|
|
@@ -817,9 +837,12 @@ When the panel is on, the server log prints the password:
|
|
|
817
837
|
|
|
818
838
|
| Area | Contents |
|
|
819
839
|
| --- | --- |
|
|
820
|
-
| Top bar |
|
|
840
|
+
| Top bar | Version, environment, pid, uptime, RSS |
|
|
821
841
|
| Cards | HTML entry count and limit, HTML bytes in memory, stale entry count, data entry count, Redis state (`connected` / `bypassed` / `off`), prewarm progress |
|
|
822
|
-
|
|
|
842
|
+
| Shared tier | **Where** the connection points (address, TLS, database), the key prefix and `namespace`, the `buildId`, which kinds are shared, the state of compressed bodies and the purge broadcast, the command timeout and the error count. When it is off, a Redis recommendation with an install snippet takes its place. |
|
|
843
|
+
| Cloudflare | Zone, plan, cache related zone settings, how long development mode has left, Tiered Cache / Cache Reserve state and the cache hit ratio. When no zone is connected, a setup snippet takes its place. |
|
|
844
|
+
| Host | The machine's memory usage and how full the disk holding the project is |
|
|
845
|
+
| Entry list | HTML: path (opens in a new tab), fresh/stale, size, status code, remaining TTL, dependency count, precompressed bodies. Data: key (click to copy), fresh/stale, remaining TTL |
|
|
823
846
|
|
|
824
847
|
The list is **filtered by key** and the filter runs on the server: a data cache
|
|
825
848
|
can hold tens of thousands of keys. At most 500 rows come back per request and
|
|
@@ -836,7 +859,10 @@ export content.
|
|
|
836
859
|
| Clear HTML cache | `clearHtmlCache()` |
|
|
837
860
|
| Clear data cache (optional prefix) | `clearDataCache(prefix)` |
|
|
838
861
|
| Drop shared keys | Scans and unlinks the `html` or `data` namespace in Redis |
|
|
862
|
+
| Count keys in Redis | `inspectRedis()` — keys per kind, `DBSIZE` and `used_memory` |
|
|
839
863
|
| Prewarm | `prewarm()` — the pass runs in the background, progress shows in the card |
|
|
864
|
+
| Cloudflare purge (everything / URLs held here / prefix / host / tag) | `purgeCloudflare()` |
|
|
865
|
+
| Change a Cloudflare setting or feature | Zone settings and Tiered Cache / Cache Reserve |
|
|
840
866
|
|
|
841
867
|
Each one propagates to the shared tier as well: clearing a single replica's
|
|
842
868
|
cache is what produces the "I cleared it and it is still old" question in a
|
|
@@ -847,6 +873,116 @@ matches a path pattern and takes down **every** query variant of a path, while
|
|
|
847
873
|
`dropHtmlCacheKey()` takes the exact key — `/list?page=2` goes and
|
|
848
874
|
`/list?page=3` stays hot.
|
|
849
875
|
|
|
876
|
+
## The CDN tier: Cloudflare
|
|
877
|
+
|
|
878
|
+
Everything above is the **origin** cache. With Cloudflare in front, the HTML
|
|
879
|
+
your visitors get usually never reaches you: the copy at the edge is served
|
|
880
|
+
until its TTL runs out. That is why `invalidateHtmlCache()` alone does not fix
|
|
881
|
+
"I updated the page but the old one still shows" — the origin refreshes, the
|
|
882
|
+
edge keeps waiting.
|
|
883
|
+
|
|
884
|
+
JSkelet lets you drive both tiers from the same place.
|
|
885
|
+
|
|
886
|
+
### Setup
|
|
887
|
+
|
|
888
|
+
The token is a secret, so it goes in the environment, not in a config file:
|
|
889
|
+
|
|
890
|
+
```bash
|
|
891
|
+
JSKELET_CLOUDFLARE_KEY=... # API token
|
|
892
|
+
JSKELET_CLOUDFLARE_ZONE_ID=... # zone identifier
|
|
893
|
+
JSKELET_CLOUDFLARE_HOSTNAME=example.com # optional
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
Which permissions the token needs depends on what you want to do: `Zone.Cache
|
|
897
|
+
Purge` to purge, `Zone.Zone Settings` to change settings, `Zone.Analytics`
|
|
898
|
+
(read) for the hit ratio and the edge breakdown. A purge-only token still opens
|
|
899
|
+
the panel; the settings sections just report an error.
|
|
900
|
+
|
|
901
|
+
The zone id and site name are not secrets, so they can also come from
|
|
902
|
+
`jskelet.config.mjs`. The environment always wins:
|
|
903
|
+
|
|
904
|
+
```js
|
|
905
|
+
cache: {
|
|
906
|
+
cloudflare: {
|
|
907
|
+
zoneId: "…",
|
|
908
|
+
hostname: "example.com", // purging wants absolute URLs; this turns paths into them
|
|
909
|
+
analyticsHours: 24,
|
|
910
|
+
},
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
Without `hostname`, purge URLs are derived from the origin the panel was opened
|
|
915
|
+
on. If you reach the panel over an internal address (`http://10.0.0.4:3000`),
|
|
916
|
+
that address means nothing to Cloudflare — there, `hostname` is required.
|
|
917
|
+
|
|
918
|
+
### What you can do
|
|
919
|
+
|
|
920
|
+
Whatever Cloudflare's cache surface offers is in the panel:
|
|
921
|
+
|
|
922
|
+
| Action | Note |
|
|
923
|
+
| --- | --- |
|
|
924
|
+
| Purge everything | The whole zone. The bluntest tool; warming back up is expensive |
|
|
925
|
+
| Purge by URL | Every page currently held in memory with one button, or `cf purge` per row |
|
|
926
|
+
| Purge by prefix / host / tag | Available on all plans now; 100 keys per request |
|
|
927
|
+
| Development mode | Bypasses the edge cache for three hours, then turns itself off |
|
|
928
|
+
| Cache level, browser cache TTL, query string sorting, Always Online | Zone settings |
|
|
929
|
+
| Tiered Cache, Regional Tiered Cache, Cache Reserve | Plan dependent; shows "unavailable" where the plan lacks it |
|
|
930
|
+
| Clear Cache Reserve | Separate from purging: `purge_everything` drops the edges, the persistent copy in R2 stays |
|
|
931
|
+
|
|
932
|
+
Long URL lists are split into batches of 100 keys and sent **sequentially**.
|
|
933
|
+
Sending them in parallel means half the batch rejected on the Free plan, where
|
|
934
|
+
purging is limited to five requests per minute.
|
|
935
|
+
|
|
936
|
+
The same surface from code:
|
|
937
|
+
|
|
938
|
+
```js
|
|
939
|
+
import { invalidateHtmlCache, purgeCloudflare, toCloudflareUrls } from "jskelet";
|
|
940
|
+
|
|
941
|
+
export async function onPostPublished(slug) {
|
|
942
|
+
const paths = ["/", `/blog/${slug}`];
|
|
943
|
+
|
|
944
|
+
invalidateHtmlCache(paths); // origin
|
|
945
|
+
await purgeCloudflare({ files: toCloudflareUrls(paths) }); // edge
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Nothing in this module throws: with no token, on a Cloudflare 403 or when the
|
|
950
|
+
network drops, the result is `{ ok: false, error }`. A CDN outage should not
|
|
951
|
+
break your publishing flow.
|
|
952
|
+
|
|
953
|
+
### "How many edges hold this page?" — what can and cannot be asked
|
|
954
|
+
|
|
955
|
+
There is no Cloudflare endpoint that lists the **inventory** of an object.
|
|
956
|
+
Hundreds of cities run independent caches and none of them will answer "do you
|
|
957
|
+
currently hold this URL". So the panel shows observation rather than inventory:
|
|
958
|
+
enter a path and the GraphQL analytics tell you which colo (IST, FRA, AMS…)
|
|
959
|
+
served it from cache and how often it went to the origin over the last N hours.
|
|
960
|
+
|
|
961
|
+
```js
|
|
962
|
+
const report = await fetchPathEdges({ path: "/blog", hours: 24 });
|
|
963
|
+
// → { colos: [{ colo: "IST", hits: 7, misses: 2 }, …], hits, misses }
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
Two limits to keep in mind while reading it: an edge that received no request
|
|
967
|
+
does not appear at all, even if it holds a copy; and the dataset is sampled, so
|
|
968
|
+
ratios are reliable while absolute counts are estimates.
|
|
969
|
+
|
|
970
|
+
There is also no way to **warm** an edge you pick. An object enters an edge
|
|
971
|
+
cache only through a real request routed there; you cannot tell Frankfurt from
|
|
972
|
+
your server to go cache something. Three things do work in practice:
|
|
973
|
+
|
|
974
|
+
- **Warm the origin** (`prewarm`): the edge that takes the first request finds
|
|
975
|
+
a ready response, so that request is not the slow one.
|
|
976
|
+
- **Tiered Cache**: edges do not go straight to the origin, they pull from an
|
|
977
|
+
upper tier — the first request in one city counts as warming for the others.
|
|
978
|
+
- **Cache Reserve**: a persistent copy in R2 for long-tail content, so requests
|
|
979
|
+
do not reach the origin when an edge evicts.
|
|
980
|
+
|
|
981
|
+
If your `hit` ratio is low, check whether the response is cacheable at all
|
|
982
|
+
before anything else: `Cache-Control: private`, `Set-Cookie` and query string
|
|
983
|
+
settings are the most common reasons an edge decides not to cache, and they
|
|
984
|
+
show up as `dynamic` in this panel.
|
|
985
|
+
|
|
850
986
|
## Prewarm — warming up at startup
|
|
851
987
|
|
|
852
988
|
The equivalent of Next's build-time prerender, except the output is not written
|
|
@@ -119,6 +119,7 @@ export default {
|
|
|
119
119
|
async cache() {
|
|
120
120
|
return {
|
|
121
121
|
html: { "/": 60, "/news/:slug": 300 },
|
|
122
|
+
query: { "/search": ["q", "page"] },
|
|
122
123
|
maxEntries: 500,
|
|
123
124
|
data: { maxEntries: 10000, staleFactor: 10 },
|
|
124
125
|
prewarm: {
|
|
@@ -572,9 +573,9 @@ Details: [03-routing.md](./03-routing.md).
|
|
|
572
573
|
## `cache()`
|
|
573
574
|
|
|
574
575
|
**Type:**
|
|
575
|
-
`() => { html?: Record<string, number>, maxEntries?: number, data?: object, trackUpstream?: boolean, trackDependencies?: boolean, transientRetry?: object | false, upstream?: object, redis?: object, prewarm?: object }` —
|
|
576
|
+
`() => { 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 }` —
|
|
576
577
|
**Default:**
|
|
577
|
-
`{ html: {}, 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 } }`
|
|
578
|
+
`{ 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 } }`
|
|
578
579
|
|
|
579
580
|
### `cache().html`
|
|
580
581
|
|
|
@@ -594,6 +595,39 @@ html: {
|
|
|
594
595
|
}
|
|
595
596
|
```
|
|
596
597
|
|
|
598
|
+
### `cache().query`
|
|
599
|
+
|
|
600
|
+
A pattern → list of query parameters allowed into the cache key.
|
|
601
|
+
|
|
602
|
+
**By default a request that carries a query parameter is dynamic**: even when
|
|
603
|
+
`cache().html` covers that path, the response never enters the HTML cache and
|
|
604
|
+
is sent with `private, no-store`. The reason is simple — caching every variant
|
|
605
|
+
of a path mints an unbounded number of keys (`?utm_source=…` and friends), and
|
|
606
|
+
once the `maxEntries` limit is reached those keys evict the real pages. Only the
|
|
607
|
+
application knows which parameter actually changes the output.
|
|
608
|
+
|
|
609
|
+
```js
|
|
610
|
+
query: {
|
|
611
|
+
"/search": ["q", "page"], // only these two belong to the key
|
|
612
|
+
"/products": ["category"],
|
|
613
|
+
"/report/:id": true, // every parameter belongs to the key
|
|
614
|
+
"/campaign": [], // the query is ignored entirely
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
- **Allowlist** (`string[]`): the listed parameters become part of the key and
|
|
619
|
+
each distinct value gets its own entry. Parameters outside the list are
|
|
620
|
+
**ignored** — the page is still cached and every campaign variant shares one
|
|
621
|
+
copy.
|
|
622
|
+
- **`true`**: every parameter belongs to the key. Nothing but `maxEntries`
|
|
623
|
+
bounds the number of entries, so use it only where the value set is closed.
|
|
624
|
+
- **`[]`**: the query is not considered at all; every variant is served the HTML
|
|
625
|
+
of the query-less version.
|
|
626
|
+
|
|
627
|
+
Parameters are written into the key **sorted**, so `?a=1&b=2` and `?b=2&a=1`
|
|
628
|
+
share one entry. `route(fn, { private: true })` is unaffected by this section; a
|
|
629
|
+
private route is never cached under any condition.
|
|
630
|
+
|
|
597
631
|
### `cache().maxEntries`
|
|
598
632
|
|
|
599
633
|
**Type:** `number` — **Default:** `500`
|
|
@@ -740,6 +774,32 @@ leaking one means handing out the right to flush the cache, and a deploy should
|
|
|
740
774
|
revoke old access on its own. Banned and unauthorised requests all get a `404`.
|
|
741
775
|
Usage and screens: [06-caching.md](./06-caching.md).
|
|
742
776
|
|
|
777
|
+
### `cache().cloudflare`
|
|
778
|
+
|
|
779
|
+
The CDN tier. JSkelet's cache is the origin cache; the copy your visitors get
|
|
780
|
+
sits at the edge. With this section connected, the panel can purge the edge,
|
|
781
|
+
read and change cache related zone settings and show the cache hit ratio.
|
|
782
|
+
|
|
783
|
+
| Field | Type | Default | Meaning |
|
|
784
|
+
| --- | --- | --- | --- |
|
|
785
|
+
| `enabled` | `boolean` | `true` | Set `false` to keep the surface off even when a token is present in the environment |
|
|
786
|
+
| `zoneId` | `string \| null` | `null` | Zone identifier (`JSKELET_CLOUDFLARE_ZONE_ID` overrides it) |
|
|
787
|
+
| `apiToken` | `string \| null` | `null` | The token; **prefer the environment**, putting it here puts a secret in the repo |
|
|
788
|
+
| `hostname` | `string \| null` | `null` | Purging wants absolute URLs; paths are resolved against this name. Falls back to the origin the panel was opened on |
|
|
789
|
+
| `analyticsHours` | `number` | `24` | Analytics window, at most `72` |
|
|
790
|
+
|
|
791
|
+
Passing the token only through `JSKELET_CLOUDFLARE_KEY` keeps the config file
|
|
792
|
+
clean. Permissions follow what you intend to do: `Zone.Cache Purge` to purge,
|
|
793
|
+
`Zone.Zone Settings` for settings, `Zone.Analytics` (read) for the hit ratio.
|
|
794
|
+
The token is never returned in a panel response — only the fact that it came
|
|
795
|
+
from the environment.
|
|
796
|
+
|
|
797
|
+
With no zone connected the panel shows a setup snippet rather than a warning,
|
|
798
|
+
and if Cloudflare returns an error that section reports it while the rest of the
|
|
799
|
+
panel keeps working. What can actually be asked — in particular why "how many
|
|
800
|
+
edges hold this page" has no exact answer — is in
|
|
801
|
+
[06-caching.md](./06-caching.md).
|
|
802
|
+
|
|
743
803
|
```js
|
|
744
804
|
panel: {
|
|
745
805
|
enabled: process.env.CACHE_PANEL === "1",
|
|
@@ -871,6 +931,9 @@ and no warning is printed.
|
|
|
871
931
|
| `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) |
|
|
872
932
|
| `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) |
|
|
873
933
|
| `JSKELET_CACHE_PANEL` | `createApp` | — | When set, turns the cache 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) |
|
|
934
|
+
| `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache surface | — | API token. Until it is set, CDN purging and edge analytics stay off; it overrides `apiToken` in the config. The token is never returned in a response. [06](./06-caching.md) |
|
|
935
|
+
| `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache surface | — | Zone identifier. No Cloudflare endpoint is called unless it is set alongside the token |
|
|
936
|
+
| `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache surface | — | The root for purge URLs. Required when the panel is opened over an internal address |
|
|
874
937
|
| `PREWARM` | `startPrewarm` | — | `0` turns prewarming off; `1` overrides `enabled: false` in the config and turns it on |
|
|
875
938
|
| `PREWARM_MAX` | `prewarm` | `400` | At most how many paths are prewarmed |
|
|
876
939
|
| `PREWARM_CONCURRENCY` | `prewarm` | prod 4, dev 1 | Number of parallel workers |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + EJS server rendering, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
|
|
7
7
|
<meta name="referrer" content="no-referrer" />
|
|
8
|
-
<title>Cache</title>
|
|
8
|
+
<title>Cache · JSkelet</title>
|
|
9
|
+
<link rel="icon" href="logo.png" />
|
|
9
10
|
<link rel="stylesheet" href="panel.css" />
|
|
10
11
|
</head>
|
|
11
12
|
<body class="login">
|
|
12
13
|
<form id="form" autocomplete="off">
|
|
14
|
+
<div class="brand">
|
|
15
|
+
<img src="logo.png" alt="" width="30" height="30" />
|
|
16
|
+
<span class="wordmark">JSkelet</span>
|
|
17
|
+
</div>
|
|
13
18
|
<h1>Cache panel</h1>
|
|
14
19
|
<p>
|
|
15
20
|
This run's password is printed in the server log
|
|
@@ -27,6 +32,9 @@
|
|
|
27
32
|
/>
|
|
28
33
|
<button class="primary" type="submit">Unlock</button>
|
|
29
34
|
<p class="error" id="error"></p>
|
|
35
|
+
<p class="hint m0">
|
|
36
|
+
Three failed attempts block this address for 24 hours.
|
|
37
|
+
</p>
|
|
30
38
|
</form>
|
|
31
39
|
|
|
32
40
|
<script type="module">
|