jskelet 0.2.5 → 0.3.1

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 (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +408 -383
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +293 -287
  7. package/docs/03-routing.md +486 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +764 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +809 -756
  44. package/src/client/admin/panel.html +495 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1251 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +241 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
package/CHANGELOG.md CHANGED
@@ -1,383 +1,408 @@
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
- ### Fixed
12
-
13
- - Cloudflare analytics in the cache panel no longer asks for an open-ended
14
- window. Queries used only `datetime_geq`, so Cloudflare closed the range at
15
- query time and a default 24h lookback became `1d` plus network delay — Free
16
- zones reject anything wider than one day. Both ends are now pinned from the
17
- same clock (`datetime_leq` included).
18
-
19
- ### Added
20
-
21
- - `cache().query`, a pattern → allowlist mapping that decides which query
22
- parameters belong to the HTML cache key. An allowlist caches one entry per
23
- distinct value of the listed parameters and ignores the rest, so every
24
- `?utm_source=…` variant of a path shares one copy; `true` puts the whole query
25
- in the key and `[]` ignores it entirely. Parameters enter the key sorted, so
26
- `?a=1&b=2` and `?b=2&a=1` are one entry.
27
- - A cache admin panel at `/_jskelet/cache`, turned on with
28
- `cache().panel: { enabled: true }` or `JSKELET_CACHE_PANEL=1`. It lists what
29
- the in-process tier holds (key, size, status, remaining TTL, dependency count,
30
- precompressed bodies for HTML; key and TTL for data), reports whether the
31
- Redis tier is connected or bypassed, and runs the operations you would
32
- otherwise hand-write an admin route for: targeted invalidation with an
33
- optional hard mode, dropping a single entry, clearing either cache, unlinking
34
- the shared keys and triggering a prewarm pass. Unlike the dev overlay it does
35
- not look at `NODE_ENV`, because "why is this page stale" is a production
36
- question — but nothing is mounted until it is explicitly enabled, so the path
37
- does not exist by default. Access is a 32-character password regenerated on
38
- every process start and printed once to the server log; there is no persistent
39
- secret to leak and a deploy revokes old access on its own. The password is
40
- never accepted in a query string, three failed attempts ban the IP for 24
41
- hours, and every banned or unauthorised response is a `404` rather than a 401
42
- that would confirm the panel exists. The panel is excluded from indexing,
43
- prewarming and navigation speculation.
44
- - A language picker in the cache panel header, Turkish and English. The first
45
- visit follows the browser's language, the choice is kept in `localStorage` and
46
- carries over to the login page, and switching costs no request. To keep this
47
- from leaking UI concerns into the server, an `/action` response now returns
48
- `{ ok, code, params }` instead of an English sentence and the panel builds the
49
- text — the framework's log and API stay in one language while the panel
50
- speaks two.
51
- - Cloudflare cache management, from the panel and from code. Set
52
- `JSKELET_CLOUDFLARE_KEY` and `JSKELET_CLOUDFLARE_ZONE_ID` (or
53
- `cache().cloudflare`) and the panel gains the CDN tier next to the origin one:
54
- purge everything, purge every URL currently held in memory with one button or
55
- a single row with `cf purge`, purge by prefix, host or cache tag, toggle
56
- development mode, cache level, browser cache TTL, query string sorting,
57
- Always Online, Tiered Cache, Regional Tiered Cache and Cache Reserve, clear
58
- Cache Reserve, and read the cache hit ratio. This matters because
59
- `invalidateHtmlCache()` refreshes the origin while the copy your visitors get
60
- keeps being served from the edge until its TTL expires. The same surface is
61
- exported as `purgeCloudflare()`, `toCloudflareUrls()`,
62
- `fetchCloudflareOverview()`, `fetchCacheAnalytics()`, `fetchPathEdges()` and
63
- `getCloudflareStatus()`; none of them throw, so a CDN outage returns
64
- `{ ok: false, error }` instead of breaking a publish flow. Long purge lists
65
- are batched at Cloudflare's 100-keys-per-request limit and sent sequentially
66
- to stay inside the rate limit. The token is read from the environment, is
67
- never returned in a response, and only cache related zone settings can be
68
- changed.
69
- - An edge breakdown for a single path: `fetchPathEdges()` reports which
70
- Cloudflare colos served it from cache and which went to the origin. This is
71
- observation, not inventory — Cloudflare has no endpoint that lists which
72
- edges currently hold a URL, and no way to warm an edge you pick, so the panel
73
- says as much rather than implying otherwise.
74
- - `getRedisDetails()` reports where the shared tier actually points — address,
75
- TLS, database, namespace, which kinds are shared and whether the purge channel
76
- is subscribed — because "connected" alone does not explain a Redis that shares
77
- nothing because of a wrong namespace. The password is never part of the
78
- output. `inspectRedis()` counts the keys per kind plus `DBSIZE` and
79
- `used_memory`; it runs a `SCAN`, so the panel keeps it behind its own button
80
- instead of the refresh loop. When Redis is off, the panel explains what a
81
- shared tier would buy and shows the memory and disk state of the host instead,
82
- which is the number that decides whether `maxEntries` is too high.
83
- - `dropHtmlCacheKey()` and `dropDataCacheKey()` drop one exact cache key.
84
- `invalidateHtmlCache()` matches a path pattern and takes down every query
85
- variant of a path, which is the right default for a webhook but wrong when you
86
- want `/list?page=2` gone and `/list?page=3` left hot.
87
-
88
- - An adaptive per-host rate limit for upstream calls, `cache().upstream`. It sits
89
- in the `fetch` wrapper rather than in the prewarm pass, because what spends the
90
- quota is the API call, not the page: one render may make one call or twenty, so
91
- `prewarm.rps` could never bound the real thing. A token bucket caps the average
92
- rate, a concurrency limit caps the calls in flight, and `rate` is treated as a
93
- ceiling that the limiter pulls down on its own — a 429 or 503 halves the rate,
94
- `Retry-After` stops the bucket for exactly as long as the upstream asked, and
95
- clean windows climb back one step at a time. A host that returns
96
- `breakerFailures` rate limits in a row is bypassed for `breakerCooldownMs`,
97
- which stops the worst waste: because a 429 counts as transient, the HTML
98
- produced by a throttled call is never stored, so a pass in that state spends
99
- quota and keeps nothing. Only 429 and 503 penalise the rate; a 400 or 500 is
100
- not a quota problem. Off by default — set `rate` to turn it on.
101
- - `getUpstreamLimiterStatus()` reports the current rate, calls in flight, 429
102
- count and breaker state per host. The dev panel's Server tab shows the same.
103
- - `getDataCacheStats()` counts how the data cache was used: fresh hits, stale
104
- hits, misses, coalesced concurrent reads, values promoted from the shared tier
105
- and — the only number that reaches the quota — real producer runs. A prewarm
106
- pass now prints its own share of that (`12 upstream calls for 430 data reads
107
- (97% from the data cache)`), which is what tells you whether the fix is a
108
- longer TTL or a rate limit. The dev report has a Data cache card for it.
109
-
110
- - The dev overlay header now shows the installed JSkelet version next to the
111
- title, labelled `latest` when it matches npm and `outdated` with the newer
112
- version when it does not, so you can tell at a glance which version the
113
- project runs without opening the Server tab.
114
- - An optional Redis tier behind both caches, turned on with
115
- `cache().redis: { enabled: true, url }` and `npm install ioredis`. The
116
- in-process cache stays primary and every request still reads it; Redis only
117
- does the two things a single process cannot. An instance that has never seen a
118
- path finds the HTML another replica already produced, so a fresh container or a
119
- post-deploy replacement does not re-render and re-fetch everything from
120
- scratch. And `invalidateHtmlCache()`, `clearHtmlCache()` and
121
- `clearDataCache()` now reach every replica over pub/sub instead of only the one
122
- that received the webhook — until now the others waited out the TTL and a
123
- visitor saw old or new content depending on where they landed. Keys live under
124
- `_jskelet:{namespace}:{buildId}:…`, where the build id makes HTML from a
125
- previous deploy expire on its own rather than pointing at asset files that no
126
- longer exist. Personalised (`storable: false`), degraded and non-200 responses
127
- are never shared. If `ioredis` is missing, Redis is unreachable or it goes down
128
- mid-flight, a warning is printed and the site keeps serving from memory.
129
- - `getRedisStatus()` reports whether the shared tier is connected, which key
130
- prefix and build id it is using, and how many command failures there have
131
- been — usable from a healthcheck endpoint. The same summary appears in the dev
132
- panel report.
133
- - Servers started with `startServer()` now shut down on `SIGTERM`/`SIGINT`
134
- instead of being killed: the listener is closed and the Redis connection is
135
- drained so in-flight writes are not cut mid-command.
136
- - Targeted HTML invalidation: `invalidateHtmlCache(target, { hard })` takes a
137
- path, the config pattern syntax (`/news/:slug`), a regular expression or a list
138
- of them, and returns how many entries were affected. By default it **stales**
139
- the entries rather than deleting them, so a webhook that touches hundreds of
140
- pages does not turn into hundreds of cold renders at the worst possible moment:
141
- visitors keep getting the old HTML while the refresh runs in the background,
142
- once per key. Matching is done against the path, so every query variant of a
143
- page is covered by one call, and a render already in flight when the purge
144
- arrives is not stored.
145
- - `clearDataCache()` now refreshes the HTML too. The `withDataCache` keys read
146
- during a render are recorded, so dropping `news:abc` stales every page that
147
- actually read it — the article, the home page listing it and the tag page —
148
- without the application declaring any tags. Turn it off with
149
- `cache().trackDependencies: false`; `getHtmlCacheEntries()` reports the
150
- dependency count per page as `deps`.
151
- - Invalidated paths go to the front of the next prewarm pass, so an updated page
152
- is refreshed without waiting for a visitor, while still respecting the `rps`
153
- limit. The pass summary counts them separately.
154
- - An upstream data cache: `withDataCache(key, ttlSeconds, producer)` and the
155
- `dataCache(fn, { key, revalidate })` wrapper, with `clearDataCache(prefix?)`,
156
- `getDataCacheSize()` and `getDataCacheEntries()` alongside them. It keeps JSON
157
- rather than HTML, so its default limit is 10,000 entries: a long-tail page that
158
- was never prewarmed still renders without touching the API. Concurrent reads of
159
- the same key collapse into one upstream request, an expired entry is served
160
- immediately while it refreshes in the background, a failing producer falls back
161
- to the stale value, and empty answers (`null`/`undefined`) are not stored
162
- unless `storeEmpty: true` is passed.
163
- - `cache().prewarm.priority` decides the warm-up order and accepts both the
164
- config pattern syntax (`/news/:slug`) and plain regular expressions. Matching
165
- paths are warmed on every pass.
166
- - Drip prewarming for large sites: `cache().prewarm.rps` (also `PREWARM_RPS`)
167
- caps requests per second regardless of parallelism, and `rotate` (on by
168
- default) makes periodic passes continue through the queue where the previous
169
- one stopped instead of re-warming the same first slice. A pass is skipped while
170
- the previous one is still running.
171
- - `cache().prewarm.retryDelayMs` (also `PREWARM_RETRY_DELAY_MS`) waits before the
172
- retry pass, since rate limit windows are measured in seconds.
173
- - `cache().maxEntries` configures the HTML cache limit, which used to be a fixed 500.
174
- - Transient upstream failures are now detected without any application code:
175
- `globalThis.fetch` is wrapped during startup and `429`, `5xx` and network
176
- errors raised inside a render are reported on their own, so rate limits stop
177
- turning existing pages into 404s even when the data layer never calls
178
- `reportUpstreamFailure()`. Requests outside a render and requests to the
179
- server itself are ignored, deterministic answers such as `404` are not
180
- reported, and the wrapper can be turned off with `cache().trackUpstream:
181
- false`.
182
- - `cache().transientRetry` (`{ attempts: 1, delayMs: 300 }` by default) retries a
183
- page that called `notFound()` while upstream was failing. Each attempt runs in
184
- a fresh upstream and per-request cache scope, so a page whose data arrives on
185
- the second try is served and cached as usual instead of degrading to an error.
186
- - The dev report now includes the data cache entry count under `cache.data`.
187
-
188
- ### Changed
189
-
190
- - The release history page in `examples/marketing` now shows one release at a
191
- time: the newest one is expanded and older releases collapse to a single
192
- header row with their date, status and change count. Every release used to be
193
- printed open in a two-column grid, which made the page an unreadable wall as
194
- soon as a few versions piled up. Version links and the quick-jump strip still
195
- work, and they open the collapsed release they point at.
196
- - The prewarm retry pass no longer retries permanent failures. A `400`, `403` or
197
- `404` does not get better on the second try, so those paths are dropped from
198
- the retry round and counted as `N not retried (permanent)` in the summary. The
199
- wait before the round now also honours the upstream rate limit: if a
200
- `Retry-After` or an open circuit breaker is holding calls back, the pass waits
201
- that out instead of retrying into the same 429.
202
- - Errors and warnings raised during a prewarm pass are no longer logged one per
203
- page. Request errors and the per-page render warnings (`was produced with
204
- missing data`, `returned notFound() while upstream is failing`, `could not be
205
- produced`) are counted while the pass runs and printed as a single summary
206
- block afterwards, grouped by message with the most frequent kinds first, so a
207
- failing upstream can no longer bury the "warmed N/M pages" line under hundreds
208
- of near-identical lines. Real traffic logs as before, and the dev tools panel
209
- still shows the per-path detail.
210
- - The marketing example's changelog page is now a timeline: releases are laid out
211
- along a rail with a sticky version column, each change group gets its own card
212
- with a coloured rule and item count, and a row of version chips at the top
213
- jumps straight to a release.
214
- - The dev tools panel is now fed over a WebSocket (`<devBasePath>/ws`) instead of
215
- polling `/stats` every two seconds. The server pushes statistics as they change
216
- and sends live reload and CSS hot-swap events over the same connection, so an
217
- open tab no longer keeps hitting the server while the panel is closed. No new
218
- dependency is involved; if the socket cannot be opened, the panel falls back to
219
- the previous SSE plus polling path.
220
- - The server now binds to `::` instead of `0.0.0.0` when no `HOST` is given, so a
221
- single dual-stack socket answers both IPv6 and IPv4. Browsers resolve
222
- `localhost` to `::1` first and, unlike ordinary requests, a WebSocket handshake
223
- does not fall back to IPv4 — which made the dev panel's live channel fail on an
224
- IPv4-only socket. Where IPv6 is unavailable the bind falls back to `0.0.0.0`.
225
- - Prewarming no longer holds up the rest of the dev server. In development it now
226
- runs with a single worker and a default limit of 4 requests per second
227
- (`prewarm.rps` / `PREWARM_RPS` still override it), so page requests and the dev
228
- panel stay responsive while a warm-up round is going on. Production behaviour
229
- is unchanged.
230
- - `notFound()` is no longer served as a 404 when a transient upstream failure
231
- (`429`, `5xx`, network error) happened during the same render. The page is
232
- retried first and, if upstream is still failing, responds with an uncached
233
- `503` and `Retry-After`. A temporary rate limit is no longer frozen into "this
234
- page does not exist" for the whole TTL. A retry that gets a clean answer saying
235
- the page is gone still returns a normal 404.
236
- - Responses produced with missing data are no longer offered to shared caches:
237
- a `degraded` render is sent with `private, no-store` instead of
238
- `public, s-maxage=…`. The `X-JSkelet-Cache` diagnostic header is still written.
239
- - The prewarm summary distinguishes paths left for the next pass
240
- (`700 deferred to the next pass`) from paths dropped entirely
241
- (`700 over the limit`).
242
- - The changelog page of the marketing example is generated from the project's
243
- `CHANGELOG.md` instead of a hand-written list, and shows the version published
244
- on npm next to the installed one.
245
- - The marketing example reads its markdown (documentation and changelog) from
246
- the repository over GitHub's raw endpoint, falling back to the installed
247
- package when the network is unavailable, so a deployment that ships without
248
- `node_modules` can still serve the docs. In development the local file wins
249
- and nothing is cached. The branch is overridable with `DOCS_REF`.
250
-
251
- ### Fixed
252
-
253
- - The dev panel's WebSocket handshake was answered with a `Sec-WebSocket-Accept`
254
- value derived from a mistyped protocol constant. Browsers verify that value and
255
- closed the connection immediately with "Incorrect 'Sec-WebSocket-Accept' header
256
- value", so the panel silently fell back to polling.
257
-
258
- ### Breaking
259
-
260
- - A request that carries a query parameter is now dynamic by default: it is not
261
- written to the HTML cache and the response is sent with `private, no-store`,
262
- even when a `cache().html` pattern covers the path. Every query variant used
263
- to become its own cache entry, which let campaign parameters
264
- (`?utm_source=…`) mint unbounded keys and evict real pages from a 500-entry
265
- store. Pages whose output genuinely depends on the query keep their cache by
266
- listing the relevant parameters under `cache().query`.
267
-
268
- ## [0.1.2] - 2026-08-30
269
-
270
- ### Added
271
-
272
- - `route(fn, { private: true })` for pages that depend on the visitor. The HTML
273
- cache is bypassed, `cache.html` patterns can no longer turn caching on for
274
- that route, and the response is sent with `private, no-store`, `Vary: Cookie`
275
- and no ETag.
276
- - A runtime guard against identity leaks: when a cacheable route reads
277
- `Cookie`, `Authorization` or a session field, the rendered HTML is never
278
- stored. In development the request fails with an explanation, in production it
279
- is served with `no-store` and logged.
280
- - `fragment()` for layout-less partial responses, with `no-store` and cache
281
- bypass built in.
282
- - CSRF protection. Cross-site state-changing requests are rejected based on
283
- `Origin` and `Sec-Fetch-Site`; requests carrying neither header still pass, so
284
- webhooks keep working. An optional double-submit token layer is enabled with
285
- `security.csrf.token` and rendered into forms by the new `csrfField()` helper.
286
- - Signed cookie helpers under `jskelet/cookies`: `parseCookies()`,
287
- `setCookie()`, `clearCookie()`, `setSignedCookie()`, `getSignedCookie()`,
288
- `randomToken()` and `safeEqual()`. Defaults are `HttpOnly`, `SameSite=Lax` and
289
- `Secure` outside development.
290
- - A `security` configuration section: `trustProxy`, `cookieSecret` and `csrf`.
291
- - `seeOther()` for the post/redirect/get flow, which needs 303 rather than the
292
- method-preserving 307 that `redirect()` sends.
293
- - Island cleanup. A `mount()` function may return a teardown callback; it is now
294
- stored and called by the new `unmount(root)` export when the subtree leaves
295
- the DOM.
296
- - Client helpers for partial updates: `swap()` and `startSwapLinks()` for
297
- fetching and replacing a region, `enhanceForm()` and `startForms()` for
298
- submitting forms without a full page load while keeping the no-JavaScript
299
- path working.
300
- - A fourth example, `examples/dashboard`: sign-in with a signed cookie session,
301
- a private page, a paginated table fragment, a CSRF-protected mutation and an
302
- island with cleanup, covered by its own `smoke.mjs`.
303
- - An npm version badge in the `README`, linking to the package page.
304
- - An English edition of the documentation under `docs/en/`, mirroring every
305
- chapter of the Turkish `docs/`.
306
- - The dev overlay now compares the installed version against the `latest` tag on
307
- npm: the Server tab shows the version, marks an `update` chip when a newer
308
- release exists and offers the upgrade command. The lookup is cached for six
309
- hours, never blocks the server and can be turned off with
310
- `JSKELET_VERSION_CHECK=0`.
311
-
312
- ### Changed
313
-
314
- - `trust proxy` is now configurable through `security.trustProxy` instead of
315
- being always on. The default is unchanged, but a server exposed directly to
316
- the internet should turn it off: while it is on, a client can forge its own
317
- `X-Forwarded-For` and rate limiting or audit logs see the wrong address.
318
- - Every message the framework prints is now English: config, router, render,
319
- cache, prewarm, asset and build warnings, CLI output, the project `jskelet
320
- init` scaffolds, and the devtools overlay and report interfaces. Visitor-facing
321
- status pages still follow `brand.lang` and keep their Turkish translations.
322
- - The dev overlay and report now show the current JSkelet logo, served with a
323
- cacheable response instead of being re-fetched on every navigation.
324
-
325
- ### Fixed
326
-
327
- - A page rendered without `revalidate` used to be sent with no `Cache-Control`
328
- at all, while still carrying a strong ETag. HTTP treats such a response as
329
- heuristically cacheable, so an intermediate proxy or the browser's back button
330
- could store a response meant for a single visitor. Dynamic pages now send
331
- `private, no-store` and no ETag.
332
- - A redirect thrown from a route that reads the session is no longer cacheable
333
- either; a stored "you need to sign in" redirect used to follow the visitor
334
- even after signing in.
335
-
336
- ## [0.1.1] - 2026-08-30
337
-
338
- ### Added
339
-
340
- - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
341
- a `LICENSE` file, issue and pull request templates, and a CI workflow.
342
- - An English-first `examples/marketing` with a Turkish translation, serving the
343
- package documentation under `/docs` and reading its version, dependencies and
344
- bundle sizes from the installed package.
345
-
346
- ### Changed
347
-
348
- - The install instructions point at the npm package instead of the git
349
- repository.
350
-
351
- ### Fixed
352
-
353
- - No more white flash between pages: the page background moved onto the root
354
- element, so it applies before the body paints. Reduced-motion preferences now
355
- switch off the decorative animations as well, not just page transitions.
356
-
357
- ## [0.1.0] - 2026-08-30
358
-
359
- Initial release.
360
-
361
- ### Added
362
-
363
- - Express 5 server with EJS rendering: `createApp()`, `startServer()`,
364
- `route()`, `renderPage()`, `renderView()`, `renderNotFound()`.
365
- - In-process HTML TTL cache with stale-while-revalidate, plus prewarm at boot.
366
- - Island runtime with visibility, eager and idle hydration strategies, a small
367
- cross-island store, and DOM helpers.
368
- - Configuration through `jskelet.config.mjs`: `brand`, `paths`, `navigation`,
369
- `icons`, `fonts`, `clientEnv`, `redirects()`, `rewrites()`, `headers()`,
370
- `cache()` and `hooks`.
371
- - Build pipeline: fonts, SVG sprite from used icons, Tailwind v4 CSS, esbuild
372
- bundles with code splitting, webp variants, hashed output and brotli/gzip
373
- precompression.
374
- - Dev server with watch build, CSS hot-swap, automatic restart and a devtools
375
- overlay (requests, errors, upstream calls, cache dump, Web Vitals).
376
- - CLI: `jskelet dev`, `jskelet build`, `jskelet start`, `jskelet init`.
377
- - Documentation under `docs/` and three examples: `minimal`, `blog`,
378
- `marketing`.
379
-
380
- [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.2...HEAD
381
- [0.1.2]: https://github.com/ayberkenis/jskelet/compare/v0.1.1...v0.1.2
382
- [0.1.1]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...v0.1.1
383
- [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
+ ### Breaking
12
+
13
+ - The cache admin panel moved to a top-level `admin()` config section at
14
+ `/_jskelet/admin` (was `cache().panel` at `/_jskelet/cache`). Enable with
15
+ `admin() { return { enabled: true } }` or `JSKELET_ADMIN=1`. `JSKELET_CACHE_PANEL` and
16
+ `cache().panel` are removed. Auth is unchanged (per-process password in the
17
+ server log, cookie session, 404 for strangers); the action CSRF header is now
18
+ `X-JSkelet-Admin`.
19
+
20
+ ### Fixed
21
+
22
+ - Cloudflare analytics in the cache panel no longer asks for an open-ended
23
+ window. Queries used only `datetime_geq`, so Cloudflare closed the range at
24
+ query time and a default 24h lookback became `1d` plus network delay — Free
25
+ zones reject anything wider than one day. Both ends are now pinned from the
26
+ same clock (`datetime_leq` included).
27
+
28
+ ### Added
29
+
30
+ - Admin panel pages under `/_jskelet/admin`: Overview, Cache, Routes, Views,
31
+ Logs and System. Configurable `allowIps` (exact or CIDR), `blockBots` (default
32
+ on — crawler UAs get 404 before login), and `logSize`. Live Logs use an
33
+ in-process ring plus SSE (`/api/logs/stream`) with client-side filters for
34
+ method, status, cache, kind, path/route and text. Routes and Views are
35
+ read-only inventories; HTTP finish middleware records timings only while the
36
+ panel is enabled.
37
+ - `trailingSlash` in `jskelet.config.mjs` (default `false`). When `true`,
38
+ canonical page URLs end with `/` and return 200; a request without the slash
39
+ is sent to the slashed form with a 308 (not 301). File URLs and
40
+ `/.well-known/**` are left alone. When `false`, no slash is enforced — unlike
41
+ Next.js, the default does not strip trailing slashes.
42
+ - `cache().query`, a pattern → allowlist mapping that decides which query
43
+ parameters belong to the HTML cache key. An allowlist caches one entry per
44
+ distinct value of the listed parameters and ignores the rest, so every
45
+ `?utm_source=…` variant of a path shares one copy; `true` puts the whole query
46
+ in the key and `[]` ignores it entirely. Parameters enter the key sorted, so
47
+ `?a=1&b=2` and `?b=2&a=1` are one entry.
48
+ - A cache admin panel surface (now under `admin()` — see Breaking) that lists
49
+ what the in-process tier holds (key, size, status, remaining TTL, dependency
50
+ count, precompressed bodies for HTML; key and TTL for data), reports whether
51
+ the Redis tier is connected or bypassed, and runs the operations you would
52
+ otherwise hand-write an admin route for: targeted invalidation with an
53
+ optional hard mode, dropping a single entry, clearing either cache, unlinking
54
+ the shared keys and triggering a prewarm pass. Unlike the dev overlay it does
55
+ not look at `NODE_ENV`, because "why is this page stale" is a production
56
+ question — but nothing is mounted until it is explicitly enabled, so the path
57
+ does not exist by default. Access is a 32-character password regenerated on
58
+ every process start and printed once to the server log; there is no persistent
59
+ secret to leak and a deploy revokes old access on its own. The password is
60
+ never accepted in a query string, three failed attempts ban the IP for 24
61
+ hours, and every banned or unauthorised response is a `404` rather than a 401
62
+ that would confirm the panel exists. The panel is excluded from indexing,
63
+ prewarming and navigation speculation.
64
+ - A language picker in the cache panel header, Turkish and English. The first
65
+ visit follows the browser's language, the choice is kept in `localStorage` and
66
+ carries over to the login page, and switching costs no request. To keep this
67
+ from leaking UI concerns into the server, an `/action` response now returns
68
+ `{ ok, code, params }` instead of an English sentence and the panel builds the
69
+ text — the framework's log and API stay in one language while the panel
70
+ speaks two.
71
+ - Cloudflare cache management, from the panel and from code. Set
72
+ `JSKELET_CLOUDFLARE_KEY` and `JSKELET_CLOUDFLARE_ZONE_ID` (or
73
+ `cache().cloudflare`) and the panel gains the CDN tier next to the origin one:
74
+ purge everything, purge every URL currently held in memory with one button or
75
+ a single row with `cf purge`, purge by prefix, host or cache tag, toggle
76
+ development mode, cache level, browser cache TTL, query string sorting,
77
+ Always Online, Tiered Cache, Regional Tiered Cache and Cache Reserve, clear
78
+ Cache Reserve, and read the cache hit ratio. This matters because
79
+ `invalidateHtmlCache()` refreshes the origin while the copy your visitors get
80
+ keeps being served from the edge until its TTL expires. The same surface is
81
+ exported as `purgeCloudflare()`, `toCloudflareUrls()`,
82
+ `fetchCloudflareOverview()`, `fetchCacheAnalytics()`, `fetchPathEdges()` and
83
+ `getCloudflareStatus()`; none of them throw, so a CDN outage returns
84
+ `{ ok: false, error }` instead of breaking a publish flow. Long purge lists
85
+ are batched at Cloudflare's 100-keys-per-request limit and sent sequentially
86
+ to stay inside the rate limit. The token is read from the environment, is
87
+ never returned in a response, and only cache related zone settings can be
88
+ changed.
89
+ - An edge breakdown for a single path: `fetchPathEdges()` reports which
90
+ Cloudflare colos served it from cache and which went to the origin. This is
91
+ observation, not inventory — Cloudflare has no endpoint that lists which
92
+ edges currently hold a URL, and no way to warm an edge you pick, so the panel
93
+ says as much rather than implying otherwise.
94
+ - `getRedisDetails()` reports where the shared tier actually points — address,
95
+ TLS, database, namespace, which kinds are shared and whether the purge channel
96
+ is subscribed — because "connected" alone does not explain a Redis that shares
97
+ nothing because of a wrong namespace. The password is never part of the
98
+ output. `inspectRedis()` counts the keys per kind plus `DBSIZE` and
99
+ `used_memory`; it runs a `SCAN`, so the panel keeps it behind its own button
100
+ instead of the refresh loop. When Redis is off, the panel explains what a
101
+ shared tier would buy and shows the memory and disk state of the host instead,
102
+ which is the number that decides whether `maxEntries` is too high.
103
+ - `dropHtmlCacheKey()` and `dropDataCacheKey()` drop one exact cache key.
104
+ `invalidateHtmlCache()` matches a path pattern and takes down every query
105
+ variant of a path, which is the right default for a webhook but wrong when you
106
+ want `/list?page=2` gone and `/list?page=3` left hot.
107
+
108
+ - An adaptive per-host rate limit for upstream calls, `cache().upstream`. It sits
109
+ in the `fetch` wrapper rather than in the prewarm pass, because what spends the
110
+ quota is the API call, not the page: one render may make one call or twenty, so
111
+ `prewarm.rps` could never bound the real thing. A token bucket caps the average
112
+ rate, a concurrency limit caps the calls in flight, and `rate` is treated as a
113
+ ceiling that the limiter pulls down on its own — a 429 or 503 halves the rate,
114
+ `Retry-After` stops the bucket for exactly as long as the upstream asked, and
115
+ clean windows climb back one step at a time. A host that returns
116
+ `breakerFailures` rate limits in a row is bypassed for `breakerCooldownMs`,
117
+ which stops the worst waste: because a 429 counts as transient, the HTML
118
+ produced by a throttled call is never stored, so a pass in that state spends
119
+ quota and keeps nothing. Only 429 and 503 penalise the rate; a 400 or 500 is
120
+ not a quota problem. Off by default — set `rate` to turn it on.
121
+ - `getUpstreamLimiterStatus()` reports the current rate, calls in flight, 429
122
+ count and breaker state per host. The dev panel's Server tab shows the same.
123
+ - `getDataCacheStats()` counts how the data cache was used: fresh hits, stale
124
+ hits, misses, coalesced concurrent reads, values promoted from the shared tier
125
+ and — the only number that reaches the quota — real producer runs. A prewarm
126
+ pass now prints its own share of that (`12 upstream calls for 430 data reads
127
+ (97% from the data cache)`), which is what tells you whether the fix is a
128
+ longer TTL or a rate limit. The dev report has a Data cache card for it.
129
+
130
+ - The dev overlay header now shows the installed JSkelet version next to the
131
+ title, labelled `latest` when it matches npm and `outdated` with the newer
132
+ version when it does not, so you can tell at a glance which version the
133
+ project runs without opening the Server tab.
134
+ - An optional Redis tier behind both caches, turned on with
135
+ `cache().redis: { enabled: true, url }` and `npm install ioredis`. The
136
+ in-process cache stays primary and every request still reads it; Redis only
137
+ does the two things a single process cannot. An instance that has never seen a
138
+ path finds the HTML another replica already produced, so a fresh container or a
139
+ post-deploy replacement does not re-render and re-fetch everything from
140
+ scratch. And `invalidateHtmlCache()`, `clearHtmlCache()` and
141
+ `clearDataCache()` now reach every replica over pub/sub instead of only the one
142
+ that received the webhook — until now the others waited out the TTL and a
143
+ visitor saw old or new content depending on where they landed. Keys live under
144
+ `_jskelet:{namespace}:{buildId}:…`, where the build id makes HTML from a
145
+ previous deploy expire on its own rather than pointing at asset files that no
146
+ longer exist. Personalised (`storable: false`), degraded and non-200 responses
147
+ are never shared. If `ioredis` is missing, Redis is unreachable or it goes down
148
+ mid-flight, a warning is printed and the site keeps serving from memory.
149
+ - `getRedisStatus()` reports whether the shared tier is connected, which key
150
+ prefix and build id it is using, and how many command failures there have
151
+ been — usable from a healthcheck endpoint. The same summary appears in the dev
152
+ panel report.
153
+ - Servers started with `startServer()` now shut down on `SIGTERM`/`SIGINT`
154
+ instead of being killed: the listener is closed and the Redis connection is
155
+ drained so in-flight writes are not cut mid-command.
156
+ - Targeted HTML invalidation: `invalidateHtmlCache(target, { hard })` takes a
157
+ path, the config pattern syntax (`/news/:slug`), a regular expression or a list
158
+ of them, and returns how many entries were affected. By default it **stales**
159
+ the entries rather than deleting them, so a webhook that touches hundreds of
160
+ pages does not turn into hundreds of cold renders at the worst possible moment:
161
+ visitors keep getting the old HTML while the refresh runs in the background,
162
+ once per key. Matching is done against the path, so every query variant of a
163
+ page is covered by one call, and a render already in flight when the purge
164
+ arrives is not stored.
165
+ - `clearDataCache()` now refreshes the HTML too. The `withDataCache` keys read
166
+ during a render are recorded, so dropping `news:abc` stales every page that
167
+ actually read it — the article, the home page listing it and the tag page —
168
+ without the application declaring any tags. Turn it off with
169
+ `cache().trackDependencies: false`; `getHtmlCacheEntries()` reports the
170
+ dependency count per page as `deps`.
171
+ - Invalidated paths go to the front of the next prewarm pass, so an updated page
172
+ is refreshed without waiting for a visitor, while still respecting the `rps`
173
+ limit. The pass summary counts them separately.
174
+ - An upstream data cache: `withDataCache(key, ttlSeconds, producer)` and the
175
+ `dataCache(fn, { key, revalidate })` wrapper, with `clearDataCache(prefix?)`,
176
+ `getDataCacheSize()` and `getDataCacheEntries()` alongside them. It keeps JSON
177
+ rather than HTML, so its default limit is 10,000 entries: a long-tail page that
178
+ was never prewarmed still renders without touching the API. Concurrent reads of
179
+ the same key collapse into one upstream request, an expired entry is served
180
+ immediately while it refreshes in the background, a failing producer falls back
181
+ to the stale value, and empty answers (`null`/`undefined`) are not stored
182
+ unless `storeEmpty: true` is passed.
183
+ - `cache().prewarm.priority` decides the warm-up order and accepts both the
184
+ config pattern syntax (`/news/:slug`) and plain regular expressions. Matching
185
+ paths are warmed on every pass.
186
+ - Drip prewarming for large sites: `cache().prewarm.rps` (also `PREWARM_RPS`)
187
+ caps requests per second regardless of parallelism, and `rotate` (on by
188
+ default) makes periodic passes continue through the queue where the previous
189
+ one stopped instead of re-warming the same first slice. A pass is skipped while
190
+ the previous one is still running.
191
+ - `cache().prewarm.retryDelayMs` (also `PREWARM_RETRY_DELAY_MS`) waits before the
192
+ retry pass, since rate limit windows are measured in seconds.
193
+ - `cache().maxEntries` configures the HTML cache limit, which used to be a fixed 500.
194
+ - Transient upstream failures are now detected without any application code:
195
+ `globalThis.fetch` is wrapped during startup and `429`, `5xx` and network
196
+ errors raised inside a render are reported on their own, so rate limits stop
197
+ turning existing pages into 404s even when the data layer never calls
198
+ `reportUpstreamFailure()`. Requests outside a render and requests to the
199
+ server itself are ignored, deterministic answers such as `404` are not
200
+ reported, and the wrapper can be turned off with `cache().trackUpstream:
201
+ false`.
202
+ - `cache().transientRetry` (`{ attempts: 1, delayMs: 300 }` by default) retries a
203
+ page that called `notFound()` while upstream was failing. Each attempt runs in
204
+ a fresh upstream and per-request cache scope, so a page whose data arrives on
205
+ the second try is served and cached as usual instead of degrading to an error.
206
+ - The dev report now includes the data cache entry count under `cache.data`.
207
+
208
+ ### Changed
209
+
210
+ - Admin panel System meters (CPU, memory, disk) show this process's share of
211
+ the host — RSS and project disk footprint against machine totals, plus
212
+ process CPU across all cores — instead of whole-machine fullness. The panel
213
+ content width is wider (`1600px`) so Overview, Routes, Views and System use
214
+ the screen better.
215
+ - The release history page in `examples/marketing` now shows one release at a
216
+ time: the newest one is expanded and older releases collapse to a single
217
+ header row with their date, status and change count. Every release used to be
218
+ printed open in a two-column grid, which made the page an unreadable wall as
219
+ soon as a few versions piled up. Version links and the quick-jump strip still
220
+ work, and they open the collapsed release they point at.
221
+ - The prewarm retry pass no longer retries permanent failures. A `400`, `403` or
222
+ `404` does not get better on the second try, so those paths are dropped from
223
+ the retry round and counted as `N not retried (permanent)` in the summary. The
224
+ wait before the round now also honours the upstream rate limit: if a
225
+ `Retry-After` or an open circuit breaker is holding calls back, the pass waits
226
+ that out instead of retrying into the same 429.
227
+ - Errors and warnings raised during a prewarm pass are no longer logged one per
228
+ page. Request errors and the per-page render warnings (`was produced with
229
+ missing data`, `returned notFound() while upstream is failing`, `could not be
230
+ produced`) are counted while the pass runs and printed as a single summary
231
+ block afterwards, grouped by message with the most frequent kinds first, so a
232
+ failing upstream can no longer bury the "warmed N/M pages" line under hundreds
233
+ of near-identical lines. Real traffic logs as before, and the dev tools panel
234
+ still shows the per-path detail.
235
+ - The marketing example's changelog page is now a timeline: releases are laid out
236
+ along a rail with a sticky version column, each change group gets its own card
237
+ with a coloured rule and item count, and a row of version chips at the top
238
+ jumps straight to a release.
239
+ - The dev tools panel is now fed over a WebSocket (`<devBasePath>/ws`) instead of
240
+ polling `/stats` every two seconds. The server pushes statistics as they change
241
+ and sends live reload and CSS hot-swap events over the same connection, so an
242
+ open tab no longer keeps hitting the server while the panel is closed. No new
243
+ dependency is involved; if the socket cannot be opened, the panel falls back to
244
+ the previous SSE plus polling path.
245
+ - The server now binds to `::` instead of `0.0.0.0` when no `HOST` is given, so a
246
+ single dual-stack socket answers both IPv6 and IPv4. Browsers resolve
247
+ `localhost` to `::1` first and, unlike ordinary requests, a WebSocket handshake
248
+ does not fall back to IPv4 — which made the dev panel's live channel fail on an
249
+ IPv4-only socket. Where IPv6 is unavailable the bind falls back to `0.0.0.0`.
250
+ - Prewarming no longer holds up the rest of the dev server. In development it now
251
+ runs with a single worker and a default limit of 4 requests per second
252
+ (`prewarm.rps` / `PREWARM_RPS` still override it), so page requests and the dev
253
+ panel stay responsive while a warm-up round is going on. Production behaviour
254
+ is unchanged.
255
+ - `notFound()` is no longer served as a 404 when a transient upstream failure
256
+ (`429`, `5xx`, network error) happened during the same render. The page is
257
+ retried first and, if upstream is still failing, responds with an uncached
258
+ `503` and `Retry-After`. A temporary rate limit is no longer frozen into "this
259
+ page does not exist" for the whole TTL. A retry that gets a clean answer saying
260
+ the page is gone still returns a normal 404.
261
+ - Responses produced with missing data are no longer offered to shared caches:
262
+ a `degraded` render is sent with `private, no-store` instead of
263
+ `public, s-maxage=…`. The `X-JSkelet-Cache` diagnostic header is still written.
264
+ - The prewarm summary distinguishes paths left for the next pass
265
+ (`700 deferred to the next pass`) from paths dropped entirely
266
+ (`700 over the limit`).
267
+ - The changelog page of the marketing example is generated from the project's
268
+ `CHANGELOG.md` instead of a hand-written list, and shows the version published
269
+ on npm next to the installed one.
270
+ - The marketing example reads its markdown (documentation and changelog) from
271
+ the repository over GitHub's raw endpoint, falling back to the installed
272
+ package when the network is unavailable, so a deployment that ships without
273
+ `node_modules` can still serve the docs. In development the local file wins
274
+ and nothing is cached. The branch is overridable with `DOCS_REF`.
275
+
276
+ ### Fixed
277
+
278
+ - The dev panel's WebSocket handshake was answered with a `Sec-WebSocket-Accept`
279
+ value derived from a mistyped protocol constant. Browsers verify that value and
280
+ closed the connection immediately with "Incorrect 'Sec-WebSocket-Accept' header
281
+ value", so the panel silently fell back to polling.
282
+
283
+ ### Breaking
284
+
285
+ - A request that carries a query parameter is now dynamic by default: it is not
286
+ written to the HTML cache and the response is sent with `private, no-store`,
287
+ even when a `cache().html` pattern covers the path. Every query variant used
288
+ to become its own cache entry, which let campaign parameters
289
+ (`?utm_source=…`) mint unbounded keys and evict real pages from a 500-entry
290
+ store. Pages whose output genuinely depends on the query keep their cache by
291
+ listing the relevant parameters under `cache().query`.
292
+
293
+ ## [0.1.2] - 2026-08-30
294
+
295
+ ### Added
296
+
297
+ - `route(fn, { private: true })` for pages that depend on the visitor. The HTML
298
+ cache is bypassed, `cache.html` patterns can no longer turn caching on for
299
+ that route, and the response is sent with `private, no-store`, `Vary: Cookie`
300
+ and no ETag.
301
+ - A runtime guard against identity leaks: when a cacheable route reads
302
+ `Cookie`, `Authorization` or a session field, the rendered HTML is never
303
+ stored. In development the request fails with an explanation, in production it
304
+ is served with `no-store` and logged.
305
+ - `fragment()` for layout-less partial responses, with `no-store` and cache
306
+ bypass built in.
307
+ - CSRF protection. Cross-site state-changing requests are rejected based on
308
+ `Origin` and `Sec-Fetch-Site`; requests carrying neither header still pass, so
309
+ webhooks keep working. An optional double-submit token layer is enabled with
310
+ `security.csrf.token` and rendered into forms by the new `csrfField()` helper.
311
+ - Signed cookie helpers under `jskelet/cookies`: `parseCookies()`,
312
+ `setCookie()`, `clearCookie()`, `setSignedCookie()`, `getSignedCookie()`,
313
+ `randomToken()` and `safeEqual()`. Defaults are `HttpOnly`, `SameSite=Lax` and
314
+ `Secure` outside development.
315
+ - A `security` configuration section: `trustProxy`, `cookieSecret` and `csrf`.
316
+ - `seeOther()` for the post/redirect/get flow, which needs 303 rather than the
317
+ method-preserving 307 that `redirect()` sends.
318
+ - Island cleanup. A `mount()` function may return a teardown callback; it is now
319
+ stored and called by the new `unmount(root)` export when the subtree leaves
320
+ the DOM.
321
+ - Client helpers for partial updates: `swap()` and `startSwapLinks()` for
322
+ fetching and replacing a region, `enhanceForm()` and `startForms()` for
323
+ submitting forms without a full page load while keeping the no-JavaScript
324
+ path working.
325
+ - A fourth example, `examples/dashboard`: sign-in with a signed cookie session,
326
+ a private page, a paginated table fragment, a CSRF-protected mutation and an
327
+ island with cleanup, covered by its own `smoke.mjs`.
328
+ - An npm version badge in the `README`, linking to the package page.
329
+ - An English edition of the documentation under `docs/en/`, mirroring every
330
+ chapter of the Turkish `docs/`.
331
+ - The dev overlay now compares the installed version against the `latest` tag on
332
+ npm: the Server tab shows the version, marks an `update` chip when a newer
333
+ release exists and offers the upgrade command. The lookup is cached for six
334
+ hours, never blocks the server and can be turned off with
335
+ `JSKELET_VERSION_CHECK=0`.
336
+
337
+ ### Changed
338
+
339
+ - `trust proxy` is now configurable through `security.trustProxy` instead of
340
+ being always on. The default is unchanged, but a server exposed directly to
341
+ the internet should turn it off: while it is on, a client can forge its own
342
+ `X-Forwarded-For` and rate limiting or audit logs see the wrong address.
343
+ - Every message the framework prints is now English: config, router, render,
344
+ cache, prewarm, asset and build warnings, CLI output, the project `jskelet
345
+ init` scaffolds, and the devtools overlay and report interfaces. Visitor-facing
346
+ status pages still follow `brand.lang` and keep their Turkish translations.
347
+ - The dev overlay and report now show the current JSkelet logo, served with a
348
+ cacheable response instead of being re-fetched on every navigation.
349
+
350
+ ### Fixed
351
+
352
+ - A page rendered without `revalidate` used to be sent with no `Cache-Control`
353
+ at all, while still carrying a strong ETag. HTTP treats such a response as
354
+ heuristically cacheable, so an intermediate proxy or the browser's back button
355
+ could store a response meant for a single visitor. Dynamic pages now send
356
+ `private, no-store` and no ETag.
357
+ - A redirect thrown from a route that reads the session is no longer cacheable
358
+ either; a stored "you need to sign in" redirect used to follow the visitor
359
+ even after signing in.
360
+
361
+ ## [0.1.1] - 2026-08-30
362
+
363
+ ### Added
364
+
365
+ - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
366
+ a `LICENSE` file, issue and pull request templates, and a CI workflow.
367
+ - An English-first `examples/marketing` with a Turkish translation, serving the
368
+ package documentation under `/docs` and reading its version, dependencies and
369
+ bundle sizes from the installed package.
370
+
371
+ ### Changed
372
+
373
+ - The install instructions point at the npm package instead of the git
374
+ repository.
375
+
376
+ ### Fixed
377
+
378
+ - No more white flash between pages: the page background moved onto the root
379
+ element, so it applies before the body paints. Reduced-motion preferences now
380
+ switch off the decorative animations as well, not just page transitions.
381
+
382
+ ## [0.1.0] - 2026-08-30
383
+
384
+ Initial release.
385
+
386
+ ### Added
387
+
388
+ - Express 5 server with EJS rendering: `createApp()`, `startServer()`,
389
+ `route()`, `renderPage()`, `renderView()`, `renderNotFound()`.
390
+ - In-process HTML TTL cache with stale-while-revalidate, plus prewarm at boot.
391
+ - Island runtime with visibility, eager and idle hydration strategies, a small
392
+ cross-island store, and DOM helpers.
393
+ - Configuration through `jskelet.config.mjs`: `brand`, `paths`, `navigation`,
394
+ `icons`, `fonts`, `clientEnv`, `redirects()`, `rewrites()`, `headers()`,
395
+ `cache()` and `hooks`.
396
+ - Build pipeline: fonts, SVG sprite from used icons, Tailwind v4 CSS, esbuild
397
+ bundles with code splitting, webp variants, hashed output and brotli/gzip
398
+ precompression.
399
+ - Dev server with watch build, CSS hot-swap, automatic restart and a devtools
400
+ overlay (requests, errors, upstream calls, cache dump, Web Vitals).
401
+ - CLI: `jskelet dev`, `jskelet build`, `jskelet start`, `jskelet init`.
402
+ - Documentation under `docs/` and three examples: `minimal`, `blog`,
403
+ `marketing`.
404
+
405
+ [Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.2...HEAD
406
+ [0.1.2]: https://github.com/ayberkenis/jskelet/compare/v0.1.1...v0.1.2
407
+ [0.1.1]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...v0.1.1
408
+ [0.1.0]: https://github.com/ayberkenis/jskelet/releases/tag/v0.1.0