jskelet 0.2.4 → 0.3.0
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 +403 -375
- package/docs/02-mimari.md +9 -3
- package/docs/03-routing.md +6 -0
- package/docs/06-cache.md +34 -12
- package/docs/07-yapilandirma.md +44 -21
- package/docs/11-tasima.md +1 -0
- package/docs/en/02-architecture.md +9 -3
- package/docs/en/03-routing.md +6 -0
- package/docs/en/06-caching.md +71 -72
- package/docs/en/07-configuration.md +44 -21
- package/docs/en/11-migration.md +1 -0
- package/package.json +1 -1
- package/src/client/{cache-panel → admin}/i18n.js +94 -8
- package/src/client/{cache-panel → admin}/login.html +2 -2
- package/src/client/{cache-panel → admin}/panel.css +49 -1
- package/src/client/admin/panel.html +486 -0
- package/src/client/{cache-panel → admin}/panel.js +333 -6
- package/src/config/defaults.js +16 -7
- package/src/config/index.js +38 -22
- package/src/log.mjs +58 -0
- package/src/server/admin/actions.js +229 -0
- package/src/server/admin/auth.js +125 -0
- package/src/server/admin/event-log.js +151 -0
- package/src/server/admin/gate.js +209 -0
- package/src/server/admin/inventory.js +188 -0
- package/src/server/admin/mount.js +56 -0
- package/src/server/admin/router.js +216 -0
- package/src/server/admin/snapshot.js +126 -0
- package/src/server/cloudflare.js +21 -9
- package/src/server/create-app.js +10 -6
- package/src/server/middleware/trailing-slash.js +53 -0
- package/src/client/cache-panel/panel.html +0 -308
- package/src/server/cache-panel.js +0 -759
package/docs/02-mimari.md
CHANGED
|
@@ -35,6 +35,7 @@ JSkelet bu gözlemi mimarinin merkezine alır:
|
|
|
35
35
|
├─ headers statik cache + config headers()
|
|
36
36
|
├─ devGate DEV_TOKEN varsa token yoksa 404
|
|
37
37
|
├─ redirects config redirects(), ilk eşleşen kazanır
|
|
38
|
+
├─ trailingSlash config trailingSlash: true ise 308
|
|
38
39
|
├─ staticPrecompressed build'de üretilmiş .br/.gz kopyalar (kalite 11)
|
|
39
40
|
├─ express.static public/ altındaki dosyalar
|
|
40
41
|
├─ (dev) devtools yalnızca NODE_ENV=development
|
|
@@ -60,13 +61,18 @@ sebebi var ve yer değiştirmek sessiz bozulmalara yol açıyor.
|
|
|
60
61
|
`express.static` isteği önce yanıtlar.
|
|
61
62
|
- **`compression`, static'ten önce.** Sonra gelirse statik dosyalar hiç
|
|
62
63
|
sıkışmaz.
|
|
63
|
-
- **`headers` → `devGate` → `redirects`.** Gate'in 404'ü
|
|
64
|
-
gelmeli: yayına açılmamış bir ortam, yönlendirme kurallarını
|
|
65
|
-
sızdırmamalı.
|
|
64
|
+
- **`headers` → `devGate` → `redirects` → `trailingSlash`.** Gate'in 404'ü
|
|
65
|
+
redirect'ten önce gelmeli: yayına açılmamış bir ortam, yönlendirme kurallarını
|
|
66
|
+
bile dışarıya sızdırmamalı. `trailingSlash` config redirects'ten sonra durur,
|
|
67
|
+
böylece açık kurallar istenen yolu önce görür; kanonik slash biçimi ikinci
|
|
68
|
+
adımda dayatılır.
|
|
66
69
|
- **`staticPrecompressed`, `express.static`ten önce.** Build'de üretilmiş
|
|
67
70
|
`.br`/`.gz` kopyalar varsa onlar servis edilir (brotli kalite 11); yoksa
|
|
68
71
|
istek altındaki `static`e düşer ve middleware anında sıkıştırır (kalite 5).
|
|
69
72
|
Hash'li ve `immutable` bir dosyayı her istekte yeniden sıkıştırmak boşa CPU.
|
|
73
|
+
- **Admin paneli** (açıksa, `admin().enabled` / `JSKELET_ADMIN`): statikten
|
|
74
|
+
sonra, body parser ve route'lardan önce. Kendi gövde ayrıştırıcısını taşır;
|
|
75
|
+
uygulama aynı yolu gölgeleyemez. Kapalıyken modül yüklenmez.
|
|
70
76
|
- **Body parser'lar statikten sonra.** Görsel isteklerinde gövde ayrıştırma
|
|
71
77
|
maliyeti ödenmesin.
|
|
72
78
|
- **`rewrites(afterFiles)`, statik denendikten sonra ve sayfalardan önce.**
|
package/docs/03-routing.md
CHANGED
|
@@ -388,6 +388,12 @@ Davranış:
|
|
|
388
388
|
için 301.
|
|
389
389
|
- `source` ya da `destination` geçersizse kural sessizce düşmez, uyarı basılır.
|
|
390
390
|
|
|
391
|
+
## Config: `trailingSlash`
|
|
392
|
+
|
|
393
|
+
`trailingSlash: true` iken kanonik sayfa URL'leri `/` ile biter ve **200**
|
|
394
|
+
döner; slash'sız istek **308** ile slash'lıya gider. Ayrıntı ve istisnalar:
|
|
395
|
+
[07-yapilandirma.md](./07-yapilandirma.md#trailingslash).
|
|
396
|
+
|
|
391
397
|
## Config: `rewrites()`
|
|
392
398
|
|
|
393
399
|
Rewrite, tarayıcının adres çubuğunu değiştirmeden isteği başka bir yere taşır.
|
package/docs/06-cache.md
CHANGED
|
@@ -768,13 +768,24 @@ yalnızca `NODE_ENV=development` iken var, panel ise ortama bakmaz — "bu sayfa
|
|
|
768
768
|
neden bayat", "webhook purge'ü geçti mi", "Redis gerçekten bağlı mı" soruları
|
|
769
769
|
üretimde soruluyor.
|
|
770
770
|
|
|
771
|
+
Panel üst düzey `admin()` ile açılır (`cache()` içinde değil) ve kökü
|
|
772
|
+
`/_jskelet/admin`'dır. Altında Overview, Cache, Routes, Views, Logs ve System
|
|
773
|
+
sayfaları vardır; Cache sayfası eski tek sayfalık panelle aynı işlemleri
|
|
774
|
+
taşır.
|
|
775
|
+
|
|
771
776
|
```js
|
|
772
777
|
// jskelet.config.mjs
|
|
773
778
|
export default {
|
|
779
|
+
admin() {
|
|
780
|
+
return {
|
|
781
|
+
enabled: process.env.JSKELET_ADMIN === "1",
|
|
782
|
+
allowIps: ["10.0.0.0/8"], // boş = IP kısıtı yok
|
|
783
|
+
blockBots: true,
|
|
784
|
+
};
|
|
785
|
+
},
|
|
774
786
|
cache() {
|
|
775
787
|
return {
|
|
776
788
|
html: { "/haber/:slug": 300 },
|
|
777
|
-
panel: { enabled: process.env.CACHE_PANEL === "1" },
|
|
778
789
|
};
|
|
779
790
|
},
|
|
780
791
|
};
|
|
@@ -782,14 +793,20 @@ export default {
|
|
|
782
793
|
|
|
783
794
|
`enabled` verilmedikçe **hiçbir şey mount edilmez**: yol yoktur, modül
|
|
784
795
|
yüklenmez, üretim sürecinde hiçbir maliyeti olmaz. Ortam değişkeni
|
|
785
|
-
(`
|
|
796
|
+
(`JSKELET_ADMIN=1`) config'i ezer; panel genelde bir arıza sırasında tek
|
|
786
797
|
seferlik açılıyor ve o an config dosyasını değiştirip yeniden dağıtmak
|
|
787
798
|
istenmiyor.
|
|
788
799
|
|
|
789
800
|
Panel açıldığında sunucu logu şifreyi basar:
|
|
790
801
|
|
|
791
802
|
```
|
|
792
|
-
|
|
803
|
+
┌─ ADMIN ──────────────────────────────────────┐
|
|
804
|
+
│ http://localhost:3000/_jskelet/admin │
|
|
805
|
+
│ │
|
|
806
|
+
│ password 3f9c… │
|
|
807
|
+
│ │
|
|
808
|
+
│ Valid until this process restarts. │
|
|
809
|
+
└──────────────────────────────────────────────┘
|
|
793
810
|
```
|
|
794
811
|
|
|
795
812
|
### Erişim ve güvenlik
|
|
@@ -799,15 +816,20 @@ Panel açıldığında sunucu logu şifreyi basar:
|
|
|
799
816
|
yetkisi demek ve bir deploy eski erişimi kendiliğinden iptal etmeli.
|
|
800
817
|
- **Şifre query string ile kabul edilmez.** Erişim logları, tarayıcı geçmişi ve
|
|
801
818
|
`Referer` başlığı sırrı taşımasın; giriş yalnızca form üzerinden.
|
|
819
|
+
- **`allowIps`** verilirse (exact IP veya CIDR) listede olmayan her istek
|
|
820
|
+
login dahil `404` alır.
|
|
821
|
+
- **`blockBots`** (varsayılan `true`) bilinen crawler UA'larını (Googlebot,
|
|
822
|
+
Bingbot, Ahrefs, …) `404` ile reddeder.
|
|
802
823
|
- **Üç başarısız denemede IP 24 saat yasaklanır** (`banAttempts`, `banHours`).
|
|
803
|
-
Yanlış şifre kadar oturumsuz
|
|
824
|
+
Yanlış şifre kadar oturumsuz yazma isteği de sayılır; başarılı giriş sayacı
|
|
825
|
+
sıfırlar.
|
|
804
826
|
- **Yasaklı ve yetkisiz her cevap `404`.** 401/403 panelin var olduğunu
|
|
805
827
|
doğrular, 404 hiç yokmuş gibi davranır. Panelin dışındaki site etkilenmez.
|
|
806
828
|
- **Hiçbir yeri indekslenmez:** her cevapta `X-Robots-Tag: noindex, nofollow,
|
|
807
829
|
noarchive, nosnippet`, `Cache-Control: no-store` ve `Referrer-Policy:
|
|
808
830
|
no-referrer`. Yol ayrıca ısıtma listesinden ve gezinme ipuçlarından muaftır.
|
|
809
|
-
- Aksiyonlar `X-JSkelet-
|
|
810
|
-
|
|
831
|
+
- Aksiyonlar `X-JSkelet-Admin` başlığı ister — çapraz siteden gönderilemeyen
|
|
832
|
+
bir başlık, yani panelin kendi CSRF freni.
|
|
811
833
|
- Oturumlar ve yasak sayaçları süreç belleğinde durur; şifresi zaten her
|
|
812
834
|
restart'ta değişen bir panel için diske yazmak yanlış takas olurdu.
|
|
813
835
|
|
|
@@ -815,12 +837,12 @@ Panel açıldığında sunucu logu şifreyi basar:
|
|
|
815
837
|
|
|
816
838
|
| Bölüm | Gösterdiği |
|
|
817
839
|
| --- | --- |
|
|
818
|
-
|
|
|
819
|
-
|
|
|
820
|
-
|
|
|
821
|
-
|
|
|
822
|
-
|
|
|
823
|
-
|
|
|
840
|
+
| Overview | HTML/data/Redis/prewarm kartları ve upstream freni özeti |
|
|
841
|
+
| Cache | Paylaşımlı kademe, Cloudflare, aksiyonlar, girdi listesi (eski panel) |
|
|
842
|
+
| Routes | Express'e kayıtlı path/method'lar, route modül dosyaları, son istek özeti |
|
|
843
|
+
| Views | `views/` altındaki şablon envanteri |
|
|
844
|
+
| Logs | Canlı SSE kuyruğu; method/status/cache/kind/path ve metin filtresi |
|
|
845
|
+
| System | Makine RAM / disk |
|
|
824
846
|
|
|
825
847
|
Liste **anahtar bazında filtrelenir** ve filtre sunucuda uygulanır: veri
|
|
826
848
|
önbelleğinde on binlerce anahtar olabiliyor. Her istekte en fazla 500 satır
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -53,6 +53,7 @@ export default {
|
|
|
53
53
|
|
|
54
54
|
layout: "views/layout.ejs",
|
|
55
55
|
routes: ["./routes/10-pages.mjs", "./routes/99-catch-all.mjs"],
|
|
56
|
+
trailingSlash: false,
|
|
56
57
|
|
|
57
58
|
static: {
|
|
58
59
|
extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2"],
|
|
@@ -214,6 +215,29 @@ Ayrıntı: [03-routing.md](./03-routing.md).
|
|
|
214
215
|
routes: ["./routes/api.js", "./routes/pages.js", "./routes/catch-all.js"]
|
|
215
216
|
```
|
|
216
217
|
|
|
218
|
+
## `trailingSlash`
|
|
219
|
+
|
|
220
|
+
**Tip:** `boolean` — **Varsayılan:** `false`
|
|
221
|
+
|
|
222
|
+
`true` iken kanonik URL'ler `/` ile biter: `/hakkinda/` doğrudan **200**
|
|
223
|
+
döner; slash'sız `/hakkinda` **308** ile `/hakkinda/` adresine gider (301
|
|
224
|
+
değil — metodu koruyan kalıcı yönlendirme, framework'ün diğer `permanent`
|
|
225
|
+
redirect'leriyle aynı). Query string korunur.
|
|
226
|
+
|
|
227
|
+
İstisnalar: kök `/`, uzantılı dosya yolları (`/robots.txt`, `/assets/app.js`)
|
|
228
|
+
ve `/.well-known/**`. Bunlara slash eklenmez.
|
|
229
|
+
|
|
230
|
+
`false` iken (varsayılan) slash dayatılmaz. Express non-strict eşleşme ile
|
|
231
|
+
`/x` ve `/x/` ikisi de 200 olabilir — Next.js'in varsayılan "slash'ı kırp"
|
|
232
|
+
davranışından bilinçli fark; mevcut siteleri kırmamak için.
|
|
233
|
+
|
|
234
|
+
Açıkken şablonlardaki `href`, sitemap ve `redirects()` hedeflerini de
|
|
235
|
+
slash'lı yazın; aksi hâlde tarayıcı her tıklamada ekstra bir 308 görür.
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
trailingSlash: true
|
|
239
|
+
```
|
|
240
|
+
|
|
217
241
|
## `static`
|
|
218
242
|
|
|
219
243
|
**Tip:** `{ extensions?: string[], prefixes?: string[] }` — **Varsayılan:**
|
|
@@ -728,33 +752,38 @@ redis: {
|
|
|
728
752
|
}
|
|
729
753
|
```
|
|
730
754
|
|
|
731
|
-
### `
|
|
755
|
+
### `admin()`
|
|
732
756
|
|
|
733
|
-
|
|
734
|
-
|
|
757
|
+
Framework yönetim paneli (`/_jskelet/admin`). Bellek içi / Redis / Cloudflare
|
|
758
|
+
önbelleğini yönetir; route ve view envanteri ile canlı log kuyruğu sunar.
|
|
735
759
|
|
|
736
760
|
Ortama bakmaz: `enabled` verilmedikçe **hiç mount edilmez** ve yol da yoktur.
|
|
737
761
|
Açıkken production'da da çalışır — asıl sorular ("bu sayfa neden bayat",
|
|
738
|
-
"webhook purge'ü geçti mi") orada soruluyor.
|
|
762
|
+
"webhook purge'ü geçti mi") orada soruluyor. `cache()` bölümünden ayrıdır.
|
|
739
763
|
|
|
740
764
|
| Alan | Tip | Varsayılan | Anlamı |
|
|
741
765
|
| --- | --- | --- | --- |
|
|
742
|
-
| `enabled` | `boolean` | `false` | Yalnızca açıkça `true` verildiğinde açılır (`
|
|
743
|
-
| `basePath` | `string` | `"/_jskelet/
|
|
766
|
+
| `enabled` | `boolean` | `false` | Yalnızca açıkça `true` verildiğinde açılır (`JSKELET_ADMIN` ezer) |
|
|
767
|
+
| `basePath` | `string` | `"/_jskelet/admin"` | Panelin kökü |
|
|
768
|
+
| `allowIps` | `string[]` | `[]` | Exact IP veya CIDR; boş = kısıt yok. Listede olmayan her istek 404 |
|
|
769
|
+
| `blockBots` | `boolean` | `true` | Bilinen crawler UA'ları 404 |
|
|
744
770
|
| `banAttempts` | `number` | `3` | Kaç başarısız denemeden sonra IP yasaklanır |
|
|
745
771
|
| `banHours` | `number` | `24` | Yasağın süresi |
|
|
746
772
|
| `sessionHours` | `number` | `12` | Oturum çerezinin ömrü |
|
|
773
|
+
| `logSize` | `number` | `500` | Canlı log ring boyutu |
|
|
747
774
|
|
|
748
|
-
Şifre **her süreç başlangıcında** üretilir ve yalnızca sunucu
|
|
775
|
+
Şifre **her süreç başlangıcında** üretilir ve yalnızca sunucu logundaki
|
|
776
|
+
`ADMIN` kutusunda görünür. Yasaklı ve yetkisiz her cevap `404`'tür. Kullanım
|
|
777
|
+
ve ekran ayrıntıları: [06-cache.md](./06-cache.md).
|
|
749
778
|
|
|
779
|
+
```js
|
|
780
|
+
admin() {
|
|
781
|
+
return {
|
|
782
|
+
enabled: process.env.JSKELET_ADMIN === "1",
|
|
783
|
+
allowIps: ["203.0.113.10", "10.0.0.0/8"],
|
|
784
|
+
};
|
|
785
|
+
}
|
|
750
786
|
```
|
|
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
787
|
|
|
759
788
|
### `cache().cloudflare`
|
|
760
789
|
|
|
@@ -780,12 +809,6 @@ hata dönerse ilgili bölüm hatayı yazar ve panelin kalanı çalışmaya devam
|
|
|
780
809
|
Neyin sorulabildiği — özellikle "bu sayfa kaç edge'de cache'li" sorusunun neden
|
|
781
810
|
tam cevabı olmadığı — [06-cache.md](./06-cache.md) içinde.
|
|
782
811
|
|
|
783
|
-
```js
|
|
784
|
-
panel: {
|
|
785
|
-
enabled: process.env.CACHE_PANEL === "1",
|
|
786
|
-
}
|
|
787
|
-
```
|
|
788
|
-
|
|
789
812
|
### `cache().prewarm`
|
|
790
813
|
|
|
791
814
|
| Alan | Tip | Varsayılan | Anlamı |
|
|
@@ -909,7 +932,7 @@ basılmaz.
|
|
|
909
932
|
| `HOST` | `startServer` | `::` | Bağlanılacak arayüz. Varsayılan çift yığın dinler (IPv6 + IPv4); IPv6 yoksa `0.0.0.0`'a düşer |
|
|
910
933
|
| `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) |
|
|
911
934
|
| `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
|
-
| `
|
|
935
|
+
| `JSKELET_ADMIN` | `createApp` | — | Ayarlıysa yönetim 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
936
|
| `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
937
|
| `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache yüzeyi | — | Zone kimliği. Token'la birlikte verilmedikçe hiçbir Cloudflare ucu çağrılmaz |
|
|
915
938
|
| `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache yüzeyi | — | Purge URL'lerinin kökü. Panel iç bir adresten açılıyorsa gerekir |
|
package/docs/11-tasima.md
CHANGED
|
@@ -16,6 +16,7 @@ alt kümesine benzetildi — `next.config` sözdizimi, Metadata API, `notFound()
|
|
|
16
16
|
| `next.config.mjs` | `jskelet.config.mjs` | Aynı ruh, daha küçük yüzey ([07](./07-yapilandirma.md)) |
|
|
17
17
|
| `headers()` | `headers()` | Aynı şekil: `{ source, headers: [{ key, value }] }` |
|
|
18
18
|
| `redirects()` | `redirects()` | `permanent` → 308, aksi hâlde 307; `statusCode` ile ezilebilir |
|
|
19
|
+
| `trailingSlash` | `trailingSlash` | `true` → kanonik URL `/` ile biter (308); varsayılan `false` slash kırpmaz ([07](./07-yapilandirma.md)) |
|
|
19
20
|
| `rewrites()` | `rewrites()` | `beforeFiles` / `afterFiles` fazları var; `fallback` yok |
|
|
20
21
|
| `compress: true` | Otomatik | `node:zlib` ile brotli + gzip |
|
|
21
22
|
| `images.deviceSizes` | `images.widths` | Build zamanı webp üretimi ([08](./08-build.md)) |
|
|
@@ -35,6 +35,7 @@ Request
|
|
|
35
35
|
├─ headers static cache + config headers()
|
|
36
36
|
├─ devGate if DEV_TOKEN is set, 404 without a token
|
|
37
37
|
├─ redirects config redirects(), first match wins
|
|
38
|
+
├─ trailingSlash 308 when config trailingSlash is true
|
|
38
39
|
├─ staticPrecompressed .br/.gz copies produced at build (quality 11)
|
|
39
40
|
├─ express.static files under public/
|
|
40
41
|
├─ (dev) devtools only when NODE_ENV=development
|
|
@@ -60,14 +61,19 @@ position has a reason, and moving things around leads to silent breakage.
|
|
|
60
61
|
because `express.static` answers the request first.
|
|
61
62
|
- **`compression` before static.** If it came after, static files would never be
|
|
62
63
|
compressed.
|
|
63
|
-
- **`headers` → `devGate` → `redirects`.** The gate's 404 must
|
|
64
|
-
redirects: an environment that has not gone live should not
|
|
65
|
-
redirect rules to the outside.
|
|
64
|
+
- **`headers` → `devGate` → `redirects` → `trailingSlash`.** The gate's 404 must
|
|
65
|
+
come before the redirects: an environment that has not gone live should not
|
|
66
|
+
leak even its redirect rules to the outside. `trailingSlash` sits after config
|
|
67
|
+
redirects so explicit rules see the requested path first; the canonical slash
|
|
68
|
+
form is enforced as a second step.
|
|
66
69
|
- **`staticPrecompressed` before `express.static`.** If there are `.br`/`.gz`
|
|
67
70
|
copies produced at build time, those are served (brotli quality 11);
|
|
68
71
|
otherwise the request falls through to the `static` below it and the
|
|
69
72
|
middleware compresses on the fly (quality 5). Recompressing a hashed,
|
|
70
73
|
`immutable` file on every request is wasted CPU.
|
|
74
|
+
- **Admin panel** (when `admin().enabled` / `JSKELET_ADMIN`): after static,
|
|
75
|
+
before body parsers and routes. Carries its own body parsers so the app
|
|
76
|
+
cannot shadow the path. When off, the module is never loaded.
|
|
71
77
|
- **Body parsers after static.** Image requests should not pay the cost of body
|
|
72
78
|
parsing.
|
|
73
79
|
- **`rewrites(afterFiles)` after static has been tried and before pages.** The
|
package/docs/en/03-routing.md
CHANGED
|
@@ -400,6 +400,12 @@ Behaviour:
|
|
|
400
400
|
- If `source` or `destination` is invalid, the rule does not drop silently; a
|
|
401
401
|
warning is printed.
|
|
402
402
|
|
|
403
|
+
## Config: `trailingSlash`
|
|
404
|
+
|
|
405
|
+
With `trailingSlash: true`, canonical page URLs end with `/` and return **200**;
|
|
406
|
+
a request without the slash is sent to the slashed form with a **308**. Details
|
|
407
|
+
and exceptions: [07-configuration.md](./07-configuration.md#trailingslash).
|
|
408
|
+
|
|
403
409
|
## Config: `rewrites()`
|
|
404
410
|
|
|
405
411
|
A rewrite moves a request somewhere else without changing the browser's address
|
package/docs/en/06-caching.md
CHANGED
|
@@ -778,78 +778,77 @@ Two more diagnostic surfaces:
|
|
|
778
778
|
|
|
779
779
|
The full list of settings: [07-configuration.md](./07-configuration.md).
|
|
780
780
|
|
|
781
|
-
## The admin panel
|
|
782
|
-
|
|
783
|
-
Instead of hand-writing the `getHtmlCacheEntries()` / `getRedisStatus()`
|
|
784
|
-
endpoints above, the framework ships a panel. It is deliberately separate from
|
|
785
|
-
the dev overlay: the overlay only exists while `NODE_ENV=development`, while the
|
|
786
|
-
panel does not look at the environment — "why is this page stale", "did the
|
|
787
|
-
webhook purge land", "is Redis actually connected" are production questions.
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
- **
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
-
|
|
832
|
-
|
|
833
|
-
- Sessions and ban counters live in process memory
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
|
839
|
-
|
|
|
840
|
-
|
|
|
841
|
-
|
|
|
842
|
-
|
|
|
843
|
-
|
|
|
844
|
-
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
781
|
+
## The admin panel
|
|
782
|
+
|
|
783
|
+
Instead of hand-writing the `getHtmlCacheEntries()` / `getRedisStatus()`
|
|
784
|
+
endpoints above, the framework ships a panel. It is deliberately separate from
|
|
785
|
+
the dev overlay: the overlay only exists while `NODE_ENV=development`, while the
|
|
786
|
+
panel does not look at the environment — "why is this page stale", "did the
|
|
787
|
+
webhook purge land", "is Redis actually connected" are production questions.
|
|
788
|
+
|
|
789
|
+
The panel is enabled with top-level `admin()` (not inside `cache()`) at
|
|
790
|
+
`/_jskelet/admin`, with Overview, Cache, Routes, Views, Logs and System pages.
|
|
791
|
+
The Cache page carries the same operations as the former single-page panel.
|
|
792
|
+
|
|
793
|
+
```js
|
|
794
|
+
// jskelet.config.mjs
|
|
795
|
+
export default {
|
|
796
|
+
admin() {
|
|
797
|
+
return {
|
|
798
|
+
enabled: process.env.JSKELET_ADMIN === "1",
|
|
799
|
+
allowIps: ["10.0.0.0/8"], // empty = no IP restriction
|
|
800
|
+
blockBots: true,
|
|
801
|
+
};
|
|
802
|
+
},
|
|
803
|
+
cache() {
|
|
804
|
+
return {
|
|
805
|
+
html: { "/news/:slug": 300 },
|
|
806
|
+
};
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
Without `enabled` **nothing is mounted**: the path does not exist, the module is
|
|
812
|
+
never loaded and it costs the production process nothing. The environment
|
|
813
|
+
variable (`JSKELET_ADMIN=1`) overrides the config, because the panel is
|
|
814
|
+
usually opened once during an incident and editing the config file and
|
|
815
|
+
redeploying is the last thing you want at that moment.
|
|
816
|
+
|
|
817
|
+
When the panel is on, the server log prints the password in an `ADMIN` box at
|
|
818
|
+
`http://localhost:3000/_jskelet/admin`.
|
|
819
|
+
|
|
820
|
+
### Access and hardening
|
|
821
|
+
|
|
822
|
+
- **The password is regenerated on every process start** (32 hex characters) and
|
|
823
|
+
only ever appears in the log. There is no persistent secret to leak.
|
|
824
|
+
- **The password is not accepted in the query string.**
|
|
825
|
+
- **`allowIps`** (exact IP or CIDR), when set, returns `404` for every request
|
|
826
|
+
outside the list — including the login page.
|
|
827
|
+
- **`blockBots`** (default `true`) rejects known crawler UAs with `404`.
|
|
828
|
+
- **Three failed attempts ban the IP for 24 hours** (`banAttempts`, `banHours`).
|
|
829
|
+
- **Banned and unauthorised requests get a `404`.**
|
|
830
|
+
- **Nothing is indexable:** `X-Robots-Tag`, `Cache-Control: no-store`,
|
|
831
|
+
`Referrer-Policy: no-referrer`; exempt from prewarming and navigation speculation.
|
|
832
|
+
- Actions require an `X-JSkelet-Admin` header — the panel's own CSRF brake.
|
|
833
|
+
- Sessions and ban counters live in process memory.
|
|
834
|
+
|
|
835
|
+
### What the panel shows
|
|
836
|
+
|
|
837
|
+
| Area | Contents |
|
|
838
|
+
| --- | --- |
|
|
839
|
+
| Overview | HTML/data/Redis/prewarm cards and upstream limiter summary |
|
|
840
|
+
| Cache | Shared tier, Cloudflare, actions, entry list (former panel) |
|
|
841
|
+
| Routes | Express path/method inventory, route modules, last-request summary |
|
|
842
|
+
| Views | Template inventory under `views/` |
|
|
843
|
+
| Logs | Live SSE queue with method/status/cache/kind/path and text filters |
|
|
844
|
+
| System | Host RAM / disk |
|
|
845
|
+
|
|
846
|
+
The list is **filtered by key** and the filter runs on the server: a data cache
|
|
847
|
+
can hold tens of thousands of keys. At most 500 rows come back per request and
|
|
848
|
+
the counter in the heading says how many matches were cut. HTML bodies and
|
|
849
|
+
cached values are **never returned** — the panel's job is to show state, not to
|
|
850
|
+
export content.
|
|
851
|
+
|
|
853
852
|
### What you can do from it
|
|
854
853
|
|
|
855
854
|
| Action | Equivalent call |
|
|
@@ -57,6 +57,7 @@ export default {
|
|
|
57
57
|
|
|
58
58
|
layout: "views/layout.ejs",
|
|
59
59
|
routes: ["./routes/10-pages.mjs", "./routes/99-catch-all.mjs"],
|
|
60
|
+
trailingSlash: false,
|
|
60
61
|
|
|
61
62
|
static: {
|
|
62
63
|
extensions: [".svg", ".png", ".webp", ".avif", ".ico", ".woff2"],
|
|
@@ -219,6 +220,29 @@ alphabetically and recursively. Details: [03-routing.md](./03-routing.md).
|
|
|
219
220
|
routes: ["./routes/api.js", "./routes/pages.js", "./routes/catch-all.js"]
|
|
220
221
|
```
|
|
221
222
|
|
|
223
|
+
## `trailingSlash`
|
|
224
|
+
|
|
225
|
+
**Type:** `boolean` — **Default:** `false`
|
|
226
|
+
|
|
227
|
+
When `true`, canonical URLs end with `/`: `/about/` returns **200** directly;
|
|
228
|
+
bare `/about` is sent to `/about/` with a **308** (not 301 — a permanent
|
|
229
|
+
redirect that preserves the method, same as the framework's other `permanent`
|
|
230
|
+
redirects). The query string is kept.
|
|
231
|
+
|
|
232
|
+
Exceptions: the root `/`, paths with a file extension (`/robots.txt`,
|
|
233
|
+
`/assets/app.js`) and `/.well-known/**`. Those do not get a slash appended.
|
|
234
|
+
|
|
235
|
+
When `false` (the default) no slash is enforced. Express non-strict matching may
|
|
236
|
+
serve both `/x` and `/x/` as 200 — a deliberate difference from Next.js's
|
|
237
|
+
default "strip the slash" behaviour, so existing sites are not broken.
|
|
238
|
+
|
|
239
|
+
With the option on, write `href`s, sitemap entries and `redirects()` destinations
|
|
240
|
+
with a trailing slash too; otherwise every click pays an extra 308.
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
trailingSlash: true
|
|
244
|
+
```
|
|
245
|
+
|
|
222
246
|
## `static`
|
|
223
247
|
|
|
224
248
|
**Type:** `{ extensions?: string[], prefixes?: string[] }` — **Default:**
|
|
@@ -744,35 +768,40 @@ redis: {
|
|
|
744
768
|
}
|
|
745
769
|
```
|
|
746
770
|
|
|
747
|
-
### `
|
|
771
|
+
### `admin()`
|
|
748
772
|
|
|
749
|
-
The
|
|
750
|
-
|
|
773
|
+
The framework admin panel (`/_jskelet/admin`). It manages the in-process /
|
|
774
|
+
Redis / Cloudflare caches and exposes route and view inventories plus a live
|
|
775
|
+
log queue.
|
|
751
776
|
|
|
752
777
|
It does not look at the environment: without `enabled` **nothing is mounted**
|
|
753
778
|
and the path does not exist. When it is on it also works in production — that is
|
|
754
779
|
where the real questions ("why is this page stale", "did the webhook purge
|
|
755
|
-
land") get asked.
|
|
780
|
+
land") get asked. It is separate from the `cache()` section.
|
|
756
781
|
|
|
757
782
|
| Field | Type | Default | Meaning |
|
|
758
783
|
| --- | --- | --- | --- |
|
|
759
|
-
| `enabled` | `boolean` | `false` | Only turns on when explicitly `true` (`
|
|
760
|
-
| `basePath` | `string` | `"/_jskelet/
|
|
784
|
+
| `enabled` | `boolean` | `false` | Only turns on when explicitly `true` (`JSKELET_ADMIN` overrides it) |
|
|
785
|
+
| `basePath` | `string` | `"/_jskelet/admin"` | Root of the panel |
|
|
786
|
+
| `allowIps` | `string[]` | `[]` | Exact IP or CIDR; empty = no restriction. Anyone outside gets 404 |
|
|
787
|
+
| `blockBots` | `boolean` | `true` | Known crawler UAs get 404 |
|
|
761
788
|
| `banAttempts` | `number` | `3` | How many failed attempts ban an IP |
|
|
762
789
|
| `banHours` | `number` | `24` | How long the ban lasts |
|
|
763
790
|
| `sessionHours` | `number` | `12` | Lifetime of the session cookie |
|
|
791
|
+
| `logSize` | `number` | `500` | Live log ring size |
|
|
764
792
|
|
|
765
793
|
The password is generated **on every process start** and only appears in the
|
|
766
|
-
server log
|
|
794
|
+
server log `ADMIN` box. Banned and unauthorised requests all get a `404`.
|
|
795
|
+
Usage and screens: [06-caching.md](./06-caching.md).
|
|
767
796
|
|
|
797
|
+
```js
|
|
798
|
+
admin() {
|
|
799
|
+
return {
|
|
800
|
+
enabled: process.env.JSKELET_ADMIN === "1",
|
|
801
|
+
allowIps: ["203.0.113.10", "10.0.0.0/8"],
|
|
802
|
+
};
|
|
803
|
+
}
|
|
768
804
|
```
|
|
769
|
-
[cache-panel] http://localhost:3000/_jskelet/cache — password for this run: 3f9c…
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
There is no persistent secret (no config field, no environment variable):
|
|
773
|
-
leaking one means handing out the right to flush the cache, and a deploy should
|
|
774
|
-
revoke old access on its own. Banned and unauthorised requests all get a `404`.
|
|
775
|
-
Usage and screens: [06-caching.md](./06-caching.md).
|
|
776
805
|
|
|
777
806
|
### `cache().cloudflare`
|
|
778
807
|
|
|
@@ -800,12 +829,6 @@ panel keeps working. What can actually be asked — in particular why "how many
|
|
|
800
829
|
edges hold this page" has no exact answer — is in
|
|
801
830
|
[06-caching.md](./06-caching.md).
|
|
802
831
|
|
|
803
|
-
```js
|
|
804
|
-
panel: {
|
|
805
|
-
enabled: process.env.CACHE_PANEL === "1",
|
|
806
|
-
}
|
|
807
|
-
```
|
|
808
|
-
|
|
809
832
|
### `cache().prewarm`
|
|
810
833
|
|
|
811
834
|
| Field | Type | Default | Meaning |
|
|
@@ -930,7 +953,7 @@ and no warning is printed.
|
|
|
930
953
|
| `HOST` | `startServer` | `::` | Interface to bind to. The default listens dual-stack (IPv6 + IPv4); it falls back to `0.0.0.0` where IPv6 is unavailable |
|
|
931
954
|
| `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) |
|
|
932
955
|
| `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) |
|
|
933
|
-
| `
|
|
956
|
+
| `JSKELET_ADMIN` | `createApp` | — | When set, turns the admin panel on; `0` turns off a panel enabled in the config. The env wins because the panel is usually opened once during an incident. [06](./06-caching.md) |
|
|
934
957
|
| `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
958
|
| `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache surface | — | Zone identifier. No Cloudflare endpoint is called unless it is set alongside the token |
|
|
936
959
|
| `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache surface | — | The root for purge URLs. Required when the panel is opened over an internal address |
|
package/docs/en/11-migration.md
CHANGED
|
@@ -17,6 +17,7 @@ will feel familiar. The *reasons* behind the differences are in
|
|
|
17
17
|
| `next.config.mjs` | `jskelet.config.mjs` | Same spirit, smaller surface ([07](./07-configuration.md)) |
|
|
18
18
|
| `headers()` | `headers()` | Same shape: `{ source, headers: [{ key, value }] }` |
|
|
19
19
|
| `redirects()` | `redirects()` | `permanent` → 308, otherwise 307; can be overridden with `statusCode` |
|
|
20
|
+
| `trailingSlash` | `trailingSlash` | `true` → canonical URLs end with `/` (308); default `false` does not strip ([07](./07-configuration.md)) |
|
|
20
21
|
| `rewrites()` | `rewrites()` | There are `beforeFiles` / `afterFiles` phases; no `fallback` |
|
|
21
22
|
| `compress: true` | Automatic | brotli + gzip via `node:zlib` |
|
|
22
23
|
| `images.deviceSizes` | `images.widths` | Build-time webp generation ([08](./08-build.md)) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|