Nuxt
GolemUI renders on the server in Nuxt with the same packages and the same <GuiForm> component as a client-only Vue app. Verified with Nuxt 4 and Vue 3.5. The contract the adapter follows is on the Vue SSR page; this page is the wiring. Requires GolemUI 1.5.0 or newer.
Quick start
Section titled “Quick start”npx degit golemui/golemui/templates/nuxt my-appcd my-appnpm installnpm startOpen http://localhost:3000 and view the page source: the form markup arrives without JavaScript. The same template runs in StackBlitz. Node.js 22.12 or newer.
Installation
Section titled “Installation”npm i @golemui/core @golemui/gui-shared @golemui/vue @golemui/gui-vue @golemui/gui-components --savePreload in a plugin
Section titled “Preload in a plugin”The widgets have to be in the registry before the first render, on the server and on the client. A server render cannot wait for a widget’s dynamic import while it produces the page, and the client would otherwise hydrate an empty tree over markup that is not empty. Nuxt awaits an async plugin before the first render on both sides.
import { preloadFormWidgets } from '@golemui/core';import { widgetLoaders } from '@golemui/gui-vue';
export default defineNuxtPlugin(async () => { await preloadFormWidgets({ widgetLoaders });});nuxt.config.ts
Section titled “nuxt.config.ts”Load the stylesheet through css. GolemUI never injects it. data-theme="auto" follows the OS color scheme.
export default defineNuxtConfig({ app: { head: { htmlAttrs: { lang: 'en', 'data-theme': 'auto' }, }, }, css: ['@golemui/gui-components/index.css'],});Render the form
Section titled “Render the form”<script setup lang="ts">import type { FormEvent } from '@golemui/core';import { GuiForm } from '@golemui/gui-vue';import { gui, type GuiFormInitConfig } from '@golemui/gui-shared';
const formDef = [ gui.inputs.dropdown('country', { label: 'Country', items: ['United States', 'Japan', 'Brazil', 'France'], }), gui.inputs.checkbox('pets', { label: 'Travelling with pets?', include: { when: '!!$form.country' }, }), gui.actions.button({ label: 'Submit', uid: 'submit', onClick: () => 'handleSubmit' }),];
const config: GuiFormInitConfig = { formName: 'signup', // keeps the form id predictable on both sides formDef,};
function onFormEvent(event: FormEvent) { if (event.name === 'handleSubmit') console.log('Submitted:', event.data);}</script>
<template> <GuiForm :config="config" @form-event="onFormEvent" /></template><script setup lang="ts">import type { FormEvent } from '@golemui/core';import { GuiForm } from '@golemui/gui-vue';import formDef from './signup.json';
const config = { formName: 'signup', // keeps the form id predictable on both sides formDef,};
function onFormEvent(event: FormEvent) { if (event.name === 'handleSubmit') console.log('Submitted:', event.data);}</script>
<template> <GuiForm :config="config" @form-event="onFormEvent" /></template>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:
export default defineNuxtConfig({ vue: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') }, },});Object props on such a tag must be bound with .prop. Bound as an attribute, the server stringifies them into the markup:
<gui-select :options.prop="languages" @change="onLanguageChanged" />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 on the Vue SSR page.
To turn it on, install @lit-labs/ssr and add the GolemUI Components module:
export default defineNuxtConfig({ modules: ['@golemui/gui-components/nuxt'], css: ['@golemui/gui-components/index.css'],});The module also sets the compiler options for the gui-* tags. To use it for those alone, set golemui: { serverRendering: false }. 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.- 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. On the server, every request re-initializes a shared instance. Scope the translator per request when the app serves several languages. See i18n.
Custom widgets
Section titled “Custom widgets”Keep the loaders in one module-scope object, spread it into the plugin’s preload call and pass the same object as customWidgetLoaders in every form config. See Custom widget loaders.
import { customWidgetLoaders } from '~/utils/custom-widget-loaders';
export default defineNuxtPlugin(async () => { await preloadFormWidgets({ widgetLoaders: { ...widgetLoaders, ...customWidgetLoaders }, });});See also
Section titled “See also”- Vue SSR: how the adapter renders and hydrates, and what the server sends.
- Server-Side Rendering Overview: the rules shared by every framework.
- Vue Integration: the full
<GuiForm>props reference.