@elurjs/kit 2.4.4

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 (255) hide show
  1. package/CHANGELOG.md +874 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1330 -0
  4. package/bin/elur-kit.js +54 -0
  5. package/dist/lib/action/define.d.cts +72 -0
  6. package/dist/lib/action/define.d.ts +72 -0
  7. package/dist/lib/action/error-store.d.cts +29 -0
  8. package/dist/lib/action/error-store.d.ts +29 -0
  9. package/dist/lib/action/index.cjs +1 -0
  10. package/dist/lib/action/index.d.cts +64 -0
  11. package/dist/lib/action/index.d.ts +64 -0
  12. package/dist/lib/action/index.js +2 -0
  13. package/dist/lib/action/origin.d.cts +20 -0
  14. package/dist/lib/action/origin.d.ts +20 -0
  15. package/dist/lib/action/scan.d.cts +28 -0
  16. package/dist/lib/action/scan.d.ts +28 -0
  17. package/dist/lib/action/server.d.cts +30 -0
  18. package/dist/lib/action/server.d.ts +30 -0
  19. package/dist/lib/action-BB6N5yD-.js +111 -0
  20. package/dist/lib/action-BB6N5yD-.js.map +1 -0
  21. package/dist/lib/action-DZHihU3Z.cjs +2 -0
  22. package/dist/lib/action-DZHihU3Z.cjs.map +1 -0
  23. package/dist/lib/adapters/bun.cjs +30 -0
  24. package/dist/lib/adapters/bun.cjs.map +1 -0
  25. package/dist/lib/adapters/bun.d.cts +13 -0
  26. package/dist/lib/adapters/bun.d.ts +13 -0
  27. package/dist/lib/adapters/bun.js +65 -0
  28. package/dist/lib/adapters/bun.js.map +1 -0
  29. package/dist/lib/adapters/index.cjs +0 -0
  30. package/dist/lib/adapters/index.d.cts +30 -0
  31. package/dist/lib/adapters/index.d.ts +30 -0
  32. package/dist/lib/adapters/index.js +0 -0
  33. package/dist/lib/adapters/netlify.cjs +10 -0
  34. package/dist/lib/adapters/netlify.cjs.map +1 -0
  35. package/dist/lib/adapters/netlify.d.cts +13 -0
  36. package/dist/lib/adapters/netlify.d.ts +13 -0
  37. package/dist/lib/adapters/netlify.js +51 -0
  38. package/dist/lib/adapters/netlify.js.map +1 -0
  39. package/dist/lib/adapters/node.cjs +46 -0
  40. package/dist/lib/adapters/node.cjs.map +1 -0
  41. package/dist/lib/adapters/node.d.cts +13 -0
  42. package/dist/lib/adapters/node.d.ts +13 -0
  43. package/dist/lib/adapters/node.js +105 -0
  44. package/dist/lib/adapters/node.js.map +1 -0
  45. package/dist/lib/adapters/shared.d.cts +17 -0
  46. package/dist/lib/adapters/shared.d.ts +17 -0
  47. package/dist/lib/adapters/vercel.cjs +2 -0
  48. package/dist/lib/adapters/vercel.cjs.map +1 -0
  49. package/dist/lib/adapters/vercel.d.cts +9 -0
  50. package/dist/lib/adapters/vercel.d.ts +9 -0
  51. package/dist/lib/adapters/vercel.js +62 -0
  52. package/dist/lib/adapters/vercel.js.map +1 -0
  53. package/dist/lib/build/build.d.cts +83 -0
  54. package/dist/lib/build/build.d.ts +83 -0
  55. package/dist/lib/build/document-shell.d.cts +48 -0
  56. package/dist/lib/build/document-shell.d.ts +48 -0
  57. package/dist/lib/build/transform-source.d.cts +26 -0
  58. package/dist/lib/build/transform-source.d.ts +26 -0
  59. package/dist/lib/build/vite-build.d.cts +78 -0
  60. package/dist/lib/build/vite-build.d.ts +78 -0
  61. package/dist/lib/cache/policy.d.cts +25 -0
  62. package/dist/lib/cache/policy.d.ts +25 -0
  63. package/dist/lib/cache.d.cts +13 -0
  64. package/dist/lib/cache.d.ts +13 -0
  65. package/dist/lib/capabilities-C0TDQoMY.cjs +2 -0
  66. package/dist/lib/capabilities-C0TDQoMY.cjs.map +1 -0
  67. package/dist/lib/capabilities-C7txofmY.js +45 -0
  68. package/dist/lib/capabilities-C7txofmY.js.map +1 -0
  69. package/dist/lib/cli/commands.d.cts +15 -0
  70. package/dist/lib/cli/commands.d.ts +15 -0
  71. package/dist/lib/cli.cjs +5132 -0
  72. package/dist/lib/cli.cjs.map +1 -0
  73. package/dist/lib/cli.d.cts +30 -0
  74. package/dist/lib/cli.d.ts +30 -0
  75. package/dist/lib/cli.js +5131 -0
  76. package/dist/lib/cli.js.map +1 -0
  77. package/dist/lib/collections-Dbc8aRLf.js +233 -0
  78. package/dist/lib/collections-Dbc8aRLf.js.map +1 -0
  79. package/dist/lib/collections-VDy5g9p8.cjs +13 -0
  80. package/dist/lib/collections-VDy5g9p8.cjs.map +1 -0
  81. package/dist/lib/config/index.cjs +2 -0
  82. package/dist/lib/config/index.cjs.map +1 -0
  83. package/dist/lib/config/index.d.cts +100 -0
  84. package/dist/lib/config/index.d.ts +100 -0
  85. package/dist/lib/config/index.js +122 -0
  86. package/dist/lib/config/index.js.map +1 -0
  87. package/dist/lib/content/collections.d.cts +69 -0
  88. package/dist/lib/content/collections.d.ts +69 -0
  89. package/dist/lib/content/frontmatter.d.cts +20 -0
  90. package/dist/lib/content/frontmatter.d.ts +20 -0
  91. package/dist/lib/content/index.cjs +2 -0
  92. package/dist/lib/content/index.cjs.map +1 -0
  93. package/dist/lib/content/index.d.cts +5 -0
  94. package/dist/lib/content/index.d.ts +5 -0
  95. package/dist/lib/content/index.js +28 -0
  96. package/dist/lib/content/index.js.map +1 -0
  97. package/dist/lib/content/markdown.d.cts +6 -0
  98. package/dist/lib/content/markdown.d.ts +6 -0
  99. package/dist/lib/content/raw.d.cts +9 -0
  100. package/dist/lib/content/raw.d.ts +9 -0
  101. package/dist/lib/content/schema.d.cts +14 -0
  102. package/dist/lib/content/schema.d.ts +14 -0
  103. package/dist/lib/errors-0X7pCFNU.cjs +2 -0
  104. package/dist/lib/errors-0X7pCFNU.cjs.map +1 -0
  105. package/dist/lib/errors-ePYxDlhU.js +70 -0
  106. package/dist/lib/errors-ePYxDlhU.js.map +1 -0
  107. package/dist/lib/errors.d.cts +84 -0
  108. package/dist/lib/errors.d.ts +84 -0
  109. package/dist/lib/hydrate-8a3TbQbs.cjs +2 -0
  110. package/dist/lib/hydrate-8a3TbQbs.cjs.map +1 -0
  111. package/dist/lib/hydrate-CgZGrTxF.js +126 -0
  112. package/dist/lib/hydrate-CgZGrTxF.js.map +1 -0
  113. package/dist/lib/image/index.cjs +2 -0
  114. package/dist/lib/image/index.cjs.map +1 -0
  115. package/dist/lib/image/index.d.cts +37 -0
  116. package/dist/lib/image/index.d.ts +37 -0
  117. package/dist/lib/image/index.js +49 -0
  118. package/dist/lib/image/index.js.map +1 -0
  119. package/dist/lib/image/pipeline.d.cts +37 -0
  120. package/dist/lib/image/pipeline.d.ts +37 -0
  121. package/dist/lib/image/registry.cjs +2 -0
  122. package/dist/lib/image/registry.cjs.map +1 -0
  123. package/dist/lib/image/registry.d.cts +26 -0
  124. package/dist/lib/image/registry.d.ts +26 -0
  125. package/dist/lib/image/registry.js +23 -0
  126. package/dist/lib/image/registry.js.map +1 -0
  127. package/dist/lib/image/service.cjs +2 -0
  128. package/dist/lib/image/service.cjs.map +1 -0
  129. package/dist/lib/image/service.d.cts +164 -0
  130. package/dist/lib/image/service.d.ts +164 -0
  131. package/dist/lib/image/service.js +308 -0
  132. package/dist/lib/image/service.js.map +1 -0
  133. package/dist/lib/index.cjs +1 -0
  134. package/dist/lib/index.cjs.map +1 -0
  135. package/dist/lib/index.d.cts +38 -0
  136. package/dist/lib/index.d.ts +38 -0
  137. package/dist/lib/index.js +1 -0
  138. package/dist/lib/index.js.map +1 -0
  139. package/dist/lib/integrations/hooks.d.cts +58 -0
  140. package/dist/lib/integrations/hooks.d.ts +58 -0
  141. package/dist/lib/integrations/index.cjs +1 -0
  142. package/dist/lib/integrations/index.d.cts +18 -0
  143. package/dist/lib/integrations/index.d.ts +18 -0
  144. package/dist/lib/integrations/index.js +2 -0
  145. package/dist/lib/integrations-7dpN1RYB.js +35 -0
  146. package/dist/lib/integrations-7dpN1RYB.js.map +1 -0
  147. package/dist/lib/integrations-DDqfWjvH.cjs +2 -0
  148. package/dist/lib/integrations-DDqfWjvH.cjs.map +1 -0
  149. package/dist/lib/interpolation-plugin-Bt-KRKVY.js +365 -0
  150. package/dist/lib/interpolation-plugin-Bt-KRKVY.js.map +1 -0
  151. package/dist/lib/interpolation-plugin-CMPCDlVJ.cjs +31 -0
  152. package/dist/lib/interpolation-plugin-CMPCDlVJ.cjs.map +1 -0
  153. package/dist/lib/island/generate-entry.d.cts +27 -0
  154. package/dist/lib/island/generate-entry.d.ts +27 -0
  155. package/dist/lib/island/hydrate.d.cts +33 -0
  156. package/dist/lib/island/hydrate.d.ts +33 -0
  157. package/dist/lib/island/index.cjs +1 -0
  158. package/dist/lib/island/index.d.cts +2 -0
  159. package/dist/lib/island/index.d.ts +2 -0
  160. package/dist/lib/island/index.js +1 -0
  161. package/dist/lib/island/island.d.cts +32 -0
  162. package/dist/lib/island/island.d.ts +32 -0
  163. package/dist/lib/island/scan.d.cts +14 -0
  164. package/dist/lib/island/scan.d.ts +14 -0
  165. package/dist/lib/manifest/index.cjs +3 -0
  166. package/dist/lib/manifest/index.cjs.map +1 -0
  167. package/dist/lib/manifest/index.d.cts +18 -0
  168. package/dist/lib/manifest/index.d.ts +18 -0
  169. package/dist/lib/manifest/index.js +110 -0
  170. package/dist/lib/manifest/index.js.map +1 -0
  171. package/dist/lib/middleware/index.d.cts +56 -0
  172. package/dist/lib/middleware/index.d.ts +56 -0
  173. package/dist/lib/middleware/stream-boundary.d.cts +53 -0
  174. package/dist/lib/middleware/stream-boundary.d.ts +53 -0
  175. package/dist/lib/node-http-DRAUhO0c.js +617 -0
  176. package/dist/lib/node-http-DRAUhO0c.js.map +1 -0
  177. package/dist/lib/node-http-DhxguYyz.cjs +13 -0
  178. package/dist/lib/node-http-DhxguYyz.cjs.map +1 -0
  179. package/dist/lib/render/render-to-string.d.cts +12 -0
  180. package/dist/lib/render/render-to-string.d.ts +12 -0
  181. package/dist/lib/render/ssr-flag.d.cts +4 -0
  182. package/dist/lib/render/ssr-flag.d.ts +4 -0
  183. package/dist/lib/route-scanner-JY6YrNH5.cjs +2 -0
  184. package/dist/lib/route-scanner-JY6YrNH5.cjs.map +1 -0
  185. package/dist/lib/route-scanner-XcLId7lK.js +100 -0
  186. package/dist/lib/route-scanner-XcLId7lK.js.map +1 -0
  187. package/dist/lib/router/client.cjs +2 -0
  188. package/dist/lib/router/client.cjs.map +1 -0
  189. package/dist/lib/router/client.d.cts +47 -0
  190. package/dist/lib/router/client.d.ts +47 -0
  191. package/dist/lib/router/client.js +298 -0
  192. package/dist/lib/router/client.js.map +1 -0
  193. package/dist/lib/router/route-scanner.d.cts +50 -0
  194. package/dist/lib/router/route-scanner.d.ts +50 -0
  195. package/dist/lib/runtime/capabilities.d.cts +47 -0
  196. package/dist/lib/runtime/capabilities.d.ts +47 -0
  197. package/dist/lib/runtime/context.d.cts +140 -0
  198. package/dist/lib/runtime/context.d.ts +140 -0
  199. package/dist/lib/runtime/handler.d.cts +39 -0
  200. package/dist/lib/runtime/handler.d.ts +39 -0
  201. package/dist/lib/runtime/index.cjs +1 -0
  202. package/dist/lib/runtime/index.d.cts +6 -0
  203. package/dist/lib/runtime/index.d.ts +6 -0
  204. package/dist/lib/runtime/index.js +4 -0
  205. package/dist/lib/runtime/node-http.d.cts +2 -0
  206. package/dist/lib/runtime/node-http.d.ts +2 -0
  207. package/dist/lib/runtime/security-headers.d.cts +13 -0
  208. package/dist/lib/runtime/security-headers.d.ts +13 -0
  209. package/dist/lib/runtime/static.d.cts +1 -0
  210. package/dist/lib/runtime/static.d.ts +1 -0
  211. package/dist/lib/runtime-CrH5X-Es.cjs +2 -0
  212. package/dist/lib/runtime-CrH5X-Es.cjs.map +1 -0
  213. package/dist/lib/runtime-Ds2-wH6P.js +520 -0
  214. package/dist/lib/runtime-Ds2-wH6P.js.map +1 -0
  215. package/dist/lib/scan-CY2O9ty6.cjs +2 -0
  216. package/dist/lib/scan-CY2O9ty6.cjs.map +1 -0
  217. package/dist/lib/scan-CjMUgnsd.js +22 -0
  218. package/dist/lib/scan-CjMUgnsd.js.map +1 -0
  219. package/dist/lib/scan-DXzYtmRQ.cjs +2 -0
  220. package/dist/lib/scan-DXzYtmRQ.cjs.map +1 -0
  221. package/dist/lib/scan-Dn93ycXG.js +33 -0
  222. package/dist/lib/scan-Dn93ycXG.js.map +1 -0
  223. package/dist/lib/seo/index.cjs +10 -0
  224. package/dist/lib/seo/index.cjs.map +1 -0
  225. package/dist/lib/seo/index.d.cts +99 -0
  226. package/dist/lib/seo/index.d.ts +99 -0
  227. package/dist/lib/seo/index.js +41 -0
  228. package/dist/lib/seo/index.js.map +1 -0
  229. package/dist/lib/shared-CfqOLsKj.cjs +183 -0
  230. package/dist/lib/shared-CfqOLsKj.cjs.map +1 -0
  231. package/dist/lib/shared-DU3iQgr7.js +236 -0
  232. package/dist/lib/shared-DU3iQgr7.js.map +1 -0
  233. package/dist/lib/ssr/match.d.cts +25 -0
  234. package/dist/lib/ssr/match.d.ts +25 -0
  235. package/dist/lib/ssr/render.d.cts +62 -0
  236. package/dist/lib/ssr/render.d.ts +62 -0
  237. package/dist/lib/ssr/server.d.cts +41 -0
  238. package/dist/lib/ssr/server.d.ts +41 -0
  239. package/dist/lib/ssr/stream.d.cts +48 -0
  240. package/dist/lib/ssr/stream.d.ts +48 -0
  241. package/dist/lib/stream-CpOG052V.js +104 -0
  242. package/dist/lib/stream-CpOG052V.js.map +1 -0
  243. package/dist/lib/stream-DmZjYqHm.cjs +17 -0
  244. package/dist/lib/stream-DmZjYqHm.cjs.map +1 -0
  245. package/dist/lib/types.d.cts +97 -0
  246. package/dist/lib/types.d.ts +97 -0
  247. package/dist/lib/vite/index.cjs +2 -0
  248. package/dist/lib/vite/index.cjs.map +1 -0
  249. package/dist/lib/vite/index.d.cts +41 -0
  250. package/dist/lib/vite/index.d.ts +41 -0
  251. package/dist/lib/vite/index.js +229 -0
  252. package/dist/lib/vite/index.js.map +1 -0
  253. package/dist/lib/vite/interpolation-plugin.d.cts +64 -0
  254. package/dist/lib/vite/interpolation-plugin.d.ts +64 -0
  255. package/package.json +292 -0
