@dokebi/template-inertia
| Install | |
|---|---|
npm install @dokebi/template-inertia |
|
| Latest Version: | 0.4.2 |
| License: | MIT |
| Last Updated: | Oct 16, 2026 |
| Links: | npm |
@dokebi/template-inertia 0.4.2
An optional package for Node >=24.0.0 and core ^0.4.0 (public on npm). Its ID is dokebi-template-inertia and it depends on dokebi-web. @dokebi/web is a peer dependency (not the pinned dependency it used to be). EJS, Vue and Vite are not installed as required dependencies of the server package.
It follows plugin contract v2 (D-0028): createPlugin({ app: app.view, options?, entry? }), where app is required. The old web, dependencies and shared inputs are gone. web is connected through a providerDependency: when dokebi-web is registered in the same app that instance is used, otherwise it is created from @dokebi/web. Per-request shared props go through entry.shared.
import { createPlugin as createInertia } from '@dokebi/template-inertia';
import { createPlugin as createWeb } from '@dokebi/web';
const web = createWeb({ app: app.view }); // register first when you read the address or add routes directly
app.register(web);
const pages = createInertia({
app: app.view,
options: { version: 'build-hash', scripts: ['/assets/app.js'], styles: ['/assets/app.css'] },
entry: { shared: (request) => ({ user: String(request.headers['x-user'] ?? 'guest') }) },
});
// Inside the setup of a business plugin that declares web and pages as dependencies:
// context.get(web).route({ method: 'GET', url: '/', handler: (req, reply) =>
// context.get(pages).render(req, reply, 'Home', { counter: { value: 0 } }) });
The x-user header above only illustrates the API; it is not authentication. Build user props from the app's authentication result. Call render(request, reply, component, props?) only for GET/HEAD and always return or await it in the handler. The first visit is answered with HTML and a JSON page script, later X-Inertia requests with page JSON, in Inertia v3's script[data-page="app"] format. rootId defaults to app, title to an empty string and version to development. Without a configured version a warn log (code: INERTIA_VERSION_DEFAULT) is written once in setup: clients then cannot notice changed assets, so real deployments set a per-build version and that manifest's scripts and styles. With a Vite build, vite: { manifest, entry, base? } does this instead. Setup reads the manifest (.vite/manifest.json, relative to the working folder or absolute, at most 4 MiB) once and fills in the entry's script, the CSS of the entry and the chunks it imports, and a version from the manifest's content hash (the first 32 characters of SHA-256). base (default /) is the URL prefix of the build files. vite together with version, scripts or styles is refused. When the entry is not in the manifest the start fails and the error names the entry (ISSUE-0005 PG-07). A missing manifest file fails with Vite manifest not found: <path>. Build the views first (npm run build)..
This plugin serves the build files itself through dokebi-web (D-0031, supplement 1). Only files reachable from the entry are served: static and dynamic import chunks, CSS and assets. Each file is registered as a GET route at the exact path <base><file>; no wildcard routes are used, so the manifest itself and files outside the list are never served. When several apps use the same chunk it is registered once. The files are in outDir when the manifest is <outDir>/.vite/manifest.json, otherwise in the manifest's folder. Responses get a content type per extension and cache-control: public, max-age=31536000, immutable, because build files are expected to have a hash in their names. Each file is read on its first request and kept in memory (one read shared by concurrent requests; a failed read is not kept); files above 4 MiB are streamed on every request instead. To serve them from a CDN or another static server, put them at the same paths.
dokebi-template-inertia:
title: My app
vite: { manifest: dist/.vite/manifest.json, entry: src/apps/shop/views/app.js, base: /build/ }
Invalid options are INVALID_PLUGIN_OPTIONS in the factory. Asset URLs must be local absolute URLs starting with /; serving files other than the Vite build is the app's or a static server's job.
props are JSON values, async lazy functions or the deferred/merge/once helpers (see below). shared(request) and the call's props are merged per request, and the call's props win. A partial-data or partial-except request for the same component evaluates only the selected props (except wins). Selected lazy functions run concurrently with Promise.all, and the result keeps the original key order. errors is always included. Non-JSON values and cycles fail instead of answering. JSON, the title and asset attributes put into HTML are escaped, and Vary: X-Inertia and Cache-Control: no-store are set. An outdated version gets 409 + X-Inertia-Location/Version without evaluating props.
redirect(request, reply, localUrl) is 302 for GET/HEAD and 303 for mutations; an Inertia fragment navigation is 409 + X-Inertia-Redirect. location(request, reply, url) is a local or HTTP(S) navigation: 409 + X-Inertia-Location for Inertia, 302 for normal requests. Choose external destinations in trusted app code.
Use Inertia for Vue page navigation and initial props, and fetch('/api/...') REST for later queries and updates. API routes return JSON and status codes directly and are not wrapped in Inertia responses. Pages and APIs share business services and authentication and authorisation, and the app checks CSRF for cookie-authenticated mutations. When servers must share state, connect the optional DB or Redis.
There is no shared Template base class. The EJS string engine and this HTTP response adapter are independent classes. The responder closes after the whole request drain, and each Application gets a new instance. Without options, the dokebi-template-inertia value of app.config is used.
Install
dokebi add uses a static install declaration to create the entry file src/infra/inertia/shared.ts (export async function shared(_request) { return {}; }), and the generated code passes it as entry. There is no install module (installModule). web is installed alongside as a manifest dependency and checked for order.
Supported: the initial HTML, page navigation, shared/lazy/partial props, deferred/merge/once props (see below), redirects, version negotiation and server rendering (see SSR below). Precognition, scrollProps and sharedProps are not provided. Session flash is delivered in the page's flash field when @dokebi/session is registered (D-0055, read once). Vue persistent layouts are the Vue client's job.
Forms and validation
Validation follows D-0074. A refused route schema and throw validationError({ field: 'message' }) (from @dokebi/web) are the same 422 VALIDATION_FAILED answer. For an Inertia request with @dokebi/session this plugin flashes errors (and old input without password-like fields) and answers 303 back to the same-origin Referer; the next page gets them in its errors prop, so useForm().errors.name shows the message. Without a session the answer is the 422 JSON with errors (read it in onHttpException). The target and the old input follow @dokebi/web's form helpers (shared with @dokebi/app's HTML forms; 0.4.2 refuses a Referer path that reads as another host, such as //other.host/…), so this version needs @dokebi/web ^0.4.2.
Submit forms with only (the props the submit changes, for example form.post('/posts', { only: ['posts'] })): the GET after the redirect is a new visit, and a full visit must send every prop again because Inertia replaces all props, so the server cannot trim it by itself. Inertia keeps the partial headers across the redirect, and errors is always included. dokebi check warns about a submit without only (inertia-form-only).
The option errorPage (for example Error, per app in defineApp({ inertia: { errorPage } })) renders that page component with { status, error, message, hint } for the other errors of Inertia visits and page loads (403, 404, 500 …). Without it a visit gets the JSON error and a page load @dokebi/web's small HTML page. Details: AI.md §3.3.
Example 24 has a real Vue 3 + Inertia + REST app, authentication and CSRF fixtures and automatic Chrome checks. npm test -w @dokebi/template-inertia is the HTTP protocol regression suite.
Document settings per scope (D-0030)
scope({ title?, rootId?, version?, scripts?, styles?, vite?, ssr?, shared? }) returns a renderer with its own document settings and extra shared props (async; with vite it reads the manifest). Unset values follow the plugin options. shared is merged after the plugin's shared props. @dokebi/app uses it for each app's own Vite entry.
deferred, merge and once props (D-0052)
Import defer, merge, deepMerge and once and use them as prop values (render props and shared props). With only plain values and lazy functions the page object is the same as before.
defer(fn, group?)ordefer(fn, { group?, merge?, once?, rescue? }): not computed on the first load (HTML or Inertia visit, SSR included) and announced asdeferredProps: { [group]: [prop] }(default groupdefault). After showing the page, the client fetches each group with one partial reload. Withrescue: true, when the function throws only that prop is left out and listed inrescuedProps, with anINERTIA_PROP_RESCUEDerror log.merge(value | fn, { mode?, matchOn? })anddeepMerge(value | fn, { matchOn? }):mergeProps(append),prependProps,deepMergeProps;matchOnbecomesmatchPropsOn: ["<prop>.<field>"]. The client merges only on partial reloads of the same component. Props listed inX-Inertia-Resetare not announced.once(value | fn, { key?, ttlMs? }):onceProps: { [key]: { prop, expiresAt } }(expiresAtin ms, or null). When an Inertia visit'sX-Inertia-Except-Once-Propslists the key, the value is not computed (the announcement stays so the client carries its value over). A partial reload that asks for it recomputes it.- Partial reloads compute only what
onlyandexceptselect, deferred and once props included. The announcement fields list only the selected props. - Errors: a failing function that is not rescue fails the render as before (web answers 500 and logs the full error). The debug log
Inertia props resolvedlists the computed function props, the left-out groups, the skipped once props and the rescues.
Server rendering (SSR, D-0039)
With ssr: { bundle, failure? } the first HTML of a page is rendered on the server. There is no separate SSR process: the SSR build output is imported once at start in the same process, and its render(page) is called for every full-page request. Inertia navigation (X-Inertia) stays JSON and never goes through SSR.
bundle: a build file that exportsrender(page) → Promise<{ head: string[]; body: string }>(relative to the working folder or absolute). Inertia v3'screateInertiaAppreturns a(page, renderToString)function when called on the server without apage, soexport const render = async (page) => (await app)(page, renderToString)is enough (client/ssr.jsof example 24).- A missing bundle or one without
renderfails the start, so turning SSR on never silently falls back to CSR. - When
renderthrows during a request, returns the wrong shape (heada string array,bodya string) or thebodyhas noid="<rootId>",failuredecides.fallback(default) answers that request with the CSR page and logs anINERTIA_SSR_FAILEDerror;errorfails the request (to see it at once during development). headgoes before</head>. When it contains a<title>(the page's<Head>), the configuredtitleis left out.bodyreplaces the default<script data-page>and the empty root.- The browser entry hydrates with
createSSRAppwhenel.hasAttribute('data-server-rendered'). ssrStats()is the number of renders and failures since start (for health checks and dashboards).scope({ ssr }): a bundle per app. Without it the plugin option applies;falseturns it off. The same file is imported once.
Measured (example 24, same process, 400 sequential requests): CSR 0.20–0.24 ms, SSR 0.24–0.28 ms (a small page), heap +1.1 MB after 450 SSR pages.
Development mode (HMR, D-0045)
With DOKEBI_VITE_DEV=1 (set by dokebi dev) the project's Vite starts in middleware mode without a build; Vite answers <base>*, and pages load @vite/client and the source entry. Editing a .vue file updates the page without a reload (one Vite per dokebi dev session runs in a host process; every process and cluster worker forwards to it, and the HMR socket is the host's loopback port on 127.0.0.1 and ::1, D-0045 addendum 3). SSR renders the source (ssr.entry, otherwise the ssr.js next to vite.entry) through Vite per request. Production is unaffected.
Seal hosts and history (D-0062, 0.2.0)
D-0062. The first HTML of a sealed request (dokebi-web sealOf) carries only <script data-page="app" type="application/json" data-sealed="true">E:…</script> (the title is the configured one), and dokebi-web seals visit JSON. The browser opens it with sealInertia(http) of @dokebi/seal/http. Combined with SSR it is SEAL_SSR_CONFLICT.
encryptHistory: true | false | 'sealed'(option andscope, default'sealed') andrender(..., props, { encryptHistory, clearHistory }).- The page after a login or logout (@dokebi/session 0.2
historyReset()) carriesclearHistory: true. When it happened in a request that rendered no page, the cookiedokebi.history-reset(Path=/, 10 minutes, HttpOnly, SameSite=Lax, Secure on https) carries it to the next page, which clears the history.
See the AI guide for error codes and development-mode details.
Related Packages
A production-ready, SSR-safe Cloudflare Turnstile component for Vue 3.
Version History
| Version | Released | License |
|---|---|---|
| 0.4.2 | MIT | |
| 0.4.1 | MIT | |
| 0.4.0 | MIT | |
| 0.2.0 | MIT | |
| 0.1.3 | MIT | |
| 0.1.2 | MIT | |
| 0.1.1 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |