vue-ssr-lite 0.1.6 → 0.1.8
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 +879 -116
- package/dist/SsrApplicationRuntime.d.ts.map +1 -1
- package/dist/SsrRuntimeTypes.d.ts +5 -1
- package/dist/SsrRuntimeTypes.d.ts.map +1 -1
- package/dist/chunks/SsrApplicationRuntime-BFebmWyk.mjs +95 -0
- package/dist/chunks/{SsrServerRuntime-CrpKROBI.mjs → SsrServerRuntime-ChrpJneS.mjs} +77 -64
- package/dist/cli.mjs +27 -29
- package/dist/client.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/server/SsrServerRuntime.d.ts.map +1 -1
- package/dist/server.mjs +1 -1
- package/package.json +4 -2
- package/dist/chunks/SsrApplicationRuntime-DB_oRq-_.mjs +0 -92
package/README.md
CHANGED
|
@@ -1,175 +1,938 @@
|
|
|
1
1
|
# vue-ssr-lite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A simple, production-ready SSR runtime for Vue 3 and Vite.
|
|
4
|
+
|
|
5
|
+
Use it to run:
|
|
6
|
+
|
|
7
|
+
- A server-rendered Vue website
|
|
8
|
+
- A normal Vue SPA
|
|
9
|
+
- Multiple SSR applications
|
|
10
|
+
- SPA and SSR applications together
|
|
11
|
+
- Applications on different domains or subdomains
|
|
12
|
+
|
|
13
|
+
It includes routing, browser hydration, production builds, a managed Node server, health checks, custom endpoints, caching, timeouts, and Vue plugin support.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Vue 3 server-side rendering
|
|
18
|
+
- Normal Vue SPA support
|
|
19
|
+
- SPA and SSR in one project
|
|
20
|
+
- Vue Router support
|
|
21
|
+
- Automatic browser hydration
|
|
22
|
+
- Multiple applications and HTML entries
|
|
23
|
+
- Domain and subdomain routing
|
|
24
|
+
- Runtime roles for one image / many process modes
|
|
25
|
+
- Apollo, GraphQL, REST, Pinia, and i18n support
|
|
26
|
+
- SEO metadata and status codes
|
|
27
|
+
- Custom server endpoints
|
|
28
|
+
- Health and readiness checks
|
|
29
|
+
- Request timeouts
|
|
30
|
+
- Optional response caching
|
|
31
|
+
- Cookie allowlists and denylists
|
|
32
|
+
- Proxy-aware host and protocol resolution
|
|
33
|
+
- Graceful shutdown
|
|
34
|
+
- TypeScript support
|
|
35
|
+
|
|
36
|
+
## Package Entry Points
|
|
37
|
+
|
|
38
|
+
| Import | Purpose |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `vue-ssr-lite` | `defineSsrApplication`, `defineSsrRuntime`, request context, hydration helpers |
|
|
41
|
+
| `vue-ssr-lite/client` | Browser hydration only |
|
|
42
|
+
| `vue-ssr-lite/server` | Managed Node server, host matching, cookies, response cache |
|
|
43
|
+
| `vue-ssr-lite/vite` | Vite plugin that wires HTML templates and virtual client entries |
|
|
44
|
+
|
|
45
|
+
CLI binary:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
vue-ssr-lite <dev|build|start> [--root .] [--runtime src/SsrRuntime.ts] [--server-output dist/server/SsrRuntime.js]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Node.js 20 or newer.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install vue-ssr-lite vue vue-router @vue/server-renderer
|
|
57
|
+
npm install --save-dev vite @vitejs/plugin-vue
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Using Yarn:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
yarn add vue-ssr-lite vue vue-router @vue/server-renderer
|
|
64
|
+
yarn add --dev vite @vitejs/plugin-vue
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Choose Your Application Type
|
|
68
|
+
|
|
69
|
+
`vue-ssr-lite` supports two application types.
|
|
70
|
+
|
|
71
|
+
### SPA
|
|
72
|
+
|
|
73
|
+
Use `kind: 'spa'` for a normal Vue application that runs in the browser.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
|
|
77
|
+
- Admin dashboard
|
|
78
|
+
- Internal application
|
|
79
|
+
- Editor
|
|
80
|
+
- Authenticated application
|
|
81
|
+
- Client-side portal
|
|
82
|
+
|
|
83
|
+
A SPA keeps its normal Vite `main.ts` entry.
|
|
84
|
+
|
|
85
|
+
### SSR
|
|
4
86
|
|
|
5
|
-
|
|
87
|
+
Use `kind: 'ssr'` for a Vue application that should render on the server and hydrate in the browser.
|
|
6
88
|
|
|
7
|
-
|
|
89
|
+
Examples:
|
|
90
|
+
|
|
91
|
+
- Public website
|
|
92
|
+
- Storefront
|
|
93
|
+
- Blog
|
|
94
|
+
- Marketing website
|
|
95
|
+
- Documentation website
|
|
96
|
+
- SEO-focused application
|
|
97
|
+
|
|
98
|
+
An SSR application is defined with `defineSsrApplication()`.
|
|
99
|
+
|
|
100
|
+
## Quick Start: SPA and SSR Together
|
|
101
|
+
|
|
102
|
+
The following example creates:
|
|
103
|
+
|
|
104
|
+
- A dashboard SPA on `app.example.com`
|
|
105
|
+
- A server-rendered website on all other hosts
|
|
106
|
+
|
|
107
|
+
## 1. Create the SPA Application
|
|
108
|
+
|
|
109
|
+
Create a normal Vue entry:
|
|
8
110
|
|
|
9
111
|
```ts
|
|
10
|
-
// src/
|
|
11
|
-
import
|
|
12
|
-
import
|
|
112
|
+
// src/spa/main.ts
|
|
113
|
+
import { createApp } from 'vue'
|
|
114
|
+
import App from './App.vue'
|
|
115
|
+
import router from './router'
|
|
116
|
+
|
|
117
|
+
const app = createApp(App)
|
|
13
118
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
119
|
+
app.use(router)
|
|
120
|
+
app.mount('#app')
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Create the SPA root component:
|
|
124
|
+
|
|
125
|
+
```vue
|
|
126
|
+
<!-- src/spa/App.vue -->
|
|
127
|
+
<script setup lang="ts">
|
|
128
|
+
import { RouterView } from 'vue-router'
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<template>
|
|
132
|
+
<RouterView />
|
|
133
|
+
</template>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Create the SPA HTML entry:
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<!-- index.html -->
|
|
140
|
+
<!doctype html>
|
|
141
|
+
<html lang="en">
|
|
142
|
+
<head>
|
|
143
|
+
<meta charset="UTF-8" />
|
|
144
|
+
<meta
|
|
145
|
+
name="viewport"
|
|
146
|
+
content="width=device-width, initial-scale=1" />
|
|
147
|
+
<title>Dashboard</title>
|
|
148
|
+
</head>
|
|
149
|
+
|
|
150
|
+
<body>
|
|
151
|
+
<div id="app"></div>
|
|
152
|
+
|
|
153
|
+
<script
|
|
154
|
+
type="module"
|
|
155
|
+
src="/src/spa/main.ts"></script>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This remains a normal Vue and Vite SPA. No special SPA application definition is required.
|
|
161
|
+
|
|
162
|
+
## 2. Create the SSR Application
|
|
163
|
+
|
|
164
|
+
Create the SSR root component:
|
|
165
|
+
|
|
166
|
+
```vue
|
|
167
|
+
<!-- src/website/App.vue -->
|
|
168
|
+
<script setup lang="ts">
|
|
169
|
+
import { RouterView } from 'vue-router'
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<template>
|
|
173
|
+
<RouterView />
|
|
174
|
+
</template>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Create the SSR routes:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// src/website/routes.ts
|
|
181
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
182
|
+
|
|
183
|
+
import HomePage from './pages/HomePage.vue'
|
|
184
|
+
import AboutPage from './pages/AboutPage.vue'
|
|
185
|
+
import NotFoundPage from './pages/NotFoundPage.vue'
|
|
186
|
+
|
|
187
|
+
export const websiteRoutes: RouteRecordRaw[] = [
|
|
188
|
+
{
|
|
189
|
+
path: '/',
|
|
190
|
+
component: HomePage,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
path: '/about',
|
|
194
|
+
component: AboutPage,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
path: '/:pathMatch(.*)*',
|
|
198
|
+
component: NotFoundPage,
|
|
199
|
+
},
|
|
200
|
+
]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Define the SSR application:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// src/website/SsrApplication.ts
|
|
207
|
+
import { defineSsrApplication } from 'vue-ssr-lite'
|
|
208
|
+
|
|
209
|
+
import App from './App.vue'
|
|
210
|
+
import { websiteRoutes } from './routes'
|
|
211
|
+
|
|
212
|
+
export const websiteApplication = defineSsrApplication({
|
|
213
|
+
id: 'website',
|
|
214
|
+
rootComponent: App,
|
|
215
|
+
routes: websiteRoutes,
|
|
23
216
|
})
|
|
217
|
+
```
|
|
24
218
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
219
|
+
Create its HTML template:
|
|
220
|
+
|
|
221
|
+
```html
|
|
222
|
+
<!-- site.html -->
|
|
223
|
+
<!doctype html>
|
|
224
|
+
<html lang="en">
|
|
225
|
+
<head>
|
|
226
|
+
<meta charset="UTF-8" />
|
|
227
|
+
<meta
|
|
228
|
+
name="viewport"
|
|
229
|
+
content="width=device-width, initial-scale=1" />
|
|
230
|
+
</head>
|
|
231
|
+
|
|
232
|
+
<body>
|
|
233
|
+
<div id="app"></div>
|
|
234
|
+
</body>
|
|
235
|
+
</html>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The SSR browser entry and hydration setup are added automatically.
|
|
239
|
+
|
|
240
|
+
## 3. Create the Runtime
|
|
241
|
+
|
|
242
|
+
The runtime tells the package which application should handle each host.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// src/SsrRuntime.ts
|
|
246
|
+
import { defineSsrRuntime } from 'vue-ssr-lite'
|
|
247
|
+
|
|
248
|
+
import { websiteApplication } from './website/SsrApplication'
|
|
249
|
+
|
|
250
|
+
export default defineSsrRuntime({
|
|
251
|
+
name: 'my-platform',
|
|
252
|
+
|
|
253
|
+
entries: [
|
|
254
|
+
{
|
|
255
|
+
id: 'dashboard',
|
|
256
|
+
kind: 'spa',
|
|
257
|
+
template: 'index.html',
|
|
258
|
+
hosts: ['app.example.com', 'localhost'],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'website',
|
|
262
|
+
kind: 'ssr',
|
|
263
|
+
template: 'site.html',
|
|
264
|
+
hosts: ['*'],
|
|
265
|
+
application: websiteApplication,
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
|
|
269
|
+
defaultEntryId: 'website',
|
|
270
|
+
|
|
271
|
+
server: {
|
|
272
|
+
host: '0.0.0.0',
|
|
273
|
+
port: Number(process.env.PORT || 4173),
|
|
274
|
+
role: process.env.APP_RUNTIME || 'unified',
|
|
275
|
+
|
|
276
|
+
publicConfig: {
|
|
277
|
+
apiUrl: process.env.PUBLIC_API_URL || 'http://localhost:4000',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
42
280
|
})
|
|
43
281
|
```
|
|
44
282
|
|
|
45
|
-
|
|
283
|
+
The specific SPA hostname is checked first. The `*` SSR entry handles all remaining hosts.
|
|
284
|
+
`defaultEntryId` is used when no host pattern matches.
|
|
285
|
+
|
|
286
|
+
When the server starts, the console prints a ready message with a clickable local URL and the active role:
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
✓ Server Ready
|
|
290
|
+
|
|
291
|
+
➜ Local: http://localhost:4173/
|
|
292
|
+
➜ Role: unified
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## 4. Configure Vite
|
|
296
|
+
|
|
297
|
+
Register the SSR application and the normal SPA HTML entry:
|
|
46
298
|
|
|
47
299
|
```ts
|
|
300
|
+
// vite.config.ts
|
|
48
301
|
import { defineConfig } from 'vite'
|
|
49
302
|
import vue from '@vitejs/plugin-vue'
|
|
303
|
+
|
|
50
304
|
import { vueSsrLite } from 'vue-ssr-lite/vite'
|
|
51
305
|
|
|
52
306
|
export default defineConfig({
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
307
|
+
plugins: [
|
|
308
|
+
vue(),
|
|
309
|
+
|
|
310
|
+
vueSsrLite({
|
|
311
|
+
applications: [
|
|
312
|
+
{
|
|
313
|
+
id: 'website',
|
|
314
|
+
definition: 'src/website/SsrApplication.ts',
|
|
315
|
+
exportName: 'websiteApplication',
|
|
316
|
+
template: 'site.html',
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
|
|
320
|
+
spaEntries: {
|
|
321
|
+
dashboard: 'index.html',
|
|
322
|
+
},
|
|
323
|
+
}),
|
|
324
|
+
],
|
|
64
325
|
})
|
|
65
326
|
```
|
|
66
327
|
|
|
67
|
-
|
|
328
|
+
### `applications`
|
|
68
329
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</html>
|
|
75
|
-
```
|
|
330
|
+
Contains applications that use server-side rendering and browser hydration.
|
|
331
|
+
|
|
332
|
+
### `spaEntries`
|
|
333
|
+
|
|
334
|
+
Contains normal Vite SPA HTML entries.
|
|
76
335
|
|
|
77
|
-
|
|
336
|
+
Do not add SPA applications to the `applications` array.
|
|
337
|
+
|
|
338
|
+
## 5. Add Commands
|
|
78
339
|
|
|
79
340
|
```json
|
|
80
341
|
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
342
|
+
"scripts": {
|
|
343
|
+
"dev": "vue-ssr-lite dev --runtime src/SsrRuntime.ts",
|
|
344
|
+
"build": "vue-ssr-lite build --runtime src/SsrRuntime.ts",
|
|
345
|
+
"start": "vue-ssr-lite start"
|
|
346
|
+
}
|
|
86
347
|
}
|
|
87
348
|
```
|
|
88
349
|
|
|
89
|
-
##
|
|
350
|
+
## 6. Start Development
|
|
90
351
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- Proxy host/protocol headers are ignored unless `trustProxy` is enabled. Cookie forwarding is deny-by-default and requires an allowlist.
|
|
95
|
-
- Head values and JSON-LD are escaped, hydration JSON escapes `<` and Unicode separators, raw HTML templates are never exposed as static assets, and production error responses contain no stack trace or internal path.
|
|
96
|
-
- The default response cache is disabled. An entry may opt into a bounded package memory cache or provide a distributed `SsrResponseCache`; the package owns the application + normalized host + route portion of every key, while `vary` adds publication/data version and locale. Requests with forwarded cookies bypass response caching, and `invalidate` supports key/tag invalidation. Apollo request caches are never reused as response caches.
|
|
352
|
+
```bash
|
|
353
|
+
npm run dev
|
|
354
|
+
```
|
|
97
355
|
|
|
98
|
-
|
|
356
|
+
Build and run production:
|
|
99
357
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
- `vue-ssr-lite` CLI: `dev`, `build`, and `start`.
|
|
358
|
+
```bash
|
|
359
|
+
npm run build
|
|
360
|
+
npm run start
|
|
361
|
+
```
|
|
105
362
|
|
|
106
|
-
|
|
363
|
+
## SSR-Only Application
|
|
107
364
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
365
|
+
For a project that only needs SSR:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
export default defineSsrRuntime({
|
|
369
|
+
name: 'my-website',
|
|
370
|
+
|
|
371
|
+
entries: [
|
|
372
|
+
{
|
|
373
|
+
id: 'website',
|
|
374
|
+
kind: 'ssr',
|
|
375
|
+
template: 'site.html',
|
|
376
|
+
hosts: ['*'],
|
|
377
|
+
application: websiteApplication,
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
|
|
381
|
+
server: {
|
|
382
|
+
publicConfig: {},
|
|
383
|
+
},
|
|
384
|
+
})
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Vite configuration:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
vueSsrLite({
|
|
391
|
+
applications: [
|
|
392
|
+
{
|
|
393
|
+
id: 'website',
|
|
394
|
+
definition: 'src/website/SsrApplication.ts',
|
|
395
|
+
exportName: 'websiteApplication',
|
|
396
|
+
template: 'site.html',
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
})
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## SPA-Only Application
|
|
403
|
+
|
|
404
|
+
A runtime can also serve a normal SPA without an SSR application:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
export default defineSsrRuntime({
|
|
408
|
+
name: 'my-dashboard',
|
|
409
|
+
|
|
410
|
+
entries: [
|
|
411
|
+
{
|
|
412
|
+
id: 'dashboard',
|
|
413
|
+
kind: 'spa',
|
|
414
|
+
template: 'index.html',
|
|
415
|
+
hosts: ['*'],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
|
|
419
|
+
server: {
|
|
420
|
+
publicConfig: {},
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Vite configuration still requires at least one SSR application in the current `vueSsrLite()` API. For a completely SPA-only project, use normal Vite without the `vueSsrLite()` plugin.
|
|
426
|
+
|
|
427
|
+
## Using the Same Vue Plugins
|
|
428
|
+
|
|
429
|
+
You may use the same plugin configuration in your SPA and SSR applications.
|
|
430
|
+
|
|
431
|
+
For example:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
// src/config/apollo.ts
|
|
435
|
+
import { defineApollo } from 'vue-apollo-client'
|
|
436
|
+
|
|
437
|
+
export default defineApollo(({ publicConfig }) => ({
|
|
438
|
+
endPoints: {
|
|
439
|
+
default:
|
|
440
|
+
publicConfig?.graphqlEndpoint ||
|
|
441
|
+
import.meta.env.VITE_GRAPHQL_ENDPOINT,
|
|
442
|
+
},
|
|
443
|
+
}))
|
|
111
444
|
```
|
|
112
445
|
|
|
113
|
-
|
|
446
|
+
Use it in the SPA:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
// src/spa/main.ts
|
|
450
|
+
import { createApp } from 'vue'
|
|
114
451
|
|
|
115
|
-
|
|
452
|
+
import apollo from '../config/apollo'
|
|
453
|
+
import App from './App.vue'
|
|
454
|
+
import router from './router'
|
|
116
455
|
|
|
117
|
-
|
|
456
|
+
const app = createApp(App)
|
|
118
457
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
- `resolveHead` and the request context response state for metadata, status codes, and redirects;
|
|
124
|
-
- runtime `entries` for SPA/SSR host classification and deployment roles;
|
|
125
|
-
- `endpoints` for application-specific robots, sitemaps, or other resources; endpoint tools create isolated Apollo clients and stop them automatically;
|
|
126
|
-
- `readiness` for API/service probes;
|
|
127
|
-
- `logger` and `onMetrics` for structured logging and performance collection.
|
|
128
|
-
- `renderError` for a consumer-owned unavailable/error document while the package enforces an error status, security headers, no-store, and response-lifecycle ownership.
|
|
458
|
+
app.use(apollo)
|
|
459
|
+
app.use(router)
|
|
460
|
+
app.mount('#app')
|
|
461
|
+
```
|
|
129
462
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
463
|
+
Use the same configuration in SSR:
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
// src/website/SsrApplication.ts
|
|
467
|
+
import { defineSsrApplication } from 'vue-ssr-lite'
|
|
468
|
+
|
|
469
|
+
import apollo from '../config/apollo'
|
|
470
|
+
import App from './App.vue'
|
|
471
|
+
import { websiteRoutes } from './routes'
|
|
472
|
+
|
|
473
|
+
export const websiteApplication = defineSsrApplication({
|
|
474
|
+
id: 'website',
|
|
475
|
+
rootComponent: App,
|
|
476
|
+
routes: websiteRoutes,
|
|
477
|
+
plugins: [apollo],
|
|
478
|
+
})
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
This allows one plugin configuration to support both application types.
|
|
482
|
+
|
|
483
|
+
You can use the same approach with:
|
|
484
|
+
|
|
485
|
+
- Apollo and GraphQL
|
|
486
|
+
- Pinia
|
|
487
|
+
- i18n
|
|
488
|
+
- Theme providers
|
|
489
|
+
- REST clients
|
|
490
|
+
- Authentication plugins
|
|
491
|
+
- Analytics plugins
|
|
492
|
+
- Custom Vue plugins
|
|
493
|
+
|
|
494
|
+
## Using GraphQL in SSR
|
|
495
|
+
|
|
496
|
+
Keep GraphQL operations in `.graphql` files:
|
|
497
|
+
|
|
498
|
+
```graphql
|
|
499
|
+
query GetPosts {
|
|
500
|
+
posts {
|
|
501
|
+
id
|
|
502
|
+
title
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Use the generated composable in the Vue page:
|
|
508
|
+
|
|
509
|
+
```vue
|
|
510
|
+
<script setup lang="ts">
|
|
511
|
+
import { useGetPostsQuery } from '../graphql'
|
|
512
|
+
|
|
513
|
+
const { result, loading, error } = useGetPostsQuery(
|
|
514
|
+
{},
|
|
515
|
+
{
|
|
516
|
+
ssr: true,
|
|
517
|
+
fetchPolicy: 'cache-first',
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
</script>
|
|
521
|
+
|
|
522
|
+
<template>
|
|
523
|
+
<main>
|
|
524
|
+
<p v-if="loading">Loading...</p>
|
|
525
|
+
|
|
526
|
+
<p v-else-if="error">
|
|
527
|
+
{{ error.message }}
|
|
528
|
+
</p>
|
|
529
|
+
|
|
530
|
+
<article
|
|
531
|
+
v-for="post in result?.posts ?? []"
|
|
532
|
+
:key="post.id">
|
|
533
|
+
<h2>{{ post.title }}</h2>
|
|
534
|
+
</article>
|
|
535
|
+
</main>
|
|
536
|
+
</template>
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
The same generated composable can also be used inside the SPA.
|
|
540
|
+
|
|
541
|
+
Use `ssr: true` for public data that should be included in server-rendered HTML.
|
|
542
|
+
|
|
543
|
+
Use `ssr: false` for browser-only or private queries.
|
|
544
|
+
|
|
545
|
+
## Using REST APIs
|
|
546
|
+
|
|
547
|
+
REST APIs can be used through:
|
|
548
|
+
|
|
549
|
+
- Native `fetch`
|
|
550
|
+
- Axios
|
|
551
|
+
- Ky
|
|
552
|
+
- A custom Vue plugin
|
|
553
|
+
- Another SSR-compatible data package
|
|
554
|
+
|
|
555
|
+
Example:
|
|
556
|
+
|
|
557
|
+
```vue
|
|
558
|
+
<script setup lang="ts">
|
|
559
|
+
import { onServerPrefetch, ref } from 'vue'
|
|
560
|
+
|
|
561
|
+
interface Post {
|
|
562
|
+
id: string
|
|
563
|
+
title: string
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const posts = ref<Post[]>([])
|
|
567
|
+
|
|
568
|
+
const loadPosts = async () => {
|
|
569
|
+
const response = await fetch('https://api.example.com/posts')
|
|
570
|
+
|
|
571
|
+
posts.value = await response.json()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
onServerPrefetch(loadPosts)
|
|
575
|
+
</script>
|
|
576
|
+
|
|
577
|
+
<template>
|
|
578
|
+
<article
|
|
579
|
+
v-for="post in posts"
|
|
580
|
+
:key="post.id">
|
|
581
|
+
<h2>{{ post.title }}</h2>
|
|
582
|
+
</article>
|
|
583
|
+
</template>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
For reusable REST caching and hydration, register an SSR-compatible Vue plugin in the `plugins` array.
|
|
587
|
+
|
|
588
|
+
## Public Configuration
|
|
589
|
+
|
|
590
|
+
Pass public values from the runtime:
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
server: {
|
|
594
|
+
publicConfig: {
|
|
595
|
+
apiUrl:
|
|
596
|
+
'https://api.example.com',
|
|
597
|
+
graphqlEndpoint:
|
|
598
|
+
'https://api.example.com/graphql',
|
|
599
|
+
},
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Access them in an SSR component:
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
import { useSsrRequestContext } from 'vue-ssr-lite'
|
|
607
|
+
|
|
608
|
+
const context = useSsrRequestContext()
|
|
609
|
+
|
|
610
|
+
const apiUrl = context.publicConfig.apiUrl
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Only include values that are safe to expose to the browser.
|
|
614
|
+
|
|
615
|
+
## SEO
|
|
616
|
+
|
|
617
|
+
Set page metadata from a component:
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
import { useSsrRequestContext } from 'vue-ssr-lite'
|
|
621
|
+
|
|
622
|
+
const context = useSsrRequestContext()
|
|
623
|
+
|
|
624
|
+
context.head.value = {
|
|
625
|
+
title: 'Products',
|
|
626
|
+
description: 'Browse our latest products.',
|
|
627
|
+
robots: 'index, follow',
|
|
628
|
+
canonicalUrl: 'https://example.com/products',
|
|
629
|
+
ogTitle: 'Products',
|
|
630
|
+
ogDescription: 'Browse our latest products.',
|
|
631
|
+
ogImage: 'https://example.com/social.jpg',
|
|
632
|
+
twitterCard: 'summary_large_image',
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Status Codes
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
const context = useSsrRequestContext()
|
|
640
|
+
|
|
641
|
+
context.response.statusCode = 404
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
## Redirects
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
const context = useSsrRequestContext()
|
|
648
|
+
|
|
649
|
+
context.response.redirect = {
|
|
650
|
+
location: '/new-page',
|
|
651
|
+
statusCode: 307,
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
## Domain Routing
|
|
656
|
+
|
|
657
|
+
Exact hostname:
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
hosts: ['example.com']
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Subdomains:
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
hosts: ['*.example.com']
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Exact hostname and subdomains:
|
|
670
|
+
|
|
671
|
+
```ts
|
|
672
|
+
hosts: ['example.com', '*.example.com']
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
Catch-all:
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
hosts: ['*']
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
Do not include protocols or paths in host values.
|
|
682
|
+
|
|
683
|
+
Set `server.trustProxy` to `true` only when the Node process sits behind a trusted reverse proxy. Then `X-Forwarded-Host` and `X-Forwarded-Proto` are used for host matching and absolute URLs. Leave it `false` for direct local traffic.
|
|
684
|
+
|
|
685
|
+
## Runtime Roles
|
|
686
|
+
|
|
687
|
+
Roles let one codebase and one Docker image run as different process modes.
|
|
688
|
+
|
|
689
|
+
- Set `server.role` to the active mode for this process.
|
|
690
|
+
- Set `roles` on each entry to list which modes may serve that entry.
|
|
691
|
+
- Omit `roles` (or omit `server.role`) to keep an entry available in every mode.
|
|
692
|
+
|
|
693
|
+
Role names are application-defined strings. Common patterns:
|
|
694
|
+
|
|
695
|
+
| Mode | Typical use |
|
|
696
|
+
| --- | --- |
|
|
697
|
+
| `unified` | Local development or a single process that serves every entry |
|
|
698
|
+
| A private role | SPA / admin / back-office only |
|
|
699
|
+
| A public role | SSR website / marketing / storefront only |
|
|
700
|
+
|
|
701
|
+
Example:
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
export default defineSsrRuntime({
|
|
705
|
+
name: 'my-platform',
|
|
706
|
+
|
|
707
|
+
entries: [
|
|
708
|
+
{
|
|
709
|
+
id: 'admin',
|
|
710
|
+
kind: 'spa',
|
|
711
|
+
template: 'index.html',
|
|
712
|
+
hosts: ['app.example.com', 'localhost'],
|
|
713
|
+
roles: ['unified', 'admin'],
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
id: 'website',
|
|
717
|
+
kind: 'ssr',
|
|
718
|
+
template: 'site.html',
|
|
719
|
+
hosts: ['*'],
|
|
720
|
+
roles: ['unified', 'website'],
|
|
721
|
+
application: websiteApplication,
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
|
|
725
|
+
server: {
|
|
726
|
+
role: process.env.APP_RUNTIME || 'unified',
|
|
727
|
+
publicConfig: {},
|
|
728
|
+
},
|
|
729
|
+
})
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
With that setup:
|
|
733
|
+
|
|
734
|
+
- `APP_RUNTIME=unified` serves both entries (host routing still applies).
|
|
735
|
+
- `APP_RUNTIME=admin` serves only the SPA entry.
|
|
736
|
+
- `APP_RUNTIME=website` serves only the SSR entry.
|
|
737
|
+
|
|
738
|
+
If a host matches an entry that the current role does not allow, the server responds with `421`.
|
|
739
|
+
|
|
740
|
+
You can also load role-specific code only when needed:
|
|
741
|
+
|
|
742
|
+
```ts
|
|
743
|
+
export default async () => {
|
|
744
|
+
const role = process.env.APP_RUNTIME || 'unified'
|
|
745
|
+
const websiteApplication =
|
|
746
|
+
role === 'admin'
|
|
747
|
+
? undefined
|
|
748
|
+
: (await import('./website/SsrApplication')).websiteApplication
|
|
749
|
+
|
|
750
|
+
return defineSsrRuntime({
|
|
751
|
+
name: 'my-platform',
|
|
752
|
+
entries: [
|
|
753
|
+
{
|
|
754
|
+
id: 'admin',
|
|
755
|
+
kind: 'spa',
|
|
756
|
+
template: 'index.html',
|
|
757
|
+
hosts: ['app.example.com'],
|
|
758
|
+
roles: ['unified', 'admin'],
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
id: 'website',
|
|
762
|
+
kind: 'ssr',
|
|
763
|
+
template: 'site.html',
|
|
764
|
+
hosts: ['*'],
|
|
765
|
+
roles: ['unified', 'website'],
|
|
766
|
+
application: websiteApplication,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
server: {
|
|
770
|
+
role,
|
|
771
|
+
publicConfig: {},
|
|
772
|
+
},
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
`defineSsrRuntime` accepts either a plain object or an async factory function.
|
|
778
|
+
|
|
779
|
+
## Custom Endpoints
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
endpoints: [
|
|
783
|
+
{
|
|
784
|
+
id: 'robots',
|
|
785
|
+
|
|
786
|
+
match(request) {
|
|
787
|
+
return request.pathname === '/robots.txt'
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
handle() {
|
|
791
|
+
return {
|
|
792
|
+
statusCode: 200,
|
|
793
|
+
body: 'User-agent: *\nAllow: /',
|
|
794
|
+
headers: {
|
|
795
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
796
|
+
},
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
]
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Custom endpoints can be used for:
|
|
804
|
+
|
|
805
|
+
- `robots.txt`
|
|
806
|
+
- `sitemap.xml`
|
|
807
|
+
- Verification files
|
|
808
|
+
- Public JSON endpoints
|
|
809
|
+
|
|
810
|
+
## Health Checks
|
|
811
|
+
|
|
812
|
+
The server includes:
|
|
813
|
+
|
|
814
|
+
```text
|
|
815
|
+
/healthz
|
|
816
|
+
/readyz
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
Add readiness checks:
|
|
820
|
+
|
|
821
|
+
```ts
|
|
822
|
+
readiness: [
|
|
823
|
+
{
|
|
824
|
+
id: 'api',
|
|
825
|
+
|
|
826
|
+
async run() {
|
|
827
|
+
const response = await fetch('https://api.example.com/health')
|
|
828
|
+
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
throw new Error('API is unavailable.')
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
]
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
## Response Caching
|
|
133
838
|
|
|
134
839
|
```ts
|
|
135
840
|
import { createSsrMemoryResponseCache } from 'vue-ssr-lite/server'
|
|
136
841
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
maxBytes: 32 * 1024 * 1024,
|
|
842
|
+
const responseCache = createSsrMemoryResponseCache({
|
|
843
|
+
maxEntries: 500,
|
|
140
844
|
})
|
|
845
|
+
```
|
|
141
846
|
|
|
142
|
-
|
|
143
|
-
responseCache: {
|
|
144
|
-
store: publicPages,
|
|
145
|
-
ttlMs: 30_000,
|
|
146
|
-
vary: (request) => request.publicConfig.publicationVersion,
|
|
147
|
-
tags: (request) => [`host:${request.host}`],
|
|
148
|
-
},
|
|
149
|
-
cacheControl: 'public, max-age=30',
|
|
847
|
+
Add it to an SSR entry:
|
|
150
848
|
|
|
151
|
-
|
|
849
|
+
```ts
|
|
850
|
+
{
|
|
851
|
+
id: 'website',
|
|
852
|
+
kind: 'ssr',
|
|
853
|
+
template: 'site.html',
|
|
854
|
+
hosts: ['*'],
|
|
855
|
+
application: websiteApplication,
|
|
856
|
+
|
|
857
|
+
responseCache: {
|
|
858
|
+
store: responseCache,
|
|
859
|
+
ttlMs: 60_000,
|
|
860
|
+
},
|
|
861
|
+
}
|
|
152
862
|
```
|
|
153
863
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
864
|
+
## Cookie Filtering
|
|
865
|
+
|
|
866
|
+
SSR requests can forward a filtered `Cookie` header to upstream APIs.
|
|
867
|
+
|
|
868
|
+
```ts
|
|
869
|
+
server: {
|
|
870
|
+
cookieAllowlist: ['session'],
|
|
871
|
+
cookieDenylist: ['admin_token', 'refresh_token'],
|
|
872
|
+
publicConfig: {},
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
- If `cookieAllowlist` is non-empty, only listed cookies are forwarded.
|
|
877
|
+
- `cookieDenylist` always removes matching cookies.
|
|
878
|
+
- Leave both empty when the browser talks to the API directly and SSR needs no cookies.
|
|
879
|
+
|
|
880
|
+
## Server Configuration
|
|
881
|
+
|
|
882
|
+
```ts
|
|
883
|
+
server: {
|
|
884
|
+
host: '0.0.0.0',
|
|
885
|
+
port: 4173,
|
|
886
|
+
role: 'unified',
|
|
887
|
+
|
|
888
|
+
publicConfig: {},
|
|
889
|
+
|
|
890
|
+
requestTimeoutMs: 15_000,
|
|
891
|
+
shutdownTimeoutMs: 10_000,
|
|
892
|
+
|
|
893
|
+
healthPath: '/healthz',
|
|
894
|
+
readinessPath: '/readyz',
|
|
895
|
+
|
|
896
|
+
trustProxy: false,
|
|
897
|
+
|
|
898
|
+
cookieAllowlist: [],
|
|
899
|
+
cookieDenylist: [],
|
|
900
|
+
|
|
901
|
+
logger: {
|
|
902
|
+
info: (event, details) => console.info(event, details ?? ''),
|
|
903
|
+
warn: (event, details) => console.warn(event, details ?? ''),
|
|
904
|
+
error: (event, details) => console.error(event, details ?? ''),
|
|
905
|
+
},
|
|
906
|
+
}
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
`/healthz` and `/readyz` include the active `role` in their JSON responses.
|
|
910
|
+
|
|
911
|
+
## Summary
|
|
158
912
|
|
|
159
|
-
|
|
913
|
+
For a SPA:
|
|
160
914
|
|
|
161
|
-
|
|
915
|
+
1. Create a normal Vue `main.ts`.
|
|
916
|
+
2. Create a normal HTML entry.
|
|
917
|
+
3. Register the HTML file in `spaEntries`.
|
|
918
|
+
4. Add a runtime entry with `kind: 'spa'`.
|
|
162
919
|
|
|
163
|
-
|
|
920
|
+
For SSR:
|
|
164
921
|
|
|
165
|
-
|
|
922
|
+
1. Create the Vue root component and routes.
|
|
923
|
+
2. Define it with `defineSsrApplication()`.
|
|
924
|
+
3. Create an SSR HTML template.
|
|
925
|
+
4. Register it in `applications`.
|
|
926
|
+
5. Add a runtime entry with `kind: 'ssr'`.
|
|
166
927
|
|
|
167
|
-
|
|
928
|
+
Optional for multi-mode deployments:
|
|
168
929
|
|
|
169
|
-
|
|
930
|
+
1. Choose role names for your project.
|
|
931
|
+
2. Set `server.role` from an environment variable.
|
|
932
|
+
3. Restrict each entry with `roles`.
|
|
170
933
|
|
|
171
|
-
|
|
934
|
+
Both application types can run from the same project, build process, and production server.
|
|
172
935
|
|
|
173
|
-
|
|
936
|
+
## License
|
|
174
937
|
|
|
175
|
-
|
|
938
|
+
MIT
|