jskelet 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +633 -620
  3. package/LICENSE +21 -21
  4. package/README.md +2 -0
  5. package/bin/jskelet.mjs +130 -130
  6. package/docs/01-baslangic.md +291 -291
  7. package/docs/02-mimari.md +310 -310
  8. package/docs/03-routing.md +515 -515
  9. package/docs/04-render-ve-sablonlar.md +700 -661
  10. package/docs/05-islands.md +486 -486
  11. package/docs/06-cache.md +1467 -1443
  12. package/docs/07-yapilandirma.md +1208 -1197
  13. package/docs/08-build.md +429 -429
  14. package/docs/09-dev-araclari.md +364 -364
  15. package/docs/10-dagitim.md +351 -338
  16. package/docs/12-panel-ve-oturum.md +479 -478
  17. package/docs/README.md +83 -83
  18. package/docs/en/01-getting-started.md +298 -298
  19. package/docs/en/02-architecture.md +329 -329
  20. package/docs/en/03-routing.md +531 -531
  21. package/docs/en/04-rendering.md +708 -669
  22. package/docs/en/05-islands.md +497 -497
  23. package/docs/en/06-caching.md +1476 -1453
  24. package/docs/en/07-configuration.md +1229 -1219
  25. package/docs/en/08-build.md +447 -447
  26. package/docs/en/09-dev-tools.md +373 -373
  27. package/docs/en/10-deployment.md +355 -340
  28. package/docs/en/11-migration.md +398 -398
  29. package/docs/en/12-dashboards-and-sessions.md +489 -488
  30. package/docs/en/README.md +87 -87
  31. package/package.json +137 -137
  32. package/src/build/ensure-build.mjs +19 -19
  33. package/src/build/paths.mjs +153 -153
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +349 -349
  36. package/src/build/tasks/css.mjs +235 -235
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +357 -357
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/build/tasks/templates.mjs +20 -20
  42. package/src/client/admin/i18n.js +764 -764
  43. package/src/client/admin/login.html +74 -74
  44. package/src/client/admin/panel.css +809 -809
  45. package/src/client/admin/panel.html +495 -495
  46. package/src/client/admin/panel.js +1251 -1251
  47. package/src/client/devtools/report.html +185 -185
  48. package/src/client/devtools/report.js +745 -745
  49. package/src/client/devtools/seo.js +628 -628
  50. package/src/client/dom.js +95 -95
  51. package/src/client/form.js +192 -192
  52. package/src/client/index.js +45 -45
  53. package/src/client/registry.js +305 -305
  54. package/src/client/safe-image.js +91 -91
  55. package/src/client/shared-cookie.js +225 -225
  56. package/src/client/store.js +36 -36
  57. package/src/client/swap.js +188 -188
  58. package/src/compile/codegen.js +336 -336
  59. package/src/compile/compile-all.js +149 -149
  60. package/src/compile/errors.js +66 -66
  61. package/src/compile/expr.js +409 -409
  62. package/src/compile/index.js +17 -17
  63. package/src/compile/parse.js +541 -541
  64. package/src/compile/resolve.js +211 -211
  65. package/src/compile/scan-exports.js +51 -51
  66. package/src/config/defaults.js +541 -534
  67. package/src/config/index.js +1500 -1469
  68. package/src/config/pattern.js +107 -107
  69. package/src/generate.mjs +163 -163
  70. package/src/http/control-flow.js +71 -71
  71. package/src/http/cookies-entry.js +21 -21
  72. package/src/http/cookies.js +277 -277
  73. package/src/http/request-cache.js +46 -46
  74. package/src/http/request-context.js +165 -165
  75. package/src/http/shared-cookie.js +178 -178
  76. package/src/index.js +101 -101
  77. package/src/init.mjs +232 -230
  78. package/src/migrate/apply.mjs +262 -262
  79. package/src/migrate/babel.mjs +79 -79
  80. package/src/migrate/classify.mjs +155 -155
  81. package/src/migrate/config.mjs +126 -126
  82. package/src/migrate/fs-walk.mjs +191 -191
  83. package/src/migrate/parse.mjs +26 -26
  84. package/src/migrate/scan.mjs +177 -177
  85. package/src/migrate/transform/expr-source.mjs +168 -168
  86. package/src/migrate/transform/island.mjs +67 -67
  87. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  88. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  89. package/src/migrate/transform/page-split.mjs +435 -435
  90. package/src/migrate/write.mjs +81 -81
  91. package/src/migrate.mjs +171 -171
  92. package/src/runtime/alias-hooks.mjs +119 -119
  93. package/src/runtime/register.mjs +4 -4
  94. package/src/server/admin/actions.js +229 -229
  95. package/src/server/admin/auth.js +125 -125
  96. package/src/server/admin/event-log.js +151 -151
  97. package/src/server/admin/gate.js +209 -209
  98. package/src/server/admin/inventory.js +188 -188
  99. package/src/server/admin/mount.js +56 -56
  100. package/src/server/admin/router.js +216 -216
  101. package/src/server/admin/snapshot.js +241 -241
  102. package/src/server/assets.js +147 -147
  103. package/src/server/auth/handoff.js +309 -309
  104. package/src/server/cache-blob.js +70 -70
  105. package/src/server/cache-control.js +45 -0
  106. package/src/server/cache-deps.js +42 -42
  107. package/src/server/cache-vary.js +113 -113
  108. package/src/server/cloudflare.js +607 -607
  109. package/src/server/create-app.js +366 -366
  110. package/src/server/data-cache.js +553 -553
  111. package/src/server/dev/report.js +485 -485
  112. package/src/server/dev/socket.js +170 -170
  113. package/src/server/dev/version-check.mjs +139 -139
  114. package/src/server/disk-cache.js +233 -233
  115. package/src/server/ejs-adapter.js +59 -59
  116. package/src/server/html-cache.js +1196 -1196
  117. package/src/server/image-optimizer.js +500 -500
  118. package/src/server/logs/access-middleware.js +66 -66
  119. package/src/server/logs/file-sink.js +193 -193
  120. package/src/server/logs/pipeline.js +165 -165
  121. package/src/server/logs/s3-put.js +214 -214
  122. package/src/server/logs/s3-sink.js +112 -112
  123. package/src/server/metadata.js +102 -102
  124. package/src/server/middleware/compression.js +205 -205
  125. package/src/server/middleware/csrf.js +134 -134
  126. package/src/server/middleware/dev-gate.js +75 -75
  127. package/src/server/middleware/headers.js +37 -37
  128. package/src/server/middleware/redirects.js +32 -32
  129. package/src/server/middleware/robots-txt.js +341 -341
  130. package/src/server/middleware/static-precompressed.js +121 -121
  131. package/src/server/middleware/trailing-slash.js +53 -53
  132. package/src/server/middleware/upstream-proxy.js +141 -141
  133. package/src/server/og-image.js +965 -356
  134. package/src/server/og-raster.mjs +21 -0
  135. package/src/server/port-guard.js +255 -255
  136. package/src/server/prewarm.js +1082 -1082
  137. package/src/server/redis.js +588 -588
  138. package/src/server/render.js +910 -910
  139. package/src/server/router.js +157 -157
  140. package/src/server/status-page.js +265 -265
  141. package/src/server/upstream-limiter.js +376 -376
  142. package/src/server/upstream-tracking.js +166 -166
  143. package/src/shared/cookie-domain.js +66 -66
  144. package/src/start.mjs +22 -22
  145. package/src/templates/layout.ejs +30 -30
  146. package/src/templates/layout.jsk +30 -30
  147. package/src/version.mjs +31 -31
  148. package/src/views/components/loader.js +101 -101
  149. package/src/views/helpers/html.js +102 -102
  150. package/src/views/helpers/tags.js +375 -375
  151. package/types/config/defaults.d.ts +6 -0
  152. package/types/config/index.d.ts +6 -0
  153. package/types/server/cache-control.d.ts +28 -0
  154. package/types/server/og-image.d.ts +51 -3
