jskelet 0.4.3 → 0.4.5

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 CHANGED
@@ -8,6 +8,15 @@ one is listed under a **Breaking** heading.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ### Added
12
+
13
+ - Runtime remote image optimizer: set `images.remote.allowHosts` to proxy
14
+ allowlisted http(s) images through `/_jskelet/image?url=&w=&q=` as resized
15
+ webp (disk cache under `.jskelet/image-cache/`). `image()` rewrites matching
16
+ remote `src` values automatically; `remoteImageUrl()` builds URLs by hand.
17
+ Requires `sharp` at runtime; without it the endpoint 302-redirects to the
18
+ source.
19
+
11
20
  ### Breaking
12
21
 
13
22
  - The cache admin panel moved to a top-level `admin()` config section at
@@ -34,6 +43,11 @@ one is listed under a **Breaking** heading.
34
43
 
35
44
  ### Added
36
45
 
46
+ - Devtools SEO check: an **SEO** tab in the overlay lists document, heading,
47
+ image, link and social-tag issues with error/warning severity. Optional page
48
+ highlights draw red or yellow boxes around the offending elements; the label
49
+ shows the short title and a click opens the full explanation. Served only in
50
+ development as `/__jskelet/dev/seo.js` beside the overlay.
37
51
  - Marketing homepage ops storyboard: Redis L2, `/_jskelet/admin` panel mock and
38
52
  Cloudflare purge flow, with tabbed visual scenes animated by the vanilla
39
53
  `motion` API (Framer Motion’s non-React package) via an `ops-story` island.
package/docs/02-mimari.md CHANGED
@@ -42,6 +42,8 @@ JSkelet bu gözlemi mimarinin merkezine alır:
42
42
  ├─ staticPrecompressed build'de üretilmiş .br/.gz kopyalar (kalite 11)
43
43
  ├─ express.static public/ altındaki dosyalar
44
44
  ├─ (dev) devtools yalnızca NODE_ENV=development
45
+ ├─ admin (açıksa) /_jskelet/admin
46
+ ├─ image optimizer (remote) /_jskelet/image — allowHosts doluysa
45
47
  ├─ body parser'lar urlencoded 64kb + json 256kb
46
48
  ├─ rewrites(afterFiles) statik denendikten sonra
47
49
  ├─ route'lar
@@ -383,12 +383,16 @@ Davranış:
383
383
 
384
384
  - `public/` altındaki yerel raster görseller için build'de üretilen webp
385
385
  varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
386
- `width`/`height` olarak eklenir. Manifest'te olmayan ya da uzak görseller
387
- olduğu gibi basılır.
388
- - `srcset` elle verilmişse ya da `unoptimized: true` ise manifest'e hiç
389
- bakılmaz.
386
+ `width`/`height` olarak eklenir. Manifest'te olmayan yerel yollar olduğu
387
+ gibi basılır.
388
+ - `images.remote.allowHosts` açıksa uzak `http(s)` URL'leri
389
+ `/_jskelet/image?url=&w=` proxy'sine çevrilir (webp). `width` varsa 1x/2x
390
+ + config `widths` ile `srcset` üretilir.
391
+ - `srcset` elle verilmişse ya da `unoptimized: true` ise ne manifest ne de
392
+ remote proxy kullanılır.
390
393
  - Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
