Vue SSR
<GuiForm> from @golemui/gui-vue renders on the server with renderToString from vue/server-renderer and hydrates through createSSRApp. This page documents the contract for any Vue server. The Nuxt guide shows the Nuxt wiring. Requires GolemUI 1.5.0 or newer.
How it works
Section titled “How it works”The server renders the Vue layer: the <form>, the layouts and every gui-* tag. Vue reflects the primitive props of a custom element as attributes, so the tags arrive with their value, label and similar attributes set (<gui-textinput value="Ada" label="First name">). The Lit elements behind those tags render nothing on the server, and Lit renders their internals once the browser upgrades them.
The Vue adapter does not use the defer-hydration hold. The elements upgrade and render as soon as the browser defines them, before or after Vue hydrates, and hydration completes without warnings and reuses the server DOM. The widgets resolve synchronously in setup() through the preloaded registry. A widget that was not preloaded logs the warning from the overview during the server render and renders empty.
Preload on both sides
Section titled “Preload on both sides”Widgets load through dynamic imports, and the server cannot wait for them while it renders. Preload them before the app is created, in a factory both entries share. See Preload the widgets for the registry rules.
import { preloadFormWidgets } from '@golemui/core';import { widgetLoaders } from '@golemui/gui-vue';import { createSSRApp, type App } from 'vue';import Root from './App.vue';
export async function createApp(): Promise<App> { await preloadFormWidgets({ widgetLoaders }); return createSSRApp(Root);}Server entry
Section titled “Server entry”import { renderToString } from 'vue/server-renderer';import { createApp } from './create-app';
export async function render(): Promise<string> { return renderToString(await createApp());}Client entry
Section titled “Client entry”import { createApp } from './create-app';
// Mounting a createSSRApp() app onto server markup hydrates it.createApp().then((app) => app.mount('#root'));HTML template
Section titled “HTML template”Link the stylesheet from the document rather than importing it in the client entry, so the page is styled with JavaScript disabled. The server replaces the placeholder with the rendered app.
<!doctype html><html lang="en" data-theme="auto"> <head> <meta charset="utf-8" /> <link rel="stylesheet" href="/src/styles.css" /> </head> <body> <div id="root"><!--app-html--></div> <script type="module" src="/src/entry-client.ts"></script> </body></html>@import '@golemui/gui-components/index.css';Compiler options
Section titled “Compiler options”<GuiForm> needs no compiler configuration. Only a component of your own that places a raw gui-* tag in its template needs the Vue compiler to leave those tags alone:
import vue from '@vitejs/plugin-vue';
export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') }, }, }), ],});Object props on such a tag must be bound with .prop (<gui-select :options.prop="languages">). Bound as an attribute, the server stringifies them into the markup.
Render the form
Section titled “Render the form”Keep the form config in the component’s module scope or in <script setup>, with a formName.
<script setup lang="ts">import type { FormSubmitEvent } from '@golemui/core';import { GuiForm } from '@golemui/gui-vue';import { gui, type GuiFormInitConfig } from '@golemui/gui-shared';
const formDef = [ gui.inputs.textInput('firstName', { label: 'First name', validator: { required: true } }), gui.inputs.textInput('lastName', { label: 'Last name', validator: { required: true } }), gui.actions.button({ label: 'Create account', actionType: 'submit' }),];
const config: GuiFormInitConfig = { formName: 'harness-form', // same form id on the server and the client formDef, data: { firstName: 'Ada', lastName: 'Lovelace' },};
const onFormSubmit = (event: FormSubmitEvent) => console.log(event.data);</script>
<template> <GuiForm :config="config" @form-submit="onFormSubmit" /></template><script setup lang="ts">import type { FormSubmitEvent } from '@golemui/core';import { GuiForm } from '@golemui/gui-vue';import formDef from './signup.json';
const config = { formName: 'harness-form', // same form id on the server and the client formDef, data: { firstName: 'Ada', lastName: 'Lovelace' },};
const onFormSubmit = (event: FormSubmitEvent) => console.log(event.data);</script>
<template> <GuiForm :config="config" @form-submit="onFormSubmit" /></template>Serving with Vite
Section titled “Serving with Vite”One Vite config builds both bundles. vite build emits the client bundle plus an index.html that already links the hashed script and stylesheet, and vite build --ssr src/entry-server.ts emits the server bundle.
vite build # client bundle and index.htmlvite build --ssr src/entry-server.ts # server bundleA request handler reads the built index.html, calls render() and replaces the placeholder:
const page = template.replace('<!--app-html-->', await render());In development, Vite’s middleware mode serves the same flow through vite.ssrLoadModule('/src/entry-server.ts'). The complete server, with both modes, is apps/vue-ssr-harness in the GolemUI repository.
What the server sends
Section titled “What the server sends”The output of renderToString for the harness form, cut down to the form tag, the first text input and the submit button. Whitespace added and Vue’s hydration comment markers removed; everything else is verbatim.
<form id="harness-form" novalidate dir="ltr"> <!-- layout wrappers omitted --> <div class="gui-textinput gui-field"> <gui-textinput uid="firstName" label="First name" touched="false" required value="Ada" placeholder="Ada" ></gui-textinput> </div> <!-- the other fields look the same --> <div class="gui-button gui-field"> <gui-button uid="event_0" actionType="submit" label="Create account" invalid="false" variant="filled" iconPosition="left" ></gui-button> </div></form>The form, the layouts and the field wrappers are there, and every gui-* tag carries its primitive props as attributes: value, label, placeholder, required. The element internals (the <input>, the <label>) appear once the browser upgrades the elements.
Render the widgets on the server
Section titled “Render the widgets on the server”The gui-* elements can also render their content on the server, so labels, inputs and values are in the HTML. It is optional. The fields are complete in the HTML before any JavaScript runs, and the page doesn’t change when the elements upgrade; in exchange, the HTML is bigger and the server does more work on each request. Without it, the page stays as described above.
To turn it on, install @lit-labs/ssr and pass the HTML Vue rendered through renderElementsInHtml:
import { renderElementsInHtml } from '@golemui/gui-components/ssr';import { renderToString } from 'vue/server-renderer';import { createApp } from './create-app';
export async function render(): Promise<string> { return renderElementsInHtml(await renderToString(await createApp()));}Each element renders from the attributes Vue gave it, and replaces that content with its live render when it upgrades. A value the adapter binds as a property, such as the options of a select, is not in the HTML: the select shows its placeholder until it upgrades. What the elements render on the server, and its limits, are in Server rendering in the GolemUI Components docs.
formNameis optional on Vue 3.5 and newer, where the adapter derives a stable id on both sides. Set it when the id in the markup must be predictable, and always on older Vue versions.- Widget handlers,
formEventandformSubmitnever run on the server.onLoadfires once the widget has mounted in the browser. formHealthis called once during the server render with the initial health, because the component subscribes insetup(). Keep the handler free of DOM access.- i18next is a module-level singleton. Scope the translator per request when the server serves several languages. See i18n.
Custom widgets
Section titled “Custom widgets”Keep the loaders in one module-scope object, spread it into both preload calls and pass the same object as customWidgetLoaders in every form config. See Custom widget loaders.
See also
Section titled “See also”- Nuxt: the Nuxt wiring, with a starter template.
- Server-Side Rendering Overview: the rules shared by every framework.
- Vue Integration: the client-side setup and the full
<GuiForm>props reference.