@@ -1,398 +1,398 @@
1
- # 11 — Migrating from Next.js
2
-
3
- This document explains how to move a project using the Next.js App Router over
4
- to JSkelet: a table of concept and API equivalents, an explicit list of what
5
- cannot be migrated, and a step-by-step plan. JSkelet's surface was deliberately
6
- modeled on the subset of Next that people actually use — concepts like the
7
- `next.config` syntax, the Metadata API, `notFound()`, `revalidate` and `cache()`
8
- will feel familiar. The *reasons* behind the differences are in
9
- [02-architecture.md](./02-architecture.md).
10
-
11
- ## `jskelet migrate` (codemod)
12
-
13
- Run the codemod against an App Router tree. Babel (`@babel/parser`,
14
- `@babel/types`) ships with JSkelet — no extra install.
15
-
16
- ```bash
17
- npx jskelet migrate scan ../my-next-app
18
- npx jskelet migrate apply ../my-next-app --out . --write
19
- npx jskelet migrate config ../my-next-app --write
20
- ```
21
-
22
- | Command | What it does |
23
- | --- | --- |
24
- | `migrate` / `migrate scan` | Inventory pages, layouts, `"use client"` modules, blockers (nested layouts, Server Actions, Suspense). |
25
- | `migrate apply` | **Automatic convert:** `page.*` → feature controller + `.jsk`; presentational components → `views/components/*.js`; clients → island `mount()` stubs. Default is dry-run; pass `--write`. Never overwrites (conflicts get a `.migrate` suffix). |
26
- | `migrate config` | Draft `jskelet.config.mjs` from `next.config` (`headers` / `redirects` / `rewrites`, `images.widths`, `NEXT_PUBLIC_*` → `clientEnv`). |
27
-
28
- Flags: `--out <dir>`, `--only pages,components,islands`, `--json`, `--strict` (exit 1 on partial/skipped).
29
-
30
- **Converted automatically:** `className`, `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}`, `next/image` → `<Image />`, `next/link` → `<Link />`, `dangerouslySetInnerHTML`, `revalidate`, simple controller prelude (`await` data + `notFound()`).
31
-
32
- **Not converted (reported):** React hooks, Server Actions, nested layout flattening, Streaming/Suspense, client-side routing. Confidence per file is `ok` / `partial` / `skipped`.
33
-
34
- ## Equivalence table
35
-
36
- ### Configuration
37
-
38
- | Next.js | JSkelet | Note |
39
- | --- | --- | --- |
40
- | `next.config.mjs` | `jskelet.config.mjs` | Same spirit, smaller surface ([07](./07-configuration.md)) |
41
- | `headers()` | `headers()` | Same shape: `{ source, headers: [{ key, value }] }` |
42
- | `redirects()` | `redirects()` | `permanent` → 308, otherwise 307; can be overridden with `statusCode` |
43
- | `trailingSlash` | `trailingSlash` | `true` → canonical URLs end with `/` (308); default `false` does not strip ([07](./07-configuration.md)) |
44
- | `rewrites()` | `rewrites()` | There are `beforeFiles` / `afterFiles` phases; no `fallback` |
45
- | `compress: true` | Automatic | brotli + gzip via `node:zlib` |
46
- | `images.deviceSizes` | `images.widths` | Build-time webp generation ([08](./08-build.md)) |
47
- | `NEXT_PUBLIC_*` | `clientEnv: [...]` | Which key is exposed is clear from the config, not from the name |
48
- | `experimental.*` | — | None |
49
-
50
- ### Routing and rendering
51
-
52
- | Next.js | JSkelet | Note |
53
- | --- | --- | --- |
54
- | `app/page.js` (file-based routing) | `app.get(...)` inside `routes/*.mjs` | The order is written explicitly ([03](./03-routing.md)) |
55
- | `app/[slug]/page.js` | `app.get("/:slug", route(...))` | Express pattern syntax |
56
- | `params`, `searchParams` | `ctx.params`, `ctx.query` | The controller's single argument |
57
- | `layout.js` | `views/layout.jsk` + `hooks.layoutContext()` | A single layout; no nested layouts |
58
- | Server component (RSC) | Controller + `.jsk` template + `views/components/**` | A function returns an HTML string |
59
- | Client component (`"use client"`) | Island (`data-island` + `mount`) | The whole page is not hydrated ([05](./05-islands.md)) |
60
- | `notFound()` | `notFound()` | Same name, same control flow |
61
- | `redirect()` | `redirect()` (307) | For permanent, `permanentRedirect()` (308) |
62
- | `not-found.js` | `hooks.notFound()` | Returns a page definition |
63
- | `error.js` | Express error handler | The framework returns minimal HTML for a 500 |
64
- | `loading.js` / Suspense | — | The server HTML is complete; no skeleton needed |
65
- | Streaming SSR | — | The response is a single chunk |
66
- | `generateMetadata()` | Controller `metadata` + `hooks.metadata()` | Same field names ([04](./04-rendering.md)) |
67
- | `opengraph-image.tsx` / `ImageResponse` | `ogHandler` + `ImageResponse` / `sendOgImage` | SVG or card fields → PNG (`sharp`); [04](./04-rendering.md) |
68
- | `generateStaticParams()` | `hooks.prewarmPaths()` | Warming at startup time, not build time |
69
- | Route Handlers (`route.js`) | A plain Express handler | `app.get/post(...)` |
70
- | Middleware (`middleware.ts`) | Express middleware + config `rewrites`/`headers`/`redirects` | `app.use(...)` |
71
-
72
- ### Data and cache
73
-
74
- | Next.js | JSkelet | Note |
75
- | --- | --- | --- |
76
- | `export const revalidate = 60` | `route(controller, { revalidate: 60 })` | Or `cache().html` ([06](./06-caching.md)) |
77
- | ISR (prerender written to disk) | In-memory TTL cache + stale-while-revalidate | Nothing is written to disk |
78
- | `fetch(..., { next: { revalidate } })` | — | The cache is at page level |
79
- | `unstable_cache` | — | No cross-request data cache; there is a page cache |
80
- | React `cache()` | `cache()` | Same behavior: in-request memoization |
81
- | `revalidatePath()` | `invalidateHtmlCache("/news/:slug")` | A path, a pattern or a regexp; stales rather than deletes by default |
82
- | `revalidateTag()` | `clearDataCache("news:")` | No tags to declare: the dependency is observed during the render ([06](./06-caching.md)) |
83
- | `cookies()`, `headers()` | `ctx.req.headers`, `ctx.req.cookies`* | Direct access to the Express object |
84
- | `dynamic = "force-dynamic"` | Not passing `revalidate` | Which means the cache is off |
85
-
86
- \* Express 5 does not parse cookies on its own; add `cookie-parser` or read the
87
- header manually.
88
-
89
- ### Components and helpers
90
-
91
- | Next.js | JSkelet | Note |
92
- | --- | --- | --- |
93
- | `next/link` | `link({ href, text })` — `jskelet/tags` | `title` automatic, `rel`/`target` automatic for external links |
94
- | `next/link` prefetching | `navigation: { prefetch, prerender }` | Speculation Rules; no client runtime ([07](./07-configuration.md)) |
95
- | `next/image` | `image({ src, alt, priority })` — `jskelet/tags` | `srcset` from the build manifest |
96
- | `next/og` `ImageResponse` | `ImageResponse` / `ogHandler` — `jskelet` | No JSX; SVG or `title`/`description` card |
97
- | `next/font/google` | `fonts: [{ family, weights }]` | Self-hosted woff2, committed |
98
- | `@phosphor-icons/react` | `icon({ name, weight })` — `jskelet/tags` | Build-time SVG sprite |
99
- | `react-dom` preconnect/preload | `preconnect: [...]` + `headHints()` | ([04](./04-rendering.md)) |
100
- | `clsx` | `cx()` — `jskelet/html` | — |
101
- | `cn()` (clsx + tailwind-merge) | `cn()` — `jskelet/html` | Same behavior |
102
- | JSX automatic escaping | `esc()` — `jskelet/html` | **You have to call it yourself** |
103
- | React Context | `createStore()` — `jskelet/client` | Minimal pub/sub |
104
- | `useState` / `useEffect` | Plain JS inside the island's `mount()` | — |
105
- | `useSyncExternalStore` | `store.subscribe()` | — |
106
- | `<Script>` | A `<script>` in the layout, or an island | — |
107
-
108
- ### What has no equivalent
109
-
110
- Account for these from the start in your migration plan:
111
-
112
- - **React itself.** Components turn into functions that return HTML strings. No
113
- JSX, no hooks, no virtual DOM.
114
- - **TypeScript.** Framework source is plain JS + JSDoc and publishes `.d.ts` for
115
- consumers. Client entries and islands may be `.ts` / `.mts` (esbuild strips
116
- types; the manifest key stays `*.js`). Server routes, hooks and
117
- `jskelet.config.mjs` remain Node ESM JavaScript — use `checkJs: true` in
118
- `jsconfig.json` for editor checking there.
119
- - **Nested layouts.** There is a single layout; you share common sections with
120
- EJS `include` or component functions.
121
- - **Streaming / Suspense / partial prerendering.** The response is produced as a
122
- single chunk.
123
- - **Client-side routing.** Navigation is a real page load. Because the server
124
- HTML comes from the cache it is very fast in practice, but there are no SPA
125
- transitions. What closes the gap is the `navigation` section:
126
- prefetch/prerender prepares the document before the click, and
127
- `viewTransition` smooths the transition
128
- ([07](./07-configuration.md)).
129
- - **Server Actions.** Form submissions are ordinary `app.post(...)` handlers.
130
- - **Automatic image optimization (at request time).** Local `public/` images
131
- are still optimized at build time. Remote `http(s)` images can be proxied at
132
- runtime when `images.remote.allowHosts` is set (`/_jskelet/image` → webp);
133
- without that config they are emitted as-is.
134
-
135
- ## A side-by-side example
136
-
137
- **Next.js (App Router):**
138
-
139
- ```jsx
140
- // app/news/[slug]/page.jsx
141
- import { notFound } from "next/navigation";
142
- import Image from "next/image";
143
- import { getArticle } from "@/lib/api";
144
-
145
- export const revalidate = 300;
146
-
147
- export async function generateMetadata({ params }) {
148
- const article = await getArticle(params.slug);
149
- return {
150
- title: article?.title,
151
- description: article?.summary,
152
- alternates: { canonical: `/news/${params.slug}` },
153
- };
154
- }
155
-
156
- export default async function Page({ params }) {
157
- const article = await getArticle(params.slug);
158
- if (!article) notFound();
159
-
160
- return (
161
- <article className="wrapper">
162
- <h1 className="text-3xl font-bold">{article.title}</h1>
163
- <Image src={article.cover} alt={article.title} priority width={1200} height={630} />
164
- <div dangerouslySetInnerHTML={{ __html: article.body }} />
165
- </article>
166
- );
167
- }
168
- ```
169
-
170
- **JSkelet:**
171
-
172
- ```js
173
- // routes/50-news.mjs
174
- import { getArticle } from "@/lib/api.js";
175
-
176
- export default function register(app, { route, notFound }) {
177
- app.get(
178
- "/news/:slug",
179
- route(
180
- async ({ params }) => {
181
- const article = await getArticle(params.slug);
182
- if (!article) notFound();
183
-
184
- return {
185
- view: "pages/article",
186
- data: { article },
187
- metadata: {
188
- title: article.title,
189
- description: article.summary,
190
- canonical: `/news/${params.slug}`,
191
- openGraph: { type: "article", image: article.cover },
192
- },
193
- };
194
- },
195
- { revalidate: 300 },
196
- ),
197
- );
198
- }
199
- ```
200
-
201
- ```jsk
202
- {# views/pages/article.jsk #}
203
- <article class="wrapper">
204
- <h1 class="text-3xl font-bold">{{ article.title }}</h1>
205
- <Image :src="article.cover" :alt="article.title" priority :width="1200" :height="630" />
206
- <div>{{{ article.body }}}</div>
207
- </article>
208
- ```
209
-
210
- Wrapping `getArticle` with `cache()` makes sure that if
211
- `hooks.layoutContext()` asks for the same article in the same render, only a
212
- single upstream request is made ([06-caching.md](./06-caching.md)).
213
-
214
- ## Step-by-step plan
215
-
216
- ### 1. Set up the skeleton (half a day)
217
-
218
- Run `npx jskelet init` in a new directory and watch `jskelet dev` come up. Leave
219
- the existing Next project as it is; let the migration run in parallel. Optionally
220
- run `jskelet migrate scan <next-root>` first to list pages and blockers.
221
-
222
- Carry over the `paths` aliases from your `jsconfig.json` — prefixes like `@/`
223
- work the same way both on the server and in the bundle
224
- ([02-architecture.md](./02-architecture.md)).
225
-
226
- ### 2. Translate `next.config.mjs` (1-2 hours)
227
-
228
- `jskelet migrate config <next-root> --write` drafts most of this. Then review:
229
-
230
- The `headers()`, `redirects()` and `rewrites()` sections are copied almost
231
- verbatim. Check the pattern syntax: JSkelet supports the `:slug`, `:path*`,
232
- `/a-:b` and `/:path*.svg` forms; more complex `path-to-regexp` expressions are
233
- not supported and produce a warning ([07-configuration.md](./07-configuration.md)).
234
-
235
- Move your `NEXT_PUBLIC_*` variables into the `clientEnv` list and simplify their
236
- names (the prefix no longer carries meaning).
237
-
238
- ### 3. Move the data layer (the easiest step)
239
-
240
- The API client and data functions under `lib/` usually do not depend on React;
241
- they are copied as-is. Make two changes:
242
-
243
- - Use `import { cache } from "jskelet"` instead of React's `cache()`.
244
- - Call `reportUpstreamFailure({ status, path })` on failed upstream responses.
245
- This prevents pages produced with missing data from being written to the cache
246
- ([06-caching.md](./06-caching.md)).
247
-
248
- ### 4. Set up the layout (half a day)
249
-
250
- Translate `app/layout.jsx` into `views/layout.jsk` (or let `migrate apply` draft
251
- it). Copying the framework's default layout (`jskelet/layout` → `.jsk`) and
252
- editing it is the fastest path.
253
-
254
- If you fetch data inside `layout.jsx` (navigation, site settings), move it into
255
- `hooks.layoutContext()`: it runs in parallel with the body render, and every
256
- field it returns becomes a layout local.
257
-
258
- Put your global metadata defaults (`titleTemplate`, `siteUrl`, `description`)
259
- into `hooks.metadata()`.
260
-
261
- ### 5. Translate the components (the longest step)
262
-
263
- `jskelet migrate apply --only components --write` converts presentational
264
- components that are props + JSX with no hooks. Everything else you finish by
265
- hand:
266
-
267
- Every React component turns into a function:
268
-
269
- ```jsx
270
- // Before
271
- export function Badge({ label, tone = "neutral", className }) {
272
- return <span className={cn("rounded px-2 py-1", TONES[tone], className)}>{label}</span>;
273
- }
274
- ```
275
-
276
- ```js
277
- // After — views/components/badge.js
278
- import { attrs, cn, esc } from "jskelet/html";
279
-
280
- export function badge({ label, tone = "neutral", class: className }) {
281
- return `<span${attrs({ class: cn("rounded px-2 py-1", TONES[tone], className) })}>${esc(label)}</span>`;
282
- }
283
- ```
284
-
285
- Things to watch out for:
286
-
287
- - **Escaping is now on you.** JSX escaped automatically; here you must call
288
- `esc()` when printing external data.
289
- - **`className` → `class`.** Since `class` is a reserved word in JS, rename it in
290
- the props as `class: className`.
291
- - **An `html` field instead of children.** Nested content is passed as a string.
292
- - Every function you place under `views/components/**` as a named export can be
293
- used in templates without importing it
294
- ([04-rendering.md](./04-rendering.md)).
295
-
296
- Keep components small and pure; leave data fetching in the controller.
297
-
298
- ### 6. Migrate the pages (hours per page)
299
-
300
- `jskelet migrate apply --only pages --write` splits each `page.*` into a
301
- feature controller plus a `.jsk` template. Review `partial` / `skipped` rows,
302
- then finish the TODO markers. File them with the order in mind:
303
-
304
- ```
305
- routes/
306
- ├── 00-health.mjs health check
307
- ├── 10-pages.mjs static paths: /, /about
308
- ├── 50-news.mjs /news/:slug
309
- └── 99-catch-all.mjs /:slug (if any, last of all)
310
- ```
311
-
312
- The places where you used `generateStaticParams()` turn into
313
- `hooks.prewarmPaths()`. If you have a function that produces the sitemap, use
314
- the same one.
315
-
316
- Move your `export const revalidate` values either into `route()`'s second
317
- argument or, to manage them from a single place, into `cache().html` patterns.
318
-
319
- ### 7. Turn client components into islands (hours per page)
320
-
321
- Every `"use client"` component becomes an island. The process:
322
-
323
- 1. Move the component's **static** output into the server template. Everything
324
- visible on first render must be in the HTML.
325
- 2. Write the remaining behavior inside `mount(element, props)`: `useState`
326
- becomes a local variable, `useEffect` a direct call, event handlers
327
- `on()`/`onClick()`.
328
- 3. Pass props as JSON with `data-island-props`.
329
- 4. Add it to the `registerAll()` map in the entry.
330
- 5. Choose the binding strategy: the default (visibility), `data-island-eager`
331
- (global behavior) or `data-island-idle` (heavy and non-critical).
332
-
333
- For components using Context, `createStore()` is the closest equivalent
334
- ([05-islands.md](./05-islands.md)).
335
-
336
- **This is where the biggest win of this step lies:** the hydrated area is not
337
- the whole page, only the parts that are genuinely interactive.
338
-
339
- ### 8. Move the CSS (1-2 hours)
340
-
341
- If your Tailwind configuration is already in v4 format, `styles/globals.css`
342
- stays almost the same. The one critical addition is the `@source` directives:
343
-
344
- ```css
345
- @import "tailwindcss" source(none);
346
-
347
- @source "../views";
348
- @source "../client";
349
- @source "../routes";
350
- @source "../lib";
351
- ```
352
-
353
- Without these, the classes used in templates (especially variants like
354
- `data-[state=open]:…`) are silently dropped
355
- ([08-build.md](./08-build.md)).
356
-
357
- If you use `next/font`, add `fonts: [{ family, weights }]` and write the
358
- `@font-face` blocks by hand; the generated files sit under `public/fonts/` with
359
- stable names.
360
-
361
- ### 9. Verify and measure
362
-
363
- - Browse with `jskelet dev` and confirm there are no errors in the dev overlay.
364
- - On the report page (`/__jskelet/dev/report`), look at each page's Web Vitals
365
- measurements, SSR size and island status ([09-dev-tools.md](./09-dev-tools.md)).
366
- - Clear the missing-icon warnings.
367
- - Compare the sizes in the `jskelet build` output with your old Next bundle.
368
- - Check that the `X-JSkelet-Cache` header returns `HIT` on the pages you expect.
369
-
370
- ### 10. Go live
371
-
372
- Go through the checklist in [10-deployment.md](./10-deployment.md). Keeping the
373
- old Next setup alongside for a while and shifting traffic gradually is useful,
374
- especially for verifying that the redirect rules are correct.
375
-
376
- ## Common mistakes during migration
377
-
378
- - **Forgetting `esc()`.** Writing `${value}` out of JSX habit means XSS. In
379
- `.jsk` templates use `{{ }}` (escaped) vs `{{{ }}}` (raw); in components call
380
- `esc()` yourself.
381
- - **Opening a new directory without adding `@source`.** The classes are silently
382
- dropped.
383
- - **Putting the catch-all route in the wrong order.** `/:slug` always goes last.
384
- - **Making the whole page an island.** The win comes from the server HTML being
385
- complete; bind the island only to the genuinely interactive part.
386
- - **Forgetting to pass `revalidate`.** The cache stays off, every request is
387
- rendered, and `X-JSkelet-Cache: MISS` is returned.
388
- - **Not calling `reportUpstreamFailure()`.** When the upstream goes down, the
389
- page produced with missing data is served for the entire TTL.
390
- - **Putting a secret key in `clientEnv`.** The values sit in the bundle as plain
391
- text.
392
-
393
- ## What's next
394
-
395
- - The reasons behind the architectural decisions:
396
- [02-architecture.md](./02-architecture.md)
397
- - Details of the island model: [05-islands.md](./05-islands.md)
398
- - Configuration reference: [07-configuration.md](./07-configuration.md)
1
+ # 11 — Migrating from Next.js
2
+
3
+ This document explains how to move a project using the Next.js App Router over
4
+ to JSkelet: a table of concept and API equivalents, an explicit list of what
5
+ cannot be migrated, and a step-by-step plan. JSkelet's surface was deliberately
6
+ modeled on the subset of Next that people actually use — concepts like the
7
+ `next.config` syntax, the Metadata API, `notFound()`, `revalidate` and `cache()`
8
+ will feel familiar. The *reasons* behind the differences are in
9
+ [02-architecture.md](./02-architecture.md).
10
+
11
+ ## `jskelet migrate` (codemod)
12
+
13
+ Run the codemod against an App Router tree. Babel (`@babel/parser`,
14
+ `@babel/types`) ships with JSkelet — no extra install.
15
+
16
+ ```bash
17
+ npx jskelet migrate scan ../my-next-app
18
+ npx jskelet migrate apply ../my-next-app --out . --write
19
+ npx jskelet migrate config ../my-next-app --write
20
+ ```
21
+
22
+ | Command | What it does |
23
+ | --- | --- |
24
+ | `migrate` / `migrate scan` | Inventory pages, layouts, `"use client"` modules, blockers (nested layouts, Server Actions, Suspense). |
25
+ | `migrate apply` | **Automatic convert:** `page.*` → feature controller + `.jsk`; presentational components → `views/components/*.js`; clients → island `mount()` stubs. Default is dry-run; pass `--write`. Never overwrites (conflicts get a `.migrate` suffix). |
26
+ | `migrate config` | Draft `jskelet.config.mjs` from `next.config` (`headers` / `redirects` / `rewrites`, `images.widths`, `NEXT_PUBLIC_*` → `clientEnv`). |
27
+
28
+ Flags: `--out <dir>`, `--only pages,components,islands`, `--json`, `--strict` (exit 1 on partial/skipped).
29
+
30
+ **Converted automatically:** `className`, `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}`, `next/image` → `<Image />`, `next/link` → `<Link />`, `dangerouslySetInnerHTML`, `revalidate`, simple controller prelude (`await` data + `notFound()`).
31
+
32
+ **Not converted (reported):** React hooks, Server Actions, nested layout flattening, Streaming/Suspense, client-side routing. Confidence per file is `ok` / `partial` / `skipped`.
33
+
34
+ ## Equivalence table
35
+
36
+ ### Configuration
37
+
38
+ | Next.js | JSkelet | Note |
39
+ | --- | --- | --- |
40
+ | `next.config.mjs` | `jskelet.config.mjs` | Same spirit, smaller surface ([07](./07-configuration.md)) |
41
+ | `headers()` | `headers()` | Same shape: `{ source, headers: [{ key, value }] }` |
42
+ | `redirects()` | `redirects()` | `permanent` → 308, otherwise 307; can be overridden with `statusCode` |
43
+ | `trailingSlash` | `trailingSlash` | `true` → canonical URLs end with `/` (308); default `false` does not strip ([07](./07-configuration.md)) |
44
+ | `rewrites()` | `rewrites()` | There are `beforeFiles` / `afterFiles` phases; no `fallback` |
45
+ | `compress: true` | Automatic | brotli + gzip via `node:zlib` |
46
+ | `images.deviceSizes` | `images.widths` | Build-time webp generation ([08](./08-build.md)) |
47
+ | `NEXT_PUBLIC_*` | `clientEnv: [...]` | Which key is exposed is clear from the config, not from the name |
48
+ | `experimental.*` | — | None |
49
+
50
+ ### Routing and rendering
51
+
52
+ | Next.js | JSkelet | Note |
53
+ | --- | --- | --- |
54
+ | `app/page.js` (file-based routing) | `app.get(...)` inside `routes/*.mjs` | The order is written explicitly ([03](./03-routing.md)) |
55
+ | `app/[slug]/page.js` | `app.get("/:slug", route(...))` | Express pattern syntax |
56
+ | `params`, `searchParams` | `ctx.params`, `ctx.query` | The controller's single argument |
57
+ | `layout.js` | `views/layout.jsk` + `hooks.layoutContext()` | A single layout; no nested layouts |
58
+ | Server component (RSC) | Controller + `.jsk` template + `views/components/**` | A function returns an HTML string |
59
+ | Client component (`"use client"`) | Island (`data-island` + `mount`) | The whole page is not hydrated ([05](./05-islands.md)) |
60
+ | `notFound()` | `notFound()` | Same name, same control flow |
61
+ | `redirect()` | `redirect()` (307) | For permanent, `permanentRedirect()` (308) |
62
+ | `not-found.js` | `hooks.notFound()` | Returns a page definition |
63
+ | `error.js` | Express error handler | The framework returns minimal HTML for a 500 |
64
+ | `loading.js` / Suspense | — | The server HTML is complete; no skeleton needed |
65
+ | Streaming SSR | — | The response is a single chunk |
66
+ | `generateMetadata()` | Controller `metadata` + `hooks.metadata()` | Same field names ([04](./04-rendering.md)) |
67
+ | `opengraph-image.tsx` / `ImageResponse` | `ogHandler` + `ImageResponse` / `sendOgImage` | SVG or card fields → PNG (`sharp`); [04](./04-rendering.md) |
68
+ | `generateStaticParams()` | `hooks.prewarmPaths()` | Warming at startup time, not build time |
69
+ | Route Handlers (`route.js`) | A plain Express handler | `app.get/post(...)` |
70
+ | Middleware (`middleware.ts`) | Express middleware + config `rewrites`/`headers`/`redirects` | `app.use(...)` |
71
+
72
+ ### Data and cache
73
+
74
+ | Next.js | JSkelet | Note |
75
+ | --- | --- | --- |
76
+ | `export const revalidate = 60` | `route(controller, { revalidate: 60 })` | Or `cache().html` ([06](./06-caching.md)) |
77
+ | ISR (prerender written to disk) | In-memory TTL cache + stale-while-revalidate | Nothing is written to disk |
78
+ | `fetch(..., { next: { revalidate } })` | — | The cache is at page level |
79
+ | `unstable_cache` | — | No cross-request data cache; there is a page cache |
80
+ | React `cache()` | `cache()` | Same behavior: in-request memoization |
81
+ | `revalidatePath()` | `invalidateHtmlCache("/news/:slug")` | A path, a pattern or a regexp; stales rather than deletes by default |
82
+ | `revalidateTag()` | `clearDataCache("news:")` | No tags to declare: the dependency is observed during the render ([06](./06-caching.md)) |
83
+ | `cookies()`, `headers()` | `ctx.req.headers`, `ctx.req.cookies`* | Direct access to the Express object |
84
+ | `dynamic = "force-dynamic"` | Not passing `revalidate` | Which means the cache is off |
85
+
86
+ \* Express 5 does not parse cookies on its own; add `cookie-parser` or read the
87
+ header manually.
88
+
89
+ ### Components and helpers
90
+
91
+ | Next.js | JSkelet | Note |
92
+ | --- | --- | --- |
93
+ | `next/link` | `link({ href, text })` — `jskelet/tags` | `title` automatic, `rel`/`target` automatic for external links |
94
+ | `next/link` prefetching | `navigation: { prefetch, prerender }` | Speculation Rules; no client runtime ([07](./07-configuration.md)) |
95
+ | `next/image` | `image({ src, alt, priority })` — `jskelet/tags` | `srcset` from the build manifest |
96
+ | `next/og` `ImageResponse` | `ImageResponse` / `ogHandler` — `jskelet` | No JSX; SVG or `title`/`description` card |
97
+ | `next/font/google` | `fonts: [{ family, weights }]` | Self-hosted woff2, committed |
98
+ | `@phosphor-icons/react` | `icon({ name, weight })` — `jskelet/tags` | Build-time SVG sprite |
99
+ | `react-dom` preconnect/preload | `preconnect: [...]` + `headHints()` | ([04](./04-rendering.md)) |
100
+ | `clsx` | `cx()` — `jskelet/html` | — |
101
+ | `cn()` (clsx + tailwind-merge) | `cn()` — `jskelet/html` | Same behavior |
102
+ | JSX automatic escaping | `esc()` — `jskelet/html` | **You have to call it yourself** |
103
+ | React Context | `createStore()` — `jskelet/client` | Minimal pub/sub |
104
+ | `useState` / `useEffect` | Plain JS inside the island's `mount()` | — |
105
+ | `useSyncExternalStore` | `store.subscribe()` | — |
106
+ | `<Script>` | A `<script>` in the layout, or an island | — |
107
+
108
+ ### What has no equivalent
109
+
110
+ Account for these from the start in your migration plan:
111
+
112
+ - **React itself.** Components turn into functions that return HTML strings. No
113
+ JSX, no hooks, no virtual DOM.
114
+ - **TypeScript.** Framework source is plain JS + JSDoc and publishes `.d.ts` for
115
+ consumers. Client entries and islands may be `.ts` / `.mts` (esbuild strips
116
+ types; the manifest key stays `*.js`). Server routes, hooks and
117
+ `jskelet.config.mjs` remain Node ESM JavaScript — use `checkJs: true` in
118
+ `jsconfig.json` for editor checking there.
119
+ - **Nested layouts.** There is a single layout; you share common sections with
120
+ EJS `include` or component functions.
121
+ - **Streaming / Suspense / partial prerendering.** The response is produced as a
122
+ single chunk.
123
+ - **Client-side routing.** Navigation is a real page load. Because the server
124
+ HTML comes from the cache it is very fast in practice, but there are no SPA
125
+ transitions. What closes the gap is the `navigation` section:
126
+ prefetch/prerender prepares the document before the click, and
127
+ `viewTransition` smooths the transition
128
+ ([07](./07-configuration.md)).
129
+ - **Server Actions.** Form submissions are ordinary `app.post(...)` handlers.
130
+ - **Automatic image optimization (at request time).** Local `public/` images
131
+ are still optimized at build time. Remote `http(s)` images can be proxied at
132
+ runtime when `images.remote.allowHosts` is set (`/_jskelet/image` → webp);
133
+ without that config they are emitted as-is.
134
+
135
+ ## A side-by-side example
136
+
137
+ **Next.js (App Router):**
138
+
139
+ ```jsx
140
+ // app/news/[slug]/page.jsx
141
+ import { notFound } from "next/navigation";
142
+ import Image from "next/image";
143
+ import { getArticle } from "@/lib/api";
144
+
145
+ export const revalidate = 300;
146
+
147
+ export async function generateMetadata({ params }) {
148
+ const article = await getArticle(params.slug);
149
+ return {
150
+ title: article?.title,
151
+ description: article?.summary,
152
+ alternates: { canonical: `/news/${params.slug}` },
153
+ };
154
+ }
155
+
156
+ export default async function Page({ params }) {
157
+ const article = await getArticle(params.slug);
158
+ if (!article) notFound();
159
+
160
+ return (
161
+ <article className="wrapper">
162
+ <h1 className="text-3xl font-bold">{article.title}</h1>
163
+ <Image src={article.cover} alt={article.title} priority width={1200} height={630} />
164
+ <div dangerouslySetInnerHTML={{ __html: article.body }} />
165
+ </article>
166
+ );
167
+ }
168
+ ```
169
+
170
+ **JSkelet:**
171
+
172
+ ```js
173
+ // routes/50-news.mjs
174
+ import { getArticle } from "@/lib/api.js";
175
+
176
+ export default function register(app, { route, notFound }) {
177
+ app.get(
178
+ "/news/:slug",
179
+ route(
180
+ async ({ params }) => {
181
+ const article = await getArticle(params.slug);
182
+ if (!article) notFound();
183
+
184
+ return {
185
+ view: "pages/article",
186
+ data: { article },
187
+ metadata: {
188
+ title: article.title,
189
+ description: article.summary,
190
+ canonical: `/news/${params.slug}`,
191
+ openGraph: { type: "article", image: article.cover },
192
+ },
193
+ };
194
+ },
195
+ { revalidate: 300 },
196
+ ),
197
+ );
198
+ }
199
+ ```
200
+
201
+ ```jsk
202
+ {# views/pages/article.jsk #}
203
+ <article class="wrapper">
204
+ <h1 class="text-3xl font-bold">{{ article.title }}</h1>
205
+ <Image :src="article.cover" :alt="article.title" priority :width="1200" :height="630" />
206
+ <div>{{{ article.body }}}</div>
207
+ </article>
208
+ ```
209
+
210
+ Wrapping `getArticle` with `cache()` makes sure that if
211
+ `hooks.layoutContext()` asks for the same article in the same render, only a
212
+ single upstream request is made ([06-caching.md](./06-caching.md)).
213
+
214
+ ## Step-by-step plan
215
+
216
+ ### 1. Set up the skeleton (half a day)
217
+
218
+ Run `npx jskelet init` in a new directory and watch `jskelet dev` come up. Leave
219
+ the existing Next project as it is; let the migration run in parallel. Optionally
220
+ run `jskelet migrate scan <next-root>` first to list pages and blockers.
221
+
222
+ Carry over the `paths` aliases from your `jsconfig.json` — prefixes like `@/`
223
+ work the same way both on the server and in the bundle
224
+ ([02-architecture.md](./02-architecture.md)).
225
+
226
+ ### 2. Translate `next.config.mjs` (1-2 hours)
227
+
228
+ `jskelet migrate config <next-root> --write` drafts most of this. Then review:
229
+
230
+ The `headers()`, `redirects()` and `rewrites()` sections are copied almost
231
+ verbatim. Check the pattern syntax: JSkelet supports the `:slug`, `:path*`,
232
+ `/a-:b` and `/:path*.svg` forms; more complex `path-to-regexp` expressions are
233
+ not supported and produce a warning ([07-configuration.md](./07-configuration.md)).
234
+
235
+ Move your `NEXT_PUBLIC_*` variables into the `clientEnv` list and simplify their
236
+ names (the prefix no longer carries meaning).
237
+
238
+ ### 3. Move the data layer (the easiest step)
239
+
240
+ The API client and data functions under `lib/` usually do not depend on React;
241
+ they are copied as-is. Make two changes:
242
+
243
+ - Use `import { cache } from "jskelet"` instead of React's `cache()`.
244
+ - Call `reportUpstreamFailure({ status, path })` on failed upstream responses.
245
+ This prevents pages produced with missing data from being written to the cache
246
+ ([06-caching.md](./06-caching.md)).
247
+
248
+ ### 4. Set up the layout (half a day)
249
+
250
+ Translate `app/layout.jsx` into `views/layout.jsk` (or let `migrate apply` draft
251
+ it). Copying the framework's default layout (`jskelet/layout` → `.jsk`) and
252
+ editing it is the fastest path.
253
+
254
+ If you fetch data inside `layout.jsx` (navigation, site settings), move it into
255
+ `hooks.layoutContext()`: it runs in parallel with the body render, and every
256
+ field it returns becomes a layout local.
257
+
258
+ Put your global metadata defaults (`titleTemplate`, `siteUrl`, `description`)
259
+ into `hooks.metadata()`.
260
+
261
+ ### 5. Translate the components (the longest step)
262
+
263
+ `jskelet migrate apply --only components --write` converts presentational
264
+ components that are props + JSX with no hooks. Everything else you finish by
265
+ hand:
266
+
267
+ Every React component turns into a function:
268
+
269
+ ```jsx
270
+ // Before
271
+ export function Badge({ label, tone = "neutral", className }) {
272
+ return <span className={cn("rounded px-2 py-1", TONES[tone], className)}>{label}</span>;
273
+ }
274
+ ```
275
+
276
+ ```js
277
+ // After — views/components/badge.js
278
+ import { attrs, cn, esc } from "jskelet/html";
279
+
280
+ export function badge({ label, tone = "neutral", class: className }) {
281
+ return `<span${attrs({ class: cn("rounded px-2 py-1", TONES[tone], className) })}>${esc(label)}</span>`;
282
+ }
283
+ ```
284
+
285
+ Things to watch out for:
286
+
287
+ - **Escaping is now on you.** JSX escaped automatically; here you must call
288
+ `esc()` when printing external data.
289
+ - **`className` → `class`.** Since `class` is a reserved word in JS, rename it in
290
+ the props as `class: className`.
291
+ - **An `html` field instead of children.** Nested content is passed as a string.
292
+ - Every function you place under `views/components/**` as a named export can be
293
+ used in templates without importing it
294
+ ([04-rendering.md](./04-rendering.md)).
295
+
296
+ Keep components small and pure; leave data fetching in the controller.
297
+
298
+ ### 6. Migrate the pages (hours per page)
299
+
300
+ `jskelet migrate apply --only pages --write` splits each `page.*` into a
301
+ feature controller plus a `.jsk` template. Review `partial` / `skipped` rows,
302
+ then finish the TODO markers. File them with the order in mind:
303
+
304
+ ```
305
+ routes/
306
+ ├── 00-health.mjs health check
307
+ ├── 10-pages.mjs static paths: /, /about
308
+ ├── 50-news.mjs /news/:slug
309
+ └── 99-catch-all.mjs /:slug (if any, last of all)
310
+ ```
311
+
312
+ The places where you used `generateStaticParams()` turn into
313
+ `hooks.prewarmPaths()`. If you have a function that produces the sitemap, use
314
+ the same one.
315
+
316
+ Move your `export const revalidate` values either into `route()`'s second
317
+ argument or, to manage them from a single place, into `cache().html` patterns.
318
+
319
+ ### 7. Turn client components into islands (hours per page)
320
+
321
+ Every `"use client"` component becomes an island. The process:
322
+
323
+ 1. Move the component's **static** output into the server template. Everything
324
+ visible on first render must be in the HTML.
325
+ 2. Write the remaining behavior inside `mount(element, props)`: `useState`
326
+ becomes a local variable, `useEffect` a direct call, event handlers
327
+ `on()`/`onClick()`.
328
+ 3. Pass props as JSON with `data-island-props`.
329
+ 4. Add it to the `registerAll()` map in the entry.
330
+ 5. Choose the binding strategy: the default (visibility), `data-island-eager`
331
+ (global behavior) or `data-island-idle` (heavy and non-critical).
332
+
333
+ For components using Context, `createStore()` is the closest equivalent
334
+ ([05-islands.md](./05-islands.md)).
335
+
336
+ **This is where the biggest win of this step lies:** the hydrated area is not
337
+ the whole page, only the parts that are genuinely interactive.
338
+
339
+ ### 8. Move the CSS (1-2 hours)
340
+
341
+ If your Tailwind configuration is already in v4 format, `styles/globals.css`
342
+ stays almost the same. The one critical addition is the `@source` directives:
343
+
344
+ ```css
345
+ @import "tailwindcss" source(none);
346
+
347
+ @source "../views";
348
+ @source "../client";
349
+ @source "../routes";
350
+ @source "../lib";
351
+ ```
352
+
353
+ Without these, the classes used in templates (especially variants like
354
+ `data-[state=open]:…`) are silently dropped
355
+ ([08-build.md](./08-build.md)).
356
+
357
+ If you use `next/font`, add `fonts: [{ family, weights }]` and write the
358
+ `@font-face` blocks by hand; the generated files sit under `public/fonts/` with
359
+ stable names.
360
+
361
+ ### 9. Verify and measure
362
+
363
+ - Browse with `jskelet dev` and confirm there are no errors in the dev overlay.
364
+ - On the report page (`/__jskelet/dev/report`), look at each page's Web Vitals
365
+ measurements, SSR size and island status ([09-dev-tools.md](./09-dev-tools.md)).
366
+ - Clear the missing-icon warnings.
367
+ - Compare the sizes in the `jskelet build` output with your old Next bundle.
368
+ - Check that the `X-JSkelet-Cache` header returns `HIT` on the pages you expect.
369
+
370
+ ### 10. Go live
371
+
372
+ Go through the checklist in [10-deployment.md](./10-deployment.md). Keeping the
373
+ old Next setup alongside for a while and shifting traffic gradually is useful,
374
+ especially for verifying that the redirect rules are correct.
375
+
376
+ ## Common mistakes during migration
377
+
378
+ - **Forgetting `esc()`.** Writing `${value}` out of JSX habit means XSS. In
379
+ `.jsk` templates use `{{ }}` (escaped) vs `{{{ }}}` (raw); in components call
380
+ `esc()` yourself.
381
+ - **Opening a new directory without adding `@source`.** The classes are silently
382
+ dropped.
383
+ - **Putting the catch-all route in the wrong order.** `/:slug` always goes last.
384
+ - **Making the whole page an island.** The win comes from the server HTML being
385
+ complete; bind the island only to the genuinely interactive part.
386
+ - **Forgetting to pass `revalidate`.** The cache stays off, every request is
387
+ rendered, and `X-JSkelet-Cache: MISS` is returned.
388
+ - **Not calling `reportUpstreamFailure()`.** When the upstream goes down, the
389
+ page produced with missing data is served for the entire TTL.
390
+ - **Putting a secret key in `clientEnv`.** The values sit in the bundle as plain
391
+ text.
392
+
393
+ ## What's next
394
+
395
+ - The reasons behind the architectural decisions:
396
+ [02-architecture.md](./02-architecture.md)
397
+ - Details of the island model: [05-islands.md](./05-islands.md)
398
+ - Configuration reference: [07-configuration.md](./07-configuration.md)