391
- yazılmaz; gürültüden ibaret olurdu.
394
+ yazılmaz; gürültüden ibaret olurdu. Remote'da tek genişlikte bile `src`
395
+ yine optimize URL'dir.
392
396
  - `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
393
397
  genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
394
398
  (`(max-width: Npx) 100vw, Npx`).
@@ -482,24 +482,51 @@ icons: { scan: ["views", "client", "routes", "lib", "content"] }
482
482
 
483
483
  ## `images`
484
484
 
485
- **Tip:** `{ widths?: number[], quality?: number, skip?: string[] } | false` —
486
- **Varsayılan:** `{}`
485
+ **Tip:**
486
+ `{ widths?: number[], quality?: number, skip?: string[], remote?: { allowHosts: string[], path?: string, maxWidth?: number, cacheMaxAge?: number, fetchTimeoutMs?: number, maxBytes?: number } | false } | false`
487
+ — **Varsayılan:** `{ widths, quality, skip, remote: false }` (remote kapalı)
488
+
489
+ `public/` altındaki png/jpg görsellerin webp varyantlarını **build**'de üretir.
490
+ `remote.allowHosts` verilirse çalışma anında uzak görselleri de proxy eder
491
+ (`/_jskelet/image?url=&w=&q=` → webp).
492
+
493
+ | Alan | Tip | Varsayılan | Anlamı |
494
+ | --- | --- | --- | --- |
495
+ | `widths` | `number[]` | `[400, 640, 960, 1280, 1920]` | Build ve remote `srcset` adayları. Kaynaktan büyük olanlar build'de elenir; kaynağın kendi genişliği (en fazla 1920) her zaman eklenir. |
496
+ | `quality` | `number` | `78` | webp kalitesi. Build'de imzaya girer; remote uçta `q` varsayılanı. |
497
+ | `skip` | `string[]` | `[]` | Build'de taranmayacak **dizin adları**. `assets` ve `fonts` her zaman atlanır. |
498
+ | `remote` | `object \| false` | kapalı | Runtime optimizer. `allowHosts` **zorunlu**; boşsa uç mount edilmez. |
487
499
 
488
- `public/` altındaki png/jpg görsellerin webp varyantlarını üretir.
500
+ ### `images.remote`
489
501
 
490
502
  | Alan | Tip | Varsayılan | Anlamı |
491
503
  | --- | --- | --- | --- |
492
- | `widths` | `number[]` | `[400, 640, 960, 1280, 1920]` | Üretilecek genişlikler. Kaynaktan büyük olanlar elenir; kaynağın kendi genişliği (en fazla 1920) her zaman eklenir. |
493
- | `quality` | `number` | `78` | webp kalitesi. Değişince kodlayıcı imzası değişir ve tüm görseller yeniden kodlanır. |
494
- | `skip` | `string[]` | `[]` | Taranmayacak **dizin adları**. `assets` ve `fonts` her zaman atlanır. |
504
+ | `allowHosts` | `string[]` | `[]` | Çekilebilecek host'lar. `*.cdn.example.com` sonek jokerini destekler. |
505
+ | `path` | `string` | `/_jskelet/image` | Optimizer GET yolu. |
506
+ | `maxWidth` | `number` | `1920` | `w` üst sınırı. |
507
+ | `cacheMaxAge` | `number` | `2592000` (30 gün) | Yanıt `Cache-Control` max-age (saniye). Disk önbelleği `.jskelet/image-cache/`. |
508
+ | `fetchTimeoutMs` | `number` | `10000` | Upstream fetch zaman aşımı. |
509
+ | `maxBytes` | `number` | `10485760` (10 MiB) | Upstream gövde üst sınırı. |
495
510
 
496
- `false` verilirse görsel adımı hiç çalışmaz. Adım `sharp` gerektirir ve watch
497
- turunda hiç çalışmaz. Ayrıntı: [08-build.md](./08-build.md).
511
+ `false` verilirse görsel adımı hiç çalışmaz. Build adımı `sharp` gerektirir ve
512
+ watch turunda hiç çalışmaz. Remote açıksa `sharp` **runtime**'da da gerekir;
513
+ yoksa optimizer kaynak URL'ye 302 yönlendirir. Ayrıntı: [08-build.md](./08-build.md).
498
514
 
499
515
  ```js
500
- images: { widths: [400, 800, 1200], quality: 82, skip: ["indirmeler"] }
516
+ images: {
517
+ widths: [400, 800, 1200],
518
+ quality: 82,
519
+ skip: ["indirmeler"],
520
+ remote: {
521
+ allowHosts: ["static.ornek.com", "*.cdn.ornek.com"],
522
+ },
523
+ }
501
524
  ```
502
525
 
526
+ `image({ src: "https://static.ornek.com/a.jpg", width: 96, alt: "…" })` bu
527
+ ayarla `src` / `srcset`'i `/_jskelet/image?url=…&w=96` biçimine çevirir.
528
+ Elle URL kurmak için `remoteImageUrl(src, { width })` (`jskelet`).
529
+
503
530
  ## `clientEnv`
504
531
 
505
532
  **Tip:** `string[]` — **Varsayılan:** `[]`
package/docs/08-build.md CHANGED
@@ -290,6 +290,13 @@ değiştirmez ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
290
290
  Bu adım `sharp` gerektirir. Kurulu değilse adım sessizce atlanır ve `image()`
291
291
  orijinal dosyaya döner. Watch turunda hiç çalışmaz.
292
292
 
293
+ ## Runtime uzak görsel proxy
294
+
295
+ `images.remote.allowHosts` verilirse `createApp` `/_jskelet/image` ucunu
296
+ mount eder. CMS / CDN kapakları build'e girmediği için `image()` bu host'lardaki
297
+ URL'leri `?url=&w=` biçiminde yeniden yazar; uç sharp ile webp üretir ve
298
+ `.jskelet/image-cache/` altına yazar. Ayrıntı: [07-yapilandirma.md](./07-yapilandirma.md).
299
+
293
300
  ## Precompress
294
301
 
295
302
  Build çıktısı varlıkların brotli (kalite 11) ve gzip (seviye 9) kopyalarını
@@ -170,9 +170,9 @@ basılır:
170
170
  ```
