jskelet 0.6.2 → 0.6.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.
Files changed (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
package/CHANGELOG.md CHANGED
@@ -1,596 +1,620 @@
1
- # Changelog
2
-
3
- All notable changes to this project are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
- While the project is on `0.x`, minor releases may contain breaking changes; each
7
- one is listed under a **Breaking** heading.
8
-
9
- ## [Unreleased]
10
-
11
- ### Added
12
-
13
- - `robots.txt` responses gain a trailing JSkelet note that disallows framework
14
- endpoints (`/_jskelet/`, `/__jskelet/`, `/_fragment/`, plus a custom admin,
15
- image, handoff, or dev path when it sits outside those prefixes). Every
16
- user-agent already named in the file is repeated in that block, so a
17
- crawler-specific group still sees the rules.
18
-
19
- ### Breaking
20
-
21
- - Dev gate is opt-in
22
- `DEV_TOKEN` in the environment no longer locks the site. Require the token
23
- only with `devGate: true` or `DEV_GATE=1`. `DEV_GATE=0` turns the gate off
24
- even when config enabled it.
25
- - Cache ceilings
26
- `cache().maxEntries` above 800, `cache().data.maxEntries` above 20,000, and
27
- `prewarm.onVisit` above `perPage` 20, `rps` 2, or `concurrency` 2 are clamped
28
- at load with a warning. `onVisit` `rps: 0` is no longer unlimited. The HTML
29
- cache also stops growing past 256 MB of stored HTML plus compressed bodies;
30
- a single page larger than that is not stored.
31
-
32
- ### Fixed
33
-
34
- - Visit warming skips pages that are already fresh when the cache key has a
35
- vary prefix or a trailing `?`. With `vary.host`, warm requests stay on
36
- loopback and send the public host as `x-forwarded-host`, so a second
37
- `127.0.0.1` HTML entry is not created. The onVisit queue holds at most 64
38
- paths.
39
-
40
- ### Removed
41
-
42
- - `examples/marketing` — the marketing site now lives as a standalone app
43
- outside this repository (`jskelet-marketing`).
44
-
45
- ## [0.6.0] - 2026-09-21
46
-
47
- ### Added
48
-
49
- - Port reclaim with `--murder`
50
- `jskelet dev --murder` and `jskelet start --murder` kill whatever already
51
- holds the listen port and bind in its place. Without the flag the CLI still
52
- refuses to start, but now with a clear error (pid + hint) instead of a bare
53
- `EADDRINUSE`.
54
- - Layout built-ins for `.jsk`
55
- `<Stylesheets />`, `<BodyScripts />`, and `<JsonLd />` emit the asset /
56
- script / JSON-LD loops from layout without calling helpers in the expression
57
- language.
58
- - Framework default layout as `.jsk`
59
- Ships as `src/templates/layout.jsk` with a checked-in `layout.render.js`
60
- (`node scripts/compile-framework-layout.mjs`, `--check` for drift).
61
- `jskelet/layout` points at the `.jsk` source; the legacy EJS copy remains at
62
- `jskelet/layout/ejs`.
63
- - JSK editor tooling
64
- The VS Code / Cursor JSK extension (v0.2.0) reports diagnostics in Problems
65
- on open/save and offers PascalCase component completions from
66
- `views/components`.
67
- - `<!DOCTYPE>` parsing in `.jsk`
68
- Doctype and other `<!…>` declarations parse correctly instead of hanging the
69
- template compiler.
70
- - `jskelet migrate` for Next.js App Router
71
- Codemod with `scan` inventory, `apply` (JSX pages → controller + `.jsk`,
72
- presentational components → HTML string helpers, `"use client"` → island
73
- stubs), and `config` draft from `next.config`. Ships with `@babel/parser` /
74
- `@babel/types`.
75
- - Published TypeScript declarations
76
- `.d.ts` files under `types/` for `jskelet`, `jskelet/client`, `jskelet/html`,
77
- `jskelet/tags`, `jskelet/cookies`, and `jskelet/log` (`npm run types`).
78
- - TypeScript client entries and islands
79
- The client build accepts `.ts` / `.mts` entries and islands via esbuild;
80
- manifest keys stay `*.js`. Conflicting stems (`main.js` + `main.ts`) fail the
81
- build. Icon usage scan includes `.ts` / `.mts`.
82
- - Local `icons/` sprite source
83
- When a flat `icons/` directory is present (`icons.dir`, default `"icons"`),
84
- it is the exclusive SVG sprite source (`house.svg` / `house-bold.svg`).
85
- Phosphor is used only when that directory is absent. The hashed sprite still
86
- lands under `public/assets/` and is precompressed.
87
- - Auth handoff hardening
88
- `allowedCookieNames` allowlist, pending-ticket and per-IP mint limits, plus
89
- RFC 6265 cookie-name validation on `serializeCookie` (`isValidCookieName`).
90
-
91
- ### Fixed
92
-
93
- - Clearer `jskelet dev` startup failures
94
- The CLI prints the real server failure (for example port already in use /
95
- `--murder` hint) instead of only `server exited (code 1)` when the child
96
- dies during startup.
97
- - Marketing trust-bar icons after `.jsk` migration
98
- `icons.scan` now covers `routes/`, so names declared in controllers
99
- (`Package`, `TextT`, `ShieldCheck`, …) land in the sprite again.
100
- - Open Graph routes with a `.png` suffix
101
- Paths like `/og/…/:slug.png` escape the dot for Express 5 / path-to-regexp,
102
- so the handler matches instead of falling through to the HTML 404.
103
- - Safer remote image optimizer redirects
104
- Redirects are no longer followed blindly; each hop is re-checked against
105
- `allowHosts`, blocked private addresses, and DNS resolution (open-redirect
106
- SSRF).
107
-
108
- ### Breaking
109
-
110
- - `ejs` is an optional peer
111
- Apps that only use `.jsk` need not install it; apps that still have `.ejs`
112
- views or layouts must `npm i ejs`. Missing EJS when an `.ejs` file is
113
- rendered throws a clear install/migrate hint.
114
- - Default layout export is `.jsk`
115
- `jskelet/layout` now resolves to `layout.jsk` (was `layout.ejs`). Use
116
- `jskelet/layout/ejs` for the legacy file.
117
- - Auth handoff requires cookie allowlist
118
- `auth.crossSubdomainHandoff` mint needs a non-empty `allowedCookieNames`
119
- list; `true` alone no longer accepts arbitrary cookie names.
120
- - Production builds omit client sourcemaps
121
- Development still emits them; production client bundles do not.
122
- - Secret-like `clientEnv` keys fail the build
123
- Names that look like secrets are rejected instead of being inlined into the
124
- client bundle.
125
-
126
- ### Changed
127
-
128
- - Marketing changelog as release notes
129
- `/changelog` hides Unreleased, lists every published release without
130
- pagination, and shows each note as a titled card with a short headline plus
131
- a longer explanation (Keep-a-Changelog sections: Added / Changed / Fixed /
132
- …).
133
- - Examples are `.jsk` only
134
- `minimal`, `blog`, `dashboard`, and `marketing` use `.jsk` for layouts,
135
- pages, and partials; EJS files were removed from those trees.
136
- - Stricter, clearer template compiler
137
- Unknown PascalCase components fail the build. Errors for forbidden function
138
- calls, unknown includes (with location), and unclosed `{#if}` / `{#each}` at
139
- EOF are clearer; icon scan recognizes `<Icon name="…" />`.
140
- - Docs present `.jsk` as the default
141
- Docs / AGENTS / README tell the build-time `.jsk` story first; EJS is
142
- documented as an optional legacy peer.
143
- - Standalone JSK VSIX packaging
144
- The VS Code / Cursor extension (`extensions/vscode-jsk`) uses the new 3D
145
- `.jsk` mark as marketplace and explorer icon, and packages as a standalone
146
- VSIX (vendors `src/compile`) for Marketplace publish.
147
- - Migrate peers are optional
148
- `@babel/parser` and `@babel/types` are optional peers used only by migrate.
149
- They are no longer installed with every `jskelet` install; missing peers
150
- throw an install hint. Unused `@babel/traverse` was dropped.
151
- - Auth handoff sits after CSRF
152
- Mint mounts after CSRF so origin checks apply to
153
- `POST /_jskelet/auth/handoff`.
154
- - Stronger security docs
155
- Docs (TR/EN) expand `trustProxy` / `csrf.token` guidance and include a
156
- fuller security-headers example under `headers()`.
157
- - Marketing copy defaults to `.jsk`
158
- EN/TR marketing and how-it-works examples present `.jsk` as the default
159
- template surface; the compare column for hand-written Express + EJS stays as
160
- a competitor. Scaffold and migrate mappings name `.jsk` pages and layouts.
161
- - New geometric brand mark
162
- Framework and marketing logos live at `src/logo.png` (admin / devtools) and
163
- `examples/marketing/public/logo.png`. Marketing serves local favicons via
164
- metadata `extraTags`.
165
- - Development 5xx diagnostics
166
- In `NODE_ENV=development`, 5xx responses show the error message and stack
167
- instead of the polished 500 page / `hooks.error()`. Production still returns
168
- the minimal status page with no internals.
169
- - Marketing fit copy for signed-in panels
170
- EN/TR fit copy treats dashboards and per-visitor panels as a supported path
171
- (`private: true`, fragments). The poor-fit column names SPA shells,
172
- collaborative client trees, streaming/RSC, and built-in real-time transport.
173
- - Marketing copy for 0.5.x surfaces
174
- EN/TR copy reflects host `vary`, early refresh, classic vs `onVisit`
175
- prewarm, local `icons/`, shared cookies, and `opengraph-image` → `ogHandler`
176
- on the migrate table. Pages serve per-locale OG cards at
177
- `/og/:locale/:page.png`.
178
- - Marketing visual language
179
- Dark cyan glass look with shared `glass-panel` surfaces, numbered lit feature
180
- cards, a 3D stack illustration on the home hero, and rotating cyan border
181
- beams on lit panels.
182
-
183
- ## [0.5.4] - 2026-09-18
184
-
185
- ### Added
186
-
187
- - Shared cross-subdomain cookies: `brand.sharedCookieRoots` plus `writeSharedCookie` / `clearSharedCookie` on server (`jskelet/cookies`) and client (`jskelet/client`). `Secure` follows https / `x-forwarded-proto` (not `NODE_ENV`); the client read-back fails into handoff when the browser rejects `Domain`. Optional `auth.crossSubdomainHandoff` mounts `POST /_jskelet/auth/handoff` (one-time ticket → `?handoff=`) and documents a `window.name` bridge. Large tokens are refused — put a short session id in the cookie, not a JWT.
188
-
189
- ## [0.5.3] - 2026-09-18
190
-
191
- ### Added
192
-
193
- - HTML cache key vary (`cache().vary`): `host: true` adds the public Host (`x-forwarded-host` or `Host`, lowercase, no port) as `h=…|` before the path; optional `headers` and `fn(req)` add further segments. Required on host-based locale sites so one locale's HTML is not served on another. Classic prewarm accepts `prewarm.origins` for multi-host warming when vary is on.
194
-
195
- ### Changed
196
-
197
- - Marketing example visual language: darker ink canvas, solid cyan primary CTAs, cyan-only glow/grid (indigo accents removed), and a measured trust bar on the homepage (payload gzip, Node, license, zero web fonts) instead of the marquee.
198
-
199
- ## [0.5.2] - 2026-09-18
200
-
201
- ### Added
202
-
203
- - Dynamic Open Graph images (Next.js `ImageResponse` / `opengraph-image`): `ogImage`, `sendOgImage`, `ogHandler`, and `ImageResponse` turn card fields or raw SVG into PNG when `sharp` is installed (SVG fallback otherwise). Wired in `examples/blog` as `/og/blog/:slug.png` and `metadata.openGraph.image`.
204
-
205
- ## [0.5.1] - 2026-09-07
206
-
207
- ### Added
208
-
209
- - Early HTML cache refresh before TTL expiry: the last successful produce time (`produceMs`) sets a lead window (`min(max(produceMs×2, 250ms), ttl/2)`). A still-fresh `HIT` in that window revalidates in the background; idle entries are soft-staled by a sweeper and drained over HTTP even without classic `prewarmPaths` (`PREWARM=0` disables both). In-flight refreshes no longer drop the entry when `staleUntil` elapses.
210
-
211
- ## [0.5.0] - 2026-09-03
212
-
213
- ### Added
214
-
215
- - Route-level stylesheets: put files in `styles/pages/*.css` and load them from the controller with `styles: ["home.css"]` (same contract as island `entries`). The layout emits them after global `app.css`; dev hot-swaps any changed `.css` manifest key without a full reload.
216
-
217
- ## [0.4.8] - 2026-09-03
218
-
219
- ### Added
220
-
221
- - Dev overlay Errors tab now lists failed SSR and browser `fetch` calls with page path, API URL, optional island name, and expandable response-body details (JSON instead of `[object Object]`). Server `console.error` / `console.warn` records also carry the current page when they fire during render.
222
-
223
- ## [0.4.7] - 2026-09-03
224
-
225
- ### Added
226
-
227
- - Visit-driven HTML prewarm (`cache().prewarm.onVisit`): after each public cacheable page response, same-origin links in the HTML are warmed in the background (document order, `perPage` cap). Mutually exclusive with classic prewarm (`max` / `priority` / `rotate` / `hooks.prewarmPaths`, etc.) — mixing them fails at config load.
228
-
229
- ## [0.4.6] - 2026-09-03
230
-
231
- ### Fixed
232
-
233
- - Remote image optimizer cache hits no longer 404. Disk cache lives under `.jskelet/image-cache/`; Express `sendFile` ignores dotfiles by default, so the file was written but the response still failed. `sendCached` now passes `dotfiles: "allow"`.
234
-
235
- ## [0.4.5] - 2026-09-03
236
-
237
- ### Added
238
-
239
- - Runtime remote image optimizer: set `images.remote.allowHosts` to proxy allowlisted http(s) images through `/_jskelet/image?url=&w=&q=` as resized webp (disk cache under `.jskelet/image-cache/`). `image()` rewrites matching remote `src` values automatically; `remoteImageUrl()` builds URLs by hand. Requires `sharp` at runtime; without it the endpoint 302-redirects to the source.
240
-
241
- ## [0.4.4] - 2026-09-02
242
-
243
- ### Added
244
-
245
- - Devtools SEO check: an **SEO** tab in the overlay lists document, heading, image, link and social-tag issues with error/warning severity. Optional page highlights draw red or yellow boxes around the offending elements; the label shows the short title and a click opens the full explanation. Served only in development as `/__jskelet/dev/seo.js` beside the overlay.
246
-
247
- ### Changed
248
-
249
- - Marketing compare live latency demo now measures two same-sized fragments (cached vs `no-store`) with an explicit 80 ms simulated upstream inside the shared producer — a hit skips that wait so the gap is visible even when RTT dominates the wall clock. The island prints transferred bytes, Server-Timing `produce` duration, and a View Source section contrasts `__NEXT_DATA__` payload tax with plain JSkelet HTML. The measured-weight block also shows an estimated Next.js App Router first-load breakdown beside this site’s real gzip totals (clearly labelled estimate, not a build from this repo).
250
-
251
- ### Fixed
252
-
253
- - Missing `/assets/*` responses no longer keep the long-lived `immutable` Cache-Control that `headersMiddleware` stamps for static prefixes. A deploy race (prune-before-write) could 404 a hashed CSS URL for a moment; a CDN then cached that HTML 404 for a year and browsers refused it as a stylesheet (`MIME type 'text/html'`). Catch-all and `notFound` handlers now set `Cache-Control: no-store`. CSS and sprite builds write the new file before pruning older hashes so the same content hash never has a gap.
254
-
255
- ## [0.4.3] - 2026-09-02
256
-
257
- ### Added
258
-
259
- - Marketing homepage ops storyboard: Redis L2, `/_jskelet/admin` panel mock and Cloudflare purge flow, with tabbed visual scenes animated by the vanilla `motion` API (Framer Motion’s non-React package) via an `ops-story` island.
260
-
261
- ## [0.4.2] - 2026-09-02
262
-
263
- ### Changed
264
-
265
- - README rewritten for the current surface: build-time `.jsk` as the default template story (EJS still supported), feature-first `init` examples, `mount` island contract, path-based `invalidateHtmlCache` (replacing the outdated “no targeted invalidation” claim), Redis / admin / data-cache callouts, and bilingual doc links under `docs/` and `docs/en/`.
266
-
267
- ## [0.4.1] - 2026-09-02
268
-
269
- ### Added
270
-
271
- - VS Code / Cursor extension skeleton under `extensions/vscode-jsk`: `.jsk` language id, TextMate highlighting (`{{ }}` / `{#if}` / `{#each}` / components), language config, and snippets. Install from that folder or launch **JSK: Extension** from the repo root. Bound attrs on HTML tags (`:src="… + '/path'"`) highlight nested single-quoted strings.
272
- - Compile-time known components are discovered from **named exports** in `views/components/**/*.js` (plus `.jsk` component files), not from the file basename — so `<SectionHead />` resolves when `sectionHead` lives in `ui.js` without a stub re-export. Docs cover the `.jsk` template-vs-component boundary and a `{ items, error }` loader / `LoadErrorState` pattern so upstream failures are not mistaken for empty data.
273
-
274
- ### Changed
275
-
276
- - Duplicate component named exports (or the same PascalCase tag in two files) now **fail** at build and at server startup instead of warning and letting the second definition win. Overwriting `components/index.js` barrel exports remains allowed.
277
- - Marketing compare/FAQ copy no longer claims targeted invalidation is missing; it points at `invalidateHtmlCache()` (and Redis pub/sub for multi-instance).
278
-
279
- ## [0.4.0] - 2026-09-02
280
-
281
- ### Added
282
-
283
- - Build-time `.jsk` templates: declarative HTML-like syntax compiled to ESM render modules under `.jskelet/templates/` (no request-time parse, `eval`, or `new Function`). Coexists with EJS; compiled `.jsk` wins when both exist. Syntax: `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}` / `{#include}`, PascalCase components (`:prop` bindings), built-ins `Link` / `Image` / `Icon` / `CsrfField` / `PreloadImage`.
284
- - Feature-first conventions: `paths.features` / `paths.shared`, multi-root views and components, `features/<name>/index.js` route registration after `routes/`. CLI: `jskelet generate feature|page|island`. `jskelet init` scaffolds a feature-first `.jsk` skeleton (`features/home/` with route, page, component and island; global `views/pages/not-found.jsk`).
285
- - Template compile step in `jskelet build`; icon scan and Tailwind docs cover `.jsk` / `features` / `shared`. Bench: `node scripts/bench-templates.mjs`.
286
-
287
- ### Changed
288
-
289
- - `examples/minimal` pages moved to `.jsk`; adds `features/demo` as a co-located route + view sample.
290
-
291
- ## [0.3.5] - 2026-09-02
292
-
293
- ### Changed
294
-
295
- - S3 logging configuration refactored; docs clarified.
296
-
297
- ## [0.3.4] - 2026-09-02
298
-
299
- ### Changed
300
-
301
- - S3 logging pipeline now prints connection details at boot.
302
-
303
- ## [0.3.3] - 2026-09-02
304
-
305
- ### Changed
306
-
307
- - Internal packaging / release bump.
308
-
309
- ## [0.3.2] - 2026-09-02
310
-
311
- ### Added
312
-
313
- - Top-level `logs` config for persistent sinks: daily NDJSON files (`logs.file`) and batched S3 PutObject (`logs.s3`) with embedded SigV4 — no `@aws-sdk` dependency. `kinds` selects `http` / `event` / `error`; `console` toggles runtime stdout lines. `JSKELET_LOG_BUCKET` (and `logs.s3.bucket`) may be a plain bucket or a `bucket/prefix/…` path. `JSKELET_S3_API_URL` sets the R2/MinIO endpoint (region defaults to `auto`). Missing credentials warn and disable the S3 sink without taking the site down. Env: `JSKELET_LOG_BUCKET`, `JSKELET_S3_ACCESS_KEY_ID`, `JSKELET_S3_SECRET_ACCESS_KEY`, `JSKELET_S3_SESSION_TOKEN`, `JSKELET_S3_REGION`, `JSKELET_S3_API_URL`.
314
-
315
- ## [0.3.1] - 2026-09-02
316
-
317
- ### Added
318
-
319
- - Admin panel pages under `/_jskelet/admin`: Overview, Cache, Routes, Views, Logs and System. Configurable `allowIps` (exact or CIDR), `blockBots` (default on — crawler UAs get 404 before login), and `logSize`. Live Logs use an in-process ring plus SSE (`/api/logs/stream`) with client-side filters for method, status, cache, kind, path/route and text. Routes and Views are read-only inventories; HTTP finish middleware records timings only while the panel is enabled.
320
- - `trailingSlash` in `jskelet.config.mjs` (default `false`). When `true`, canonical page URLs end with `/` and return 200; a request without the slash is sent to the slashed form with a 308 (not 301). File URLs and `/.well-known/**` are left alone. When `false`, no slash is enforced — unlike Next.js, the default does not strip trailing slashes.
321
- - A cache admin panel surface (now under `admin()` — see Breaking) that lists what the in-process tier holds (key, size, status, remaining TTL, dependency count, precompressed bodies for HTML; key and TTL for data), reports whether the Redis tier is connected or bypassed, and runs the operations you would otherwise hand-write an admin route for: targeted invalidation with an optional hard mode, dropping a single entry, clearing either cache, unlinking the shared keys and triggering a prewarm pass. Unlike the dev overlay it does not look at `NODE_ENV`, because "why is this page stale" is a production question — but nothing is mounted until it is explicitly enabled, so the path does not exist by default. Access is a 32-character password regenerated on every process start and printed once to the server log; there is no persistent secret to leak and a deploy revokes old access on its own. The password is never accepted in a query string, three failed attempts ban the IP for 24 hours, and every banned or unauthorised response is a `404` rather than a 401 that would confirm the panel exists. The panel is excluded from indexing, prewarming and navigation speculation.
322
-
323
- ### Changed
324
-
325
- - Admin panel System meters (CPU, memory, disk) show this process's share of the host — RSS and project disk footprint against machine totals, plus process CPU across all cores — instead of whole-machine fullness. The panel content width is wider (`1600px`) so Overview, Routes, Views and System use the screen better.
326
-
327
- ### Breaking
328
-
329
- - The cache admin panel moved to a top-level `admin()` config section at `/_jskelet/admin` (was `cache().panel` at `/_jskelet/cache`). Enable with `admin() { return { enabled: true } }` or `JSKELET_ADMIN=1`. `JSKELET_CACHE_PANEL` and `cache().panel` are removed. Auth is unchanged (per-process password in the server log, cookie session, 404 for strangers); the action CSRF header is now `X-JSkelet-Admin`.
330
-
331
- ## [0.2.5] - 2026-09-01
332
-
333
- ### Fixed
334
-
335
- - Cloudflare analytics in the cache panel no longer asks for an open-ended window. Queries used only `datetime_geq`, so Cloudflare closed the range at query time and a default 24h lookback became `1d` plus network delay — Free zones reject anything wider than one day. Both ends are now pinned from the same clock (`datetime_leq` included).
336
-
337
- ## [0.2.4] - 2026-08-31
338
-
339
- ### Added
340
-
341
- - A language picker in the cache panel header, Turkish and English. The first visit follows the browser's language, the choice is kept in `localStorage` and carries over to the login page, and switching costs no request. To keep this from leaking UI concerns into the server, an `/action` response now returns `{ ok, code, params }` instead of an English sentence and the panel builds the text — the framework's log and API stay in one language while the panel speaks two.
342
-
343
- ## [0.2.3] - 2026-08-31
344
-
345
- ### Added
346
-
347
- - `cache().query`, a pattern → allowlist mapping that decides which query parameters belong to the HTML cache key. An allowlist caches one entry per distinct value of the listed parameters and ignores the rest, so every `?utm_source=…` variant of a path shares one copy; `true` puts the whole query in the key and `[]` ignores it entirely. Parameters enter the key sorted, so `?a=1&b=2` and `?b=2&a=1` are one entry.
348
- - Cloudflare cache management, from the panel and from code. Set `JSKELET_CLOUDFLARE_KEY` and `JSKELET_CLOUDFLARE_ZONE_ID` (or `cache().cloudflare`) and the panel gains the CDN tier next to the origin one: purge everything, purge every URL currently held in memory with one button or a single row with `cf purge`, purge by prefix, host or cache tag, toggle development mode, cache level, browser cache TTL, query string sorting, Always Online, Tiered Cache, Regional Tiered Cache and Cache Reserve, clear Cache Reserve, and read the cache hit ratio. This matters because `invalidateHtmlCache()` refreshes the origin while the copy your visitors get keeps being served from the edge until its TTL expires. The same surface is exported as `purgeCloudflare()`, `toCloudflareUrls()`, `fetchCloudflareOverview()`, `fetchCacheAnalytics()`, `fetchPathEdges()` and `getCloudflareStatus()`; none of them throw, so a CDN outage returns `{ ok: false, error }` instead of breaking a publish flow. Long purge lists are batched at Cloudflare's 100-keys-per-request limit and sent sequentially to stay inside the rate limit. The token is read from the environment, is never returned in a response, and only cache related zone settings can be changed.
349
- - An edge breakdown for a single path: `fetchPathEdges()` reports which Cloudflare colos served it from cache and which went to the origin. This is observation, not inventory — Cloudflare has no endpoint that lists which edges currently hold a URL, and no way to warm an edge you pick, so the panel says as much rather than implying otherwise.
350
- - `getRedisDetails()` reports where the shared tier actually points — address, TLS, database, namespace, which kinds are shared and whether the purge channel is subscribed — because "connected" alone does not explain a Redis that shares nothing because of a wrong namespace. The password is never part of the output. `inspectRedis()` counts the keys per kind plus `DBSIZE` and `used_memory`; it runs a `SCAN`, so the panel keeps it behind its own button instead of the refresh loop. When Redis is off, the panel explains what a shared tier would buy and shows the memory and disk state of the host instead, which is the number that decides whether `maxEntries` is too high.
351
-
352
- ### Changed
353
-
354
- - The release history page in `examples/marketing` now shows one release at a time: the newest one is expanded and older releases collapse to a single header row with their date, status and change count. Every release used to be printed open in a two-column grid, which made the page an unreadable wall as soon as a few versions piled up. Version links and the quick-jump strip still work, and they open the collapsed release they point at.
355
-
356
- ### Breaking
357
-
358
- - A request that carries a query parameter is now dynamic by default: it is not written to the HTML cache and the response is sent with `private, no-store`, even when a `cache().html` pattern covers the path. Every query variant used to become its own cache entry, which let campaign parameters (`?utm_source=…`) mint unbounded keys and evict real pages from a 500-entry store. Pages whose output genuinely depends on the query keep their cache by listing the relevant parameters under `cache().query`.
359
-
360
- ## [0.2.2] - 2026-08-31
361
-
362
- ### Added
363
-
364
- - A cache admin panel at `/_jskelet/cache`, turned on with `cache().panel: { enabled: true }` or `JSKELET_CACHE_PANEL=1`. It lists what the in-process tier holds (key, size, status, remaining TTL, dependency count, precompressed bodies for HTML; key and TTL for data), reports whether the Redis tier is connected or bypassed, and runs the operations you would otherwise hand-write an admin route for: targeted invalidation with an optional hard mode, dropping a single entry, clearing either cache, unlinking the shared keys and triggering a prewarm pass. Unlike the dev overlay it does not look at `NODE_ENV`, because "why is this page stale" is a production question — but nothing is mounted until it is explicitly enabled, so the path does not exist by default. Access is a 32-character password regenerated on every process start and printed once to the server log; there is no persistent secret to leak and a deploy revokes old access on its own. The password is never accepted in a query string, three failed attempts ban the IP for 24 hours, and every banned or unauthorised response is a `404` rather than a 401 that would confirm the panel exists. The panel is excluded from indexing, prewarming and navigation speculation.
365
- - `dropHtmlCacheKey()` and `dropDataCacheKey()` drop one exact cache key. `invalidateHtmlCache()` matches a path pattern and takes down every query variant of a path, which is the right default for a webhook but wrong when you want `/list?page=2` gone and `/list?page=3` left hot.
366
- - An adaptive per-host rate limit for upstream calls, `cache().upstream`. It sits in the `fetch` wrapper rather than in the prewarm pass, because what spends the quota is the API call, not the page: one render may make one call or twenty, so `prewarm.rps` could never bound the real thing. A token bucket caps the average rate, a concurrency limit caps the calls in flight, and `rate` is treated as a ceiling that the limiter pulls down on its own — a 429 or 503 halves the rate, `Retry-After` stops the bucket for exactly as long as the upstream asked, and clean windows climb back one step at a time. A host that returns `breakerFailures` rate limits in a row is bypassed for `breakerCooldownMs`, which stops the worst waste: because a 429 counts as transient, the HTML produced by a throttled call is never stored, so a pass in that state spends quota and keeps nothing. Only 429 and 503 penalise the rate; a 400 or 500 is not a quota problem. Off by default — set `rate` to turn it on.
367
- - `getUpstreamLimiterStatus()` reports the current rate, calls in flight, 429 count and breaker state per host. The dev panel's Server tab shows the same.
368
- - `getDataCacheStats()` counts how the data cache was used: fresh hits, stale hits, misses, coalesced concurrent reads, values promoted from the shared tier and — the only number that reaches the quota — real producer runs. A prewarm pass now prints its own share of that (`12 upstream calls for 430 data reads (97% from the data cache)`), which is what tells you whether the fix is a longer TTL or a rate limit. The dev report has a Data cache card for it.
369
- - The dev overlay header now shows the installed JSkelet version next to the title, labelled `latest` when it matches npm and `outdated` with the newer version when it does not, so you can tell at a glance which version the project runs without opening the Server tab.
370
-
371
- ### Changed
372
-
373
- - The prewarm retry pass no longer retries permanent failures. A `400`, `403` or `404` does not get better on the second try, so those paths are dropped from the retry round and counted as `N not retried (permanent)` in the summary. The wait before the round now also honours the upstream rate limit: if a `Retry-After` or an open circuit breaker is holding calls back, the pass waits that out instead of retrying into the same 429.
374
-
375
- ## [0.2.1] - 2026-08-31
376
-
377
- ### Changed
378
-
379
- - Errors and warnings raised during a prewarm pass are no longer logged one per page. Request errors and the per-page render warnings (`was produced with missing data`, `returned notFound() while upstream is failing`, `could not be produced`) are counted while the pass runs and printed as a single summary block afterwards, grouped by message with the most frequent kinds first, so a failing upstream can no longer bury the "warmed N/M pages" line under hundreds of near-identical lines. Real traffic logs as before, and the dev tools panel still shows the per-path detail.
380
-
381
- ## [0.2.0] - 2026-08-31
382
-
383
- ### Added
384
-
385
- - An optional Redis tier behind both caches, turned on with `cache().redis: { enabled: true, url }` and `npm install ioredis`. The in-process cache stays primary and every request still reads it; Redis only does the two things a single process cannot. An instance that has never seen a path finds the HTML another replica already produced, so a fresh container or a post-deploy replacement does not re-render and re-fetch everything from scratch. And `invalidateHtmlCache()`, `clearHtmlCache()` and `clearDataCache()` now reach every replica over pub/sub instead of only the one that received the webhook — until now the others waited out the TTL and a visitor saw old or new content depending on where they landed. Keys live under `_jskelet:{namespace}:{buildId}:…`, where the build id makes HTML from a previous deploy expire on its own rather than pointing at asset files that no longer exist. Personalised (`storable: false`), degraded and non-200 responses are never shared. If `ioredis` is missing, Redis is unreachable or it goes down mid-flight, a warning is printed and the site keeps serving from memory.
386
- - `getRedisStatus()` reports whether the shared tier is connected, which key prefix and build id it is using, and how many command failures there have been — usable from a healthcheck endpoint. The same summary appears in the dev panel report.
387
- - Servers started with `startServer()` now shut down on `SIGTERM`/`SIGINT` instead of being killed: the listener is closed and the Redis connection is drained so in-flight writes are not cut mid-command.
388
-
389
- ### Changed
390
-
391
- - Request errors raised during a prewarm pass are no longer logged one by one. They are counted while the pass runs and printed as a single summary line afterwards, grouped by status and message with the most frequent kinds first, so a flaky upstream can no longer bury the "warmed N/M pages" line under hundreds of stack traces. Errors from real traffic are logged as before, and the dev tools panel still shows the per-path detail.
392
-
393
- ## [0.1.9] - 2026-08-31
394
-
395
- ### Fixed
396
-
397
- - The dev panel's WebSocket handshake was answered with a `Sec-WebSocket-Accept` value derived from a mistyped protocol constant. Browsers verify that value and closed the connection immediately with "Incorrect 'Sec-WebSocket-Accept' header value", so the panel silently fell back to polling.
398
-
399
- ## [0.1.8] - 2026-08-31
400
-
401
- ### Changed
402
-
403
- - The marketing example's changelog page is now a timeline: releases are laid out along a rail with a sticky version column, each change group gets its own card with a coloured rule and item count, and a row of version chips at the top jumps straight to a release.
404
- - The server now binds to `::` instead of `0.0.0.0` when no `HOST` is given, so a single dual-stack socket answers both IPv6 and IPv4. Browsers resolve `localhost` to `::1` first and, unlike ordinary requests, a WebSocket handshake does not fall back to IPv4 — which made the dev panel's live channel fail on an IPv4-only socket. Where IPv6 is unavailable the bind falls back to `0.0.0.0`.
405
-
406
- ## [0.1.7] - 2026-08-31
407
-
408
- ### Changed
409
-
410
- - Devtools WebSocket connection handling hardened.
411
-
412
- ## [0.1.6] - 2026-08-31
413
-
414
- ### Added
415
-
416
- - Targeted HTML invalidation: `invalidateHtmlCache(target, { hard })` takes a path, the config pattern syntax (`/news/:slug`), a regular expression or a list of them, and returns how many entries were affected. By default it **stales** the entries rather than deleting them, so a webhook that touches hundreds of pages does not turn into hundreds of cold renders at the worst possible moment: visitors keep getting the old HTML while the refresh runs in the background, once per key. Matching is done against the path, so every query variant of a page is covered by one call, and a render already in flight when the purge arrives is not stored.
417
- - `clearDataCache()` now refreshes the HTML too. The `withDataCache` keys read during a render are recorded, so dropping `news:abc` stales every page that actually read it — the article, the home page listing it and the tag page — without the application declaring any tags. Turn it off with `cache().trackDependencies: false`; `getHtmlCacheEntries()` reports the dependency count per page as `deps`.
418
- - Invalidated paths go to the front of the next prewarm pass, so an updated page is refreshed without waiting for a visitor, while still respecting the `rps` limit. The pass summary counts them separately.
419
-
420
- ### Changed
421
-
422
- - Prewarming no longer holds up the rest of the dev server. In development it now runs with a single worker and a default limit of 4 requests per second (`prewarm.rps` / `PREWARM_RPS` still override it), so page requests and the dev panel stay responsive while a warm-up round is going on. Production behaviour is unchanged.
423
-
424
- ## [0.1.5] - 2026-08-30
425
-
426
- ### Changed
427
-
428
- - The dev tools panel is now fed over a WebSocket (`<devBasePath>/ws`) instead of polling `/stats` every two seconds. The server pushes statistics as they change and sends live reload and CSS hot-swap events over the same connection, so an open tab no longer keeps hitting the server while the panel is closed. No new dependency is involved; if the socket cannot be opened, the panel falls back to the previous SSE plus polling path.
429
-
430
- ## [0.1.4] - 2026-08-30
431
-
432
- ### Added
433
-
434
- - Transient upstream failures are now detected without any application code: `globalThis.fetch` is wrapped during startup and `429`, `5xx` and network errors raised inside a render are reported on their own, so rate limits stop turning existing pages into 404s even when the data layer never calls `reportUpstreamFailure()`. Requests outside a render and requests to the server itself are ignored, deterministic answers such as `404` are not reported, and the wrapper can be turned off with `cache().trackUpstream: false`.
435
- - `cache().transientRetry` (`{ attempts: 1, delayMs: 300 }` by default) retries a page that called `notFound()` while upstream was failing. Each attempt runs in a fresh upstream and per-request cache scope, so a page whose data arrives on the second try is served and cached as usual instead of degrading to an error.
436
-
437
- ### Changed
438
-
439
- - The changelog page of the marketing example is generated from the project's `CHANGELOG.md` instead of a hand-written list, and shows the version published on npm next to the installed one.
440
- - The marketing example reads its markdown (documentation and changelog) from the repository over GitHub's raw endpoint, falling back to the installed package when the network is unavailable, so a deployment that ships without `node_modules` can still serve the docs. In development the local file wins and nothing is cached. The branch is overridable with `DOCS_REF`.
441
-
442
- ## [0.1.3] - 2026-08-30
443
-
444
- ### Changed
445
-
446
- - Patch release; no user-facing changelog entries beyond 0.1.2.
447
-
448
- ## [0.1.2] - 2026-08-30
449
-
450
- ### Added
451
-
452
- - `route(fn, { private: true })` for pages that depend on the visitor. The HTML
453
- cache is bypassed, `cache.html` patterns can no longer turn caching on for
454
- that route, and the response is sent with `private, no-store`, `Vary: Cookie`
455
- and no ETag.
456
- - A runtime guard against identity leaks: when a cacheable route reads
457
- `Cookie`, `Authorization` or a session field, the rendered HTML is never
458
- stored. In development the request fails with an explanation, in production it
459
- is served with `no-store` and logged.
460
- - `fragment()` for layout-less partial responses, with `no-store` and cache
461
- bypass built in.
462
- - CSRF protection. Cross-site state-changing requests are rejected based on
463
- `Origin` and `Sec-Fetch-Site`; requests carrying neither header still pass, so
464
- webhooks keep working. An optional double-submit token layer is enabled with
465
- `security.csrf.token` and rendered into forms by the new `csrfField()` helper.
466
- - Signed cookie helpers under `jskelet/cookies`: `parseCookies()`,
467
- `setCookie()`, `clearCookie()`, `setSignedCookie()`, `getSignedCookie()`,
468
- `randomToken()` and `safeEqual()`. Defaults are `HttpOnly`, `SameSite=Lax` and
469
- `Secure` outside development.
470
- - A `security` configuration section: `trustProxy`, `cookieSecret` and `csrf`.
471
- - `seeOther()` for the post/redirect/get flow, which needs 303 rather than the
472
- method-preserving 307 that `redirect()` sends.
473
- - Island cleanup. A `mount()` function may return a teardown callback; it is now
474
- stored and called by the new `unmount(root)` export when the subtree leaves
475
- the DOM.
476
- - Client helpers for partial updates: `swap()` and `startSwapLinks()` for
477
- fetching and replacing a region, `enhanceForm()` and `startForms()` for
478
- submitting forms without a full page load while keeping the no-JavaScript
479
- path working.
480
- - A fourth example, `examples/dashboard`: sign-in with a signed cookie session,
481
- a private page, a paginated table fragment, a CSRF-protected mutation and an
482
- island with cleanup, covered by its own `smoke.mjs`.
483
- - An npm version badge in the `README`, linking to the package page.
484
- - An English edition of the documentation under `docs/en/`, mirroring every
485
- chapter of the Turkish `docs/`.
486
- - The dev overlay now compares the installed version against the `latest` tag on
487
- npm: the Server tab shows the version, marks an `update` chip when a newer
488
- release exists and offers the upgrade command. The lookup is cached for six
489
- hours, never blocks the server and can be turned off with
490
- `JSKELET_VERSION_CHECK=0`.
491
-
492
- ### Changed
493
-
494
- - `trust proxy` is now configurable through `security.trustProxy` instead of
495
- being always on. The default is unchanged, but a server exposed directly to
496
- the internet should turn it off: while it is on, a client can forge its own
497
- `X-Forwarded-For` and rate limiting or audit logs see the wrong address.
498
- - Every message the framework prints is now English: config, router, render,
499
- cache, prewarm, asset and build warnings, CLI output, the project `jskelet
500
- init` scaffolds, and the devtools overlay and report interfaces. Visitor-facing
501
- status pages still follow `brand.lang` and keep their Turkish translations.
502
- - The dev overlay and report now show the current JSkelet logo, served with a
503
- cacheable response instead of being re-fetched on every navigation.
504
-
505
- ### Fixed
506
-
507
- - A page rendered without `revalidate` used to be sent with no `Cache-Control`
508
- at all, while still carrying a strong ETag. HTTP treats such a response as
509
- heuristically cacheable, so an intermediate proxy or the browser's back button
510
- could store a response meant for a single visitor. Dynamic pages now send
511
- `private, no-store` and no ETag.
512
- - A redirect thrown from a route that reads the session is no longer cacheable
513
- either; a stored "you need to sign in" redirect used to follow the visitor
514
- even after signing in.
515
-
516
- ## [0.1.1] - 2026-08-30
517
-
518
- ### Added
519
-
520
- - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
521
- a `LICENSE` file, issue and pull request templates, and a CI workflow.
522
- - An English-first `examples/marketing` with a Turkish translation, serving the
523
- package documentation under `/docs` and reading its version, dependencies and
524
- bundle sizes from the installed package.
525
-
526
- ### Changed
527
-
528
- - The install instructions point at the npm package instead of the git
529
- repository.
530
-
531
- ### Fixed
532
-
533
- - No more white flash between pages: the page background moved onto the root
534
- element, so it applies before the body paints. Reduced-motion preferences now
535
- switch off the decorative animations as well, not just page transitions.
536
-
537
- ## [0.1.0] - 2026-08-30
538
-
539
- Initial release.
540
-
541
- ### Added
542
-
543
- - Express 5 server with EJS rendering: `createApp()`, `startServer()`,
544
- `route()`, `renderPage()`, `renderView()`, `renderNotFound()`.
545
- - In-process HTML TTL cache with stale-while-revalidate, plus prewarm at boot.
546
- - Island runtime with visibility, eager and idle hydration strategies, a small
547
- cross-island store, and DOM helpers.
548
- - Configuration through `jskelet.config.mjs`: `brand`, `paths`, `navigation`,
549
- `icons`, `fonts`, `clientEnv`, `redirects()`, `rewrites()`, `headers()`,
550
- `cache()` and `hooks`.
551
- - Build pipeline: fonts, SVG sprite from used icons, Tailwind v4 CSS, esbuild
552
- bundles with code splitting, webp variants, hashed output and brotli/gzip
553
- precompression.
554
- - Dev server with watch build, CSS hot-swap, automatic restart and a devtools
555
- overlay (requests, errors, upstream calls, cache dump, Web Vitals).
556
- - CLI: `jskelet dev`, `jskelet build`, `jskelet start`, `jskelet init`.
557
- - Documentation under `docs/` and three examples: `minimal`, `blog`,
558
- `marketing`.
559
-
560
- [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.6.0...HEAD
561
- [0.6.0]: https://github.com/ayberkenis/jskelet/compare/v0.5.4...v0.6.0
562
- [0.5.4]: https://github.com/ayberkenis/jskelet/compare/v0.5.3...v0.5.4
563
- [0.5.3]: https://github.com/ayberkenis/jskelet/compare/v0.5.2...v0.5.3
564
- [0.5.2]: https://github.com/ayberkenis/jskelet/compare/v0.5.1...v0.5.2
565
- [0.5.1]: https://github.com/ayberkenis/jskelet/compare/v0.5.0...v0.5.1
566
- [0.5.0]: https://github.com/ayberkenis/jskelet/compare/v0.4.8...v0.5.0
567
- [0.4.8]: https://github.com/ayberkenis/jskelet/compare/v0.4.7...v0.4.8
568
- [0.4.7]: https://github.com/ayberkenis/jskelet/compare/v0.4.6...v0.4.7
569
- [0.4.6]: https://github.com/ayberkenis/jskelet/compare/v0.4.5...v0.4.6
570
- [0.4.5]: https://github.com/ayberkenis/jskelet/compare/v0.4.4...v0.4.5
571
- [0.4.4]: https://github.com/ayberkenis/jskelet/compare/v0.4.3...v0.4.4
572
- [0.4.3]: https://github.com/ayberkenis/jskelet/compare/v0.4.2...v0.4.3
573
- [0.4.2]: https://github.com/ayberkenis/jskelet/compare/v0.4.1...v0.4.2
574
- [0.4.1]: https://github.com/ayberkenis/jskelet/compare/v0.4.0...v0.4.1
575
- [0.4.0]: https://github.com/ayberkenis/jskelet/compare/v0.3.5...v0.4.0
576
- [0.3.5]: https://github.com/ayberkenis/jskelet/compare/v0.3.4...v0.3.5
577
- [0.3.4]: https://github.com/ayberkenis/jskelet/compare/v0.3.3...v0.3.4
578
- [0.3.3]: https://github.com/ayberkenis/jskelet/compare/v0.3.2...v0.3.3
579
- [0.3.2]: https://github.com/ayberkenis/jskelet/compare/v0.3.1...v0.3.2
580
- [0.3.1]: https://github.com/ayberkenis/jskelet/compare/v0.2.5...v0.3.1
581
- [0.2.5]: https://github.com/ayberkenis/jskelet/compare/v0.2.4...v0.2.5
582
- [0.2.4]: https://github.com/ayberkenis/jskelet/compare/v0.2.3...v0.2.4
583
- [0.2.3]: https://github.com/ayberkenis/jskelet/compare/v0.2.2...v0.2.3
584
- [0.2.2]: https://github.com/ayberkenis/jskelet/compare/v0.2.1...v0.2.2
585
- [0.2.1]: https://github.com/ayberkenis/jskelet/compare/v0.2.0...v0.2.1
586
- [0.2.0]: https://github.com/ayberkenis/jskelet/compare/v0.1.9...v0.2.0
587
- [0.1.9]: https://github.com/ayberkenis/jskelet/compare/v0.1.8...v0.1.9
588
- [0.1.8]: https://github.com/ayberkenis/jskelet/compare/v0.1.7...v0.1.8
589
- [0.1.7]: https://github.com/ayberkenis/jskelet/compare/v0.1.6...v0.1.7
590
- [0.1.6]: https://github.com/ayberkenis/jskelet/compare/v0.1.5...v0.1.6
591
- [0.1.5]: https://github.com/ayberkenis/jskelet/compare/v0.1.4...v0.1.5
592
- [0.1.4]: https://github.com/ayberkenis/jskelet/compare/v0.1.3...v0.1.4
593
- [0.1.3]: https://github.com/ayberkenis/jskelet/compare/v0.1.2...v0.1.3
594
- [0.1.2]: https://github.com/ayberkenis/jskelet/compare/v0.1.1...v0.1.2
595
- [0.1.1]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...v0.1.1
596
- [0.1.0]: https://github.com/ayberkenis/jskelet/releases/tag/v0.1.0
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ While the project is on `0.x`, minor releases may contain breaking changes; each
7
+ one is listed under a **Breaking** heading.
8
+
9
+ ## [Unreleased]
10
+
11
+ ### Changed
12
+
13
+ - Shared cache entries of 1 KB or more are stored as brotli (`JSK\x01`
14
+ prefix) instead of plain JSON. With Redis off, the same body is written
15
+ under `.jskelet/cache/<buildId>/` so a restart can skip the render. The
16
+ in-process cache is unchanged, smaller records stay JSON, and existing
17
+ plain JSON values are still read.
18
+ - Early HTML refresh no longer marks every due page in one second. A pass
19
+ soft-stales at most four entries (soonest expiry first), and the expiry
20
+ warmer refetches them at one request at a time, two per second, instead of
21
+ the classic prewarm rate.
22
+ - The data cache stops growing past 64 MB of stored JSON. A single value
23
+ larger than that is not stored; the caller still receives it. HTML cache
24
+ entries keep the raw body plus only the compressed encoding last requested.
25
+ - In production, precompressed asset `stat` results (hit or miss) stay in
26
+ memory for the process lifetime. The remote image disk cache drops the
27
+ oldest file once `.jskelet/image-cache/` passes 256 MB.
28
+
29
+ ### Added
30
+
31
+ - `robots.txt` responses gain a trailing JSkelet note that disallows framework
32
+ endpoints (`/_jskelet/`, `/__jskelet/`, `/_fragment/`, plus a custom admin,
33
+ image, handoff, or dev path when it sits outside those prefixes). Every
34
+ user-agent already named in the file is repeated in that block, so a
35
+ crawler-specific group still sees the rules.
36
+
37
+ ### Breaking
38
+
39
+ - File logs are no longer daily plain-text files. With `logs.file.enabled`,
40
+ lines are sealed as zstd chunks (`jskelet-<time>-<n>.ndjson.zst`) and kept
41
+ for at most 5 minutes; the oldest expired chunk is deleted. `logs.drainLog`
42
+ receives each sealed chunk (`{ body, encoding: "zstd", bytes, lines, at }`)
43
+ so the app can forward it. A throwing hook warns and leaves the site up.
44
+ With the file sink off, `drainLog` still runs and nothing is written to disk.
45
+ - Dev gate is opt-in
46
+ `DEV_TOKEN` in the environment no longer locks the site. Require the token
47
+ only with `devGate: true` or `DEV_GATE=1`. `DEV_GATE=0` turns the gate off
48
+ even when config enabled it.
49
+ - Cache ceilings
50
+ `cache().maxEntries` above 800, `cache().data.maxEntries` above 20,000, and
51
+ `prewarm.onVisit` above `perPage` 20, `rps` 2, or `concurrency` 2 are clamped
52
+ at load with a warning. `onVisit` `rps: 0` is no longer unlimited. The HTML
53
+ cache also stops growing past 256 MB of stored HTML plus compressed bodies;
54
+ a single page larger than that is not stored.
55
+
56
+ ### Fixed
57
+
58
+ - Visit warming skips pages that are already fresh when the cache key has a
59
+ vary prefix or a trailing `?`. With `vary.host`, warm requests stay on
60
+ loopback and send the public host as `x-forwarded-host`, so a second
61
+ `127.0.0.1` HTML entry is not created. The onVisit queue holds at most 64
62
+ paths.
63
+
64
+ ### Removed
65
+
66
+ - `examples/marketing` — the marketing site now lives as a standalone app
67
+ outside this repository (`jskelet-marketing`).
68
+
69
+ ## [0.6.0] - 2026-09-21
70
+
71
+ ### Added
72
+
73
+ - Port reclaim with `--murder`
74
+ `jskelet dev --murder` and `jskelet start --murder` kill whatever already
75
+ holds the listen port and bind in its place. Without the flag the CLI still
76
+ refuses to start, but now with a clear error (pid + hint) instead of a bare
77
+ `EADDRINUSE`.
78
+ - Layout built-ins for `.jsk`
79
+ `<Stylesheets />`, `<BodyScripts />`, and `<JsonLd />` emit the asset /
80
+ script / JSON-LD loops from layout without calling helpers in the expression
81
+ language.
82
+ - Framework default layout as `.jsk`
83
+ Ships as `src/templates/layout.jsk` with a checked-in `layout.render.js`
84
+ (`node scripts/compile-framework-layout.mjs`, `--check` for drift).
85
+ `jskelet/layout` points at the `.jsk` source; the legacy EJS copy remains at
86
+ `jskelet/layout/ejs`.
87
+ - JSK editor tooling
88
+ The VS Code / Cursor JSK extension (v0.2.0) reports diagnostics in Problems
89
+ on open/save and offers PascalCase component completions from
90
+ `views/components`.
91
+ - `<!DOCTYPE>` parsing in `.jsk`
92
+ Doctype and other `<!…>` declarations parse correctly instead of hanging the
93
+ template compiler.
94
+ - `jskelet migrate` for Next.js App Router
95
+ Codemod with `scan` inventory, `apply` (JSX pages → controller + `.jsk`,
96
+ presentational components → HTML string helpers, `"use client"` → island
97
+ stubs), and `config` draft from `next.config`. Ships with `@babel/parser` /
98
+ `@babel/types`.
99
+ - Published TypeScript declarations
100
+ `.d.ts` files under `types/` for `jskelet`, `jskelet/client`, `jskelet/html`,
101
+ `jskelet/tags`, `jskelet/cookies`, and `jskelet/log` (`npm run types`).
102
+ - TypeScript client entries and islands
103
+ The client build accepts `.ts` / `.mts` entries and islands via esbuild;
104
+ manifest keys stay `*.js`. Conflicting stems (`main.js` + `main.ts`) fail the
105
+ build. Icon usage scan includes `.ts` / `.mts`.
106
+ - Local `icons/` sprite source
107
+ When a flat `icons/` directory is present (`icons.dir`, default `"icons"`),
108
+ it is the exclusive SVG sprite source (`house.svg` / `house-bold.svg`).
109
+ Phosphor is used only when that directory is absent. The hashed sprite still
110
+ lands under `public/assets/` and is precompressed.
111
+ - Auth handoff hardening
112
+ `allowedCookieNames` allowlist, pending-ticket and per-IP mint limits, plus
113
+ RFC 6265 cookie-name validation on `serializeCookie` (`isValidCookieName`).
114
+
115
+ ### Fixed
116
+
117
+ - Clearer `jskelet dev` startup failures
118
+ The CLI prints the real server failure (for example port already in use /
119
+ `--murder` hint) instead of only `server exited (code 1)` when the child
120
+ dies during startup.
121
+ - Marketing trust-bar icons after `.jsk` migration
122
+ `icons.scan` now covers `routes/`, so names declared in controllers
123
+ (`Package`, `TextT`, `ShieldCheck`, …) land in the sprite again.
124
+ - Open Graph routes with a `.png` suffix
125
+ Paths like `/og/…/:slug.png` escape the dot for Express 5 / path-to-regexp,
126
+ so the handler matches instead of falling through to the HTML 404.
127
+ - Safer remote image optimizer redirects
128
+ Redirects are no longer followed blindly; each hop is re-checked against
129
+ `allowHosts`, blocked private addresses, and DNS resolution (open-redirect
130
+ SSRF).
131
+
132
+ ### Breaking
133
+
134
+ - `ejs` is an optional peer
135
+ Apps that only use `.jsk` need not install it; apps that still have `.ejs`
136
+ views or layouts must `npm i ejs`. Missing EJS when an `.ejs` file is
137
+ rendered throws a clear install/migrate hint.
138
+ - Default layout export is `.jsk`
139
+ `jskelet/layout` now resolves to `layout.jsk` (was `layout.ejs`). Use
140
+ `jskelet/layout/ejs` for the legacy file.
141
+ - Auth handoff requires cookie allowlist
142
+ `auth.crossSubdomainHandoff` mint needs a non-empty `allowedCookieNames`
143
+ list; `true` alone no longer accepts arbitrary cookie names.
144
+ - Production builds omit client sourcemaps
145
+ Development still emits them; production client bundles do not.
146
+ - Secret-like `clientEnv` keys fail the build
147
+ Names that look like secrets are rejected instead of being inlined into the
148
+ client bundle.
149
+
150
+ ### Changed
151
+
152
+ - Marketing changelog as release notes
153
+ `/changelog` hides Unreleased, lists every published release without
154
+ pagination, and shows each note as a titled card with a short headline plus
155
+ a longer explanation (Keep-a-Changelog sections: Added / Changed / Fixed /
156
+ …).
157
+ - Examples are `.jsk` only
158
+ `minimal`, `blog`, `dashboard`, and `marketing` use `.jsk` for layouts,
159
+ pages, and partials; EJS files were removed from those trees.
160
+ - Stricter, clearer template compiler
161
+ Unknown PascalCase components fail the build. Errors for forbidden function
162
+ calls, unknown includes (with location), and unclosed `{#if}` / `{#each}` at
163
+ EOF are clearer; icon scan recognizes `<Icon name="…" />`.
164
+ - Docs present `.jsk` as the default
165
+ Docs / AGENTS / README tell the build-time `.jsk` story first; EJS is
166
+ documented as an optional legacy peer.
167
+ - Standalone JSK VSIX packaging
168
+ The VS Code / Cursor extension (`extensions/vscode-jsk`) uses the new 3D
169
+ `.jsk` mark as marketplace and explorer icon, and packages as a standalone
170
+ VSIX (vendors `src/compile`) for Marketplace publish.
171
+ - Migrate peers are optional
172
+ `@babel/parser` and `@babel/types` are optional peers used only by migrate.
173
+ They are no longer installed with every `jskelet` install; missing peers
174
+ throw an install hint. Unused `@babel/traverse` was dropped.
175
+ - Auth handoff sits after CSRF
176
+ Mint mounts after CSRF so origin checks apply to
177
+ `POST /_jskelet/auth/handoff`.
178
+ - Stronger security docs
179
+ Docs (TR/EN) expand `trustProxy` / `csrf.token` guidance and include a
180
+ fuller security-headers example under `headers()`.
181
+ - Marketing copy defaults to `.jsk`
182
+ EN/TR marketing and how-it-works examples present `.jsk` as the default
183
+ template surface; the compare column for hand-written Express + EJS stays as
184
+ a competitor. Scaffold and migrate mappings name `.jsk` pages and layouts.
185
+ - New geometric brand mark
186
+ Framework and marketing logos live at `src/logo.png` (admin / devtools) and
187
+ `examples/marketing/public/logo.png`. Marketing serves local favicons via
188
+ metadata `extraTags`.
189
+ - Development 5xx diagnostics
190
+ In `NODE_ENV=development`, 5xx responses show the error message and stack
191
+ instead of the polished 500 page / `hooks.error()`. Production still returns
192
+ the minimal status page with no internals.
193
+ - Marketing fit copy for signed-in panels
194
+ EN/TR fit copy treats dashboards and per-visitor panels as a supported path
195
+ (`private: true`, fragments). The poor-fit column names SPA shells,
196
+ collaborative client trees, streaming/RSC, and built-in real-time transport.
197
+ - Marketing copy for 0.5.x surfaces
198
+ EN/TR copy reflects host `vary`, early refresh, classic vs `onVisit`
199
+ prewarm, local `icons/`, shared cookies, and `opengraph-image` → `ogHandler`
200
+ on the migrate table. Pages serve per-locale OG cards at
201
+ `/og/:locale/:page.png`.
202
+ - Marketing visual language
203
+ Dark cyan glass look with shared `glass-panel` surfaces, numbered lit feature
204
+ cards, a 3D stack illustration on the home hero, and rotating cyan border
205
+ beams on lit panels.
206
+
207
+ ## [0.5.4] - 2026-09-18
208
+
209
+ ### Added
210
+
211
+ - Shared cross-subdomain cookies: `brand.sharedCookieRoots` plus `writeSharedCookie` / `clearSharedCookie` on server (`jskelet/cookies`) and client (`jskelet/client`). `Secure` follows https / `x-forwarded-proto` (not `NODE_ENV`); the client read-back fails into handoff when the browser rejects `Domain`. Optional `auth.crossSubdomainHandoff` mounts `POST /_jskelet/auth/handoff` (one-time ticket → `?handoff=`) and documents a `window.name` bridge. Large tokens are refused — put a short session id in the cookie, not a JWT.
212
+
213
+ ## [0.5.3] - 2026-09-18
214
+
215
+ ### Added
216
+
217
+ - HTML cache key vary (`cache().vary`): `host: true` adds the public Host (`x-forwarded-host` or `Host`, lowercase, no port) as `h=…|` before the path; optional `headers` and `fn(req)` add further segments. Required on host-based locale sites so one locale's HTML is not served on another. Classic prewarm accepts `prewarm.origins` for multi-host warming when vary is on.
218
+
219
+ ### Changed
220
+
221
+ - Marketing example visual language: darker ink canvas, solid cyan primary CTAs, cyan-only glow/grid (indigo accents removed), and a measured trust bar on the homepage (payload gzip, Node, license, zero web fonts) instead of the marquee.
222
+
223
+ ## [0.5.2] - 2026-09-18
224
+
225
+ ### Added
226
+
227
+ - Dynamic Open Graph images (Next.js `ImageResponse` / `opengraph-image`): `ogImage`, `sendOgImage`, `ogHandler`, and `ImageResponse` turn card fields or raw SVG into PNG when `sharp` is installed (SVG fallback otherwise). Wired in `examples/blog` as `/og/blog/:slug.png` and `metadata.openGraph.image`.
228
+
229
+ ## [0.5.1] - 2026-09-07
230
+
231
+ ### Added
232
+
233
+ - Early HTML cache refresh before TTL expiry: the last successful produce time (`produceMs`) sets a lead window (`min(max(produceMs×2, 250ms), ttl/2)`). A still-fresh `HIT` in that window revalidates in the background; idle entries are soft-staled by a sweeper and drained over HTTP even without classic `prewarmPaths` (`PREWARM=0` disables both). In-flight refreshes no longer drop the entry when `staleUntil` elapses.
234
+
235
+ ## [0.5.0] - 2026-09-03
236
+
237
+ ### Added
238
+
239
+ - Route-level stylesheets: put files in `styles/pages/*.css` and load them from the controller with `styles: ["home.css"]` (same contract as island `entries`). The layout emits them after global `app.css`; dev hot-swaps any changed `.css` manifest key without a full reload.
240
+
241
+ ## [0.4.8] - 2026-09-03
242
+
243
+ ### Added
244
+
245
+ - Dev overlay Errors tab now lists failed SSR and browser `fetch` calls with page path, API URL, optional island name, and expandable response-body details (JSON instead of `[object Object]`). Server `console.error` / `console.warn` records also carry the current page when they fire during render.
246
+
247
+ ## [0.4.7] - 2026-09-03
248
+
249
+ ### Added
250
+
251
+ - Visit-driven HTML prewarm (`cache().prewarm.onVisit`): after each public cacheable page response, same-origin links in the HTML are warmed in the background (document order, `perPage` cap). Mutually exclusive with classic prewarm (`max` / `priority` / `rotate` / `hooks.prewarmPaths`, etc.) — mixing them fails at config load.
252
+
253
+ ## [0.4.6] - 2026-09-03
254
+
255
+ ### Fixed
256
+
257
+ - Remote image optimizer cache hits no longer 404. Disk cache lives under `.jskelet/image-cache/`; Express `sendFile` ignores dotfiles by default, so the file was written but the response still failed. `sendCached` now passes `dotfiles: "allow"`.
258
+
259
+ ## [0.4.5] - 2026-09-03
260
+
261
+ ### Added
262
+
263
+ - Runtime remote image optimizer: set `images.remote.allowHosts` to proxy allowlisted http(s) images through `/_jskelet/image?url=&w=&q=` as resized webp (disk cache under `.jskelet/image-cache/`). `image()` rewrites matching remote `src` values automatically; `remoteImageUrl()` builds URLs by hand. Requires `sharp` at runtime; without it the endpoint 302-redirects to the source.
264
+
265
+ ## [0.4.4] - 2026-09-02
266
+
267
+ ### Added
268
+
269
+ - Devtools SEO check: an **SEO** tab in the overlay lists document, heading, image, link and social-tag issues with error/warning severity. Optional page highlights draw red or yellow boxes around the offending elements; the label shows the short title and a click opens the full explanation. Served only in development as `/__jskelet/dev/seo.js` beside the overlay.
270
+
271
+ ### Changed
272
+
273
+ - Marketing compare live latency demo now measures two same-sized fragments (cached vs `no-store`) with an explicit 80 ms simulated upstream inside the shared producer — a hit skips that wait so the gap is visible even when RTT dominates the wall clock. The island prints transferred bytes, Server-Timing `produce` duration, and a View Source section contrasts `__NEXT_DATA__` payload tax with plain JSkelet HTML. The measured-weight block also shows an estimated Next.js App Router first-load breakdown beside this site’s real gzip totals (clearly labelled estimate, not a build from this repo).
274
+
275
+ ### Fixed
276
+
277
+ - Missing `/assets/*` responses no longer keep the long-lived `immutable` Cache-Control that `headersMiddleware` stamps for static prefixes. A deploy race (prune-before-write) could 404 a hashed CSS URL for a moment; a CDN then cached that HTML 404 for a year and browsers refused it as a stylesheet (`MIME type 'text/html'`). Catch-all and `notFound` handlers now set `Cache-Control: no-store`. CSS and sprite builds write the new file before pruning older hashes so the same content hash never has a gap.
278
+
279
+ ## [0.4.3] - 2026-09-02
280
+
281
+ ### Added
282
+
283
+ - Marketing homepage ops storyboard: Redis L2, `/_jskelet/admin` panel mock and Cloudflare purge flow, with tabbed visual scenes animated by the vanilla `motion` API (Framer Motion’s non-React package) via an `ops-story` island.
284
+
285
+ ## [0.4.2] - 2026-09-02
286
+
287
+ ### Changed
288
+
289
+ - README rewritten for the current surface: build-time `.jsk` as the default template story (EJS still supported), feature-first `init` examples, `mount` island contract, path-based `invalidateHtmlCache` (replacing the outdated “no targeted invalidation” claim), Redis / admin / data-cache callouts, and bilingual doc links under `docs/` and `docs/en/`.
290
+
291
+ ## [0.4.1] - 2026-09-02
292
+
293
+ ### Added
294
+
295
+ - VS Code / Cursor extension skeleton under `extensions/vscode-jsk`: `.jsk` language id, TextMate highlighting (`{{ }}` / `{#if}` / `{#each}` / components), language config, and snippets. Install from that folder or launch **JSK: Extension** from the repo root. Bound attrs on HTML tags (`:src="… + '/path'"`) highlight nested single-quoted strings.
296
+ - Compile-time known components are discovered from **named exports** in `views/components/**/*.js` (plus `.jsk` component files), not from the file basename — so `<SectionHead />` resolves when `sectionHead` lives in `ui.js` without a stub re-export. Docs cover the `.jsk` template-vs-component boundary and a `{ items, error }` loader / `LoadErrorState` pattern so upstream failures are not mistaken for empty data.
297
+
298
+ ### Changed
299
+
300
+ - Duplicate component named exports (or the same PascalCase tag in two files) now **fail** at build and at server startup instead of warning and letting the second definition win. Overwriting `components/index.js` barrel exports remains allowed.
301
+ - Marketing compare/FAQ copy no longer claims targeted invalidation is missing; it points at `invalidateHtmlCache()` (and Redis pub/sub for multi-instance).
302
+
303
+ ## [0.4.0] - 2026-09-02
304
+
305
+ ### Added
306
+
307
+ - Build-time `.jsk` templates: declarative HTML-like syntax compiled to ESM render modules under `.jskelet/templates/` (no request-time parse, `eval`, or `new Function`). Coexists with EJS; compiled `.jsk` wins when both exist. Syntax: `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}` / `{#include}`, PascalCase components (`:prop` bindings), built-ins `Link` / `Image` / `Icon` / `CsrfField` / `PreloadImage`.
308
+ - Feature-first conventions: `paths.features` / `paths.shared`, multi-root views and components, `features/<name>/index.js` route registration after `routes/`. CLI: `jskelet generate feature|page|island`. `jskelet init` scaffolds a feature-first `.jsk` skeleton (`features/home/` with route, page, component and island; global `views/pages/not-found.jsk`).
309
+ - Template compile step in `jskelet build`; icon scan and Tailwind docs cover `.jsk` / `features` / `shared`. Bench: `node scripts/bench-templates.mjs`.
310
+
311
+ ### Changed
312
+
313
+ - `examples/minimal` pages moved to `.jsk`; adds `features/demo` as a co-located route + view sample.
314
+
315
+ ## [0.3.5] - 2026-09-02
316
+
317
+ ### Changed
318
+
319
+ - S3 logging configuration refactored; docs clarified.
320
+
321
+ ## [0.3.4] - 2026-09-02
322
+
323
+ ### Changed
324
+
325
+ - S3 logging pipeline now prints connection details at boot.
326
+
327
+ ## [0.3.3] - 2026-09-02
328
+
329
+ ### Changed
330
+
331
+ - Internal packaging / release bump.
332
+
333
+ ## [0.3.2] - 2026-09-02
334
+
335
+ ### Added
336
+
337
+ - Top-level `logs` config for persistent sinks: daily NDJSON files (`logs.file`) and batched S3 PutObject (`logs.s3`) with embedded SigV4 — no `@aws-sdk` dependency. `kinds` selects `http` / `event` / `error`; `console` toggles runtime stdout lines. `JSKELET_LOG_BUCKET` (and `logs.s3.bucket`) may be a plain bucket or a `bucket/prefix/…` path. `JSKELET_S3_API_URL` sets the R2/MinIO endpoint (region defaults to `auto`). Missing credentials warn and disable the S3 sink without taking the site down. Env: `JSKELET_LOG_BUCKET`, `JSKELET_S3_ACCESS_KEY_ID`, `JSKELET_S3_SECRET_ACCESS_KEY`, `JSKELET_S3_SESSION_TOKEN`, `JSKELET_S3_REGION`, `JSKELET_S3_API_URL`.
338
+
339
+ ## [0.3.1] - 2026-09-02
340
+
341
+ ### Added
342
+
343
+ - Admin panel pages under `/_jskelet/admin`: Overview, Cache, Routes, Views, Logs and System. Configurable `allowIps` (exact or CIDR), `blockBots` (default on — crawler UAs get 404 before login), and `logSize`. Live Logs use an in-process ring plus SSE (`/api/logs/stream`) with client-side filters for method, status, cache, kind, path/route and text. Routes and Views are read-only inventories; HTTP finish middleware records timings only while the panel is enabled.
344
+ - `trailingSlash` in `jskelet.config.mjs` (default `false`). When `true`, canonical page URLs end with `/` and return 200; a request without the slash is sent to the slashed form with a 308 (not 301). File URLs and `/.well-known/**` are left alone. When `false`, no slash is enforced — unlike Next.js, the default does not strip trailing slashes.
345
+ - A cache admin panel surface (now under `admin()` — see Breaking) that lists what the in-process tier holds (key, size, status, remaining TTL, dependency count, precompressed bodies for HTML; key and TTL for data), reports whether the Redis tier is connected or bypassed, and runs the operations you would otherwise hand-write an admin route for: targeted invalidation with an optional hard mode, dropping a single entry, clearing either cache, unlinking the shared keys and triggering a prewarm pass. Unlike the dev overlay it does not look at `NODE_ENV`, because "why is this page stale" is a production question — but nothing is mounted until it is explicitly enabled, so the path does not exist by default. Access is a 32-character password regenerated on every process start and printed once to the server log; there is no persistent secret to leak and a deploy revokes old access on its own. The password is never accepted in a query string, three failed attempts ban the IP for 24 hours, and every banned or unauthorised response is a `404` rather than a 401 that would confirm the panel exists. The panel is excluded from indexing, prewarming and navigation speculation.
346
+
347
+ ### Changed
348
+
349
+ - Admin panel System meters (CPU, memory, disk) show this process's share of the host — RSS and project disk footprint against machine totals, plus process CPU across all cores — instead of whole-machine fullness. The panel content width is wider (`1600px`) so Overview, Routes, Views and System use the screen better.
350
+
351
+ ### Breaking
352
+
353
+ - The cache admin panel moved to a top-level `admin()` config section at `/_jskelet/admin` (was `cache().panel` at `/_jskelet/cache`). Enable with `admin() { return { enabled: true } }` or `JSKELET_ADMIN=1`. `JSKELET_CACHE_PANEL` and `cache().panel` are removed. Auth is unchanged (per-process password in the server log, cookie session, 404 for strangers); the action CSRF header is now `X-JSkelet-Admin`.
354
+
355
+ ## [0.2.5] - 2026-09-01
356
+
357
+ ### Fixed
358
+
359
+ - Cloudflare analytics in the cache panel no longer asks for an open-ended window. Queries used only `datetime_geq`, so Cloudflare closed the range at query time and a default 24h lookback became `1d` plus network delay — Free zones reject anything wider than one day. Both ends are now pinned from the same clock (`datetime_leq` included).
360
+
361
+ ## [0.2.4] - 2026-08-31
362
+
363
+ ### Added
364
+
365
+ - A language picker in the cache panel header, Turkish and English. The first visit follows the browser's language, the choice is kept in `localStorage` and carries over to the login page, and switching costs no request. To keep this from leaking UI concerns into the server, an `/action` response now returns `{ ok, code, params }` instead of an English sentence and the panel builds the text — the framework's log and API stay in one language while the panel speaks two.
366
+
367
+ ## [0.2.3] - 2026-08-31
368
+
369
+ ### Added
370
+
371
+ - `cache().query`, a pattern → allowlist mapping that decides which query parameters belong to the HTML cache key. An allowlist caches one entry per distinct value of the listed parameters and ignores the rest, so every `?utm_source=…` variant of a path shares one copy; `true` puts the whole query in the key and `[]` ignores it entirely. Parameters enter the key sorted, so `?a=1&b=2` and `?b=2&a=1` are one entry.
372
+ - Cloudflare cache management, from the panel and from code. Set `JSKELET_CLOUDFLARE_KEY` and `JSKELET_CLOUDFLARE_ZONE_ID` (or `cache().cloudflare`) and the panel gains the CDN tier next to the origin one: purge everything, purge every URL currently held in memory with one button or a single row with `cf purge`, purge by prefix, host or cache tag, toggle development mode, cache level, browser cache TTL, query string sorting, Always Online, Tiered Cache, Regional Tiered Cache and Cache Reserve, clear Cache Reserve, and read the cache hit ratio. This matters because `invalidateHtmlCache()` refreshes the origin while the copy your visitors get keeps being served from the edge until its TTL expires. The same surface is exported as `purgeCloudflare()`, `toCloudflareUrls()`, `fetchCloudflareOverview()`, `fetchCacheAnalytics()`, `fetchPathEdges()` and `getCloudflareStatus()`; none of them throw, so a CDN outage returns `{ ok: false, error }` instead of breaking a publish flow. Long purge lists are batched at Cloudflare's 100-keys-per-request limit and sent sequentially to stay inside the rate limit. The token is read from the environment, is never returned in a response, and only cache related zone settings can be changed.
373
+ - An edge breakdown for a single path: `fetchPathEdges()` reports which Cloudflare colos served it from cache and which went to the origin. This is observation, not inventory — Cloudflare has no endpoint that lists which edges currently hold a URL, and no way to warm an edge you pick, so the panel says as much rather than implying otherwise.
374
+ - `getRedisDetails()` reports where the shared tier actually points — address, TLS, database, namespace, which kinds are shared and whether the purge channel is subscribed — because "connected" alone does not explain a Redis that shares nothing because of a wrong namespace. The password is never part of the output. `inspectRedis()` counts the keys per kind plus `DBSIZE` and `used_memory`; it runs a `SCAN`, so the panel keeps it behind its own button instead of the refresh loop. When Redis is off, the panel explains what a shared tier would buy and shows the memory and disk state of the host instead, which is the number that decides whether `maxEntries` is too high.
375
+
376
+ ### Changed
377
+
378
+ - The release history page in `examples/marketing` now shows one release at a time: the newest one is expanded and older releases collapse to a single header row with their date, status and change count. Every release used to be printed open in a two-column grid, which made the page an unreadable wall as soon as a few versions piled up. Version links and the quick-jump strip still work, and they open the collapsed release they point at.
379
+
380
+ ### Breaking
381
+
382
+ - A request that carries a query parameter is now dynamic by default: it is not written to the HTML cache and the response is sent with `private, no-store`, even when a `cache().html` pattern covers the path. Every query variant used to become its own cache entry, which let campaign parameters (`?utm_source=…`) mint unbounded keys and evict real pages from a 500-entry store. Pages whose output genuinely depends on the query keep their cache by listing the relevant parameters under `cache().query`.
383
+
384
+ ## [0.2.2] - 2026-08-31
385
+
386
+ ### Added
387
+
388
+ - A cache admin panel at `/_jskelet/cache`, turned on with `cache().panel: { enabled: true }` or `JSKELET_CACHE_PANEL=1`. It lists what the in-process tier holds (key, size, status, remaining TTL, dependency count, precompressed bodies for HTML; key and TTL for data), reports whether the Redis tier is connected or bypassed, and runs the operations you would otherwise hand-write an admin route for: targeted invalidation with an optional hard mode, dropping a single entry, clearing either cache, unlinking the shared keys and triggering a prewarm pass. Unlike the dev overlay it does not look at `NODE_ENV`, because "why is this page stale" is a production question — but nothing is mounted until it is explicitly enabled, so the path does not exist by default. Access is a 32-character password regenerated on every process start and printed once to the server log; there is no persistent secret to leak and a deploy revokes old access on its own. The password is never accepted in a query string, three failed attempts ban the IP for 24 hours, and every banned or unauthorised response is a `404` rather than a 401 that would confirm the panel exists. The panel is excluded from indexing, prewarming and navigation speculation.
389
+ - `dropHtmlCacheKey()` and `dropDataCacheKey()` drop one exact cache key. `invalidateHtmlCache()` matches a path pattern and takes down every query variant of a path, which is the right default for a webhook but wrong when you want `/list?page=2` gone and `/list?page=3` left hot.
390
+ - An adaptive per-host rate limit for upstream calls, `cache().upstream`. It sits in the `fetch` wrapper rather than in the prewarm pass, because what spends the quota is the API call, not the page: one render may make one call or twenty, so `prewarm.rps` could never bound the real thing. A token bucket caps the average rate, a concurrency limit caps the calls in flight, and `rate` is treated as a ceiling that the limiter pulls down on its own — a 429 or 503 halves the rate, `Retry-After` stops the bucket for exactly as long as the upstream asked, and clean windows climb back one step at a time. A host that returns `breakerFailures` rate limits in a row is bypassed for `breakerCooldownMs`, which stops the worst waste: because a 429 counts as transient, the HTML produced by a throttled call is never stored, so a pass in that state spends quota and keeps nothing. Only 429 and 503 penalise the rate; a 400 or 500 is not a quota problem. Off by default — set `rate` to turn it on.
391
+ - `getUpstreamLimiterStatus()` reports the current rate, calls in flight, 429 count and breaker state per host. The dev panel's Server tab shows the same.
392
+ - `getDataCacheStats()` counts how the data cache was used: fresh hits, stale hits, misses, coalesced concurrent reads, values promoted from the shared tier and — the only number that reaches the quota — real producer runs. A prewarm pass now prints its own share of that (`12 upstream calls for 430 data reads (97% from the data cache)`), which is what tells you whether the fix is a longer TTL or a rate limit. The dev report has a Data cache card for it.
393
+ - The dev overlay header now shows the installed JSkelet version next to the title, labelled `latest` when it matches npm and `outdated` with the newer version when it does not, so you can tell at a glance which version the project runs without opening the Server tab.
394
+
395
+ ### Changed
396
+
397
+ - The prewarm retry pass no longer retries permanent failures. A `400`, `403` or `404` does not get better on the second try, so those paths are dropped from the retry round and counted as `N not retried (permanent)` in the summary. The wait before the round now also honours the upstream rate limit: if a `Retry-After` or an open circuit breaker is holding calls back, the pass waits that out instead of retrying into the same 429.
398
+
399
+ ## [0.2.1] - 2026-08-31
400
+
401
+ ### Changed
402
+
403
+ - Errors and warnings raised during a prewarm pass are no longer logged one per page. Request errors and the per-page render warnings (`was produced with missing data`, `returned notFound() while upstream is failing`, `could not be produced`) are counted while the pass runs and printed as a single summary block afterwards, grouped by message with the most frequent kinds first, so a failing upstream can no longer bury the "warmed N/M pages" line under hundreds of near-identical lines. Real traffic logs as before, and the dev tools panel still shows the per-path detail.
404
+
405
+ ## [0.2.0] - 2026-08-31
406
+
407
+ ### Added
408
+
409
+ - An optional Redis tier behind both caches, turned on with `cache().redis: { enabled: true, url }` and `npm install ioredis`. The in-process cache stays primary and every request still reads it; Redis only does the two things a single process cannot. An instance that has never seen a path finds the HTML another replica already produced, so a fresh container or a post-deploy replacement does not re-render and re-fetch everything from scratch. And `invalidateHtmlCache()`, `clearHtmlCache()` and `clearDataCache()` now reach every replica over pub/sub instead of only the one that received the webhook — until now the others waited out the TTL and a visitor saw old or new content depending on where they landed. Keys live under `_jskelet:{namespace}:{buildId}:…`, where the build id makes HTML from a previous deploy expire on its own rather than pointing at asset files that no longer exist. Personalised (`storable: false`), degraded and non-200 responses are never shared. If `ioredis` is missing, Redis is unreachable or it goes down mid-flight, a warning is printed and the site keeps serving from memory.
410
+ - `getRedisStatus()` reports whether the shared tier is connected, which key prefix and build id it is using, and how many command failures there have been — usable from a healthcheck endpoint. The same summary appears in the dev panel report.
411
+ - Servers started with `startServer()` now shut down on `SIGTERM`/`SIGINT` instead of being killed: the listener is closed and the Redis connection is drained so in-flight writes are not cut mid-command.
412
+
413
+ ### Changed
414
+
415
+ - Request errors raised during a prewarm pass are no longer logged one by one. They are counted while the pass runs and printed as a single summary line afterwards, grouped by status and message with the most frequent kinds first, so a flaky upstream can no longer bury the "warmed N/M pages" line under hundreds of stack traces. Errors from real traffic are logged as before, and the dev tools panel still shows the per-path detail.
416
+
417
+ ## [0.1.9] - 2026-08-31
418
+
419
+ ### Fixed
420
+
421
+ - The dev panel's WebSocket handshake was answered with a `Sec-WebSocket-Accept` value derived from a mistyped protocol constant. Browsers verify that value and closed the connection immediately with "Incorrect 'Sec-WebSocket-Accept' header value", so the panel silently fell back to polling.
422
+
423
+ ## [0.1.8] - 2026-08-31
424
+
425
+ ### Changed
426
+
427
+ - The marketing example's changelog page is now a timeline: releases are laid out along a rail with a sticky version column, each change group gets its own card with a coloured rule and item count, and a row of version chips at the top jumps straight to a release.
428
+ - The server now binds to `::` instead of `0.0.0.0` when no `HOST` is given, so a single dual-stack socket answers both IPv6 and IPv4. Browsers resolve `localhost` to `::1` first and, unlike ordinary requests, a WebSocket handshake does not fall back to IPv4 — which made the dev panel's live channel fail on an IPv4-only socket. Where IPv6 is unavailable the bind falls back to `0.0.0.0`.
429
+
430
+ ## [0.1.7] - 2026-08-31
431
+
432
+ ### Changed
433
+
434
+ - Devtools WebSocket connection handling hardened.
435
+
436
+ ## [0.1.6] - 2026-08-31
437
+
438
+ ### Added
439
+
440
+ - Targeted HTML invalidation: `invalidateHtmlCache(target, { hard })` takes a path, the config pattern syntax (`/news/:slug`), a regular expression or a list of them, and returns how many entries were affected. By default it **stales** the entries rather than deleting them, so a webhook that touches hundreds of pages does not turn into hundreds of cold renders at the worst possible moment: visitors keep getting the old HTML while the refresh runs in the background, once per key. Matching is done against the path, so every query variant of a page is covered by one call, and a render already in flight when the purge arrives is not stored.
441
+ - `clearDataCache()` now refreshes the HTML too. The `withDataCache` keys read during a render are recorded, so dropping `news:abc` stales every page that actually read it — the article, the home page listing it and the tag page — without the application declaring any tags. Turn it off with `cache().trackDependencies: false`; `getHtmlCacheEntries()` reports the dependency count per page as `deps`.
442
+ - Invalidated paths go to the front of the next prewarm pass, so an updated page is refreshed without waiting for a visitor, while still respecting the `rps` limit. The pass summary counts them separately.
443
+
444
+ ### Changed
445
+
446
+ - Prewarming no longer holds up the rest of the dev server. In development it now runs with a single worker and a default limit of 4 requests per second (`prewarm.rps` / `PREWARM_RPS` still override it), so page requests and the dev panel stay responsive while a warm-up round is going on. Production behaviour is unchanged.
447
+
448
+ ## [0.1.5] - 2026-08-30
449
+
450
+ ### Changed
451
+
452
+ - The dev tools panel is now fed over a WebSocket (`<devBasePath>/ws`) instead of polling `/stats` every two seconds. The server pushes statistics as they change and sends live reload and CSS hot-swap events over the same connection, so an open tab no longer keeps hitting the server while the panel is closed. No new dependency is involved; if the socket cannot be opened, the panel falls back to the previous SSE plus polling path.
453
+
454
+ ## [0.1.4] - 2026-08-30
455
+
456
+ ### Added
457
+
458
+ - Transient upstream failures are now detected without any application code: `globalThis.fetch` is wrapped during startup and `429`, `5xx` and network errors raised inside a render are reported on their own, so rate limits stop turning existing pages into 404s even when the data layer never calls `reportUpstreamFailure()`. Requests outside a render and requests to the server itself are ignored, deterministic answers such as `404` are not reported, and the wrapper can be turned off with `cache().trackUpstream: false`.
459
+ - `cache().transientRetry` (`{ attempts: 1, delayMs: 300 }` by default) retries a page that called `notFound()` while upstream was failing. Each attempt runs in a fresh upstream and per-request cache scope, so a page whose data arrives on the second try is served and cached as usual instead of degrading to an error.
460
+
461
+ ### Changed
462
+
463
+ - The changelog page of the marketing example is generated from the project's `CHANGELOG.md` instead of a hand-written list, and shows the version published on npm next to the installed one.
464
+ - The marketing example reads its markdown (documentation and changelog) from the repository over GitHub's raw endpoint, falling back to the installed package when the network is unavailable, so a deployment that ships without `node_modules` can still serve the docs. In development the local file wins and nothing is cached. The branch is overridable with `DOCS_REF`.
465
+
466
+ ## [0.1.3] - 2026-08-30
467
+
468
+ ### Changed
469
+
470
+ - Patch release; no user-facing changelog entries beyond 0.1.2.
471
+
472
+ ## [0.1.2] - 2026-08-30
473
+
474
+ ### Added
475
+
476
+ - `route(fn, { private: true })` for pages that depend on the visitor. The HTML
477
+ cache is bypassed, `cache.html` patterns can no longer turn caching on for
478
+ that route, and the response is sent with `private, no-store`, `Vary: Cookie`
479
+ and no ETag.
480
+ - A runtime guard against identity leaks: when a cacheable route reads
481
+ `Cookie`, `Authorization` or a session field, the rendered HTML is never
482
+ stored. In development the request fails with an explanation, in production it
483
+ is served with `no-store` and logged.
484
+ - `fragment()` for layout-less partial responses, with `no-store` and cache
485
+ bypass built in.
486
+ - CSRF protection. Cross-site state-changing requests are rejected based on
487
+ `Origin` and `Sec-Fetch-Site`; requests carrying neither header still pass, so
488
+ webhooks keep working. An optional double-submit token layer is enabled with
489
+ `security.csrf.token` and rendered into forms by the new `csrfField()` helper.
490
+ - Signed cookie helpers under `jskelet/cookies`: `parseCookies()`,
491
+ `setCookie()`, `clearCookie()`, `setSignedCookie()`, `getSignedCookie()`,
492
+ `randomToken()` and `safeEqual()`. Defaults are `HttpOnly`, `SameSite=Lax` and
493
+ `Secure` outside development.
494
+ - A `security` configuration section: `trustProxy`, `cookieSecret` and `csrf`.
495
+ - `seeOther()` for the post/redirect/get flow, which needs 303 rather than the
496
+ method-preserving 307 that `redirect()` sends.
497
+ - Island cleanup. A `mount()` function may return a teardown callback; it is now
498
+ stored and called by the new `unmount(root)` export when the subtree leaves
499
+ the DOM.
500
+ - Client helpers for partial updates: `swap()` and `startSwapLinks()` for
501
+ fetching and replacing a region, `enhanceForm()` and `startForms()` for
502
+ submitting forms without a full page load while keeping the no-JavaScript
503
+ path working.
504
+ - A fourth example, `examples/dashboard`: sign-in with a signed cookie session,
505
+ a private page, a paginated table fragment, a CSRF-protected mutation and an
506
+ island with cleanup, covered by its own `smoke.mjs`.
507
+ - An npm version badge in the `README`, linking to the package page.
508
+ - An English edition of the documentation under `docs/en/`, mirroring every
509
+ chapter of the Turkish `docs/`.
510
+ - The dev overlay now compares the installed version against the `latest` tag on
511
+ npm: the Server tab shows the version, marks an `update` chip when a newer
512
+ release exists and offers the upgrade command. The lookup is cached for six
513
+ hours, never blocks the server and can be turned off with
514
+ `JSKELET_VERSION_CHECK=0`.
515
+
516
+ ### Changed
517
+
518
+ - `trust proxy` is now configurable through `security.trustProxy` instead of
519
+ being always on. The default is unchanged, but a server exposed directly to
520
+ the internet should turn it off: while it is on, a client can forge its own
521
+ `X-Forwarded-For` and rate limiting or audit logs see the wrong address.
522
+ - Every message the framework prints is now English: config, router, render,
523
+ cache, prewarm, asset and build warnings, CLI output, the project `jskelet
524
+ init` scaffolds, and the devtools overlay and report interfaces. Visitor-facing
525
+ status pages still follow `brand.lang` and keep their Turkish translations.
526
+ - The dev overlay and report now show the current JSkelet logo, served with a
527
+ cacheable response instead of being re-fetched on every navigation.
528
+
529
+ ### Fixed
530
+
531
+ - A page rendered without `revalidate` used to be sent with no `Cache-Control`
532
+ at all, while still carrying a strong ETag. HTTP treats such a response as
533
+ heuristically cacheable, so an intermediate proxy or the browser's back button
534
+ could store a response meant for a single visitor. Dynamic pages now send
535
+ `private, no-store` and no ETag.
536
+ - A redirect thrown from a route that reads the session is no longer cacheable
537
+ either; a stored "you need to sign in" redirect used to follow the visitor
538
+ even after signing in.
539
+
540
+ ## [0.1.1] - 2026-08-30
541
+
542
+ ### Added
543
+
544
+ - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
545
+ a `LICENSE` file, issue and pull request templates, and a CI workflow.
546
+ - An English-first `examples/marketing` with a Turkish translation, serving the
547
+ package documentation under `/docs` and reading its version, dependencies and
548
+ bundle sizes from the installed package.
549
+
550
+ ### Changed
551
+
552
+ - The install instructions point at the npm package instead of the git
553
+ repository.
554
+
555
+ ### Fixed
556
+
557
+ - No more white flash between pages: the page background moved onto the root
558
+ element, so it applies before the body paints. Reduced-motion preferences now
559
+ switch off the decorative animations as well, not just page transitions.
560
+
561
+ ## [0.1.0] - 2026-08-30
562
+
563
+ Initial release.
564
+
565
+ ### Added
566
+
567
+ - Express 5 server with EJS rendering: `createApp()`, `startServer()`,
568
+ `route()`, `renderPage()`, `renderView()`, `renderNotFound()`.
569
+ - In-process HTML TTL cache with stale-while-revalidate, plus prewarm at boot.
570
+ - Island runtime with visibility, eager and idle hydration strategies, a small
571
+ cross-island store, and DOM helpers.
572
+ - Configuration through `jskelet.config.mjs`: `brand`, `paths`, `navigation`,
573
+ `icons`, `fonts`, `clientEnv`, `redirects()`, `rewrites()`, `headers()`,
574
+ `cache()` and `hooks`.
575
+ - Build pipeline: fonts, SVG sprite from used icons, Tailwind v4 CSS, esbuild
576
+ bundles with code splitting, webp variants, hashed output and brotli/gzip
577
+ precompression.
578
+ - Dev server with watch build, CSS hot-swap, automatic restart and a devtools
579
+ overlay (requests, errors, upstream calls, cache dump, Web Vitals).
580
+ - CLI: `jskelet dev`, `jskelet build`, `jskelet start`, `jskelet init`.
581
+ - Documentation under `docs/` and three examples: `minimal`, `blog`,
582
+ `marketing`.
583
+
584
+ [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.6.0...HEAD
585
+ [0.6.0]: https://github.com/ayberkenis/jskelet/compare/v0.5.4...v0.6.0
586
+ [0.5.4]: https://github.com/ayberkenis/jskelet/compare/v0.5.3...v0.5.4
587
+ [0.5.3]: https://github.com/ayberkenis/jskelet/compare/v0.5.2...v0.5.3
588
+ [0.5.2]: https://github.com/ayberkenis/jskelet/compare/v0.5.1...v0.5.2
589
+ [0.5.1]: https://github.com/ayberkenis/jskelet/compare/v0.5.0...v0.5.1
590
+ [0.5.0]: https://github.com/ayberkenis/jskelet/compare/v0.4.8...v0.5.0
591
+ [0.4.8]: https://github.com/ayberkenis/jskelet/compare/v0.4.7...v0.4.8
592
+ [0.4.7]: https://github.com/ayberkenis/jskelet/compare/v0.4.6...v0.4.7
593
+ [0.4.6]: https://github.com/ayberkenis/jskelet/compare/v0.4.5...v0.4.6
594
+ [0.4.5]: https://github.com/ayberkenis/jskelet/compare/v0.4.4...v0.4.5
595
+ [0.4.4]: https://github.com/ayberkenis/jskelet/compare/v0.4.3...v0.4.4
596
+ [0.4.3]: https://github.com/ayberkenis/jskelet/compare/v0.4.2...v0.4.3
597
+ [0.4.2]: https://github.com/ayberkenis/jskelet/compare/v0.4.1...v0.4.2
598
+ [0.4.1]: https://github.com/ayberkenis/jskelet/compare/v0.4.0...v0.4.1
599
+ [0.4.0]: https://github.com/ayberkenis/jskelet/compare/v0.3.5...v0.4.0
600
+ [0.3.5]: https://github.com/ayberkenis/jskelet/compare/v0.3.4...v0.3.5
601
+ [0.3.4]: https://github.com/ayberkenis/jskelet/compare/v0.3.3...v0.3.4
602
+ [0.3.3]: https://github.com/ayberkenis/jskelet/compare/v0.3.2...v0.3.3
603
+ [0.3.2]: https://github.com/ayberkenis/jskelet/compare/v0.3.1...v0.3.2
604
+ [0.3.1]: https://github.com/ayberkenis/jskelet/compare/v0.2.5...v0.3.1
605
+ [0.2.5]: https://github.com/ayberkenis/jskelet/compare/v0.2.4...v0.2.5
606
+ [0.2.4]: https://github.com/ayberkenis/jskelet/compare/v0.2.3...v0.2.4
607
+ [0.2.3]: https://github.com/ayberkenis/jskelet/compare/v0.2.2...v0.2.3
608
+ [0.2.2]: https://github.com/ayberkenis/jskelet/compare/v0.2.1...v0.2.2
609
+ [0.2.1]: https://github.com/ayberkenis/jskelet/compare/v0.2.0...v0.2.1
610
+ [0.2.0]: https://github.com/ayberkenis/jskelet/compare/v0.1.9...v0.2.0
611
+ [0.1.9]: https://github.com/ayberkenis/jskelet/compare/v0.1.8...v0.1.9
612
+ [0.1.8]: https://github.com/ayberkenis/jskelet/compare/v0.1.7...v0.1.8
613
+ [0.1.7]: https://github.com/ayberkenis/jskelet/compare/v0.1.6...v0.1.7
614
+ [0.1.6]: https://github.com/ayberkenis/jskelet/compare/v0.1.5...v0.1.6
615
+ [0.1.5]: https://github.com/ayberkenis/jskelet/compare/v0.1.4...v0.1.5
616
+ [0.1.4]: https://github.com/ayberkenis/jskelet/compare/v0.1.3...v0.1.4
617
+ [0.1.3]: https://github.com/ayberkenis/jskelet/compare/v0.1.2...v0.1.3
618
+ [0.1.2]: https://github.com/ayberkenis/jskelet/compare/v0.1.1...v0.1.2
619
+ [0.1.1]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...v0.1.1
620
+ [0.1.0]: https://github.com/ayberkenis/jskelet/releases/tag/v0.1.0