@mauroandre/velojs 0.0.37 → 0.0.39
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/README.md +19 -9
- package/agent/AGENTS.md +143 -0
- package/agent/skills/velojs-actions/SKILL.md +192 -0
- package/agent/skills/velojs-components/SKILL.md +99 -0
- package/agent/skills/velojs-endpoints/SKILL.md +187 -0
- package/agent/skills/velojs-event-streams/SKILL.md +705 -0
- package/agent/skills/velojs-getting-started/SKILL.md +191 -0
- package/agent/skills/velojs-hooks/SKILL.md +103 -0
- package/agent/skills/velojs-link-component/SKILL.md +67 -0
- package/agent/skills/velojs-loaders/SKILL.md +134 -0
- package/agent/skills/velojs-middlewares/SKILL.md +143 -0
- package/agent/skills/velojs-production-deploy/SKILL.md +96 -0
- package/agent/skills/velojs-project-graph/SKILL.md +97 -0
- package/agent/skills/velojs-routes/SKILL.md +209 -0
- package/agent/skills/velojs-scripts-component/SKILL.md +62 -0
- package/agent/skills/velojs-server-api/SKILL.md +155 -0
- package/agent/skills/velojs-sockets/SKILL.md +303 -0
- package/agent/skills/velojs-static-generation/SKILL.md +148 -0
- package/agent/skills/velojs-testing/SKILL.md +472 -0
- package/agent/skills/velojs-type-reference/SKILL.md +142 -0
- package/agent/skills/velojs-vite-plugin/SKILL.md +71 -0
- package/dist/cli.js +42 -31
- package/dist/graph.d.ts +37 -0
- package/dist/graph.js +202 -0
- package/dist/init.js +16 -7
- package/dist/vite.js +279 -248
- package/package.json +6 -2
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: velojs-sockets
|
|
3
|
+
description: "WebSockets via `socket_*` and `useSocket`: bidirectional realtime like terminals, chat, collaborative editing. Use when the client must both send and receive continuously; for server-to-client only, prefer event-streams."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sockets
|
|
7
|
+
|
|
8
|
+
Sockets are **declarative WebSocket handlers** co-located with the page that uses them. Export `socket_<name>` from a page or layout module and VeloJS registers a WebSocket route at `/_socket/{moduleId}/{name}`, with the same middleware inheritance as pages, actions, and streams.
|
|
9
|
+
|
|
10
|
+
Use sockets when you need **bidirectional** server ↔ client communication: interactive terminals, collaborative editing, live cursors, anything where the client needs to send messages too. For one-way push (server → client), use [Event Streams](/docs/event-streams).
|
|
11
|
+
|
|
12
|
+
## The shortest example
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// app/workers/WorkerTerminal.tsx
|
|
16
|
+
import type { SocketHandler } from "@mauroandre/velojs";
|
|
17
|
+
import { parseJson } from "@mauroandre/velojs/sockets";
|
|
18
|
+
import { useSocket, useParams } from "@mauroandre/velojs/hooks";
|
|
19
|
+
|
|
20
|
+
type Incoming = { type: "data"; data: string } | { type: "resize"; cols: number; rows: number };
|
|
21
|
+
type Outgoing = { type: "data"; data: string } | { type: "exit"; code: number };
|
|
22
|
+
|
|
23
|
+
export const socket_terminal: SocketHandler = async ({
|
|
24
|
+
incoming, send, close, keepOpen, abortSignal, c, query,
|
|
25
|
+
}) => {
|
|
26
|
+
const user = c.get("user");
|
|
27
|
+
// The client sends the id as `?channel=` — see the note below on `params`.
|
|
28
|
+
const workerId = query.channel;
|
|
29
|
+
if (!workerId) { close(1008, "missing channel"); return; }
|
|
30
|
+
|
|
31
|
+
const session = await createTerminal({ user, workerId });
|
|
32
|
+
|
|
33
|
+
session.on("data", (chunk) => send({ type: "data", data: chunk.toString() }));
|
|
34
|
+
session.on("exit", (code) => send({ type: "exit", code }));
|
|
35
|
+
abortSignal.addEventListener("abort", () => session.destroy());
|
|
36
|
+
|
|
37
|
+
keepOpen();
|
|
38
|
+
|
|
39
|
+
for await (const msg of parseJson<Incoming>(incoming)) {
|
|
40
|
+
if (msg.type === "data") session.write(msg.data);
|
|
41
|
+
else if (msg.type === "resize") session.resize(msg.cols, msg.rows);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Component = () => {
|
|
46
|
+
const params = useParams<{ workerId: string }>();
|
|
47
|
+
const { send, status, lastMessage } = useSocket(socket_terminal, {
|
|
48
|
+
channel: params.workerId,
|
|
49
|
+
onMessage: (raw) => {
|
|
50
|
+
if (typeof raw !== "string") return;
|
|
51
|
+
const msg = JSON.parse(raw) as Outgoing;
|
|
52
|
+
if (msg.type === "data") appendToBuffer(msg.data);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div>
|
|
58
|
+
<pre>{/* terminal buffer */}</pre>
|
|
59
|
+
<input onKeyDown={(e) => send({ type: "data", data: e.key })} />
|
|
60
|
+
<span>{status.value}</span>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## The handler signature
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
export type SocketHandler = (args: {
|
|
70
|
+
incoming: AsyncIterable<string | Uint8Array>;
|
|
71
|
+
send: (msg: string | Uint8Array | object) => void;
|
|
72
|
+
close: (code?: number, reason?: string) => void;
|
|
73
|
+
keepOpen: () => void;
|
|
74
|
+
abortSignal: AbortSignal;
|
|
75
|
+
/** Always empty for `socket_*` — see below. */
|
|
76
|
+
params: Record<string, string>;
|
|
77
|
+
query: Record<string, string>;
|
|
78
|
+
}) => void | Promise<void>;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `params` is always empty — read the id from `query.channel`
|
|
82
|
+
|
|
83
|
+
A `socket_*` handler is mounted at its own static path — `/_socket/{moduleId}/{name}` — not at the URL of the page it lives on. That path has no `:segments`, so `params` is always `{}`, even when the page route is `/workers/:workerId` and even when a middleware on that route reads the param fine.
|
|
84
|
+
|
|
85
|
+
The client sends the value as `?channel=`, so it arrives in **`query.channel`**:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// ❌ undefined in production
|
|
89
|
+
const workerId = params.workerId;
|
|
90
|
+
|
|
91
|
+
// ✅
|
|
92
|
+
const workerId = query.channel;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> This one is easy to miss because **`app.socket()` lets you inject `params` directly**, so a test written against the wrong version passes while production breaks. Pass `channel` in tests instead, matching what `useSocket({ channel })` really transmits.
|
|
96
|
+
|
|
97
|
+
### `incoming` — `AsyncIterable<string | Uint8Array>`
|
|
98
|
+
|
|
99
|
+
Raw WebSocket frames. Text frames arrive as `string`, binary frames as `Uint8Array`. Drive the loop with `for await`:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
for await (const frame of incoming) {
|
|
103
|
+
// handle frame
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For the 90% case where every frame is JSON, wrap with `parseJson`:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { parseJson } from "@mauroandre/velojs/sockets";
|
|
111
|
+
|
|
112
|
+
for await (const msg of parseJson<MyMsgType>(incoming)) {
|
|
113
|
+
// msg is typed, malformed frames are skipped with a console.warn
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `send(msg)` — push to the client
|
|
118
|
+
|
|
119
|
+
- `string` / `Uint8Array` → sent as-is
|
|
120
|
+
- `object` → `JSON.stringify`'d
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
send("hello"); // text frame
|
|
124
|
+
send(new Uint8Array([1,2,3])); // binary frame
|
|
125
|
+
send({ type: "tick", n: 42 }); // JSON text frame
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `keepOpen()` — control auto-close on return
|
|
129
|
+
|
|
130
|
+
When your handler returns (or its promise resolves), VeloJS checks: did you call `keepOpen()`?
|
|
131
|
+
|
|
132
|
+
- **Yes** → socket stays open until `abortSignal` fires.
|
|
133
|
+
- **No** → socket closes immediately.
|
|
134
|
+
|
|
135
|
+
A `for await (const msg of incoming)` loop keeps the handler alive naturally while messages flow — `keepOpen()` is redundant in that case but harmless. When you set up listeners and return (no loop), call `keepOpen()` or everything you registered will be torn down immediately.
|
|
136
|
+
|
|
137
|
+
### `close(code?, reason?)` — server-initiated close
|
|
138
|
+
|
|
139
|
+
Close the socket imperatively. Useful for protocol-level end-of-session:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
if (!authorize(user, query.channel)) {
|
|
143
|
+
close(1008, "policy violation");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `abortSignal` — lifecycle hook
|
|
149
|
+
|
|
150
|
+
Fires when:
|
|
151
|
+
- The client disconnects (`ws.close()` on the browser side, tab closes, network drop).
|
|
152
|
+
- The server calls `close()` (this handler).
|
|
153
|
+
- `app.close()` runs in tests.
|
|
154
|
+
|
|
155
|
+
Register all cleanup here:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
abortSignal.addEventListener("abort", () => {
|
|
159
|
+
session.destroy();
|
|
160
|
+
bus.unsubscribe(sub);
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Path derivation
|
|
165
|
+
|
|
166
|
+
`socket_<name>` exported from `app/workers/WorkerTerminal.tsx` registers at `/_socket/workers/WorkerTerminal/<name>`. Same convention as `stream_*` (`/_event/...`) and `action_*` (`/_action/...`) — the plugin preserves the file path casing.
|
|
167
|
+
|
|
168
|
+
## Middleware inheritance
|
|
169
|
+
|
|
170
|
+
Sockets inherit `middlewares` from parent route nodes, just like pages, actions, and streams. Auth rejection happens at upgrade time (the client sees an HTTP error before the WebSocket opens):
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// app/routes.tsx
|
|
174
|
+
export default [
|
|
175
|
+
{
|
|
176
|
+
module: Root,
|
|
177
|
+
isRoot: true,
|
|
178
|
+
children: [
|
|
179
|
+
{
|
|
180
|
+
path: "/workers/:workerId",
|
|
181
|
+
middlewares: [authMiddleware, masterMiddleware],
|
|
182
|
+
module: WorkerTerminal,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`authMiddleware` runs **before** the upgrade. If it rejects, no WebSocket is opened and the client's `useSocket` hook sees `status = "closed"` with an error.
|
|
190
|
+
|
|
191
|
+
## The client hook — `useSocket`
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const { send, status, lastMessage, error, close } = useSocket(stub, {
|
|
195
|
+
channel?: string,
|
|
196
|
+
onMessage?: (msg: string | Uint8Array) => void,
|
|
197
|
+
onOpen?: () => void,
|
|
198
|
+
onClose?: (evt: CloseEvent) => void,
|
|
199
|
+
onError?: (err: Event) => void,
|
|
200
|
+
enabled?: boolean,
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- `status: Signal<"connecting" | "open" | "closed">` — reactive, bind to UI.
|
|
205
|
+
- `lastMessage: Signal<string | Uint8Array | null>` — the most recent frame.
|
|
206
|
+
- `send(msg)` — queues messages sent while `connecting`; drops messages sent while `closed`.
|
|
207
|
+
- `close(code?, reason?)` — client-initiated disconnect.
|
|
208
|
+
|
|
209
|
+
**No auto-reconnect.** If the server closes the socket (network drop, server restart), `status` goes to `"closed"` and stays there. The caller decides whether/when to reconnect — typically by toggling `enabled` or re-mounting the component. This is intentional: sockets are usually stateful (pty session, collaborative cursor) and a blind reconnect would silently lose state.
|
|
210
|
+
|
|
211
|
+
## Testing
|
|
212
|
+
|
|
213
|
+
The testing toolkit ships `app.socket()`:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { createTestApp } from "@mauroandre/velojs/testing";
|
|
217
|
+
|
|
218
|
+
const app = await createTestApp({ routes });
|
|
219
|
+
|
|
220
|
+
const ws = await app.socket(socket_terminal, {
|
|
221
|
+
user: { id: "alice" },
|
|
222
|
+
channel: "w42",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
ws.send({ type: "data", data: "ls\n" });
|
|
226
|
+
|
|
227
|
+
const reply = await ws.next({ timeoutMs: 500 });
|
|
228
|
+
expect(JSON.parse(reply as string)).toEqual({ type: "data", data: "..." });
|
|
229
|
+
|
|
230
|
+
await ws.close();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### What `app.socket()` does
|
|
234
|
+
|
|
235
|
+
- Invokes your handler directly with a mock `Context`.
|
|
236
|
+
- Wires `incoming` ↔ `ws.send` and `ws.next()` ↔ `send()`.
|
|
237
|
+
- Tracks the `AbortController` so `app.close()` aborts open sessions.
|
|
238
|
+
|
|
239
|
+
### What `app.socket()` does NOT do
|
|
240
|
+
|
|
241
|
+
- **Middleware does not run.** If your socket relies on `authMiddleware` to set `c.get("user")`, pass `user` via the option:
|
|
242
|
+
```ts
|
|
243
|
+
const ws = await app.socket(handler, { user: await buildUser(...) });
|
|
244
|
+
```
|
|
245
|
+
Test middleware behavior separately through a regular endpoint or page that uses the same middleware.
|
|
246
|
+
- **No real HTTP upgrade.** Binary frames pass through `Uint8Array`, but there's no true TCP socket.
|
|
247
|
+
|
|
248
|
+
See [17-testing.md](/docs/testing) for the full toolkit.
|
|
249
|
+
|
|
250
|
+
## Common patterns
|
|
251
|
+
|
|
252
|
+
### Terminal / interactive shell
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
export const socket_terminal: SocketHandler = async ({ incoming, send, keepOpen, abortSignal, query }) => {
|
|
256
|
+
const pty = await spawnPty(query.channel);
|
|
257
|
+
pty.onData((chunk) => send({ type: "data", data: chunk }));
|
|
258
|
+
pty.onExit((code) => send({ type: "exit", code }));
|
|
259
|
+
abortSignal.addEventListener("abort", () => pty.kill());
|
|
260
|
+
keepOpen();
|
|
261
|
+
for await (const msg of parseJson<{ type: string; data?: string }>(incoming)) {
|
|
262
|
+
if (msg.type === "data" && msg.data) pty.write(msg.data);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Event-driven (no `for await`)
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
export const socket_notifications: SocketHandler = async ({ send, keepOpen, abortSignal, c }) => {
|
|
271
|
+
const user = c.get("user");
|
|
272
|
+
const sub = bus.subscribe(`notifications:${user.id}`, (notification) => {
|
|
273
|
+
send(notification);
|
|
274
|
+
});
|
|
275
|
+
abortSignal.addEventListener("abort", () => bus.unsubscribe(sub));
|
|
276
|
+
keepOpen(); // required — handler returns but we want the socket to stay open
|
|
277
|
+
};
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Auth rejection
|
|
281
|
+
|
|
282
|
+
Use middleware (preferred, rejects before upgrade):
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
{ path: "/workers/:id", middlewares: [authMiddleware], module: WorkerTerminal }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Or reject inside the handler after upgrade:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
export const socket_foo: SocketHandler = async ({ c, close }) => {
|
|
292
|
+
const user = c.get("user");
|
|
293
|
+
if (!user) return close(1008, "unauthorized");
|
|
294
|
+
// ...proceed
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Related
|
|
299
|
+
|
|
300
|
+
- [Event Streams](/docs/event-streams) — one-way server → client push (SSE). Use when you don't need client → server.
|
|
301
|
+
- [`pipe` in streams](/docs/event-streams#pipe-unified-vocabulary) — share vocabulary (`send / keepOpen / abortSignal`) with `socket_*`.
|
|
302
|
+
- [Middlewares](/docs/middlewares)
|
|
303
|
+
- [Testing](/docs/testing)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: velojs-static-generation
|
|
3
|
+
description: "Static site generation with `velojs build --static`: prerendered HTML + `index.json`, `staticPaths` for dynamic routes, and what does not work in static mode. Use when deploying to a CDN/bucket or building a fully static site."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Static Site Generation (SSG)
|
|
7
|
+
|
|
8
|
+
VeloJS can generate fully static HTML sites — perfect for landing pages, documentation, blogs, and any content that doesn't change per request. Static sites are fast, cheap to host, and excellent for SEO since search engines see the complete HTML.
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
When you run `velojs build --static`, VeloJS:
|
|
13
|
+
|
|
14
|
+
1. Builds the client bundle (JS, CSS) — same as a normal build
|
|
15
|
+
2. Builds the server entry — same as a normal build
|
|
16
|
+
3. **Crawls every route** in your app, runs the loaders, and renders the HTML
|
|
17
|
+
4. Saves two files per route:
|
|
18
|
+
- `index.html` — the complete pre-rendered page (what search engines see)
|
|
19
|
+
- `index.json` — the loader data (used for SPA-like navigation between pages)
|
|
20
|
+
|
|
21
|
+
The result is a `dist/` folder with pure HTML + assets that you can deploy to any static hosting (CDN, S3, GitHub Pages, Netlify, etc).
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
Build your app as a static site:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx velojs build --static
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The output:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
dist/
|
|
35
|
+
├── index.html # Home page
|
|
36
|
+
├── index.json # Home page data
|
|
37
|
+
├── 404.html # Catch-all page (if defined) — see Custom 404 page
|
|
38
|
+
├── about/
|
|
39
|
+
│ ├── index.html # About page
|
|
40
|
+
│ └── index.json # About page data
|
|
41
|
+
├── docs/
|
|
42
|
+
│ ├── getting-started/
|
|
43
|
+
│ │ ├── index.html
|
|
44
|
+
│ │ └── index.json
|
|
45
|
+
│ └── routes/
|
|
46
|
+
│ ├── index.html
|
|
47
|
+
│ └── index.json
|
|
48
|
+
└── client/
|
|
49
|
+
├── client.C3f9Ax_1.js # Client bundle (content-hashed)
|
|
50
|
+
└── client.C3f9Ax_1.css # Styles (content-hashed)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## SPA-like navigation
|
|
54
|
+
|
|
55
|
+
Even though the site is static, navigation between pages feels instant — no full page reloads. Here's how:
|
|
56
|
+
|
|
57
|
+
1. **First visit**: the browser loads the pre-rendered `index.html` (fast, SEO-friendly)
|
|
58
|
+
2. **Subsequent clicks**: JavaScript fetches the `index.json` for the target page and updates the UI without reloading
|
|
59
|
+
|
|
60
|
+
This gives you the best of both worlds: static HTML for SEO and performance, with SPA-like transitions for the user experience.
|
|
61
|
+
|
|
62
|
+
## Loaders in static mode
|
|
63
|
+
|
|
64
|
+
Loaders work normally — they run once during the build and their data is frozen into the HTML and JSON files. This is perfect for:
|
|
65
|
+
|
|
66
|
+
- Fetching content from a CMS or API at build time
|
|
67
|
+
- Reading markdown files from disk
|
|
68
|
+
- Any data that doesn't change per user or per request
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
export const loader = async () => {
|
|
72
|
+
// This runs once during `velojs build --static`
|
|
73
|
+
const { getPostsFromCMS } = await import("./cms.service.js");
|
|
74
|
+
return getPostsFromCMS();
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The data returned by the loader is embedded in the page's HTML (for SEO) and also saved as a JSON file (for SPA navigation).
|
|
79
|
+
|
|
80
|
+
## Dynamic routes with staticPaths
|
|
81
|
+
|
|
82
|
+
Routes with URL parameters (like `/users/:id`) can't be pre-rendered without knowing all possible values. Export a `staticPaths` function to tell VeloJS which pages to generate:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// app/pages/UserDetail.tsx
|
|
86
|
+
|
|
87
|
+
export const staticPaths = async () => {
|
|
88
|
+
const { getUsers } = await import("./user.service.js");
|
|
89
|
+
const users = await getUsers();
|
|
90
|
+
return users.map(u => ({ id: u.id }));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// This generates:
|
|
94
|
+
// /users/1/index.html
|
|
95
|
+
// /users/2/index.html
|
|
96
|
+
// /users/3/index.html
|
|
97
|
+
// ... one page per user
|
|
98
|
+
|
|
99
|
+
export const loader = async ({ params }: LoaderArgs) => {
|
|
100
|
+
const { getUser } = await import("./user.service.js");
|
|
101
|
+
return getUser(params.id);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const Component = () => {
|
|
105
|
+
const { data } = useLoader<User>();
|
|
106
|
+
return <h1>{data.value?.name}</h1>;
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Routes with `:params` that don't export `staticPaths` are skipped during the static build (with a warning).
|
|
111
|
+
|
|
112
|
+
## Custom 404 page
|
|
113
|
+
|
|
114
|
+
If your app defines a catch-all route (`{ path: "*", module: NotFound }` — see [Routes](/docs/routes#the-404-page)), the static build emits it as **`dist/404.html`** at the output root, following the universal `/404.html` convention.
|
|
115
|
+
|
|
116
|
+
Most static hosts serve `404.html` automatically for unmatched paths (GitHub Pages, Netlify, Cloudflare Pages, S3/CloudFront). On a bare nginx you point `error_page 404 /404.html;` at it. The HTTP status on a direct hit is then up to the host — in static mode VeloJS only produces the page content, not the response status.
|
|
117
|
+
|
|
118
|
+
## What doesn't work in static mode
|
|
119
|
+
|
|
120
|
+
Since there's no server running after the build, some features are not available:
|
|
121
|
+
|
|
122
|
+
| Feature | Status | Why |
|
|
123
|
+
|---------|--------|-----|
|
|
124
|
+
| `Component` | Works | Pre-rendered to HTML |
|
|
125
|
+
| `loader` | Works | Runs once at build time, data frozen |
|
|
126
|
+
| `useLoader` / `Loader` | Works | Hydrates from HTML, SPA nav via JSON |
|
|
127
|
+
| `Link` | Works | SPA navigation without reload |
|
|
128
|
+
| `useParams`, `useQuery` | Works | Read from URL at runtime |
|
|
129
|
+
| `action_*` | Not available | No server to handle POST requests |
|
|
130
|
+
| `middlewares` | Not available | No per-request server context |
|
|
131
|
+
| `addRoutes` / `onServer` | Not available | No server running |
|
|
132
|
+
| `getCookie` / `setCookie` | Not available | No request/response context |
|
|
133
|
+
| SSE / WebSocket | Not available | No server for real-time connections |
|
|
134
|
+
|
|
135
|
+
**Rule of thumb**: if your page only displays data (no forms, no mutations, no auth), it works perfectly as static. If it needs server interaction at runtime, use the normal SSR build (`velojs build` without `--static`).
|
|
136
|
+
|
|
137
|
+
## Deployment
|
|
138
|
+
|
|
139
|
+
The `dist/` folder is a self-contained static site. Deploy it anywhere:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Any static file server
|
|
143
|
+
npx serve dist
|
|
144
|
+
|
|
145
|
+
# Or upload to S3, Netlify, Vercel, GitHub Pages, etc.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
No Node.js server needed in production.
|