171
171
 
172
172
  Overlay dosyası **build'e dâhil değildir.** Sunucu onu framework paketinden ham
173
- olarak servis eder; bu yüzden içinde bundler yoktur, tek dosya olarak çalışır ve
174
- prod çıktısına hiçbir şey eklemez. Tüm arayüz shadow DOM içinde durur, sayfanın
175
- CSS'i ile karışmaz.
173
+ olarak servis eder; bundler yoktur. Aynı dizindeki `seo.js` gibi kardeş
174
+ modüller native ESM import ile yüklenir ve prod çıktısına hiçbir şey eklenmez.
175
+ Tüm arayüz shadow DOM içinde durur, sayfanın CSS'i ile karışmaz.
176
176
 
177
177
  Gösterdikleri:
178
178
 
@@ -180,6 +180,14 @@ Gösterdikleri:
180
180
  (`img`/`script`/`link`), ve sunucudaki `console.error` / `console.warn`
181
181
  çıktıları. Sunucu tarafında `console` sarılır, böylece uyarılar terminalde
182
182
  kaybolmaz.
183
+ - **SEO:** açık sayfanın istemci tarafı taraması — title ve meta description
184
+ uzunluğu, `html lang`, viewport, canonical, robots/`noindex`, Open Graph ve
185
+ Twitter etiketleri, H1/outline, görsel `alt`, boş linkler ve JSON-LD parse
186
+ hataları. Bulgular panelde listelenir; **Highlight issues on the page**
187
+ açılınca sorunlu elemanların üzerine kırmızı (error) veya sarı (warning)
188
+ dikdörtgenler çizilir, kenarda kısa başlık durur. Etikete (ya da panel
189
+ satırına) tıklanınca ayrıntılı açıklama açılır. Tarama overlay'in yanındaki
190
+ `/seo.js` dosyasındadır; production paketine girmez.
183
191
  - **İstekler:** her HTML isteğinin metodu, yolu, durumu, süresi ve
184
192
  `X-JSkelet-Cache` değeri. Aynı satırlar terminale de basılır.
185
193
  - **Web Vitals:** TTFB, FCP, LCP, CLS, INP, DCL, load, uzun task sayısı ve
@@ -261,6 +269,7 @@ Rapor katmanı yalnızca development'ta yüklenir, üretim çıktısına hiç gi
261
269
  | Yol | Metot | İşi |
262
270
  | --- | --- | --- |
263
271
  | `/overlay.js` | GET | Overlay script'i |
272
+ | `/seo.js` | GET | SEO tarama + sayfa highlight yardımcısı (overlay import eder) |
264
273
  | `/logo.png` | GET | Overlay logosu |
265
274
  | `/ws` | GET (upgrade) | Canlı kanal: istatistikler, live reload ve CSS hot-swap olayları |
266
275
  | `/events` | GET | SSE: yalnızca WebSocket kurulamazsa kullanılan yedek olay akışı |
@@ -97,6 +97,7 @@ ENV HOST=0.0.0.0
97
97
 
98
98
  COPY package.json package-lock.json ./
99
99
  # sharp ve tailwind yalnızca build zamanı gerekli; çalışma imajına girmesin.
100
+ # images.remote kullanıyorsanız sharp'ı production dependencies'e alın.
100
101
  RUN npm ci --omit=dev && npm cache clean --force
101
102
 
102
103
  COPY --from=build /app/jskelet.config.mjs ./jskelet.config.mjs
@@ -126,9 +127,11 @@ Notlar:
126
127
  anında yapılıyor. `lib/` yalnızca projenizde varsa kopyalayın.