package/README.md ADDED
@@ -0,0 +1,1330 @@
1
+ # Elur Kit
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@elurjs/kit.svg)](https://www.npmjs.com/package/@elurjs/kit)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ > Full-stack framework for Elur — file-based routing, SSG, SSR, ISR, streaming, islands, actions, content collections, cache adapters, and SPA-like navigation. Zero extra runtime dependencies on the client: Elur stays at ~15 KB. Optional build-time compiler lowers `html\`\`` templates to imperative DOM code for ~25–44% faster renders.
7
+
8
+ ## What is Elur Kit?
9
+
10
+ Elur Kit is a framework built on top of [Elur](https://elur.dev/). It brings conventions similar to Next.js App Router / Astro / SvelteKit to Elur:
11
+
12
+ - `src/app/page.ts` for pages
13
+ - `src/app/page.data.ts` for loaders
14
+ - `src/app/page.action.ts` for server actions
15
+ - `src/app/layout.ts` for layouts
16
+ - `src/app/route.ts` for API endpoints
17
+ - `src/app/loading.ts` for streaming boundaries
18
+ - `src/content/` for Markdown content collections
19
+ - `src/islands/` for client-side interactive components
20
+
21
+ ### Key features
22
+
23
+ - **Routing**: file-based with dynamic segments, optional catch-all `[[...slug]]`, route conflict detection, safe URL decoding, redirects/rewrites/route headers
24
+ - **Rendering**: SSG, SSR, ISR with explicit cache policy (public/private/dynamic), streaming with `ReadableStream` (**experimental** — fallback buffered por adapter; ver nota de streaming)
25
+ - **Build-time compiler** (optional, recommended): integrates [`@elurjs/core-compiler`](https://www.npmjs.com/package/@elurjs/core-compiler) via [`@elurjs/vite-plugin-elur`](https://www.npmjs.com/package/@elurjs/vite-plugin-elur) to lower `html\`\`` templates to imperative DOM code at build time — eliminates `detectContext`, `buildHTML`, and both `TreeWalker` passes in runtime
26
+ - **Partial attribute interpolation**: `class="btn ${size}"` works out of the box via the Vite plugin's state-machine lexer (or the kit's legacy transform as fallback)
27
+ - **Actions**: typed `defineAction()` with input validation, AbortSignal, idempotency, concurrency modes (latest/queue/parallel)
28
+ - **Cache**: `CacheAdapter` with filesystem, Redis, and Cloudflare KV storage, SHA-256 keys, atomic writes, single-flight, stale-while-revalidate, tag-based invalidation
29
+ - **Security**: HMAC-signed action error cookies, body limits, CSRF verification, default security headers (CSP, HSTS, X-Frame-Options, etc.), conditional static serving (ETag/Last-Modified)
30
+ - **Content**: per-request scope via `AsyncLocalStorage`, collection name containment, frontmatter parser, Markdown rendering, recursive nested collections
31
+ - **SEO**: sitemap generation from route manifest, sitemap index for large sites, robots.txt, JSON-LD with safe escaping
32
+ - **Integrations**: typed hooks for `elur-i18n`, `elur-auth`, `elur-query`, `elur-testing` — without adding them as dependencies
33
+ - **CLI**: `dev`, `build`, `preview`, `start`, `check`, `routes`, `doctor`, `adapter`
34
+ - **Observability**: structured logger with request ID, Server-Timing, sensitive data redaction
35
+ - **Adapters**: Node, Bun, Vercel, Netlify with capability-based deployment
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install @elurjs/core @elurjs/kit
41
+ ```
42
+
43
+ For the best performance, also install the Vite plugin (includes the build-time compiler):
44
+
45
+ ```bash
46
+ npm install @elurjs/vite-plugin-elur
47
+ ```
48
+
49
+ The plugin is an optional peer dependency. When installed, it activates:
50
+
51
+ - **Build-time compiler** — lowers `html\`\`` to imperative DOM code
52
+ - **Partial attribute interpolation** — state-machine lexer (replaces the kit's legacy transform)
53
+ - **HMR with state preservation** — signals, stores, forms, routers survive hot updates
54
+ - **Scroll/focus preservation** — restored after re-mount
55
+
56
+ ```bash
57
+ # or
58
+ bun add @elurjs/core @elurjs/kit @elurjs/vite-plugin-elur
59
+ ```
60
+
61
+ ## Quick example
62
+
63
+ ```ts
64
+ // src/app/page.data.ts
65
+ import type { PageDataLoad } from "@elurjs/kit";
66
+
67
+ export const load: PageDataLoad = async () => {
68
+ return { title: "Hello Elur Kit" };
69
+ };
70
+ ```
71
+
72
+ ```ts
73
+ // src/app/page.ts
74
+ import { html, signal } from "@elurjs/core";
75
+ import type { PageProps } from "@elurjs/kit";
76
+ import { load } from "./page.data.ts";
77
+
78
+ export default function HomePage({ data }: PageProps<typeof load>) {
79
+ const liked = signal(false);
80
+
81
+ return html`
82
+ <article>
83
+ <h1>${data.title}</h1>
84
+ <button @click=${() => (liked.value = !liked.value)}>
85
+ ${() => (liked.value ? "★ Liked" : "☆ Like")}
86
+ </button>
87
+ </article>
88
+ `;
89
+ }
90
+ ```
91
+
92
+ At build time, `elur-kit` runs the loader and renders the page to static HTML using `renderToString`.
93
+
94
+ ## CLI
95
+
96
+ After installing, the `elur-kit` binary is available in your project:
97
+
98
+ ```bash
99
+ elur-kit build
100
+ elur-kit dev
101
+ elur-kit preview
102
+ elur-kit start
103
+ elur-kit adapter vercel
104
+ elur-kit adapter netlify
105
+ elur-kit adapter bun
106
+ elur-kit adapter node
107
+ ```
108
+
109
+ By default it looks for `src/app/` and `src/islands/` and writes to `dist/`:
110
+
111
+ ```bash
112
+ elur-kit build
113
+ # → dist/index.html
114
+ # → dist/_elur/entry-client.js (after bundling the generated entry)
115
+ ```
116
+
117
+ Run the dev server with rebuild-on-change:
118
+
119
+ ```bash
120
+ elur-kit dev
121
+ ```
122
+
123
+ If you have a `vite.client.config.ts`, the client hydration bundle is built automatically. You can still pass an explicit config with `--client-config <path>`.
124
+
125
+ Serve the production build:
126
+
127
+ ```bash
128
+ elur-kit build
129
+ elur-kit preview
130
+ ```
131
+
132
+ Run the SSR server (renders pages on demand):
133
+
134
+ ```bash
135
+ elur-kit build # generate or update the client bundle
136
+ elur-kit start
137
+ ```
138
+
139
+ Enable ISR with a cache directory and default TTL:
140
+
141
+ ```bash
142
+ elur-kit start --cache-dir .elur/cache --default-revalidate 60
143
+ ```
144
+
145
+ Options:
146
+
147
+ | Flag | Default | Description |
148
+ | --- | --- | --- |
149
+ | `-r, --root <dir>` | `cwd` | Project root |
150
+ | `-a, --app <dir>` | `src/app` | Pages directory relative to root |
151
+ | `-i, --islands <dir>` | `src/islands` | Islands directory relative to root |
152
+ | `-o, --out <dir>` | `dist` | Output directory relative to root |
153
+ | `-p, --port <number>` | `3000` | Server port |
154
+ | `-h, --host <address>` | `127.0.0.1` | Server host |
155
+ | `-l, --lang <lang>` | `es` | HTML `lang` attribute |
156
+ | `--hydrate-import <spec>` | `@elurjs/kit/island` | Import path for `hydrateIslands` in generated entry |
157
+ | `--client-config <path>` | `vite.client.config.ts` (auto-detected) | Vite config used to build the client bundle in dev mode |
158
+
159
+ ## Core features (v2.0)
160
+
161
+ - **File-based routing** — `src/app/page.ts` maps to URLs with dynamic segments (`[slug]`), catch-all (`[...slug]`), optional catch-all (`[[...slug]]`), route groups `(group)`, and route conflict detection.
162
+ - **SSG, SSR, ISR** — static generation, on-demand SSR, and incremental static regeneration with explicit cache policy (`public`/`private`/`dynamic`), SHA-256 cache keys, atomic writes, single-flight, and tag-based invalidation.
163
+ - **Streaming (experimental)** — `ReadableStream`-based streaming with `loading.ts` boundaries, `createStreamingResponse()`, and `createBufferedResponse()` fallback for adapters without streaming. **Etiquetado como experimental** hasta completar la matriz de paridad streaming/buffered cross-host y la implementación de `renderToChunks()` en el core.
164
+ - **Server actions** — typed `defineAction()` with input validation (`.parse()`), AbortSignal propagation, idempotency metadata, concurrency modes (`latest`/`queue`/`parallel`), and progressive enhancement (plain HTML forms).
165
+ - **RequestContext** — per-request context with `params`, `locals`, `cookies` (CookieJar), `signal` (AbortSignal), `requestId`, `platform`, `route`, and mutable `response` state (headers, Set-Cookie, status). Aligned with runtime-security §4.
166
+ - **Unified Web handler** — `createWebHandler()` is the single entry point for all runtimes (Node, Bun, Vercel, Netlify, Vite dev **y el CLI `dev`/`preview`**). Every runtime is a thin wrapper; no duplicated routing/actions/static pipelines.
167
+ - **Cache security** — `shouldCachePublic()` rejects requests with cookies/Authorization. `isResultCacheable()` rejects HTML with action error markers. No personalized ISR cache leakage.
168
+ - **Public error sanitization** — production 500s use `toPublicErrorInfo()`/`publicErrorResponse()` (JSON, `no-store`), never exposing stacks, paths or secrets; request id is kept in internal logs.
169
+ - **CSRF protection** — `verifyOrigin()` checks `Origin`, `Referer`, `Host`, and `Sec-Fetch-Site` with allow-list and `strictOrigin` mode.
170
+ - **Static serving** — containment-enforced path resolution (rejects traversal, NUL, backslashes, symlinks), ETag/Last-Modified conditional requests, Range/If-Range with 206/416, HEAD sin body, immutable caching for hashed assets.
171
+ - **Security headers** — CSP with nonce support, HSTS (HTTPS only), X-Content-Type-Options, Referrer-Policy, X-Frame-Options, Permissions-Policy.
172
+ - **Content layer** — per-request scope via `AsyncLocalStorage`, collection name containment (no path traversal), frontmatter parser, Markdown rendering, recursive nested collections (`getCollection()` scans subdirectories and derives nested slugs).
173
+ - **SEO** — sitemap generation from route manifest, sitemap index for >50,000 URLs, robots.txt, JSON-LD with safe escaping (`<`, `>`, `&`, U+2028, U+2029).
174
+ - **Image optimization** — manifest-driven `<picture>` with content-addressed hashed variants, `<source>` per format, real dimensions, no upscales, Sharp optional.
175
+ - **Islands** — lazy `import()` per island, null/error isolation, `load`/`idle`/`visible` directives, auto-scan of `src/islands/`.
176
+ - **Client router** — AbortController + navigation token (no races), head/assets merge, aria-live announcer, canonical URL, View Transitions with reduced-motion fallback.
177
+ - **Middleware** — `src/middleware.ts` with path matchers, `next()` carries params/locals, cleanup in `finally`, runs in dev/preview/adapters.
178
+ - **Integrations** — typed hooks for `elur-i18n`, `elur-auth`, `elur-query`, `elur-testing` without adding them as dependencies.
179
+ - **CLI** — `dev`, `build`, `preview`, `start`, `check`, `routes`, `doctor`, `adapter` with reliable exit codes.
180
+ - **Observability** — structured logger with request ID, Server-Timing, sensitive data redaction (cookies, auth, tokens).
181
+ - **Adapters** — Node, Bun, Vercel, Netlify with relocatable paths (`import.meta.url`) and capability-based deployment.
182
+ - **Atomic build** — staging outside `dist/`, Vite JS API (no `npx`), `copyPublicAssets()`, final swap only on success.
183
+ - **`throw new Response()`** — first-class HTTP control flow from loaders and layout loaders (redirects, 404, etc.).
184
+ - **HMAC-signed action errors** — action error cookies signed with SHA-256, rejects tampered/forged values.
185
+
186
+ ## What's new in v2.4
187
+
188
+ - **Fixed: image pipeline silently no-op** — the CLI bundle was
189
+ inlining its own copy of the image registry, so `consumeImageRegistry()`
190
+ always returned `[]` and the two-pass sharp pipeline never ran (no
191
+ manifest, no variants, no `<picture>`). The registry state is now in a
192
+ dedicated shared chunk (`image/registry.js`) that both the CLI and the
193
+ library import, ensuring a single module instance.
194
+ - **`happy-dom` fully removed** — the kit no longer depends on
195
+ happy-dom in any form. SSR uses the core's DOM-free
196
+ `renderToString` (`@elurjs/core/server`) directly. The legacy DOM
197
+ fallback (`renderWithDom`) was deleted along with all `external`/
198
+ `globals` references in the vite build configs.
199
+ - **`raw()` now supports server rendering** — added
200
+ `ELUR_RENDER_PROTOCOL.renderServer` to `raw()` so it works with the
201
+ core's DOM-free SSR (previously relied on the happy-dom fallback).
202
+ - **Config file renamed** — `elur.config.ts` → `elur.config.ts`. The
203
+ generic name was a design error that could collide with other tools.
204
+ Legacy `elur.config.*` files still work but emit a deprecation warning.
205
+ - **Integration `build` hook** (v2.4.1+) — the `build` hook in
206
+ `ElurKitIntegration` was declared but never invoked. Now `build()` fires
207
+ `runIntegrationHook(integrations, "build", [result, ctx])` after all
208
+ pages, image variants, and the manifest are written. Integrations can
209
+ generate post-build artifacts (sitemaps, robots.txt, search indexes)
210
+ into the output directory. `BuildResult.outDir` (v2.4.2) exposes the
211
+ actual output path (the atomic staging temp dir in CLI mode) so
212
+ artifacts survive the staging commit.
213
+ - **Client-only islands** (v2.4.3) — `island()` no longer crashes for
214
+ components that access `document`/`window`/`navigator` in their body
215
+ (carousels, charts, third-party widgets). Two opt-out mechanisms,
216
+ mirroring Astro `client:only` and Next.js `dynamic(..., { ssr: false })`:
217
+ - `directive: "only"` — skip SSR entirely, hydrate on `load`.
218
+ - `options: { ssr: false }` — skip SSR with any directive
219
+ (`load`/`idle`/`visible`).
220
+ - `options: { fallback }` — HTML rendered inside the island marker
221
+ when SSR is skipped or the component returns `null`. Accepts a
222
+ `ElurTemplate` (reactive) or a plain string.
223
+ - `isSSR()` — exported guard for environment reads
224
+ (`window.matchMedia`, `localStorage`, `navigator`). See
225
+ [Islands](#islands) for the limitation on `document.querySelectorAll`.
226
+ SSR errors are never silently swallowed — they propagate wrapped with
227
+ the island name and remediation hints.
228
+
229
+ #### Using the `build` hook for sitemaps
230
+
231
+ ```ts
232
+ // elur.config.ts
233
+ import { defineConfig } from "@elurjs/kit";
234
+ import { generateSitemap, generateRobots } from "@elurjs/kit/seo";
235
+ import type { ElurKitIntegration } from "@elurjs/kit";
236
+
237
+ const sitemapIntegration: ElurKitIntegration = {
238
+ name: "sitemap",
239
+ build: async (result) => {
240
+ const outDir = (result as { outDir: string }).outDir;
241
+ await generateSitemap({
242
+ siteUrl: "https://example.com",
243
+ outDir,
244
+ urls: [
245
+ { url: "/", priority: 1.0, changefreq: "weekly" },
246
+ { url: "/about", priority: 0.8, changefreq: "monthly" },
247
+ ],
248
+ });
249
+ await generateRobots({ siteUrl: "https://example.com", outDir });
250
+ },
251
+ };
252
+
253
+ export default defineConfig({
254
+ integrations: [sitemapIntegration],
255
+ });
256
+ ```
257
+
258
+ The hook fires **before** the CLI's atomic staging commit, so artifacts
259
+ written to `result.outDir` survive the swap into `dist/`. Do not write
260
+ to `join(context.root, "dist")` directly — that path is replaced by the
261
+ staging swap.
262
+
263
+ ## What's new in v2.3
264
+
265
+ - **Build-time compiler integration** — the kit now detects
266
+ [`@elurjs/vite-plugin-elur`](https://www.npmjs.com/package/@elurjs/vite-plugin-elur)
267
+ (>= 1.1.0) at runtime and skips its legacy interpolation transform
268
+ automatically. The plugin's state-machine lexer takes precedence,
269
+ providing compile-time errors, raw-text tag handling, and boolean
270
+ attribute validation that the kit's heuristic transform lacked.
271
+ - **`@elurjs/vite-plugin-elur` as optional peer dependency** —
272
+ `npm install @elurjs/vite-plugin-elur` activates the build-time
273
+ compiler (`@elurjs/core-compiler`), HMR with state preservation,
274
+ and partial attribute interpolation via a state-machine lexer.
275
+ - **`pluginSupportsPartialInterpolation()`** — new exported function
276
+ detects the Vite plugin at runtime.
277
+ - **`shouldUseLegacyInterpolation("auto")`** — now returns `false`
278
+ when the plugin is installed, `true` only when neither the plugin
279
+ nor the core supports partials.
280
+
281
+ ### Using the kit with the Vite plugin (recommended)
282
+
283
+ ```ts
284
+ // vite.config.ts
285
+ import { defineConfig } from "vite";
286
+ import { elurJsKit } from "@elurjs/kit/vite";
287
+ import elurJsPlugin from "@elurjs/vite-plugin-elur";
288
+
289
+ export default defineConfig({
290
+ plugins: [
291
+ elurJsKit(),
292
+ elurJsPlugin(), // compiler: true by default
293
+ ],
294
+ });
295
+ ```
296
+
297
+ When both plugins are installed:
298
+
299
+ | Feature | Kit only | Kit + Vite plugin |
300
+ | --- | --- | --- |
301
+ | Partial attr interpolation | Legacy transform (heuristic) | State-machine lexer (compile-time) |
302
+ | Build-time compiler | No | Yes (`html\`\`` → imperative DOM) |
303
+ | HMR state preservation | No | Yes (signals, stores, forms, routers) |
304
+ | Scroll/focus preservation | No | Yes |
305
+ | SSR | Works (kit handles it) | Works (plugin skips compiler in SSR) |
306
+
307
+ The Vite plugin detects SSR via `transformOptions.ssr` (Vite 5–7) or
308
+ `this.environment.config.consumer === "server"` (Vite 8) and skips
309
+ the compiler and HMR transforms for SSR modules. Client modules
310
+ receive the full transform pipeline.
311
+
312
+ ## What's new in v2.2
313
+
314
+ - **Native partial attribute interpolation** — when the installed
315
+ Elur core exposes `templateFeatures.partialAttributeInterpolation`
316
+ (core >= 3.3), the kit no longer injects the legacy
317
+ `elurJsInterpolationPlugin` transform (`interpolation: "auto"`, the
318
+ default). Partial attributes run through the runtime's native
319
+ normalization, preserving fine-grained reactivity.
320
+ - New `interpolation: "auto" | "legacy" | "off"` option on
321
+ `elurJsKit()`, `buildClientBundle()` and `transformProjectFiles()`.
322
+ - `interpolation: "legacy"` forces the old transform for migrations
323
+ (deprecated, one-time warning); `interpolation: "off"` disables it.
324
+ - `transformPartialInterpolations` stays exported for direct
325
+ consumers and is marked deprecated.
326
+ - **`coreSupportsPartialInterpolation()`** and
327
+ **`shouldUseLegacyInterpolation()`** exported from
328
+ `@elurjs/kit/vite` for programmatic resolution.
329
+
330
+ ## What's new in v2.1
331
+
332
+ - **#1: Route-level code-splitting** — the generated client entry uses `import()`
333
+ per island, producing separate chunks per page. Islands not on the current
334
+ page stay out of the initial bundle.
335
+ - **#2: Layout Slots** — `*.slot.ts` files (e.g. `sidebar.slot.ts`,
336
+ `header.slot.ts`) are detected by the route scanner and passed to layout
337
+ components as named slots: `Layout({ children, slots: { sidebar, header } })`.
338
+ - **#3: Redis / Cloudflare KV cache adapters** —
339
+ `createRedisCacheAdapter()` and `createCloudflareKVCacheAdapter()` for
340
+ serverless and distributed deployments. Same `CacheAdapter` interface as
341
+ the filesystem adapter, with tag-based invalidation.
342
+ - **#4: Real Suspense streaming** — `streamBoundary()` now emits a
343
+ `<template>` chunk + replacement script that swaps the fallback `<div>`
344
+ for the resolved content in-place via `replaceWith`, instead of the old
345
+ `innerHTML` append.
346
+ - **#5: `happy-dom` optional** — moved from `dependencies` to
347
+ `peerDependenciesMeta.optional`. The SSR runtime loads it via dynamic
348
+ `import()` only when the core renderer needs a DOM fallback.
349
+ *(Note: fully removed in v2.4 — the core's DOM-free `renderToString`
350
+ made the fallback unnecessary.)*
351
+
352
+ ## What's new in v2.0
353
+
354
+ - **Breaking: Node >=20.19.0** — dropped Node 18 support. Vite 7/8 and the core engine require Node 20.19+.
355
+ - **Breaking: Core v3** — `@elurjs/core` peer dependency upgraded to `^3.0.0`. New subpaths `@elurjs/core/server` and `@elurjs/core/hydrate` for SSR without DOM simulation and real hydration over existing DOM.
356
+ - **Breaking: Image pipeline** — `image()` now emits `<picture>` from a content-addressed manifest. No more broken `srcset` URLs. Sharp is optional.
357
+ - **Breaking: Config** — `defineConfig()` from `@elurjs/kit/config`. No `__dirname` in ESM configs.
358
+ - **Breaking: Build** — atomic staging, Vite JS API, `copyPublicAssets()`. No partial output on failure.
359
+ - **Breaking: Adapters** — relocatable paths via `import.meta.url`. No absolute paths embedded.
360
+ - **Security: Path traversal** — `resolveStaticFile()` rejects encoded traversal, NUL, backslashes, Unicode normalization, symlink escape.
361
+ - **Security: CSRF** — `verifyOrigin()` checks `Origin`/`Referer`/`Host`/`Sec-Fetch-Site` with allow-list and `strictOrigin`.
362
+ - **Security: Cache isolation** — no public caching of personalized responses. HMAC-signed action errors.
363
+ - **Security: JSON-LD** — escapes `<`, `>`, `&`, U+2028, U+2029.
364
+ - **Security: Body limits** — 413 responses for oversized JSON/form bodies.
365
+ - **Security: Public errors (v2.0.2)** — production 500s no longer leak `String(err)`; sanitized JSON via `toPublicErrorInfo()`/`publicErrorResponse()`.
366
+ - **Security: Static ranges (v2.0.2)** — `Range`/`If-Range` with 206/416 and uniform HEAD responses.
367
+ - **DX: CLI** — `check`, `routes`, `doctor` commands with reliable exit codes.
368
+ - **DX: Logger** — structured logger with request ID, Server-Timing, redaction.
369
+ - **DX: Scaffold** — `create-elur-app` with `template-kit` option.
370
+ - **Images (v2.0.2)** — SHA-256 transform keys, path containment, atomic writes, single-flight, `images.strict`, `getImage()` and `ImageService`.
371
+ - **Capabilities (v2.0.2)** — `AdapterCapabilities` per host with build-time `validateCapabilities()`.
372
+ - **Islands (v2.0.2)** — discriminated `{ load }` lazy loaders + `lazyIsland()`; loader detection never probes the component.
373
+ - **Tests: 442 tests** — unit, integration, security fuzz, cache concurrency, CSRF matrix, package smoke, SSR benchmark, static range, error sanitization, image hardening, capabilities, cross-runtime parity.
374
+ - **Audit: 0 vulnerabilities** — `bun audit` clean. `publint` All good.
375
+
376
+ ## What's new in v1.3
377
+
378
+ - **Security** — CSRF protection via Origin header verification; action errors stored in ephemeral cookie instead of URL params.
379
+ - **Metadata API** — `generateMetadata()` in pages, head merge on SPA navigation, scroll restoration on back/forward.
380
+ - **Content layer** — typed Markdown collections with YAML frontmatter parser (zero deps), optional `zod` validation, `marked` rendering, `raw()` HTML helper, HMR for `.md` files.
381
+ - **Image optimization** — `image()` with responsive srcset/sizes/lazy/fetchpriority; `sharp` pipeline generates WebP/AVIF variants at build time.
382
+ - **Prefetch + View Transitions** — IntersectionObserver-based prefetch on viewport + hover/focus; native View Transitions API with reduced-motion fallback.
383
+ - **Middleware** — `src/middleware.ts` with `config.matcher`, runs before routing in SSR and Vite dev server.
384
+ - **TypeScript 7** — upgraded to the native Go compiler (10x faster typecheck).
385
+
386
+ ## What's new in v1.2
387
+
388
+ - **Automatic attribute interpolation** — no more manual workarounds for `href="/blog/${slug}"`. When the installed Elur core supports partial attribute interpolation natively (`templateFeatures.partialAttributeInterpolation`, core ≥ 3.3), the kit skips its legacy transform and lets the runtime handle the syntax — with reactivity preserved. On older cores the legacy rewrite still applies automatically (`interpolation: "auto"`), and `interpolation: "legacy"` forces it for migrations (deprecated; emits a one-time warning).
389
+ - **Client router in the bundle** — the SPA router lives in the generated client entry (`/_elur/entry-client.js`) instead of being inlined into every page, keeping the HTML clean and the routing code cacheable.
390
+ - **SSR fallback in preview** — `preview` now renders dynamic routes on demand when a static file is missing, so slugs work even without `generateStaticParams`.
391
+ - **Auto client bundle build** — when `vite.client.config.ts` is present, `build` and `dev` build the hydration bundle automatically; no `--client-config` flag is required.
392
+ - **No server paths in HTML** — the serialized action registry only exposes action names per page (`{"/contact":["subscribe"]}`), never file system paths or implementation details.
393
+
394
+ ## Roadmap
395
+
396
+ | Version | Focus |
397
+ | --- | --- |
398
+ | v0.1 | SSG + file-based routing |
399
+ | v0.2 | Islands, data loading, actions, API routes |
400
+ | v0.3 | CLI + dev server |
401
+ | v0.4 | `generateStaticParams`, route groups, preview server |
402
+ | v0.5 | SSR runtime + adapter-node |
403
+ | v0.6 | Vite plugin + DX improvements |
404
+ | v0.7 | Vercel adapter + DX improvements |
405
+ | v0.8 | Netlify adapter + Bun adapter |
406
+ | v0.9 | Server actions ✅ |
407
+ | v1.0 | Stabilization: test suite, error handling ✅, Node adapter ✅, and action DX ✅ |
408
+ | v1.1 | Streaming boundaries + ISR ✅ |
409
+ | v1.2 | Interpolation plugin, SPA router, preview SSR fallback ✅ |
410
+ | v1.3 | Security, metadata API, content layer, image optimization, prefetch, View Transitions, middleware ✅ |
411
+ | v2.0 | Core v3 (SSR without Happy DOM, real hydration), atomic build, manifest-driven images, unified Web handler, RequestContext (§4), CSRF/static/cache hardening, CLI commands, structured logger, scaffold, 442 tests, 0 vulnerabilities ✅ |
412
+ | v2.0.2 | Cumplimiento: keyed hydration, streaming chunks/protocols (core), static Range/HEAD, errores sanitizados, imágenes hardening + `getImage`/`ImageService`, capabilities, islands `lazyIsland`, E2E Playwright (16 tests) ✅ |
413
+ | v2.1 | Route-level code-splitting, layout slots, Redis/Cloudflare KV cache adapters, real Suspense streaming, `happy-dom` optional ✅ |
414
+ | v2.2 | Native partial attribute interpolation (`interpolation: "auto"/"legacy"/"off"`), `coreSupportsPartialInterpolation()` / `shouldUseLegacyInterpolation()` exported ✅ |
415
+ | v2.3 | Build-time compiler integration via `@elurjs/vite-plugin-elur` (optional peer), `pluginSupportsPartialInterpolation()`, legacy interpolation delegates to plugin ✅ |
416
+ | v2.4 | CLI image registry singleton fix, `happy-dom` fully removed, `raw()` SSR support, config renamed to `elur.config.*` ✅ |
417
+ | v2.4.2 | Integration `build` hook wired into `build()`, `BuildResult.outDir` for post-build artifacts ✅ |
418
+ | v2.4.3 | Client-only islands (`directive: "only"`, `options: { ssr: false, fallback }`), `isSSR()` export, SSR error wrapping ✅ |
419
+ | v2.4.4 | Fix: `"only"` directive now hydrates immediately like `"load"`. Fix: islands without SSR DOM use fresh `_render` mount instead of `hydrateTemplate` ✅ |
420
+
421
+ ## API
422
+
423
+ ### `renderToString(factory)`
424
+
425
+ Renders a Elur template to an HTML string in Node.js.
426
+
427
+ ```ts
428
+ import { renderToString } from "@elurjs/kit";
429
+ import HomePage from "./src/app/page";
430
+
431
+ const body = await renderToString(() => HomePage({ data: { title: "Hi" } }));
432
+ ```
433
+
434
+ ### `documentShell(options)`
435
+
436
+ Wraps rendered HTML in a full document shell with `<script id="elur-data">`.
437
+
438
+ ```ts
439
+ import { documentShell } from "@elurjs/kit";
440
+
441
+ const html = documentShell({
442
+ title: "My Page",
443
+ body,
444
+ data: { title: "My Page" },
445
+ clientEntry: "/_elur/entry-client.js",
446
+ });
447
+ ```
448
+
449
+ ### Islands
450
+
451
+ Create an interactive component in `src/islands/`:
452
+
453
+ ```ts
454
+ // src/islands/LikeButton.ts
455
+ import { html, signal } from "@elurjs/core";
456
+
457
+ export default function LikeButton({ postId }: { postId: string }) {
458
+ const liked = signal(false);
459
+ return html`
460
+ <button @click=${() => (liked.value = !liked.value)}>
461
+ ${() => (liked.value ? "★ Liked" : "☆ Like")}
462
+ </button>
463
+ `;
464
+ }
465
+ ```
466
+
467
+ Mark it as an island in a page:
468
+
469
+ ```ts
470
+ // src/app/page.ts
471
+ import { html, island } from "@elurjs/kit";
472
+ import LikeButton from "../islands/LikeButton";
473
+
474
+ export default function HomePage() {
475
+ return html`
476
+ <article>
477
+ <h1>Hello</h1>
478
+ ${island("LikeButton", LikeButton, { postId: "123" }, "load")}
479
+ </article>
480
+ `;
481
+ }
482
+ ```
483
+
484
+ Hydrate it on the client. You can write the entry by hand:
485
+
486
+ ```ts
487
+ // src/entry-client.ts
488
+ import { hydrateIslands } from "@elurjs/kit/island";
489
+ import LikeButton from "./islands/LikeButton";
490
+
491
+ hydrateIslands({ LikeButton });
492
+ ```
493
+
494
+ Lazy (code-split) islands use a discriminated `{ load }` loader so the hydrator
495
+ can tell eager components from lazy loaders **without invoking them** (no probe
496
+ side effects):
497
+
498
+ ```ts
499
+ import { lazyIsland, hydrateIslands } from "@elurjs/kit/island";
500
+
501
+ const registry = {
502
+ LikeButton: lazyIsland(() => import("./islands/LikeButton").then((m) => m.default)),
503
+ };
504
+ hydrateIslands(registry);
505
+ ```
506
+
507
+ …or let `build()` generate it for you by scanning `src/islands/` (see
508
+ [Auto island scan](#auto-island-scan) below). Each `.ts` file becomes an island
509
+ whose registry name is its path relative to `islandsDir`
510
+ (`nav/MobileMenu.ts` → `"nav/MobileMenu"`).
511
+
512
+ Directives:
513
+
514
+ | Directive | Hydration trigger | SSR? |
515
+ | --- | --- | --- |
516
+ | `load` | Immediately | Yes (component runs on server) |
517
+ | `idle` | `requestIdleCallback` | Yes (component runs on server) |
518
+ | `visible` | `IntersectionObserver` | Yes (component runs on server) |
519
+ | `only` | Immediately | **No** — client-only, component never runs on server |
520
+
521
+ #### Client-only islands (`directive: "only"` / `ssr: false`)
522
+
523
+ Components that access browser-only globals (`document`, `window`,
524
+ `navigator`, `localStorage`, ...) in their body — carousels, charts,
525
+ third-party widgets — cannot run on the server. Use `directive: "only"`
526
+ (shortcut, hydrates on `load`) or `options: { ssr: false }` (combines
527
+ with any directive) to skip SSR entirely:
528
+
529
+ ```ts
530
+ import { html, island } from "@elurjs/kit";
531
+
532
+ // Client-only, hydrates on load, empty fallback
533
+ island("Carousel", Carousel, { slides: [...] }, "only")
534
+
535
+ // Client-only + fallback HTML (string or ElurTemplate)
536
+ island("Carousel", Carousel, { slides: [...] }, "only", {
537
+ fallback: "<div class=\"skeleton\" />",
538
+ })
539
+
540
+ // Client-only + hydrate when visible (more flexible than "only")
541
+ island("Chart", Chart, { data }, "visible", { ssr: false })
542
+ ```
543
+
544
+ When SSR is skipped, only `options.fallback` is rendered inside the
545
+ island marker. The client hydrates from scratch.
546
+
547
+ #### `fallback` option
548
+
549
+ `options.fallback` accepts a plain string or a `ElurTemplate` (reactive,
550
+ with signals). It is rendered when:
551
+
552
+ - SSR is skipped (`"only"` or `ssr: false`), or
553
+ - The component returns `null` / `false` / `undefined` during SSR.
554
+
555
+ ```ts
556
+ island("Widget", Widget, { id: 1 }, "load", {
557
+ fallback: html`<p class="placeholder">Loading…</p>`,
558
+ })
559
+ ```
560
+
561
+ #### `isSSR()` — environment reads
562
+
563
+ For components that only need *environment* reads (`window.matchMedia`,
564
+ `localStorage`, `navigator.userAgent`), guard the access with `isSSR()`
565
+ instead of skipping SSR entirely — this preserves the SSR fallback HTML:
566
+
567
+ ```ts
568
+ import { html, signal } from "@elurjs/core";
569
+ import { isSSR } from "@elurjs/kit";
570
+
571
+ function ThemeToggle() {
572
+ const prefersDark = isSSR() ? false : window.matchMedia("(prefers-color-scheme: dark)").matches;
573
+ const dark = signal(prefersDark);
574
+ return html`<button @click=${() => (dark.value = !dark.value)}>${() => (dark.value ? "🌙" : "☀")}</button>`;
575
+ }
576
+
577
+ island("ThemeToggle", ThemeToggle, {}, "load") // SSR works, no "only" needed
578
+ ```
579
+
580
+ :::warning Limitation
581
+ `isSSR()` is **not** a replacement for `"only"` / `ssr: false`. It only
582
+ works for environment reads. `document.querySelectorAll(".slide")` of
583
+ the component's own children will **not** work with `isSSR()` because
584
+ the DOM is not inserted when the function body runs (neither on the
585
+ server nor during hydration). For DOM queries of own children, use
586
+ `ElurComponent.onMount()` + `ref` — `onMount` runs after the DOM is
587
+ inserted, the equivalent of React's `useEffect`.
588
+ :::
589
+
590
+ #### SSR errors are not silenced
591
+
592
+ If an island component throws during SSR (with a directive other than
593
+ `"only"` and `ssr` not set to `false`), the error propagates wrapped
594
+ with the island name and remediation hints — it is never silently
595
+ swallowed. This matches Astro and Next.js, which never `try/catch` to
596
+ "auto-detect" client-only components:
597
+
598
+ ```
599
+ [elur-kit] Island "Carousel" threw during SSR: document is not defined
600
+ If the component accesses browser-only globals (document, window, etc.),
601
+ use directive: "only" or options: { ssr: false } to skip server rendering.
602
+ For environment reads (matchMedia, localStorage, navigator) you may guard
603
+ the access with isSSR() from "@elurjs/kit".
604
+ ```
605
+
606
+ ### `build(config)`
607
+
608
+ Scans `src/app/` and generates the full static site in `dist/`. You can call it
609
+ from code or use the `elur-kit build` CLI (see [CLI](#cli)).
610
+
611
+ ```ts
612
+ import { build } from "@elurjs/kit";
613
+
614
+ await build({
615
+ appDir: "./src/app",
616
+ outDir: "./dist",
617
+ clientEntry: "/_elur/entry-client.js",
618
+ // Optional: auto-generate the hydration entry from src/islands/
619
+ islandsDir: "./src/islands",
620
+ generatedEntry: "./.elur/entry-client.ts",
621
+ });
622
+ ```
623
+
624
+ The scanner recognizes:
625
+
626
+ | File | URL | Notes |
627
+ | --- | --- | --- |
628
+ | `src/app/page.ts` | `/` | Home page |
629
+ | `src/app/about/page.ts` | `/about` | Static page |
630
+ | `src/app/blog/[slug]/page.ts` | `/blog/:slug` | Dynamic route (requires `generateStaticParams`) |
631
+ | `src/app/[...slug]/page.ts` | `/:slug*` | Catch-all route (requires `generateStaticParams`) |
632
+ | `src/app/(marketing)/about/page.ts` | `/about` | Route group (ignored in URL, can add layout) |
633
+ | `src/app/layout.ts` | all children | Root layout |
634
+ | `src/app/blog/layout.ts` | `/blog/*` | Nested layout |
635
+ | `src/app/(marketing)/layout.ts` | `/pricing`, `/features` | Group layout |
636
+ | `src/app/404.page.ts` | error | Custom 404 page (SSG, SSR, adapters) |
637
+ | `src/app/500.page.ts` | error | Custom 500 page (SSG, SSR, adapters) |
638
+
639
+ ### Dynamic routes with `generateStaticParams`
640
+
641
+ Dynamic routes are skipped during SSG unless the page exports a
642
+ `generateStaticParams` function. It returns an array of param objects, one per
643
+ static HTML file to generate:
644
+
645
+ ```ts
646
+ // src/app/blog/[slug]/page.ts
647
+ import { html } from "@elurjs/core";
648
+ import type { PageProps, GenerateStaticParams } from "@elurjs/kit";
649
+ import { load } from "./page.data.ts";
650
+
651
+ export const generateStaticParams: GenerateStaticParams = async () => {
652
+ return [{ slug: "hello-world" }, { slug: "elur-kit" }];
653
+ };
654
+
655
+ export default function BlogPostPage({ data, params }: PageProps<typeof load>) {
656
+ return html`
657
+ <article>
658
+ <h1>${data.title}</h1>
659
+ <p>Slug: ${params.slug}</p>
660
+ </article>
661
+ `;
662
+ }
663
+ ```
664
+
665
+ ```ts
666
+ // src/app/blog/[slug]/page.data.ts
667
+ import type { PageDataLoad } from "@elurjs/kit";
668
+
669
+ export const load: PageDataLoad = async ({ params }) => {
670
+ return { title: `Post: ${params.slug}` };
671
+ };
672
+ ```
673
+
674
+ Running `elur-kit build` then produces:
675
+
676
+ ```
677
+ dist/blog/hello-world/index.html
678
+ dist/blog/elur-kit/index.html
679
+ ```
680
+
681
+ Catch-all routes use a string array for the spread param:
682
+
683
+ ```ts
684
+ export const generateStaticParams = async () => {
685
+ return [{ slug: ["docs", "intro"] }]; // -> /docs/intro
686
+ };
687
+ ```
688
+
689
+ ### Server actions
690
+
691
+ Create a `page.action.ts` file next to a `page.ts` and export async functions.
692
+ They run on the server and can be called from the client with `callAction()` or
693
+ `elurJsAction()`:
694
+
695
+ ```ts
696
+ // src/app/contact/page.action.ts
697
+ export async function submitContact(data: { name: string; email: string }) {
698
+ // validate, write to DB, send email, etc.
699
+ return { ok: true };
700
+ }
701
+ ```
702
+
703
+ ```ts
704
+ // src/app/contact/page.ts or any island
705
+ import { elurJsAction } from "@elurjs/kit/action";
706
+
707
+ const contact = elurJsAction("submitContact", { page: "/contact" });
708
+
709
+ // inside a template
710
+ html`
711
+ <form @submit=${(e: Event) => {
712
+ e.preventDefault();
713
+ contact.submit({ name: "Ada", email: "ada@example.com" });
714
+ }}>
715
+ <input name="name" />
716
+ <input name="email" />
717
+ <button type="submit" disabled=${() => contact.pending.value}>
718
+ ${() => (contact.pending.value ? "Sending..." : "Send")}
719
+ </button>
720
+ </form>
721
+ ${() => contact.error.value ? html`<p>${contact.error.value.message}</p>` : null}
722
+ ${() => contact.data.value ? html`<p>Sent!</p>` : null}
723
+ `
724
+ ```
725
+
726
+ `elurJsAction` returns a reactive handle with:
727
+
728
+ - `submit(input)` — calls the action and updates the signals.
729
+ - `pending` — signal that is `true` while the action is running.
730
+ - `error` — signal with the last error, or `null`.
731
+ - `data` — signal with the last successful result, or `null`.
732
+
733
+ The `page` option scopes the action to a specific route, avoiding name
734
+ collisions between different `page.action.ts` files. If you omit it, the
735
+ framework falls back to searching all scanned actions by name.
736
+
737
+ For lower-level control, use `callAction` directly:
738
+
739
+ ```ts
740
+ import { callAction } from "@elurjs/kit/action";
741
+
742
+ const result = await callAction("submitContact", { name: "Ada", email: "ada@example.com" }, { page: "/contact" });
743
+ ```
744
+
745
+ #### Progressive enhancement
746
+
747
+ Actions also work without JavaScript. Add hidden fields to a plain HTML form
748
+ and POST to `/__elur/actions`:
749
+
750
+ ```html
751
+ <form action="/__elur/actions" method="POST">
752
+ <input type="hidden" name="__elur_js_action_name" value="submitContact" />
753
+ <input type="hidden" name="__elur_js_action_page" value="/contact" />
754
+ <input name="name" />
755
+ <input name="email" />
756
+ <button type="submit">Send</button>
757
+ </form>
758
+ ```
759
+
760
+ The server runs the action and redirects back to the referring page (or to the
761
+ string returned by the action). If the client sends `Accept: application/json`,
762
+ the result is returned as JSON instead.
763
+
764
+ The framework exposes the `POST /__elur/actions` endpoint in every server mode
765
+ (`dev`, `preview`, `start` and all deployment adapters). The action name is
766
+ resolved against the scanned `page.action.ts` modules and its return value is
767
+ serialized as JSON.
768
+
769
+ ### Route groups
770
+
771
+ Folders whose name is wrapped in parentheses are ignored in the URL but can
772
+ hold a `layout.ts` that applies to all their children:
773
+
774
+ ```
775
+ src/app/
776
+ ├── (marketing)/
777
+ │ ├── layout.ts
778
+ │ ├── pricing/
779
+ │ │ └── page.ts # -> /pricing
780
+ │ └── features/
781
+ │ └── page.ts # -> /features
782
+ ```
783
+
784
+ This is useful for shared layouts that don't affect the public path, such as a
785
+ marketing shell that differs from a dashboard shell.
786
+
787
+ ### Error pages
788
+
789
+ Create optional `src/app/404.page.ts` and `src/app/500.page.ts` files to customize
790
+ the response when a route is missing or when a page fails to render:
791
+
792
+ ```ts
793
+ // src/app/404.page.ts
794
+ import { html } from "@elurjs/core";
795
+
796
+ export default function NotFoundPage() {
797
+ return html`
798
+ <article>
799
+ <h1>404</h1>
800
+ <p>Page not found.</p>
801
+ <a href="/">Back home</a>
802
+ </article>
803
+ `;
804
+ }
805
+ ```
806
+
807
+ ```ts
808
+ // src/app/500.page.ts
809
+ import { html } from "@elurjs/core";
810
+
811
+ export default function ErrorPage() {
812
+ return html`
813
+ <article>
814
+ <h1>500</h1>
815
+ <p>Something went wrong.</p>
816
+ <a href="/">Back home</a>
817
+ </article>
818
+ `;
819
+ }
820
+ ```
821
+
822
+ The framework renders these pages:
823
+
824
+ - During `elur-kit build` as `dist/404.html` and `dist/500.html`.
825
+ - During `elur-kit start` and in the Vite plugin for unmatched routes and render errors.
826
+ - In every deployment adapter (`vercel`, `netlify`, `bun`, `node`) for unmatched routes and SSR render failures.
827
+
828
+ Error pages receive the same `PageProps` as regular pages and can export their own `404.page.data.ts` or `500.page.data.ts` loaders.
829
+
830
+ ### SSR runtime
831
+
832
+ `elur-kit start` runs a Node HTTP server that renders pages on demand,
833
+ matching the request URL against the scanned routes and running loaders with
834
+ params and search params. Static files are served from the output directory
835
+ first, so the client bundle and other assets keep working:
836
+
837
+ ```bash
838
+ elur-kit build # build the client bundle and any static files
839
+ elur-kit start # SSR server on http://127.0.0.1:3000
840
+ ```
841
+
842
+ You can also use the lower-level API to embed the SSR server in a custom Node
843
+ app:
844
+
845
+ ```ts
846
+ import { createSsrServer } from "@elurjs/kit";
847
+
848
+ const ssr = await createSsrServer({
849
+ appDir: "./src/app",
850
+ publicDir: "./dist",
851
+ clientEntry: "/_elur/entry-client.js",
852
+ port: 3000,
853
+ });
854
+ await ssr.listen();
855
+ ```
856
+
857
+ ### Vite plugin
858
+
859
+ The official Vite plugin gives you a Vite-native dev server with SSR rendering
860
+ and automatic island entry generation:
861
+
862
+ ```ts
863
+ import { defineConfig } from "vite";
864
+ import { elurJsKit } from "@elurjs/kit/vite";
865
+
866
+ export default defineConfig({
867
+ plugins: [elurJsKit()],
868
+ });
869
+ ```
870
+
871
+ Then run the Vite dev server:
872
+
873
+ ```bash
874
+ npx vite
875
+ ```
876
+
877
+ The plugin scans `src/app/`, writes `.elur/entry-client.ts` and renders every
878
+ page on demand. For production, keep using `elur-kit build` to generate static
879
+ HTML and the client bundle.
880
+
881
+ #### Using with the build-time compiler (recommended)
882
+
883
+ For the best performance, install
884
+ [`@elurjs/vite-plugin-elur`](https://www.npmjs.com/package/@elurjs/vite-plugin-elur)
885
+ and add it to your Vite config alongside the kit plugin:
886
+
887
+ ```ts
888
+ import { defineConfig } from "vite";
889
+ import { elurJsKit } from "@elurjs/kit/vite";
890
+ import elurJsPlugin from "@elurjs/vite-plugin-elur";
891
+
892
+ export default defineConfig({
893
+ plugins: [
894
+ elurJsKit(),
895
+ elurJsPlugin(), // compiler: true by default
896
+ ],
897
+ });
898
+ ```
899
+
900
+ The Vite plugin activates:
901
+
902
+ - **Build-time compiler** — lowers `html\`\`` templates to imperative DOM
903
+ code (`firstChild`/`nextSibling` navigation, inline `setAttribute`,
904
+ grouped effects, event delegation). Eliminates `detectContext`,
905
+ `buildHTML`, and both `TreeWalker` passes in runtime.
906
+ - **Partial attribute interpolation** — state-machine lexer rewrites
907
+ `class="btn ${size}"` to `class=${__elurCompose("btn ", size)}` at
908
+ build time. Takes precedence over the kit's legacy transform.
909
+ - **HMR with state preservation** — signals, stores, forms, and
910
+ routers declared at module scope survive hot updates.
911
+ - **Scroll/focus preservation** — scroll position and focused element
912
+ are restored after re-mount.
913
+
914
+ The plugin is SSR-safe: it detects SSR via `transformOptions.ssr`
915
+ (Vite 5–7) or `this.environment.config.consumer === "server"` (Vite 8)
916
+ and skips the compiler and HMR transforms for SSR modules. Client
917
+ modules receive the full transform pipeline.
918
+
919
+ To disable the compiler (keep HMR and interpolation):
920
+
921
+ ```ts
922
+ elurJsPlugin({ compiler: false })
923
+ ```
924
+
925
+ #### Partial attribute interpolation
926
+
927
+ Partial interpolations inside attribute values (`href="/blog/${slug}"`)
928
+ are handled in three ways, in priority order:
929
+
930
+ 1. **Vite plugin** (recommended) — when
931
+ `@elurjs/vite-plugin-elur` >= 1.1.0 is installed, its
932
+ state-machine lexer rewrites partial interpolations at build time
933
+ with compile-time error detection, raw-text tag handling, and
934
+ boolean attribute validation. The kit detects the plugin via
935
+ `pluginSupportsPartialInterpolation()` and skips its own transform.
936
+ 2. **Core native** — when the Elur core exposes
937
+ `templateFeatures.partialAttributeInterpolation` (core >= 3.3),
938
+ the runtime normalizes partial attributes natively.
939
+ 3. **Kit legacy transform** — fallback for projects without the
940
+ plugin and with older cores. Heuristic HTML tag walker, less
941
+ powerful than the plugin's lexer.
942
+
943
+ Control the behavior with the `interpolation` option on `elurJsKit()`:
944
+
945
+ ```ts
946
+ elurJsKit({ interpolation: "auto" }) // default — plugin > core > legacy
947
+ elurJsKit({ interpolation: "legacy" }) // force legacy transform (deprecated)
948
+ elurJsKit({ interpolation: "off" }) // never transform
949
+ ```
950
+
951
+ The same option is available on `buildClientBundle()` and
952
+ `transformProjectFiles()`.
953
+
954
+ ### Adapters
955
+
956
+ Deploy to Vercel with the built-in adapter. First build the site, then generate
957
+ the Vercel output:
958
+
959
+ ```bash
960
+ elur-kit build
961
+ elur-kit adapter vercel
962
+ ```
963
+
964
+ This produces a `.vercel/output` directory that includes:
965
+
966
+ - `static/` — the static files from `dist/`.
967
+ - `functions/__elur-kit.func/index.js` — a bundled SSR function for unmatched routes.
968
+ - `config.json` — Vercel Build Output API v3 routing config.
969
+
970
+ You can also use the adapter programmatically:
971
+
972
+ ```ts
973
+ import { vercelAdapter } from "@elurjs/kit/adapters/vercel";
974
+
975
+ await vercelAdapter.build({
976
+ root: process.cwd(),
977
+ appDir: "src/app",
978
+ islandsDir: "src/islands",
979
+ outDir: "dist",
980
+ clientEntry: "/_elur/entry-client.js",
981
+ lang: "es",
982
+ });
983
+ ```
984
+
985
+ ### Netlify adapter
986
+
987
+ Deploy to Netlify with the built-in adapter:
988
+
989
+ ```bash
990
+ elur-kit build
991
+ elur-kit adapter netlify
992
+ ```
993
+
994
+ This produces:
995
+
996
+ - `netlify/functions/__elur-kit.mjs` — bundled SSR function for Netlify Functions v2.
997
+ - `netlify.toml` — redirects unmatched routes to the function.
998
+
999
+ The static files stay in `dist/` and are served directly by Netlify. Programmatic usage:
1000
+
1001
+ ```ts
1002
+ import { netlifyAdapter } from "@elurjs/kit/adapters/netlify";
1003
+
1004
+ await netlifyAdapter.build({
1005
+ root: process.cwd(),
1006
+ appDir: "src/app",
1007
+ islandsDir: "src/islands",
1008
+ outDir: "dist",
1009
+ clientEntry: "/_elur/entry-client.js",
1010
+ lang: "es",
1011
+ });
1012
+ ```
1013
+
1014
+ ### Bun adapter
1015
+
1016
+ Run a production server with Bun:
1017
+
1018
+ ```bash
1019
+ elur-kit build
1020
+ elur-kit adapter bun
1021
+ bun run .elur/bun-server.ts
1022
+ ```
1023
+
1024
+ This generates:
1025
+
1026
+ - `.elur/bun-index.ts` — SSR handler entry.
1027
+ - `.elur/bun-server.ts` — Bun server that serves `dist/` static files and renders pages on demand.
1028
+
1029
+ The server respects the `PORT` environment variable (default `3000`). Programmatic usage:
1030
+
1031
+ ```ts
1032
+ import { bunAdapter } from "@elurjs/kit/adapters/bun";
1033
+
1034
+ await bunAdapter.build({
1035
+ root: process.cwd(),
1036
+ appDir: "src/app",
1037
+ islandsDir: "src/islands",
1038
+ outDir: "dist",
1039
+ clientEntry: "/_elur/entry-client.js",
1040
+ lang: "es",
1041
+ });
1042
+ ```
1043
+
1044
+ ### Node adapter
1045
+
1046
+ Run a production server with Node (>=20.19.0):
1047
+
1048
+ ```bash
1049
+ elur-kit build
1050
+ elur-kit adapter node
1051
+ node .elur/node-server.mjs
1052
+ ```
1053
+
1054
+ This generates a single bundled `.elur/node-server.mjs` that serves `dist/` static files and renders pages on demand. The server respects the `PORT` environment variable (default `3000`). Programmatic usage:
1055
+
1056
+ ```ts
1057
+ import { nodeAdapter } from "@elurjs/kit/adapters/node";
1058
+
1059
+ await nodeAdapter.build({
1060
+ root: process.cwd(),
1061
+ appDir: "src/app",
1062
+ islandsDir: "src/islands",
1063
+ outDir: "dist",
1064
+ clientEntry: "/_elur/entry-client.js",
1065
+ lang: "es",
1066
+ });
1067
+ ```
1068
+
1069
+ ### Auto island scan
1070
+
1071
+ When you pass `islandsDir` and `generatedEntry`, `build()` walks the islands
1072
+ directory and writes a client entry that imports every island and registers it
1073
+ with `hydrateIslands`. Point your bundler (Vite/Rollup) at the generated file:
1074
+
1075
+ ```ts
1076
+ await build({
1077
+ appDir: "./src/app",
1078
+ outDir: "./dist",
1079
+ clientEntry: "/_elur/entry-client.js",
1080
+ islandsDir: "./src/islands",
1081
+ generatedEntry: "./.elur/entry-client.ts",
1082
+ });
1083
+ ```
1084
+
1085
+ Given `src/islands/LikeButton.ts` and `src/islands/nav/MobileMenu.ts`, the
1086
+ generated `.elur/entry-client.ts` looks like:
1087
+
1088
+ ```ts
1089
+ // AUTO-GENERATED by @elurjs/kit. Do not edit.
1090
+ import { hydrateIslands } from "@elurjs/kit/island";
1091
+ import LikeButton_0 from "../src/islands/LikeButton";
1092
+ import MobileMenu_1 from "../src/islands/nav/MobileMenu";
1093
+
1094
+ hydrateIslands({
1095
+ "LikeButton": LikeButton_0,
1096
+ "nav/MobileMenu": MobileMenu_1,
1097
+ });
1098
+ ```
1099
+
1100
+ The `build()` result also reports the discovered islands:
1101
+
1102
+ ```ts
1103
+ const result = await build({ /* ... */ });
1104
+ result.islands; // [{ name: "LikeButton", filePath: "…" }, …]
1105
+ result.generatedEntry; // absolute path to the generated entry
1106
+ ```
1107
+
1108
+ You can also call the lower-level helpers directly:
1109
+
1110
+ ```ts
1111
+ import { scanIslands, generateClientEntry } from "@elurjs/kit";
1112
+
1113
+ const islands = await scanIslands("./src/islands");
1114
+ await generateClientEntry({ islands, outFile: "./.elur/entry-client.ts" });
1115
+ ```
1116
+
1117
+ ### Metadata API
1118
+
1119
+ Pages can export a `generateMetadata` function or return a `metadata` field
1120
+ from loaders. The framework generates `<title>`, `<meta>`, `<link>`, OpenGraph
1121
+ and Twitter card tags, all marked with `data-elur-head` so the SPA router
1122
+ can swap them on navigation.
1123
+
1124
+ ```ts
1125
+ // src/app/blog/[slug]/page.ts
1126
+ import type { PageMetadata } from "@elurjs/kit";
1127
+
1128
+ export const generateMetadata = async ({ params }): Promise<PageMetadata> => {
1129
+ return {
1130
+ title: `Blog: ${params.slug}`,
1131
+ description: "A blog post",
1132
+ canonical: `https://example.com/blog/${params.slug}`,
1133
+ openGraph: { type: "article", image: "/og/blog.jpg" },
1134
+ twitter: { card: "summary_large_image" },
1135
+ };
1136
+ };
1137
+ ```
1138
+
1139
+ You can also return `metadata` from a loader:
1140
+
1141
+ ```ts
1142
+ // src/app/page.data.ts
1143
+ export const load = async () => {
1144
+ return { title: "Home", metadata: { title: "My Site — Home" } };
1145
+ };
1146
+ ```
1147
+
1148
+ ### Content layer
1149
+
1150
+ Typed Markdown collections with YAML frontmatter. Define collections in
1151
+ `src/content/config.ts` and query them from loaders:
1152
+
1153
+ ```ts
1154
+ // src/content/config.ts
1155
+ import { defineCollection } from "@elurjs/kit/content";
1156
+
1157
+ export const collections = {
1158
+ blog: defineCollection({ /* schema: z.object({ title: z.string() }) */ }),
1159
+ };
1160
+ ```
1161
+
1162
+ ```ts
1163
+ // src/app/blog/[slug]/page.data.ts
1164
+ import { getEntry } from "@elurjs/kit/content";
1165
+
1166
+ export const load = async ({ params }) => {
1167
+ const post = await getEntry("blog", params.slug);
1168
+ if (!post) throw new Response("Not Found", { status: 404 });
1169
+ return { post };
1170
+ };
1171
+ ```
1172
+
1173
+ ```ts
1174
+ // src/app/blog/[slug]/page.ts
1175
+ import { raw } from "@elurjs/kit/content";
1176
+ import { renderEntryHTML } from "@elurjs/kit/content";
1177
+
1178
+ export default function BlogPost({ data }) {
1179
+ return html`
1180
+ <article>
1181
+ <h1>${data.post.data.title}</h1>
1182
+ ${raw(await renderEntryHTML(data.post))}
1183
+ </article>
1184
+ `;
1185
+ }
1186
+ ```
1187
+
1188
+ Optional peer dependencies:
1189
+ - `marked` — Markdown rendering (`renderMarkdown`, `renderEntryHTML`)
1190
+ - `zod` — schema validation (`defineCollection({ schema: z.object(...) })`)
1191
+
1192
+ ### Image optimization
1193
+
1194
+ The `image()` helper emits responsive `<img>` tags with `srcset`, `sizes`,
1195
+ lazy loading, and CLS-preventing `width`/`height`:
1196
+
1197
+ ```ts
1198
+ import { image } from "@elurjs/kit";
1199
+
1200
+ export default function HeroPage() {
1201
+ return html`
1202
+ ${image({
1203
+ src: "/images/hero.jpg",
1204
+ alt: "Hero image",
1205
+ width: 1920,
1206
+ height: 1080,
1207
+ widths: [640, 1280, 1920],
1208
+ sizes: "100vw",
1209
+ priority: true, // above-the-fold: eager load, fetchpriority="high"
1210
+ })}
1211
+ `;
1212
+ }
1213
+ ```
1214
+
1215
+ When `sharp` is installed (optional peer dep), `build()` automatically
1216
+ generates WebP and AVIF variants at the requested widths with content-based
1217
+ hashing for indefinite caching.
1218
+
1219
+ The image pipeline (v2.0.2) is hardened:
1220
+
1221
+ - **SHA-256 transform keys** — a variant's hash incorporates the source content
1222
+ digest + normalized transform options + encoder/naming versions, so changing
1223
+ quality or encoder invalidates the URL as required.
1224
+ - **Path containment** — sources and outputs are validated against traversal,
1225
+ NUL, separators and symlink escape; no reads/writes outside allowed roots.
1226
+ - **Atomic writes + single-flight** — variants are written via temp+rename with
1227
+ a bounded concurrency pool and one in-flight transform per key.
1228
+ - **`images.strict`** — fails the build on a missing source or failed transform
1229
+ instead of emitting a partially-written variant.
1230
+
1231
+ Programmatic API:
1232
+
1233
+ ```ts
1234
+ import { getImage, createImageService } from "@elurjs/kit/image";
1235
+
1236
+ const meta = await getImage(
1237
+ { src: "/images/hero.jpg", alt: "Hero", widths: [640, 1280], formats: ["avif", "webp"] },
1238
+ { publicDir: "public", outDir: "dist" },
1239
+ );
1240
+ // meta.sources, meta.generated, meta.attributes ...
1241
+
1242
+ const service = createImageService({ publicDir: "public", outDir: "dist" });
1243
+ // service.resolve(request, ctx); service.capabilities ...
1244
+ ```
1245
+
1246
+ ### Adapter capabilities
1247
+
1248
+ Each adapter declares an explicit capability contract used for build-time
1249
+ diagnostics (runtime-security §8.5):
1250
+
1251
+ ```ts
1252
+ interface AdapterCapabilities {
1253
+ streaming: boolean;
1254
+ filesystem: "none" | "readonly" | "persistent" | "ephemeral";
1255
+ imageRuntime: boolean;
1256
+ backgroundWork: boolean;
1257
+ maxBodySize?: number;
1258
+ }
1259
+ ```
1260
+
1261
+ - `DEFAULT_CAPABILITIES` (Node/Bun), `SERVERLESS_CAPABILITIES` (Vercel/Netlify),
1262
+ `EDGE_CAPABILITIES` and `createCapabilities()` are exported from
1263
+ `@elurjs/kit/runtime`.
1264
+ - `validateCapabilities(caps, { isr, images, streaming })` is checked by the
1265
+ CLI `adapter` command so incompatible host+feature combinations fail at build.
1266
+
1267
+ ### Middleware
1268
+
1269
+ Create `src/middleware.ts` to run logic before every request (auth, redirects,
1270
+ header injection):
1271
+
1272
+ ```ts
1273
+ import type { Middleware } from "@elurjs/kit";
1274
+
1275
+ const middleware: Middleware = (request) => {
1276
+ if (!request.headers.get("Cookie")?.includes("session=")) {
1277
+ return Response.redirect(new URL("/login", request.url), 307);
1278
+ }
1279
+ };
1280
+
1281
+ export default middleware;
1282
+
1283
+ export const config = {
1284
+ matcher: ["/dashboard/:path*", "/admin/:path*"],
1285
+ };
1286
+ ```
1287
+
1288
+ ### Prefetch and View Transitions
1289
+
1290
+ The SPA router automatically prefetches pages when links enter the viewport
1291
+ (IntersectionObserver) and on hover/focus. Prefetched pages are cached for
1292
+ 30 seconds. Add `data-no-prefetch` to any link to opt out.
1293
+
1294
+ When the browser supports the View Transitions API, page transitions use
1295
+ `document.startViewTransition()` for smooth cross-fade animations. This is
1296
+ automatically disabled when the user has `prefers-reduced-motion: reduce`.
1297
+
1298
+ ## Project conventions
1299
+
1300
+ ```text
1301
+ my-app/
1302
+ ├── src/
1303
+ │ ├── app/
1304
+ │ │ ├── layout.ts # root layout
1305
+ │ │ ├── page.ts # home page
1306
+ │ │ ├── page.data.ts # home loader
1307
+ │ │ ├── page.action.ts # home server actions
1308
+ │ │ ├── 404.page.ts # custom 404 page
1309
+ │ │ ├── 500.page.ts # custom 500 page
1310
+ │ │ ├── blog/
1311
+ │ │ │ ├── page.ts
1312
+ │ │ │ ├── page.data.ts
1313
+ │ │ │ └── page.action.ts
1314
+ │ │ └── api/
1315
+ │ │ └── posts/
1316
+ │ │ └── route.ts # API endpoint
1317
+ │ ├── content/ # content layer (Markdown collections)
1318
+ │ │ ├── config.ts # collection definitions
1319
+ │ │ └── blog/
1320
+ │ │ ├── hello-world.md
1321
+ │ │ └── second-post.md
1322
+ │ └── islands/ # interactive components
1323
+ ├── middleware.ts # optional middleware (auth, redirects)
1324
+ ├── elur.config.ts
1325
+ └── vite.config.ts
1326
+ ```
1327
+
1328
+ ## License
1329
+
1330
+ MIT © Deiver Vasquez