@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.
- package/CHANGELOG.md +874 -0
- package/LICENSE +21 -0
- package/README.md +1330 -0
- package/bin/elur-kit.js +54 -0
- package/dist/lib/action/define.d.cts +72 -0
- package/dist/lib/action/define.d.ts +72 -0
- package/dist/lib/action/error-store.d.cts +29 -0
- package/dist/lib/action/error-store.d.ts +29 -0
- package/dist/lib/action/index.cjs +1 -0
- package/dist/lib/action/index.d.cts +64 -0
- package/dist/lib/action/index.d.ts +64 -0
- package/dist/lib/action/index.js +2 -0
- package/dist/lib/action/origin.d.cts +20 -0
- package/dist/lib/action/origin.d.ts +20 -0
- package/dist/lib/action/scan.d.cts +28 -0
- package/dist/lib/action/scan.d.ts +28 -0
- package/dist/lib/action/server.d.cts +30 -0
- package/dist/lib/action/server.d.ts +30 -0
- package/dist/lib/action-BB6N5yD-.js +111 -0
- package/dist/lib/action-BB6N5yD-.js.map +1 -0
- package/dist/lib/action-DZHihU3Z.cjs +2 -0
- package/dist/lib/action-DZHihU3Z.cjs.map +1 -0
- package/dist/lib/adapters/bun.cjs +30 -0
- package/dist/lib/adapters/bun.cjs.map +1 -0
- package/dist/lib/adapters/bun.d.cts +13 -0
- package/dist/lib/adapters/bun.d.ts +13 -0
- package/dist/lib/adapters/bun.js +65 -0
- package/dist/lib/adapters/bun.js.map +1 -0
- package/dist/lib/adapters/index.cjs +0 -0
- package/dist/lib/adapters/index.d.cts +30 -0
- package/dist/lib/adapters/index.d.ts +30 -0
- package/dist/lib/adapters/index.js +0 -0
- package/dist/lib/adapters/netlify.cjs +10 -0
- package/dist/lib/adapters/netlify.cjs.map +1 -0
- package/dist/lib/adapters/netlify.d.cts +13 -0
- package/dist/lib/adapters/netlify.d.ts +13 -0
- package/dist/lib/adapters/netlify.js +51 -0
- package/dist/lib/adapters/netlify.js.map +1 -0
- package/dist/lib/adapters/node.cjs +46 -0
- package/dist/lib/adapters/node.cjs.map +1 -0
- package/dist/lib/adapters/node.d.cts +13 -0
- package/dist/lib/adapters/node.d.ts +13 -0
- package/dist/lib/adapters/node.js +105 -0
- package/dist/lib/adapters/node.js.map +1 -0
- package/dist/lib/adapters/shared.d.cts +17 -0
- package/dist/lib/adapters/shared.d.ts +17 -0
- package/dist/lib/adapters/vercel.cjs +2 -0
- package/dist/lib/adapters/vercel.cjs.map +1 -0
- package/dist/lib/adapters/vercel.d.cts +9 -0
- package/dist/lib/adapters/vercel.d.ts +9 -0
- package/dist/lib/adapters/vercel.js +62 -0
- package/dist/lib/adapters/vercel.js.map +1 -0
- package/dist/lib/build/build.d.cts +83 -0
- package/dist/lib/build/build.d.ts +83 -0
- package/dist/lib/build/document-shell.d.cts +48 -0
- package/dist/lib/build/document-shell.d.ts +48 -0
- package/dist/lib/build/transform-source.d.cts +26 -0
- package/dist/lib/build/transform-source.d.ts +26 -0
- package/dist/lib/build/vite-build.d.cts +78 -0
- package/dist/lib/build/vite-build.d.ts +78 -0
- package/dist/lib/cache/policy.d.cts +25 -0
- package/dist/lib/cache/policy.d.ts +25 -0
- package/dist/lib/cache.d.cts +13 -0
- package/dist/lib/cache.d.ts +13 -0
- package/dist/lib/capabilities-C0TDQoMY.cjs +2 -0
- package/dist/lib/capabilities-C0TDQoMY.cjs.map +1 -0
- package/dist/lib/capabilities-C7txofmY.js +45 -0
- package/dist/lib/capabilities-C7txofmY.js.map +1 -0
- package/dist/lib/cli/commands.d.cts +15 -0
- package/dist/lib/cli/commands.d.ts +15 -0
- package/dist/lib/cli.cjs +5132 -0
- package/dist/lib/cli.cjs.map +1 -0
- package/dist/lib/cli.d.cts +30 -0
- package/dist/lib/cli.d.ts +30 -0
- package/dist/lib/cli.js +5131 -0
- package/dist/lib/cli.js.map +1 -0
- package/dist/lib/collections-Dbc8aRLf.js +233 -0
- package/dist/lib/collections-Dbc8aRLf.js.map +1 -0
- package/dist/lib/collections-VDy5g9p8.cjs +13 -0
- package/dist/lib/collections-VDy5g9p8.cjs.map +1 -0
- package/dist/lib/config/index.cjs +2 -0
- package/dist/lib/config/index.cjs.map +1 -0
- package/dist/lib/config/index.d.cts +100 -0
- package/dist/lib/config/index.d.ts +100 -0
- package/dist/lib/config/index.js +122 -0
- package/dist/lib/config/index.js.map +1 -0
- package/dist/lib/content/collections.d.cts +69 -0
- package/dist/lib/content/collections.d.ts +69 -0
- package/dist/lib/content/frontmatter.d.cts +20 -0
- package/dist/lib/content/frontmatter.d.ts +20 -0
- package/dist/lib/content/index.cjs +2 -0
- package/dist/lib/content/index.cjs.map +1 -0
- package/dist/lib/content/index.d.cts +5 -0
- package/dist/lib/content/index.d.ts +5 -0
- package/dist/lib/content/index.js +28 -0
- package/dist/lib/content/index.js.map +1 -0
- package/dist/lib/content/markdown.d.cts +6 -0
- package/dist/lib/content/markdown.d.ts +6 -0
- package/dist/lib/content/raw.d.cts +9 -0
- package/dist/lib/content/raw.d.ts +9 -0
- package/dist/lib/content/schema.d.cts +14 -0
- package/dist/lib/content/schema.d.ts +14 -0
- package/dist/lib/errors-0X7pCFNU.cjs +2 -0
- package/dist/lib/errors-0X7pCFNU.cjs.map +1 -0
- package/dist/lib/errors-ePYxDlhU.js +70 -0
- package/dist/lib/errors-ePYxDlhU.js.map +1 -0
- package/dist/lib/errors.d.cts +84 -0
- package/dist/lib/errors.d.ts +84 -0
- package/dist/lib/hydrate-8a3TbQbs.cjs +2 -0
- package/dist/lib/hydrate-8a3TbQbs.cjs.map +1 -0
- package/dist/lib/hydrate-CgZGrTxF.js +126 -0
- package/dist/lib/hydrate-CgZGrTxF.js.map +1 -0
- package/dist/lib/image/index.cjs +2 -0
- package/dist/lib/image/index.cjs.map +1 -0
- package/dist/lib/image/index.d.cts +37 -0
- package/dist/lib/image/index.d.ts +37 -0
- package/dist/lib/image/index.js +49 -0
- package/dist/lib/image/index.js.map +1 -0
- package/dist/lib/image/pipeline.d.cts +37 -0
- package/dist/lib/image/pipeline.d.ts +37 -0
- package/dist/lib/image/registry.cjs +2 -0
- package/dist/lib/image/registry.cjs.map +1 -0
- package/dist/lib/image/registry.d.cts +26 -0
- package/dist/lib/image/registry.d.ts +26 -0
- package/dist/lib/image/registry.js +23 -0
- package/dist/lib/image/registry.js.map +1 -0
- package/dist/lib/image/service.cjs +2 -0
- package/dist/lib/image/service.cjs.map +1 -0
- package/dist/lib/image/service.d.cts +164 -0
- package/dist/lib/image/service.d.ts +164 -0
- package/dist/lib/image/service.js +308 -0
- package/dist/lib/image/service.js.map +1 -0
- package/dist/lib/index.cjs +1 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +38 -0
- package/dist/lib/index.d.ts +38 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/integrations/hooks.d.cts +58 -0
- package/dist/lib/integrations/hooks.d.ts +58 -0
- package/dist/lib/integrations/index.cjs +1 -0
- package/dist/lib/integrations/index.d.cts +18 -0
- package/dist/lib/integrations/index.d.ts +18 -0
- package/dist/lib/integrations/index.js +2 -0
- package/dist/lib/integrations-7dpN1RYB.js +35 -0
- package/dist/lib/integrations-7dpN1RYB.js.map +1 -0
- package/dist/lib/integrations-DDqfWjvH.cjs +2 -0
- package/dist/lib/integrations-DDqfWjvH.cjs.map +1 -0
- package/dist/lib/interpolation-plugin-Bt-KRKVY.js +365 -0
- package/dist/lib/interpolation-plugin-Bt-KRKVY.js.map +1 -0
- package/dist/lib/interpolation-plugin-CMPCDlVJ.cjs +31 -0
- package/dist/lib/interpolation-plugin-CMPCDlVJ.cjs.map +1 -0
- package/dist/lib/island/generate-entry.d.cts +27 -0
- package/dist/lib/island/generate-entry.d.ts +27 -0
- package/dist/lib/island/hydrate.d.cts +33 -0
- package/dist/lib/island/hydrate.d.ts +33 -0
- package/dist/lib/island/index.cjs +1 -0
- package/dist/lib/island/index.d.cts +2 -0
- package/dist/lib/island/index.d.ts +2 -0
- package/dist/lib/island/index.js +1 -0
- package/dist/lib/island/island.d.cts +32 -0
- package/dist/lib/island/island.d.ts +32 -0
- package/dist/lib/island/scan.d.cts +14 -0
- package/dist/lib/island/scan.d.ts +14 -0
- package/dist/lib/manifest/index.cjs +3 -0
- package/dist/lib/manifest/index.cjs.map +1 -0
- package/dist/lib/manifest/index.d.cts +18 -0
- package/dist/lib/manifest/index.d.ts +18 -0
- package/dist/lib/manifest/index.js +110 -0
- package/dist/lib/manifest/index.js.map +1 -0
- package/dist/lib/middleware/index.d.cts +56 -0
- package/dist/lib/middleware/index.d.ts +56 -0
- package/dist/lib/middleware/stream-boundary.d.cts +53 -0
- package/dist/lib/middleware/stream-boundary.d.ts +53 -0
- package/dist/lib/node-http-DRAUhO0c.js +617 -0
- package/dist/lib/node-http-DRAUhO0c.js.map +1 -0
- package/dist/lib/node-http-DhxguYyz.cjs +13 -0
- package/dist/lib/node-http-DhxguYyz.cjs.map +1 -0
- package/dist/lib/render/render-to-string.d.cts +12 -0
- package/dist/lib/render/render-to-string.d.ts +12 -0
- package/dist/lib/render/ssr-flag.d.cts +4 -0
- package/dist/lib/render/ssr-flag.d.ts +4 -0
- package/dist/lib/route-scanner-JY6YrNH5.cjs +2 -0
- package/dist/lib/route-scanner-JY6YrNH5.cjs.map +1 -0
- package/dist/lib/route-scanner-XcLId7lK.js +100 -0
- package/dist/lib/route-scanner-XcLId7lK.js.map +1 -0
- package/dist/lib/router/client.cjs +2 -0
- package/dist/lib/router/client.cjs.map +1 -0
- package/dist/lib/router/client.d.cts +47 -0
- package/dist/lib/router/client.d.ts +47 -0
- package/dist/lib/router/client.js +298 -0
- package/dist/lib/router/client.js.map +1 -0
- package/dist/lib/router/route-scanner.d.cts +50 -0
- package/dist/lib/router/route-scanner.d.ts +50 -0
- package/dist/lib/runtime/capabilities.d.cts +47 -0
- package/dist/lib/runtime/capabilities.d.ts +47 -0
- package/dist/lib/runtime/context.d.cts +140 -0
- package/dist/lib/runtime/context.d.ts +140 -0
- package/dist/lib/runtime/handler.d.cts +39 -0
- package/dist/lib/runtime/handler.d.ts +39 -0
- package/dist/lib/runtime/index.cjs +1 -0
- package/dist/lib/runtime/index.d.cts +6 -0
- package/dist/lib/runtime/index.d.ts +6 -0
- package/dist/lib/runtime/index.js +4 -0
- package/dist/lib/runtime/node-http.d.cts +2 -0
- package/dist/lib/runtime/node-http.d.ts +2 -0
- package/dist/lib/runtime/security-headers.d.cts +13 -0
- package/dist/lib/runtime/security-headers.d.ts +13 -0
- package/dist/lib/runtime/static.d.cts +1 -0
- package/dist/lib/runtime/static.d.ts +1 -0
- package/dist/lib/runtime-CrH5X-Es.cjs +2 -0
- package/dist/lib/runtime-CrH5X-Es.cjs.map +1 -0
- package/dist/lib/runtime-Ds2-wH6P.js +520 -0
- package/dist/lib/runtime-Ds2-wH6P.js.map +1 -0
- package/dist/lib/scan-CY2O9ty6.cjs +2 -0
- package/dist/lib/scan-CY2O9ty6.cjs.map +1 -0
- package/dist/lib/scan-CjMUgnsd.js +22 -0
- package/dist/lib/scan-CjMUgnsd.js.map +1 -0
- package/dist/lib/scan-DXzYtmRQ.cjs +2 -0
- package/dist/lib/scan-DXzYtmRQ.cjs.map +1 -0
- package/dist/lib/scan-Dn93ycXG.js +33 -0
- package/dist/lib/scan-Dn93ycXG.js.map +1 -0
- package/dist/lib/seo/index.cjs +10 -0
- package/dist/lib/seo/index.cjs.map +1 -0
- package/dist/lib/seo/index.d.cts +99 -0
- package/dist/lib/seo/index.d.ts +99 -0
- package/dist/lib/seo/index.js +41 -0
- package/dist/lib/seo/index.js.map +1 -0
- package/dist/lib/shared-CfqOLsKj.cjs +183 -0
- package/dist/lib/shared-CfqOLsKj.cjs.map +1 -0
- package/dist/lib/shared-DU3iQgr7.js +236 -0
- package/dist/lib/shared-DU3iQgr7.js.map +1 -0
- package/dist/lib/ssr/match.d.cts +25 -0
- package/dist/lib/ssr/match.d.ts +25 -0
- package/dist/lib/ssr/render.d.cts +62 -0
- package/dist/lib/ssr/render.d.ts +62 -0
- package/dist/lib/ssr/server.d.cts +41 -0
- package/dist/lib/ssr/server.d.ts +41 -0
- package/dist/lib/ssr/stream.d.cts +48 -0
- package/dist/lib/ssr/stream.d.ts +48 -0
- package/dist/lib/stream-CpOG052V.js +104 -0
- package/dist/lib/stream-CpOG052V.js.map +1 -0
- package/dist/lib/stream-DmZjYqHm.cjs +17 -0
- package/dist/lib/stream-DmZjYqHm.cjs.map +1 -0
- package/dist/lib/types.d.cts +97 -0
- package/dist/lib/types.d.ts +97 -0
- package/dist/lib/vite/index.cjs +2 -0
- package/dist/lib/vite/index.cjs.map +1 -0
- package/dist/lib/vite/index.d.cts +41 -0
- package/dist/lib/vite/index.d.ts +41 -0
- package/dist/lib/vite/index.js +229 -0
- package/dist/lib/vite/index.js.map +1 -0
- package/dist/lib/vite/interpolation-plugin.d.cts +64 -0
- package/dist/lib/vite/interpolation-plugin.d.ts +64 -0
- package/package.json +292 -0
package/README.md
ADDED
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
# Elur Kit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@elurjs/kit)
|
|
4
|
+
[](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
|