127
128
  - **`.jskelet/` gerekir:** `manifest.json` olmadan `asset()` hash'li URL'leri
128
129
  bulamaz ve `jskelet start` build'i baştan çalıştırmaya kalkar.
129
- - **`sharp` çalışma imajında gerekmez:** yalnızca build zamanı görsel
130
+ - **`sharp` çalışma imajında genelde gerekmez:** yalnızca build zamanı görsel
130
131
  optimizasyonu için. `--omit=dev` ile dışarıda kalır (devDependency olarak
131
- kurulmuşsa).
132
+ kurulmuşsa). **`images.remote` açıksa** sharp runtime bağımlılığıdır —
133
+ production `dependencies`'e alın ya da runtime imajında ayrıca kurun; yoksa
134
+ optimizer kaynak URL'ye 302 yönlendirir.
132
135
  - `jskelet start`ı `npx` olmadan çağırmak isterseniz
133
136
  `CMD ["node", "node_modules/jskelet/bin/jskelet.mjs", "start"]` de çalışır.
134
137
 
package/docs/11-tasima.md CHANGED
@@ -96,9 +96,10 @@ Bunları taşıma planında baştan hesaba katın:
96
96
  önce belgeyi hazırlar, `viewTransition` geçişi yumuşatır
97
97
  ([07](./07-yapilandirma.md)).
98
98
  - **Server Actions.** Form gönderimleri normal `app.post(...)` handler'larıdır.
99
- - **Otomatik görsel optimizasyonu (istek anında).** Optimizasyon build zamanında
100
- yapılır ve yalnızca `public/` altındaki yerel görselleri kapsar; uzak görseller
101
- olduğu gibi basılır.
99
+ - **Otomatik görsel optimizasyonu (istek anında).** Yerel `public/` görselleri
100
+ hâlâ build zamanında optimize edilir. Uzak `http(s)` görseller
101
+ `images.remote.allowHosts` verilirse çalışma anında proxy edilir
102
+ (`/_jskelet/image` → webp); config yoksa olduğu gibi basılır.
102
103
 
103
104
  ## Yan yana örnek
104
105
 
package/docs/README.md CHANGED
@@ -24,7 +24,7 @@ eşlenik tutuluyor; birini değiştiriyorsan diğerini de değiştir.
24
24
  | [06-cache.md](./06-cache.md) | `withHtmlCache`, `revalidate`, stale-while-revalidate, cache anahtarı, `X-JSkelet-Cache`, istek içi cache, degraded render, prewarm |
25
25
  | [07-yapilandirma.md](./07-yapilandirma.md) | `jskelet.config.mjs` tam referansı, `source` desen sözdizimi, ortam değişkenleri tablosu |
26
26
  | [08-build.md](./08-build.md) | Build hattı, manifest, hash'li varlıklar, CSS/Tailwind `@source`, fontlar, ikon sprite, görsel optimizasyonu, precompress |
27
- | [09-dev-araclari.md](./09-dev-araclari.md) | `jskelet dev` akışı, watch dizinleri, CSS hot-swap, devtools overlay (Alt+D), rapor sayfası, dev gate |
27
+ | [09-dev-araclari.md](./09-dev-araclari.md) | `jskelet dev` akışı, watch dizinleri, CSS hot-swap, devtools overlay (Alt+D, SEO highlight), rapor sayfası, dev gate |
28
28
  | [10-dagitim.md](./10-dagitim.md) | Prod build + start, ortam değişkenleri, Docker, ters proxy, sağlık kontrolü |
29
29
  | [11-tasima.md](./11-tasima.md) | Next.js'ten taşıma: karşılık tablosu ve adım adım plan |
30
30
  | [12-panel-ve-oturum.md](./12-panel-ve-oturum.md) | Kişiye özel sayfalar: `private: true`, imzalı cookie oturumu, CSRF, `fragment()`, parça takası ve form döngüsü |
@@ -42,6 +42,8 @@ Request
42
42
  ├─ staticPrecompressed .br/.gz copies produced at build (quality 11)
43
43
  ├─ express.static files under public/
44
44
  ├─ (dev) devtools only when NODE_ENV=development
45
+ ├─ admin (if enabled) /_jskelet/admin
46
+ ├─ image optimizer (remote) /_jskelet/image — when allowHosts is set
45
47
  ├─ body parsers urlencoded 64kb + json 256kb
