@netrojs/create-vono 0.0.1

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 ADDED
@@ -0,0 +1,768 @@
1
+ # ◈ Vono
2
+
3
+ **Full-stack Hono + Vue 3 framework — Streaming SSR · SPA · Code Splitting · Type-safe Loaders · SEO · TypeScript**
4
+
5
+ [![npm](https://img.shields.io/npm/v/@netrojs/vono)](https://www.npmjs.com/package/@netrojs/vono)
6
+ [![license](https://img.shields.io/npm/l/@netrojs/vono)](./LICENSE)
7
+
8
+ ---
9
+
10
+ ## Table of contents
11
+
12
+ - [What is Vono?](#what-is-vono)
13
+ - [How it works](#how-it-works)
14
+ - [Quick start](#quick-start)
15
+ - [Manual installation](#manual-installation)
16
+ - [File structure](#file-structure)
17
+ - [Routes](#routes)
18
+ - [definePage()](#definepage)
19
+ - [defineGroup()](#definegroup)
20
+ - [defineLayout()](#definelayout)
21
+ - [defineApiRoute()](#defineapiroute)
22
+ - [Type-safe loaders](#type-safe-loaders)
23
+ - [InferPageData\<T\>](#inferpagedata)
24
+ - [usePageData()](#usepagedata)
25
+ - [State hydration & lifecycle hooks](#state-hydration--lifecycle-hooks)
26
+ - [SEO](#seo)
27
+ - [Middleware](#middleware)
28
+ - [Server middleware](#server-middleware)
29
+ - [Client middleware](#client-middleware)
30
+ - [Layouts](#layouts)
31
+ - [Dynamic params](#dynamic-params)
32
+ - [Code splitting](#code-splitting)
33
+ - [SPA navigation & prefetch](#spa-navigation--prefetch)
34
+ - [API routes](#api-routes)
35
+ - [Production build](#production-build)
36
+ - [Multi-runtime deployment](#multi-runtime-deployment)
37
+ - [Vite plugin reference](#vite-plugin-reference)
38
+ - [API reference](#api-reference)
39
+ - [How SSR hydration works internally](#how-ssr-hydration-works-internally)
40
+
41
+ ---
42
+
43
+ ## What is Vono?
44
+
45
+ Vono is a **file-free, config-driven full-stack framework** that glues [Hono](https://hono.dev) (server) to [Vue 3](https://vuejs.org) (UI). You define your routes once in a plain TypeScript array. Vono:
46
+
47
+ 1. **Renders them on the server** using Vue's streaming `renderToWebStream` — the browser gets `<head>` (CSS, scripts) immediately while the body streams in.
48
+ 2. **Hydrates them in the browser** as a Vue 3 SPA — subsequent navigations fetch only a small JSON payload and swap the reactive data in-place, no full reload.
49
+ 3. **Infers types** from your loader all the way through to the component — one definition, zero duplication.
50
+
51
+ ### Feature matrix
52
+
53
+ | Feature | Detail |
54
+ |---|---|
55
+ | **Streaming SSR** | `renderToWebStream` — `<head>` is flushed before the body starts, so the browser can parse CSS and begin JS evaluation while Vue is still rendering. Lower TTFB than buffered SSR. |
56
+ | **SPA navigation** | Vue Router 4 on the client. Navigations send `x-vono-spa: 1` and receive a small JSON `{ state, seo, params }` payload — no HTML re-render. |
57
+ | **Code splitting** | Pass `() => import('./Page.vue')` as `component`. Vono resolves the import before SSR and wraps it in `defineAsyncComponent` on the client for lazy loading. |
58
+ | **Type-safe loaders** | `definePage<TData>()` infers `TData` from your loader. `InferPageData<typeof page>` extracts it for use in components. `usePageData<T>()` returns it fully typed and reactive. |
59
+ | **Full SEO** | Per-page title, description, Open Graph, Twitter Cards, JSON-LD structured data — injected into `<head>` on SSR and synced via the DOM on SPA navigation. |
60
+ | **Server middleware** | Hono `MiddlewareHandler` — applied per-app, per-group (`defineGroup`), or per-route. Ideal for auth, rate limiting, logging. |
61
+ | **Client middleware** | `useClientMiddleware()` — runs on SPA navigation before the data fetch. Ideal for auth guards, analytics, scroll restoration. |
62
+ | **Route groups** | `defineGroup()` shares a URL prefix, layout, and middleware stack across multiple pages. |
63
+ | **API routes** | `defineApiRoute()` co-locates Hono JSON endpoints alongside your page routes — same file, same middleware. |
64
+ | **Multi-runtime** | `serve()` auto-detects Node.js, Bun, Deno. Edge runtimes (Cloudflare Workers, Vercel Edge) use `vono.handler` directly. |
65
+ | **Zero config** | One Vite plugin (`vonoVitePlugin`) orchestrates both the SSR server bundle and the client SPA bundle. |
66
+
67
+ ---
68
+
69
+ ## How it works
70
+
71
+ ```
72
+ Browser request
73
+
74
+
75
+ Hono (server.ts)
76
+ │ matches route
77
+
78
+ loader(ctx) ──────────────────────────► typed TData object
79
+
80
+
81
+ renderToWebStream(Vue SSR app)
82
+
83
+ ├──► streams <head> immediately ──► browser parses CSS + scripts
84
+
85
+ └──► streams <body> … ──► browser renders progressive HTML
86
+
87
+ client.ts boots
88
+
89
+ createSSRApp() hydrates DOM
90
+
91
+ window.__VONO_STATE__ seeds
92
+ reactive page data (zero fetch)
93
+
94
+ Vue Router takes over navigation
95
+
96
+ SPA nav ──► fetch JSON ──► update reactive data
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Quick start
102
+
103
+ ```bash
104
+ npm create @netrojs/vono@latest my-app
105
+ cd my-app
106
+ npm install
107
+ npm run dev
108
+ ```
109
+
110
+ Or with Bun:
111
+
112
+ ```bash
113
+ bun create @netrojs/vono@latest my-app
114
+ cd my-app
115
+ bun install
116
+ bun run dev
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Manual installation
122
+
123
+ ```bash
124
+ npm i @netrojs/vono vue vue-router @vue/server-renderer hono
125
+ npm i -D vite @vitejs/plugin-vue @hono/vite-dev-server @hono/node-server vue-tsc typescript
126
+ ```
127
+
128
+ ---
129
+
130
+ ## File structure
131
+
132
+ ```
133
+ my-app/
134
+ ├── app.ts ← createVono() + default export for dev server
135
+ ├── server.ts ← Production server entry (await serve(...))
136
+ ├── client.ts ← Browser hydration entry (boot(...))
137
+ ├── vite.config.ts
138
+ ├── tsconfig.json
139
+ ├── global.d.ts ← Window augmentation for SSR-injected keys
140
+ └── app/
141
+ ├── routes.ts ← All route definitions (pages, groups, APIs)
142
+ ├── layouts/
143
+ │ └── RootLayout.vue
144
+ ├── pages/
145
+ │ ├── home.vue
146
+ │ ├── blog/
147
+ │ │ ├── index.vue
148
+ │ │ └── [slug].vue
149
+ │ └── dashboard/
150
+ │ └── index.vue
151
+ └── style.css
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Routes
157
+
158
+ All routes are defined in a plain TypeScript array and passed to `createVono()` and `boot()`.
159
+
160
+ ### `definePage()`
161
+
162
+ The core building block. Every page is a `definePage()` call.
163
+
164
+ ```ts
165
+ import { definePage } from '@netrojs/vono'
166
+
167
+ export const homePage = definePage({
168
+ // URL path — supports [param] and [...catchAll] syntax
169
+ path: '/',
170
+
171
+ // Hono middleware applied only to this route (runs before the loader)
172
+ middleware: [logRequest],
173
+
174
+ // Server-side data fetcher — return value is typed and passed to usePageData()
175
+ loader: async (c) => ({
176
+ posts: await db.posts.findMany(),
177
+ user: c.get('user'), // access Hono context variables
178
+ }),
179
+
180
+ // Static SEO object OR a function that receives (loaderData, params)
181
+ seo: (data, params) => ({
182
+ title: `${data.posts.length} posts — My Blog`,
183
+ description: 'The latest posts from our blog.',
184
+ ogType: 'website',
185
+ }),
186
+
187
+ // Layout override for this specific page
188
+ layout: myLayout, // or `false` to disable the app-level layout
189
+
190
+ // Vue component — use () => import() for automatic code splitting
191
+ component: () => import('./pages/home.vue'),
192
+ })
193
+
194
+ // Export the inferred type for use in components
195
+ export type HomeData = InferPageData<typeof homePage>
196
+ ```
197
+
198
+ **Loader context** (`LoaderCtx`) is the full Hono `Context` object — you have access to `c.req`, `c.env`, `c.get()` / `c.set()`, `c.redirect()`, response helpers, and anything set by upstream middleware.
199
+
200
+ ### `defineGroup()`
201
+
202
+ Groups share a URL prefix, a layout, and a middleware stack.
203
+
204
+ ```ts
205
+ import { defineGroup } from '@netrojs/vono'
206
+
207
+ export const dashboardGroup = defineGroup({
208
+ prefix: '/dashboard',
209
+ layout: dashboardLayout,
210
+ middleware: [requireAuth], // applied to every child route
211
+ routes: [
212
+ definePage({ path: '', component: () => import('./pages/dashboard/index.vue') }),
213
+ definePage({ path: '/posts', component: () => import('./pages/dashboard/posts.vue') }),
214
+ definePage({ path: '/users', component: () => import('./pages/dashboard/users.vue') }),
215
+ ],
216
+ })
217
+ ```
218
+
219
+ - Child paths are concatenated: prefix `/dashboard` + path `/posts` → `/dashboard/posts`.
220
+ - Use `path: ''` (empty string) for the index route of a group (`/dashboard`).
221
+ - Groups can be nested.
222
+
223
+ ### `defineLayout()`
224
+
225
+ Wraps a Vue component as a Vono layout. The component must render `<slot />` where the page content goes.
226
+
227
+ ```ts
228
+ import { defineLayout } from '@netrojs/vono'
229
+ import RootLayout from './layouts/RootLayout.vue'
230
+
231
+ export const rootLayout = defineLayout(RootLayout)
232
+ ```
233
+
234
+ Pass it to `createVono({ layout: rootLayout })` for an app-wide default, to `defineGroup({ layout })` for a section, or directly to `definePage({ layout })` for a single page. Set `layout: false` on a page to opt out of any inherited layout.
235
+
236
+ ### `defineApiRoute()`
237
+
238
+ Co-locate a Hono JSON API alongside your page routes. The callback receives a Hono sub-app mounted at `path`.
239
+
240
+ ```ts
241
+ import { defineApiRoute } from '@netrojs/vono'
242
+
243
+ export const postsApi = defineApiRoute('/api/posts', (app, globalMiddleware) => {
244
+ app.get('/', (c) => c.json({ posts: await db.posts.findMany() }))
245
+ app.get('/:slug', (c) => c.json(await db.posts.findBySlug(c.req.param('slug'))))
246
+ app.post('/', requireAuth, async (c) => {
247
+ const body = await c.req.json()
248
+ return c.json(await db.posts.create(body), 201)
249
+ })
250
+ })
251
+ ```
252
+
253
+ API routes are registered on the Hono app **before** the catch-all page handler, so they always take priority.
254
+
255
+ ---
256
+
257
+ ## Type-safe loaders
258
+
259
+ The `loader` function's return type is inferred automatically:
260
+
261
+ ```ts
262
+ export const postPage = definePage({
263
+ path: '/blog/[slug]',
264
+ loader: async (c) => {
265
+ const post = await db.findPost(c.req.param('slug'))
266
+ return { post, related: await db.relatedPosts(post.id) }
267
+ },
268
+ component: () => import('./pages/blog/[slug].vue'),
269
+ })
270
+ ```
271
+
272
+ TypeScript infers `TData = { post: Post; related: Post[] }` from the loader automatically. The full chain is type-safe: server loader → SSR render → `window.__VONO_STATE__` → `usePageData<T>()` in the component.
273
+
274
+ ### `InferPageData<T>`
275
+
276
+ Extract the loader type from an exported page definition — your **single source of truth**:
277
+
278
+ ```ts
279
+ // routes.ts
280
+ export const postPage = definePage({ loader: async () => ({ post: ... }), ... })
281
+ export type PostData = InferPageData<typeof postPage>
282
+ // ^ { post: Post } — derived from the loader, never written twice
283
+
284
+ // pages/blog/[slug].vue
285
+ import type { PostData } from '../routes'
286
+ const data = usePageData<PostData>()
287
+ // ^ fully typed reactive object
288
+ ```
289
+
290
+ This pattern means you never manually maintain a parallel type — change the loader and TypeScript propagates the error to every component immediately.
291
+
292
+ ---
293
+
294
+ ## `usePageData()`
295
+
296
+ Available inside any component rendered inside a Vono route:
297
+
298
+ ```ts
299
+ import { usePageData } from '@netrojs/vono/client'
300
+ import type { PostData } from '../routes'
301
+
302
+ const data = usePageData<PostData>()
303
+ // data.post → typed Post
304
+ // data.related → typed Post[]
305
+ ```
306
+
307
+ The returned object is **reactive** — when Vue Router performs a SPA navigation, Vono fetches the new JSON payload and updates the reactive store in-place. Components re-render automatically without being unmounted, preserving scroll position and any local state.
308
+
309
+ Calling `usePageData()` outside of a component `setup()` throws a clear error.
310
+
311
+ ---
312
+
313
+ ## State hydration & lifecycle hooks
314
+
315
+ Vono performs **full SSR hydration**. This means:
316
+
317
+ 1. The server renders the complete HTML string and injects loader data as `window.__VONO_STATE__`.
318
+ 2. `boot()` calls `createSSRApp()` (not `createApp()`), which tells Vue to hydrate the existing DOM rather than re-render from scratch.
319
+ 3. Vue's reactivity system is activated on the existing DOM nodes — no flicker, no double render.
320
+ 4. All Vue lifecycle hooks work exactly as expected after hydration:
321
+
322
+ ```vue
323
+ <script setup lang="ts">
324
+ import { ref, computed, watch, onMounted, onUnmounted, onBeforeMount } from 'vue'
325
+ import { useRoute, useRouter } from 'vue-router'
326
+ import { usePageData } from '@netrojs/vono/client'
327
+
328
+ const data = usePageData<MyData>()
329
+ const route = useRoute()
330
+ const router = useRouter()
331
+
332
+ // ref / computed / watch — all work as normal
333
+ const count = ref(0)
334
+ const doubled = computed(() => count.value * 2)
335
+ watch(() => data.title, (t) => document.title = t)
336
+
337
+ // onMounted fires after hydration on the client (not on the server)
338
+ // Safe to access DOM APIs, start timers, attach event listeners
339
+ onMounted(() => {
340
+ console.log('Hydrated!', document.title)
341
+ })
342
+
343
+ onUnmounted(() => {
344
+ // Clean up subscriptions, timers, etc.
345
+ })
346
+ </script>
347
+ ```
348
+
349
+ **Key rules:**
350
+ - `onMounted` and DOM APIs are **client-only** — they are never called during SSR. This is standard Vue SSR behaviour.
351
+ - `ref`, `computed`, `watch`, `provide/inject` all work in both SSR and client contexts.
352
+ - `useRoute()` and `useRouter()` work after hydration because `boot()` installs the Vue Router instance into the app before mounting.
353
+ - Do not access `window`, `document`, or `localStorage` outside of `onMounted` (or `if (import.meta.env.SSR)` guards) — they are undefined on the server.
354
+
355
+ ---
356
+
357
+ ## SEO
358
+
359
+ Define SEO per page, either as a static object or as a function that receives the loader data and URL params:
360
+
361
+ ```ts
362
+ // Static
363
+ definePage({
364
+ seo: {
365
+ title: 'Home — My Site',
366
+ description: 'Welcome to my site.',
367
+ ogTitle: 'Home',
368
+ ogImage: 'https://my-site.com/og/home.png',
369
+ twitterCard: 'summary_large_image',
370
+ jsonLd: {
371
+ '@context': 'https://schema.org',
372
+ '@type': 'WebSite',
373
+ name: 'My Site',
374
+ url: 'https://my-site.com',
375
+ },
376
+ },
377
+ ...
378
+ })
379
+
380
+ // Dynamic — function receives (loaderData, params)
381
+ definePage({
382
+ seo: (data, params) => ({
383
+ title: `${data.post.title} — My Blog`,
384
+ description: data.post.excerpt,
385
+ ogType: 'article',
386
+ ogImage: `https://my-site.com/og/${params.slug}.png`,
387
+ canonical: `https://my-site.com/blog/${params.slug}`,
388
+ }),
389
+ ...
390
+ })
391
+ ```
392
+
393
+ Global defaults are set in `createVono({ seo: { ... } })` and merged with per-page values (page wins on any key they both define).
394
+
395
+ On SPA navigation, `syncSEO()` is called automatically to update `document.title` and all `<meta>` tags in-place.
396
+
397
+ **Supported fields:**
398
+
399
+ | Field | HTML output |
400
+ |---|---|
401
+ | `title` | `<title>` |
402
+ | `description` | `<meta name="description">` |
403
+ | `keywords` | `<meta name="keywords">` |
404
+ | `author` | `<meta name="author">` |
405
+ | `robots` | `<meta name="robots">` |
406
+ | `canonical` | `<link rel="canonical">` |
407
+ | `themeColor` | `<meta name="theme-color">` |
408
+ | `ogTitle`, `ogDescription`, `ogImage`, `ogUrl`, `ogType`, `ogSiteName`, `ogImageAlt` | `<meta property="og:…">` |
409
+ | `twitterCard`, `twitterSite`, `twitterTitle`, `twitterDescription`, `twitterImage` | `<meta name="twitter:…">` |
410
+ | `jsonLd` | `<script type="application/ld+json">` |
411
+
412
+ ---
413
+
414
+ ## Middleware
415
+
416
+ ### Server middleware
417
+
418
+ Vono server middleware is a standard Hono `MiddlewareHandler`:
419
+
420
+ ```ts
421
+ import type { HonoMiddleware } from '@netrojs/vono'
422
+
423
+ const requireAuth: HonoMiddleware = async (c, next) => {
424
+ const token = c.req.header('Authorization')?.replace('Bearer ', '')
425
+ if (!token || !verifyToken(token)) {
426
+ return c.json({ error: 'Unauthorized' }, 401)
427
+ }
428
+ // Pass user to downstream handlers via Hono context
429
+ c.set('user', decodeToken(token))
430
+ await next()
431
+ }
432
+
433
+ const logRequest: HonoMiddleware = async (c, next) => {
434
+ const start = Date.now()
435
+ await next()
436
+ console.log(`${c.req.method} ${c.req.path} → ${Date.now() - start}ms`)
437
+ }
438
+ ```
439
+
440
+ **Three levels of application:**
441
+
442
+ ```ts
443
+ // 1. App-wide (runs before every page and API route)
444
+ createVono({ middleware: [logRequest], routes })
445
+
446
+ // 2. Per group (runs for every route inside the group)
447
+ defineGroup({ middleware: [requireAuth], prefix: '/dashboard', routes: [...] })
448
+
449
+ // 3. Per page (runs only for that specific route)
450
+ definePage({ middleware: [rateLimit], path: '/api/expensive', ... })
451
+ ```
452
+
453
+ Middleware is executed in order: app → group → route. Return early (without calling `next()`) to short-circuit the chain.
454
+
455
+ ### Client middleware
456
+
457
+ Runs on every SPA navigation **before** the JSON data fetch:
458
+
459
+ ```ts
460
+ import { useClientMiddleware } from '@netrojs/vono/client'
461
+
462
+ // Call before boot() — typically in client.ts
463
+ useClientMiddleware(async (url, next) => {
464
+ // Auth guard — redirect to login if session expired
465
+ if (url.startsWith('/dashboard') && !isLoggedIn()) {
466
+ await navigate('/login')
467
+ return // don't call next() — cancels the navigation
468
+ }
469
+
470
+ // Analytics
471
+ analytics.track('pageview', { url })
472
+
473
+ await next() // proceed with the navigation
474
+ })
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Layouts
480
+
481
+ A layout is a Vue component that wraps page content via `<slot />`:
482
+
483
+ ```vue
484
+ <!-- layouts/RootLayout.vue -->
485
+ <script setup lang="ts">
486
+ import { RouterLink } from 'vue-router'
487
+ </script>
488
+
489
+ <template>
490
+ <div class="app">
491
+ <nav>
492
+ <RouterLink to="/">Home</RouterLink>
493
+ <RouterLink to="/blog">Blog</RouterLink>
494
+ </nav>
495
+ <main>
496
+ <slot /> <!-- page content renders here -->
497
+ </main>
498
+ <footer>© 2025</footer>
499
+ </div>
500
+ </template>
501
+ ```
502
+
503
+ Register and apply it:
504
+
505
+ ```ts
506
+ // routes.ts
507
+ export const rootLayout = defineLayout(RootLayout)
508
+
509
+ // App-wide default
510
+ createVono({ layout: rootLayout, routes })
511
+
512
+ // Per-section override
513
+ defineGroup({ layout: dashboardLayout, prefix: '/dashboard', routes: [...] })
514
+
515
+ // Per-page override
516
+ definePage({ layout: false, ... }) // disables layout for this page
517
+ ```
518
+
519
+ ---
520
+
521
+ ## Dynamic params
522
+
523
+ Use bracket syntax in paths. Params are available in `loader`, `seo`, and components via `useRoute()`:
524
+
525
+ ```ts
526
+ // Single param
527
+ definePage({ path: '/blog/[slug]', loader: (c) => ({ post: db.findPost(c.req.param('slug')) }) })
528
+
529
+ // Multiple params
530
+ definePage({ path: '/user/[id]/post/[postId]', loader: (c) => ({
531
+ user: db.findUser(c.req.param('id')),
532
+ post: db.findPost(c.req.param('postId')),
533
+ }) })
534
+
535
+ // Catch-all (matches /files/a/b/c → params.path = 'a/b/c')
536
+ definePage({ path: '/files/[...path]', loader: (c) => ({ path: c.req.param('path') }) })
537
+ ```
538
+
539
+ Inside a component:
540
+
541
+ ```vue
542
+ <script setup lang="ts">
543
+ import { useRoute } from 'vue-router' // re-exported from @netrojs/vono/client
544
+ const route = useRoute()
545
+ // route.params.slug — string
546
+ </script>
547
+ ```
548
+
549
+ ---
550
+
551
+ ## Code splitting
552
+
553
+ Every page with `component: () => import('./pages/X.vue')` generates a separate JS chunk. Vono handles the split correctly in both environments:
554
+
555
+ - **Server (SSR):** `isAsyncLoader()` detects the factory, awaits the import, and renders the resolved component synchronously.
556
+ - **Client (hydration):** The current route's chunk is pre-loaded before `app.mount()` to guarantee the client VDOM matches the SSR HTML. All other route chunks are lazy-loaded on demand via `defineAsyncComponent`.
557
+
558
+ No configuration needed — just use dynamic imports.
559
+
560
+ ---
561
+
562
+ ## SPA navigation & prefetch
563
+
564
+ After hydration, Vue Router handles all same-origin navigation. Vono's `router.beforeEach` hook intercepts every navigation and:
565
+
566
+ 1. Sends `GET <url>` with `x-vono-spa: 1` header.
567
+ 2. The server recognises the header and returns `{ state, seo, params }` JSON (skipping SSR entirely).
568
+ 3. The reactive page data store is updated in-place — components re-render reactively.
569
+ 4. `syncSEO()` updates all meta tags.
570
+
571
+ **Prefetch on hover** (enabled by default) warms the fetch cache before the user clicks:
572
+
573
+ ```ts
574
+ boot({ routes, prefetchOnHover: true })
575
+ ```
576
+
577
+ **Manual prefetch:**
578
+
579
+ ```ts
580
+ import { prefetch } from '@netrojs/vono/client'
581
+ prefetch('/blog/my-post')
582
+ ```
583
+
584
+ ---
585
+
586
+ ## API routes
587
+
588
+ API routes are standard Hono apps mounted at the given path:
589
+
590
+ ```ts
591
+ export const usersApi = defineApiRoute('/api/users', (app) => {
592
+ app.get('/', async (c) => c.json(await db.users.findMany()))
593
+ app.post('/', requireAuth, async (c) => {
594
+ const body = await c.req.json<{ name: string; email: string }>()
595
+ return c.json(await db.users.create(body), 201)
596
+ })
597
+ app.delete('/:id', requireAuth, async (c) => {
598
+ await db.users.delete(c.req.param('id'))
599
+ return c.body(null, 204)
600
+ })
601
+ })
602
+ ```
603
+
604
+ The Hono sub-app is mounted **before** the page handler catch-all, so API routes always win. You can call your own API from `loader()` or from the client using `fetch()`.
605
+
606
+ ---
607
+
608
+ ## Production build
609
+
610
+ ```bash
611
+ npm run build
612
+ ```
613
+
614
+ This runs `vite build` which triggers `vonoVitePlugin`:
615
+
616
+ 1. **SSR bundle** — `dist/server/server.js` (ES module, `target: node18`, top-level await enabled, all dependencies externalised).
617
+ 2. **Client bundle** — `dist/assets/` (ES module chunks + `.vite/manifest.json` for asset fingerprinting).
618
+
619
+ ```bash
620
+ npm run start
621
+ # node dist/server/server.js
622
+ ```
623
+
624
+ The production server reads the manifest, injects the correct hashed script and CSS URLs, and serves static assets from `dist/assets/`.
625
+
626
+ ### Why `target: 'node18'` matters
627
+
628
+ The SSR bundle uses `await serve(...)` at the top level. esbuild's default browser targets (`chrome87`, `es2020`, etc.) do not support top-level await, causing the build to fail with:
629
+
630
+ ```
631
+ Top-level await is not available in the configured target environment
632
+ ```
633
+
634
+ `vonoVitePlugin` explicitly sets `target: 'node18'` for the SSR build, which tells esbuild to emit ES2022+ syntax — including top-level await — in the output.
635
+
636
+ ---
637
+
638
+ ## Multi-runtime deployment
639
+
640
+ ### Node.js
641
+
642
+ ```ts
643
+ // server.ts
644
+ import { serve } from '@netrojs/vono/server'
645
+ import { vono } from './app'
646
+
647
+ await serve({ app: vono, port: 3000, runtime: 'node' })
648
+ ```
649
+
650
+ ### Bun
651
+
652
+ ```ts
653
+ await serve({ app: vono, port: 3000, runtime: 'bun' })
654
+ ```
655
+
656
+ ### Deno
657
+
658
+ ```ts
659
+ await serve({ app: vono, port: 3000, runtime: 'deno' })
660
+ ```
661
+
662
+ ### Cloudflare Workers / Edge
663
+
664
+ ```ts
665
+ // worker.ts — export the handler; no serve() call
666
+ import { vono } from './app'
667
+ export default { fetch: vono.handler }
668
+ ```
669
+
670
+ ### Vercel Edge
671
+
672
+ ```ts
673
+ // api/index.ts
674
+ import { vono } from '../../app'
675
+ export const config = { runtime: 'edge' }
676
+ export default vono.handler
677
+ ```
678
+
679
+ ---
680
+
681
+ ## Vite plugin reference
682
+
683
+ ```ts
684
+ // vite.config.ts
685
+ import { vonoVitePlugin } from '@netrojs/vono/vite'
686
+
687
+ vonoVitePlugin({
688
+ serverEntry: 'server.ts', // default
689
+ clientEntry: 'client.ts', // default
690
+ serverOutDir: 'dist/server', // default
691
+ clientOutDir: 'dist/assets', // default
692
+ serverExternal: ['pg', 'ioredis'], // extra packages kept external in SSR bundle
693
+ vueOptions: { /* @vitejs/plugin-vue options for the client build */ },
694
+ })
695
+ ```
696
+
697
+ The plugin:
698
+ - On `vite build`: configures the SSR server bundle (target `node18`, externals, ESM output).
699
+ - In `closeBundle`: triggers a separate `build()` call for the client SPA bundle with manifest enabled.
700
+
701
+ ---
702
+
703
+ ## API reference
704
+
705
+ ### `@netrojs/vono` (core, isomorphic)
706
+
707
+ | Export | Description |
708
+ |---|---|
709
+ | `definePage(def)` | Define a page route |
710
+ | `defineGroup(def)` | Define a route group |
711
+ | `defineLayout(component)` | Wrap a Vue component as a layout |
712
+ | `defineApiRoute(path, register)` | Define a Hono API sub-app |
713
+ | `compilePath(path)` | Compile a Vono path to a RegExp + keys |
714
+ | `matchPath(compiled, pathname)` | Match a pathname against a compiled path |
715
+ | `toVueRouterPath(path)` | Convert `[param]` syntax to `:param` syntax |
716
+ | `isAsyncLoader(fn)` | Detect an async component loader |
717
+ | `InferPageData<T>` | Extract loader data type from a `PageDef` |
718
+ | `SPA_HEADER`, `STATE_KEY`, `PARAMS_KEY`, `SEO_KEY`, `DATA_KEY` | Shared constants |
719
+
720
+ ### `@netrojs/vono/server`
721
+
722
+ | Export | Description |
723
+ |---|---|
724
+ | `createVono(options)` | Create the Hono app + streaming SSR handler |
725
+ | `serve(options)` | Start the server on Node / Bun / Deno |
726
+ | `detectRuntime()` | Auto-detect the current JS runtime |
727
+ | `vonoVitePlugin(options)` | Vite plugin for dual-bundle production builds |
728
+
729
+ ### `@netrojs/vono/client`
730
+
731
+ | Export | Description |
732
+ |---|---|
733
+ | `boot(options)` | Hydrate the SSR HTML and mount the Vue SPA |
734
+ | `usePageData<T>()` | Access the current page's loader data (reactive) |
735
+ | `useClientMiddleware(fn)` | Register a client-side navigation middleware |
736
+ | `prefetch(url)` | Warm the SPA data cache for a URL |
737
+ | `syncSEO(seo)` | Imperatively sync SEO meta tags |
738
+ | `useRoute()` | Vue Router's `useRoute` (re-exported) |
739
+ | `useRouter()` | Vue Router's `useRouter` (re-exported) |
740
+ | `RouterLink` | Vue Router's `RouterLink` (re-exported) |
741
+
742
+ ### `@netrojs/vono/vite`
743
+
744
+ | Export | Description |
745
+ |---|---|
746
+ | `vonoVitePlugin(options)` | Same as the server export — convenience alias |
747
+
748
+ ---
749
+
750
+ ## How SSR hydration works internally
751
+
752
+ Understanding this prevents subtle bugs:
753
+
754
+ **On the server**, for each request Vono:
755
+ 1. Matches the URL against compiled route patterns.
756
+ 2. Runs server middleware, then the loader.
757
+ 3. Creates a **fresh** `createSSRApp()` + `createRouter()` per request — no shared state between requests (critical for correctness in concurrent environments).
758
+ 4. Initialises `createMemoryHistory()` at the **request URL** before constructing the router. This prevents Vue Router from emitting `[Vue Router warn]: No match found for location with path "/"` — the warning fires when the router performs its startup navigation to the history's initial location (`/`) before any routes match.
759
+ 5. Awaits `router.isReady()`, then calls `renderToWebStream()` to stream HTML.
760
+ 6. Injects `window.__VONO_STATE__`, `__VONO_PARAMS__`, and `__VONO_SEO__` as inline `<script>` tags in the `<body>`.
761
+
762
+ **On the client**, `boot()`:
763
+ 1. Reads the injected `window.__VONO_STATE__[pathname]` and seeds a module-level reactive store — no network request on first load.
764
+ 2. Calls `createSSRApp()` (not `createApp()`), which tells Vue to hydrate (adopt) the existing server-rendered DOM.
765
+ 3. Installs `readonly(reactiveStore)` as `DATA_KEY` into the Vue app via `provide()` — `usePageData()` reads from here.
766
+ 4. Pre-loads the current route's async component chunk synchronously (before `mount()`) to ensure the client VDOM matches the SSR HTML byte-for-byte, preventing hydration mismatches.
767
+ 5. Mounts the app — Vue reconciles the virtual DOM against the real DOM without re-rendering anything.
768
+ 6. On subsequent SPA navigations, `router.beforeEach` fetches JSON, updates the reactive store in-place, and calls `syncSEO()`.