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 CHANGED
@@ -1,175 +1,938 @@
1
1
  # vue-ssr-lite
2
2
 
3
- `vue-ssr-lite` is a managed Vue 3 SSR runtime. A consumer supplies one application definition and a small Vite registration; the package owns the HTTP server, Vite middleware, per-request Vue/Router/Apollo lifecycle, server rendering, state transfer, browser hydration, static assets, health checks, errors, timeouts, instrumentation, startup, and graceful shutdown.
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
- ## Minimal integration
87
+ Use `kind: 'ssr'` for a Vue application that should render on the server and hydrate in the browser.
6
88
 
7
- Create one application definition:
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/PublicSsrRuntime.ts
11
- import PublicApp from './PublicApp.vue'
12
- import { defineSsrApplication, defineSsrRuntime } from 'vue-ssr-lite'
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
- export const publicApplication = defineSsrApplication({
15
- id: 'public',
16
- rootComponent: PublicApp,
17
- routes: [
18
- { path: '/:path(.*)*', component: PublicApp },
19
- ],
20
- apollo: ({ publicConfig }) => ({
21
- endPoints: { default: publicConfig.graphqlEndpoint },
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
- export default () => defineSsrRuntime({
26
- name: 'public-site',
27
- entries: [{
28
- id: 'public',
29
- kind: 'ssr',
30
- template: 'site.html',
31
- hosts: ['*'],
32
- application: publicApplication,
33
- }],
34
- server: {
35
- host: process.env.HOST || '0.0.0.0',
36
- port: Number(process.env.PORT || 4173),
37
- trustProxy: process.env.TRUST_PROXY === 'true',
38
- publicConfig: {
39
- graphqlEndpoint: process.env.GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
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
- Register its template and exported application in Vite:
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
- plugins: [
54
- vue(),
55
- vueSsrLite({
56
- applications: [{
57
- id: 'public',
58
- definition: 'src/PublicSsrRuntime.ts',
59
- exportName: 'publicApplication',
60
- template: 'site.html',
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
- The source `site.html` only needs a mount element:
328
+ ### `applications`
68
329
 
69
- ```html
70
- <!doctype html>
71
- <html lang="en">
72
- <head><meta charset="UTF-8"></head>
73
- <body><div id="app"></div></body>
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
- Use package-owned lifecycle commands:
336
+ Do not add SPA applications to the `applications` array.
337
+
338
+ ## 5. Add Commands
78
339
 
79
340
  ```json
80
341
  {
81
- "scripts": {
82
- "dev": "vue-ssr-lite dev --runtime src/PublicSsrRuntime.ts",
83
- "build": "vue-ssr-lite build --runtime src/PublicSsrRuntime.ts",
84
- "start": "vue-ssr-lite start"
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
- ## Runtime behavior
350
+ ## 6. Start Development
90
351
 
91
- - Every render creates a new Vue app, router, Apollo client set/cache, request context, application state, head state, response state, and abort signal.
92
- - Generated `vue-apollo-client` composables use native Vue server prefetch. After render, named caches are extracted into an inert `application/json` state element. The browser restores them before creating query composables or mounting.
93
- - Development runs Vite in middleware mode and loads the runtime module through Vite for safe hot updates. Production serves compiled client assets and imports `dist/server/SsrRuntime.js` without loading Vite.
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
- ## Public exports
356
+ Build and run production:
99
357
 
100
- - `vue-ssr-lite`: isomorphic application/runtime definitions, request context, app creation, serialization, and public types.
101
- - `vue-ssr-lite/client`: browser hydration only; contains no Node server imports.
102
- - `vue-ssr-lite/server`: SSR renderer, Node HTTP lifecycle, host/proxy/cookie normalization, HTML/assets, health/readiness, response-cache controls, and graceful shutdown.
103
- - `vue-ssr-lite/vite`: HTML transformation, virtual hydration entries, framework deduplication, and SSR dependency handling.
104
- - `vue-ssr-lite` CLI: `dev`, `build`, and `start`.
358
+ ```bash
359
+ npm run build
360
+ npm run start
361
+ ```
105
362
 
106
- The build command produces:
363
+ ## SSR-Only Application
107
364
 
108
- ```text
109
- dist/client/** compiled HTML, manifest, and hashed browser assets
110
- dist/server/SsrRuntime.js consumer runtime/application server bundle
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
- The package build itself produces separate `index.mjs`, `client.mjs`, `server.mjs`, `vite.mjs`, and `cli.mjs` entry points.
446
+ Use it in the SPA:
447
+
448
+ ```ts
449
+ // src/spa/main.ts
450
+ import { createApp } from 'vue'
114
451
 
115
- ## Extension points
452
+ import apollo from '../config/apollo'
453
+ import App from './App.vue'
454
+ import router from './router'
116
455
 
117
- Application-specific behavior belongs in the consumer:
456
+ const app = createApp(App)
118
457
 
119
- - `createInitialState` and `createExtension` for typed public/request data;
120
- - `routes` for static, nested, dynamic, or catch-all routing;
121
- - `apollo` for endpoints, named clients, cache policies, request-aware context, customer auth callbacks, and uploads;
122
- - `install` for application plugins and typed provides;
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
- Anonymous response caching is explicit and remains off unless an SSR entry adds
131
- `responseCache`. A safe public entry sets a public `cacheControl`, chooses a
132
- short TTL, and provides publication/locale variation plus invalidation tags:
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 publicPages = createSsrMemoryResponseCache({
138
- maxEntries: 500,
139
- maxBytes: 32 * 1024 * 1024,
842
+ const responseCache = createSsrMemoryResponseCache({
843
+ maxEntries: 500,
140
844
  })
845
+ ```
141
846
 
142
- // In the SSR runtime entry:
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
- // On publication: publicPages.invalidate({ tags: [`host:${hostname}`] })
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
- For multi-process deployments, provide the same interface backed by a shared
155
- cache. Compression is intentionally integrated at the reverse-proxy/platform
156
- layer so precompressed hashed assets and dynamic HTML use one deployment-wide
157
- policy without buffering a second copy inside the Node process.
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
- Endpoint handlers return plain response values; they never receive Node response objects. Application code never calls `renderToString`, extracts/restores Apollo state, injects HTML, serves assets, starts a server, or performs request cleanup.
913
+ For a SPA:
160
914
 
161
- ## Dependency and bundling contract
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
- Vue, Vue Router, `@vue/server-renderer`, Apollo Client, GraphQL, Vite, and `vue-apollo-client` are peers so the consumer owns one compatible runtime instance. The Vite plugin deduplicates them. The Apollo/GraphQL ESM/CJS cluster is inlined into the SSR consumer bundle; Vue, Vue Router, and the renderer remain external so Node resolves their official runtime exports. Application-specific browser-oriented UI dependencies remain the consumer's documented `ssr.noExternal` responsibility.
920
+ For SSR:
164
921
 
165
- Node-only modules are reachable only from `/server`, `/vite`, and the CLI. Browser hydration imports `/client`, preventing `node:http`, filesystem, and Vite code from entering client output.
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
- ## Error and redirect contract
928
+ Optional for multi-mode deployments:
168
929
 
169
- Consumers set `context.response.statusCode`, headers, or a redirect. Relative and same-origin redirects are allowed by default. A canonical-domain redirect must explicitly set `allowExternal: true`. Invalid configuration, hosts, templates, and application registrations fail predictably. Timeouts return 504; unhandled rendering failures return 500; non-HTML misses return JSON 404; disabled deployment-role entries return 421.
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
- ## Validation
934
+ Both application types can run from the same project, build process, and production server.
172
935
 
173
- Focused tests cover host/proxy/cookie behavior, safe state/head injection, concurrent request isolation, router/state/head separation, Apollo cache isolation/restoration, and server lifecycle. Consumer integrations must additionally validate their routes, domains, templates, SEO, GraphQL failures, real-browser hydration warnings and duplicate requests, Docker startup, reverse-proxy headers, and shutdown behavior.
936
+ ## License
174
937
 
175
- See [SsrPerformanceBaseline.md](./docs/SsrPerformanceBaseline.md) and [SsrBuiltoCompatibility.md](./docs/SsrBuiltoCompatibility.md).
938
+ MIT