46
48
  ├─ rewrites(afterFiles) after static has been tried
47
49
  ├─ routes
@@ -389,12 +389,16 @@ Behaviour:
389
389
 
390
390
  - For local raster images under `public/`, the webp variants generated at build
391
391
  time (`.jskelet/images.json`) are added automatically as `srcset` plus
392
- intrinsic `width`/`height`. Images that are not in the manifest, or remote
393
- ones, are emitted as-is.
394
- - If `srcset` is given by hand, or `unoptimized: true` is set, the manifest is
395
- not consulted at all.
392
+ intrinsic `width`/`height`. Local paths missing from the manifest are emitted
393
+ as-is.
394
+ - When `images.remote.allowHosts` is set, remote `http(s)` URLs are rewritten to
395
+ the `/_jskelet/image?url=&w=` proxy (webp). If `width` is set, `srcset`
396
+ includes 1x/2x plus config `widths`.
397
+ - If `srcset` is given by hand, or `unoptimized: true` is set, neither the
398
+ manifest nor the remote proxy is used.
396
399
  - If only **one** variant was produced (because the source is already small),
397
- `srcset`/`sizes` are not written; they would be pure noise.
400
+ `srcset`/`sizes` are not written; they would be pure noise. For remote images,
401
+ a single width still rewrites `src` to the optimized URL.
398
402
  - If `sizes` is not given a reasonable default is produced: the image is not
399
403
  scaled beyond its own intrinsic width, and it fills the viewport on narrow
400
404
  screens (`(max-width: Npx) 100vw, Npx`).
@@ -494,24 +494,52 @@ icons: { scan: ["views", "client", "routes", "lib", "content"] }
494
494
 
495
495
  ## `images`
496
496
 
497
- **Type:** `{ widths?: number[], quality?: number, skip?: string[] } | false` —
498
- **Default:** `{}`
497
+ **Type:**
498
+ `{ widths?: number[], quality?: number, skip?: string[], remote?: { allowHosts: string[], path?: string, maxWidth?: number, cacheMaxAge?: number, fetchTimeoutMs?: number, maxBytes?: number } | false } | false`
499
+ — **Default:** `{ widths, quality, skip, remote: false }` (remote off)
499
500
 
500
- Generates webp variants of the png/jpg images under `public/`.
501
+ Generates webp variants of png/jpg files under `public/` at **build** time.
502
+ When `remote.allowHosts` is set, also proxies remote images at runtime
503
+ (`/_jskelet/image?url=&w=&q=` → webp).
501
504
 
502
505
  | Field | Type | Default | Meaning |
503
506
  | --- | --- | --- | --- |
504
- | `widths` | `number[]` | `[400, 640, 960, 1280, 1920]` | Widths to generate. Ones larger than the source are dropped; the source's own width (at most 1920) is always added. |
505
- | `quality` | `number` | `78` | webp quality. When it changes, the encoder signature changes and every image is re-encoded. |
506
- | `skip` | `string[]` | `[]` | **Directory names** not to scan. `assets` and `fonts` are always skipped. |
507
+ | `widths` | `number[]` | `[400, 640, 960, 1280, 1920]` | Candidates for build and remote `srcset`. Ones larger than the source are dropped at build; the source's own width (at most 1920) is always added. |
508
+ | `quality` | `number` | `78` | webp quality. Part of the build encoder signature; default `q` on the remote endpoint. |
509
+ | `skip` | `string[]` | `[]` | **Directory names** not to scan at build. `assets` and `fonts` are always skipped. |
510
+ | `remote` | `object \| false` | off | Runtime optimizer. `allowHosts` is **required**; empty disables the route. |
511
+
512
+ ### `images.remote`
507
513
 
508
- If `false` is given, the image step never runs. The step requires `sharp` and
509
- never runs on a watch pass. Details: [08-build.md](./08-build.md).
514
+ | Field | Type | Default | Meaning |
515
+ | --- | --- | --- | --- |
516
+ | `allowHosts` | `string[]` | `[]` | Hosts that may be fetched. Supports a `*.cdn.example.com` suffix wildcard. |
517
+ | `path` | `string` | `/_jskelet/image` | Optimizer GET path. |
518
+ | `maxWidth` | `number` | `1920` | Cap for `w`. |
519
+ | `cacheMaxAge` | `number` | `2592000` (30 days) | Response `Cache-Control` max-age (seconds). Disk cache under `.jskelet/image-cache/`. |
520
+ | `fetchTimeoutMs` | `number` | `10000` | Upstream fetch timeout. |
521
+ | `maxBytes` | `number` | `10485760` (10 MiB) | Upstream body size limit. |
522
+
523
+ If `false` is given, neither surface runs. The build step requires `sharp` and
524
+ never runs on a watch pass. With remote enabled, `sharp` is also needed at
525
+ **runtime**; without it the optimizer 302-redirects to the source URL. Details:
526
+ [08-build.md](./08-build.md).
510
527
 
