jskelet 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -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)