Astro
Astro renders the complete GolemUI form on the server through @golemui/lit/ssr, widget internals included, with no framework in between. The frontmatter renders the form to HTML and a client script resumes it. Verified with Astro 7 and Lit 3. The contract is on the Lit 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/astro my-appcd my-appnpm installnpm startOpen http://localhost:4321 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/lit @golemui/gui-lit @golemui/gui-components @golemui/gui-validators --savenpm i @lit-labs/ssr@^4.1.0 @astrojs/node --save@lit-labs/ssr, version 4.1.0 or newer, is the optional peer dependency that powers the server render. @astrojs/node is only needed to render per request.
astro.config.mjs
Section titled “astro.config.mjs”Render every page on request with the Node adapter, or drop output and adapter to run the same Lit render once, at build time, under Astro’s default static output.
import node from '@astrojs/node';import { defineConfig } from 'astro/config';
export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }),});With output: 'server', a page can still opt into build-time rendering with export const prerender = true; in its frontmatter.
Shared form module
Section titled “Shared form module”Keep the form config in a module both the frontmatter and the client script import, so both sides build the form from the same definition. Nothing in it may touch the DOM, and formName is mandatory.
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' }),];
export const config: GuiFormInitConfig = { formName: 'signup', // mandatory: same form id on the server and the client formDef,};import formDef from './signup.json';
export const config = { formName: 'signup', // mandatory: same form id on the server and the client formDef,};Render on the server
Section titled “Render on the server”The frontmatter runs on the server only. Preload the widgets, render the form to a string and inject it with set:html. Every element in the output carries defer-hydration and stays inert until the client script resumes it. Import the stylesheet here too: GolemUI never injects it, and an import in the frontmatter puts it in the page head.
---import { preloadFormWidgets } from '@golemui/core';import { widgetLoaders } from '@golemui/gui-lit';import { renderTemplate } from '@golemui/lit/ssr';import { html } from 'lit';import { config } from '../form';import '@golemui/gui-components/index.css';
await preloadFormWidgets({ widgetLoaders });const formHtml = await renderTemplate(html`<gui-form .config=${config}></gui-form>`);---
<html lang="en" data-theme="auto"> <head> <meta charset="utf-8" /> </head> <body> <main> <Fragment set:html={formHtml} /> </main> </body></html>Resume in the browser
Section titled “Resume in the browser”The page <script> preloads the widgets again, attaches the listeners, and resumes the form. The resume replaces the server markup with one live render before the next paint and removes the defer-hydration hold; it does not hydrate the server markup. Listeners and properties go on before the resume, so the events of the first render are not missed.
<script> import { preloadFormWidgets, type FormEvent } from '@golemui/core'; import { widgetLoaders, type FormElement } from '@golemui/gui-lit'; import { initValidators } from '@golemui/gui-validators'; import { resumeServerRenderedForm, type FormElement as CoreFormElement } from '@golemui/lit'; import { config } from '../form';
await preloadFormWidgets({ widgetLoaders });
const form = document.querySelector<FormElement>('gui-form')!; form.addEventListener('formEvent', (event) => { const { name, data } = (event as CustomEvent<FormEvent>).detail; if (name === 'handleSubmit') console.log('Submitted:', data); });
resumeServerRenderedForm(form as unknown as CoreFormElement, { config: config as never, validators: initValidators(), });</script>resumeServerRenderedForm is typed for the core form element. <gui-form> follows the same contract with the widget set config, hence the casts, and builds its own validators.
formNameis mandatory.- Import
@golemui/lit/ssrfrom the frontmatter or other server code only. It has no browser build. - Listeners and properties (
formHealthBoundary,autocomplete) go on the element beforeresumeServerRenderedForm. - Handlers never run on the server.
onLoadfires once the widget has rendered in the browser.
Custom widgets
Section titled “Custom widgets”Register your own Lit widgets with safeDefine from @golemui/lit, so they render on the server and honor the defer-hydration hold. Keep the loaders in one module-scope object, spread it into both preload calls and pass the same object as customWidgetLoaders. See Custom widgets in Lit SSR.
Other frameworks in Astro
Section titled “Other frameworks in Astro”See also
Section titled “See also”- Lit SSR: the server API, what the server sends, and the setup outside Astro.
- Server-Side Rendering Overview: the rules shared by every framework.
- Lit Integration: the full
<gui-form>properties reference.