511
528
  ```js
512
- images: { widths: [400, 800, 1200], quality: 82, skip: ["downloads"] }
529
+ images: {
530
+ widths: [400, 800, 1200],
531
+ quality: 82,
532
+ skip: ["downloads"],
533
+ remote: {
534
+ allowHosts: ["static.example.com", "*.cdn.example.com"],
535
+ },
536
+ }
513
537
  ```
514
538
 
539
+ `image({ src: "https://static.example.com/a.jpg", width: 96, alt: "…" })`
540
+ rewrites `src` / `srcset` to `/_jskelet/image?url=…&w=96`. To build URLs by
541
+ hand, use `remoteImageUrl(src, { width })` from `jskelet`.
542
+
515
543
  ## `clientEnv`
516
544
 
517
545
  **Type:** `string[]` — **Default:** `[]`
@@ -305,6 +305,13 @@ to the `.jskelet/images.json` manifest. `image()` looks at that manifest and add
305
305
  This step requires `sharp`. If it is not installed the step is silently skipped
306
306
  and `image()` falls back to the original file. It never runs on a watch pass.
307
307
 
308
+ ## Runtime remote image proxy
309
+
310
+ When `images.remote.allowHosts` is set, `createApp` mounts `/_jskelet/image`.
311
+ CMS / CDN covers never enter the build, so `image()` rewrites those host URLs to
312
+ `?url=&w=`; the endpoint encodes webp with sharp and stores files under
313
+ `.jskelet/image-cache/`. Details: [07-configuration.md](./07-configuration.md).
314
+
308
315
  ## Precompress
309
316
 
310
317
  Produces brotli (quality 11) and gzip (level 9) copies of the built assets:
@@ -173,9 +173,10 @@ by clicking the backdrop. It is only emitted by the layout when
173
173
  ```
174
174
 
175
175
  The overlay file is **not part of the build.** The server serves it raw from the
176
- framework package; that is why there is no bundler involved, it works as a
177
- single file, and it adds nothing to the production output. The entire UI lives
178
- inside a shadow DOM and does not mix with the page's CSS.
176
+ framework package; that is why there is no bundler involved. It loads sibling
177
+ modules such as `seo.js` with native ESM imports, and it adds nothing to the
178
+ production output. The entire UI lives inside a shadow DOM and does not mix with
179
+ the page's CSS.
179
180
 
180
181
  What it shows:
181
182
 
@@ -183,6 +184,14 @@ What it shows:
183
184
  (`img`/`script`/`link`), and the server's `console.error` / `console.warn`
184
185
  output. On the server side `console` is wrapped so warnings do not get lost in
185
186
  the terminal.
187
+ - **SEO:** a client-side scan of the current page — title and meta description
188
+ length, `html lang`, viewport, canonical, robots/`noindex`, Open Graph and
189
+ Twitter tags, H1/outline, image `alt`, empty links, and JSON-LD parse errors.
190
+ Findings appear in the panel; turning on **Highlight issues on the page**
191
+ draws red (error) or yellow (warning) boxes on the elements, with a short
192
+ title on the border. Clicking the label (or a row in the panel) opens the
193
+ full explanation. The scan lives in `/seo.js` next to the overlay and is not
194
+ part of the production bundle.
186
195
  - **Requests:** the method, path, status, duration and `X-JSkelet-Cache` value
187
196
  of every HTML request. The same lines are printed to the terminal too.
188
197
  - **Web Vitals:** TTFB, FCP, LCP, CLS, INP, DCL, load, long task count and
@@ -265,6 +274,7 @@ Under `brand.devBasePath` (default `/__jskelet/dev`):
265
274
  | Path | Method | Job |
266
275
  | --- | --- | --- |
267
276
  | `/overlay.js` | GET | The overlay script |
277
+ | `/seo.js` | GET | SEO scan + page highlight helper (imported by the overlay) |
268
278
  | `/logo.png` | GET | The overlay logo |
269
279
  | `/ws` | GET (upgrade) | Live channel: statistics, live reload and CSS hot-swap events |
270
280
  | `/events` | GET | SSE: the fallback event stream, used only when WebSocket cannot be established |
@@ -98,6 +98,7 @@ ENV HOST=0.0.0.0
98
98
 
99
99
  COPY package.json package-lock.json ./
100
100
  # sharp and tailwind are only needed at build time; keep them out of the runtime image.
101
+ # If you use images.remote, move sharp to production dependencies.
101
102
  RUN npm ci --omit=dev && npm cache clean --force
102
103
 
103
104
  COPY --from=build /app/jskelet.config.mjs ./jskelet.config.mjs
@@ -127,9 +128,11 @@ Notes:
127
128
  rendering happens at runtime. Copy `lib/` only if your project has one.
128
129
  - **`.jskelet/` is needed:** without `manifest.json`, `asset()` cannot find the
129
130
  hashed URLs and `jskelet start` will try to run the build from scratch.
130
- - **`sharp` is not needed in the runtime image:** it is only for build-time image
131
- optimization. `--omit=dev` leaves it out (if it was installed as a
132
- devDependency).
131
+ - **`sharp` is usually not needed in the runtime image:** it is only for
132
+ build-time image optimization. `--omit=dev` leaves it out (if it was installed
133
+ as a devDependency). **If `images.remote` is enabled**, sharp is a runtime
134
+ dependency — move it to production `dependencies` or install it in the
135
+ runtime image; otherwise the optimizer 302-redirects to the source URL.
133
136
  - If you would rather call `jskelet start` without `npx`,
134
137
  `CMD ["node", "node_modules/jskelet/bin/jskelet.mjs", "start"]` works too.
135
138
 
@@ -99,9 +99,10 @@ Account for these from the start in your migration plan:
99
99
  `viewTransition` smooths the transition
100
100
  ([07](./07-configuration.md)).
101
101
  - **Server Actions.** Form submissions are ordinary `app.post(...)` handlers.
102
- - **Automatic image optimization (at request time).** Optimization happens at
103
- build time and only covers local images under `public/`; remote images are
104
- emitted as-is.
102
+ - **Automatic image optimization (at request time).** Local `public/` images
103
+ are still optimized at build time. Remote `http(s)` images can be proxied at
104
+ runtime when `images.remote.allowHosts` is set (`/_jskelet/image` → webp);
105
+ without that config they are emitted as-is.
105
106
 
106
107
  ## A side-by-side example
107
108
 
package/docs/en/README.md CHANGED
@@ -27,7 +27,7 @@ change one, change the other.
27
27
  | [06-caching.md](./06-caching.md) | `withHtmlCache`, `revalidate`, stale-while-revalidate, the cache key, `X-JSkelet-Cache`, in-request cache, degraded render, prewarm |
28
28
  | [07-configuration.md](./07-configuration.md) | Full `jskelet.config.mjs` reference, the `source` pattern syntax, environment variable table |
29
29
  | [08-build.md](./08-build.md) | The build pipeline, the manifest, hashed assets, CSS/Tailwind `@source`, fonts, icon sprite, image optimization, precompress |
30
- | [09-dev-tools.md](./09-dev-tools.md) | The `jskelet dev` flow, watch directories, CSS hot-swap, devtools overlay (Alt+D), the report page, the dev gate |
30
+ | [09-dev-tools.md](./09-dev-tools.md) | The `jskelet dev` flow, watch directories, CSS hot-swap, devtools overlay (Alt+D, SEO highlight), the report page, the dev gate |
31
31
  | [10-deployment.md](./10-deployment.md) | Prod build + start, environment variables, Docker, reverse proxy, health check |
32
32
  | [11-migration.md](./11-migration.md) | Migrating from Next.js: the equivalence table and a step-by-step plan |
33
33
  | [12-dashboards-and-sessions.md](./12-dashboards-and-sessions.md) | Per-visitor pages: `private: true`, signed cookie sessions, CSRF, `fragment()`, swapping regions and the form loop |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "A framework that feels like no framework: Express 5 + build-time .jsk (or EJS) SSR, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
5
5
  "type": "module",
6
6
  "license": "MIT",