Server rendering
Render whole pages as escaped HTML and add owned markers only at explicit island roots.
Render a page
The server entry is @takazudo/zfb/zudo-react/server. Its renderToString function renders a synchronous description tree to HTML. It accepts strings, numbers, empty values, arrays, descriptions, and scalar-valued reactive children. A string passed directly to the renderer is escaped text.
import { renderToString } from "@takazudo/zfb/zudo-react/server";
const html = renderToString(
<html>
<head><title>Example</title></head>
<body><main><h1>Hello</h1></main></body>
</html>
);This call produces a whole document. The renderer does not add a doctype. Text escapes ampersand and angle brackets; attributes also escape double quotes. It preserves prop insertion order with double-quoted attributes, emits paired template elements, and emits a literal lowercase closing head tag. Void HTML elements have no closing tag and reject children or rawHtml. SVG non-void elements are paired.
Static output has no runtime comments or attributes outside explicit island boundaries. Ordinary component, array, and fragment boundaries do not add separators or whitespace. A page can therefore be rendered without hydration protocol markers.
zfb may await a top-level asynchronous page function before calling the renderer. An async component or promise nested inside the rendered tree is rejected with ZR_ASYNC_COMPONENT; the runtime does not await nested work.
Mark an island explicitly
Use islandRoot when a rendered subtree will be activated in the browser. It creates an inert wrapper description; component setup and prop serialization happen only when the wrapper is rendered.
import { islandRoot, renderToString } from "@takazudo/zfb/zudo-react/server";
const identity = { component: "Counter", build: "build-7" };
const root = islandRoot(<Counter />, {
identity,
when: "visible",
fallback: <p>Loading the counter</p>
});
const html = renderToString(<main>{root}</main>);The wrapper is a div. It has exactly one component marker: data-zfb-island or data-zfb-island-skip-ssr. It also carries data-when, data-zfb-transport="json/1", data-zfb-protocol="zudo-react/1", a nonempty data-zfb-build identity, and data-props even when props are empty. These attributes are emitted in that order, with data-media between data-when and transport only for media mode. The default data-when value is load; idle, visible, and media are also supported. Media mode requires a nonempty data-media query. The skip-SSR option selects the skip-SSR marker, validates identity and props without running the child, and renders its fallback without internal hydration markers.
For an island rendered by zfb, the component name and build identity must match the generated zfb metadata and client RootOptions.
A caller-owned client mount may choose its own nonempty, stable build string, provided it matches the wrapper and RootOptions.identity.build; see Mount from a host you own.
For a zfb-generated server island, the scanner's marker is the defining function's name; it does not read displayName. At server render time, displayName must be absent or equal to that name. A mismatch fails with ZR_ISLAND_IDENTITY rather than renaming the target. Anonymous or unregistered components also cannot be used as island children. Use the existing zfb island integration for scanner registration, scheduling, and persistence; see the island guide.
The wrapper is retained by zfb. zudo-react markers describe ranges only inside that wrapper, so a surrounding document stays marker-free. Each root owns its local marker numbering; authors do not need a global element ID or a component registry of their own.
Render an inner island subtree
For a caller that supplies its own wrapper attributes, renderToString also accepts an island identity option. This emits the same inner range markers without emitting a wrapper. The caller is responsible for supplying the matching container metadata.
const inner = renderToString(<Counter />, { island: identity });This option emits inner markers only; it does not register a component with the static scanner or define the client mount boundary. For server-rendered islands, use the supported zfb boundary described in the island API guide. A client-only host may own its wrapper and call mount as described in Hydration.
Props use JSON transport
These serialization rules apply to the server-rendered islandRoot path and its data-props attribute. A host that calls client mount directly passes props in memory, including callbacks and child descriptions; see Mount from a host you own.
serializeProps returns JSON text; the renderer applies HTML attribute escaping once when it emits the text as data-props. The transport accepts only plain data records, dense arrays, strings, booleans, null, and finite numbers. It omits own enumerable string data properties on records whose value is undefined, at every nesting level, while preserving explicit null:
import { serializeProps } from "@takazudo/zfb/zudo-react/server";
serializeProps({
locale: undefined,
currentSlug: undefined,
metadata: { title: "Card", description: undefined },
records: [{ description: undefined }, { description: null }],
});
// {"metadata":{"title":"Card"},"records":[{},{"description":null}]}The server component receives the same normalized props that are serialized, so Object.hasOwn reports the same optional keys during SSR and hydration. Records inside arrays follow the same omission rule without removing or moving array elements. An undefined array entry and an array hole are errors; null is a present value. Repeated references serialize by value. The transport also rejects cycles, functions, symbols, bigints, non-finite numbers, class instances, runtime descriptions, signals, and computed values.
Validation happens before JSON serialization. Arrays must be dense and have no extra named or symbol properties. Records may be ordinary or null-prototype records; only own enumerable string data properties are accepted. Accessors, non-enumerable fields, custom toJSON methods, symbols, and prototype-sensitive record keys are rejected without being invoked. Props do not carry live signal identity across the boundary; construct signals during client setup.
Read a render failure
When server rendering rejects an element during zfb build or zfb dev, the error carries three kinds of information. Read them in this order:
The ZR_TAG code identifies a tag outside the active HTML or SVG vocabulary. ZR_ATTRIBUTE identifies an unsupported attribute name or a value outside its active rules, including the custom-element string-only rule. See the supported HTML and SVG vocabulary for the complete lists and exact message examples.
[zudo-react] ZR_PROP_DIALECT render static-render root: use `autocomplete` instead of `autoComplete` — at components/search-field.tsx:4:7
[zfb-runtime] render threw for "/": ZR_PROP_DIALECT: input.autoComplete (use `autocomplete` instead of `autoComplete`) at root in static render
TypeError: ...
at fail (file:///zfb/bundle.mjs:3505:23)Authored span.
— at components/is the line and column of the failing JSX element in your source file. zfb reports it only when it can map the element's call back to a project file whose content matches the build's source map. The build does not need a precedingsearch- field. tsx: 4: 7 zfb check.Structural context. The first line also carries the stable code, the component (
static-renderfor a page render, otherwise the island's component name), the element path, and, for a React-style prop, the supported spelling to use instead. This part is always present.Generated stack. The
file:frames are positions in the generated server bundle. They locate zfb's renderer, not your element, so do not edit by those numbers./ / / zfb/ bundle. mjs: …
Elements written with h(), elements created inside a dependency, and code changed by a preprocessor have no reliable source position. For them zfb prints the structural context without an authored span, rather than a guessed line. It may add an internal-frame hint such as — internal frame node_modules/…, <generated>/…, or external/… for debugging; that label is never your source location. The first line never contains props, raw HTML, or absolute local paths.
Rendering Islands outside a zfb build
Since v3.0.0, Island validates its child before rendering. The build identity and the registered island names come from metadata that zfb's server bundle sets on globalThis.__zfb. A page rendered by another bundler or a plain Node script, such as a package that renders its own layout for a static export, has no such metadata. Every Island then fails with an error like this one:
ZR_ISLAND_IDENTITY: ThemeToggle has no build identitySupply the identity with withIslandTestContext
withIslandTestContext from @takazudo/ sets that metadata for one synchronous callback and restores the previous globalThis.__zfb afterwards, including when rendering throws. Despite the subpath name, it is the supported way to render SDK Island boundaries outside a zfb build.
import { Island } from "@takazudo/zfb";
import { renderToString } from "@takazudo/zfb/zudo-react/server";
import { withIslandTestContext } from "@takazudo/zfb/zudo-react/testing";
import { SidebarToggle } from "./sidebar-toggle";
export function renderLayout(): string {
return withIslandTestContext({ components: [SidebarToggle], build: "my-site-1" }, () =>
renderToString(
<body>
<Island when="idle">
<SidebarToggle />
</Island>
</body>,
),
);
}componentslists every component used as anIslandchild inside the callback, as an array or as a record whose values are components. Each name is taken from the component'sdisplayName, or from its function name when it has none. AnIslandwhose child is not in the list fails withZR_ISLAND_IDENTITY: <name> is not registered by the scanner.buildis the string written to each wrapper'sdata-zfb-buildattribute. It defaults to"test". Pass a stable, nonempty value.Compile the JSX with
@takazudo/as the JSX import source (for example, esbuildzfb/ zudo- react jsxImportSource). Elements created by another JSX runtime are not zudo-react descriptions, andIslandrejects them withZR_ISLAND_CHILD.The callback must be synchronous. A callback that returns a promise fails with
ZR_ISLAND_TEST: context callback must be synchronous, because the registration lives on a shared global. Load data before entering the callback.
Preserve function names in a minified bundle
Island identifies its child by the function's name property. zfb's own bundler always preserves function names, so a zfb build never loses them. A consumer bundler that minifies without that option renames functions, and the check then fails. With a displayName override, the minified name conflicts with it:
ZR_ISLAND_IDENTITY: dc conflicts with displayName SidebarToggleWhen the same component is passed to withIslandTestContext, the helper rejects it first with ZR_ISLAND_IDENTITY: invalid component name dc. Without a displayName, the render succeeds, but the island marker carries the minified name (data-zfb-island="dc"). No client built from the original source will recognize that name.
Enable function-name preservation in the bundler that produces the standalone render:
esbuild:
keepNames: truein the JavaScript API, or--keep-nameson the command line.Rolldown, including Vite 8:
output.keepNames: true. In Vite 8, set it asbuild.rolldownOptions.output.keepNames.Terser, including Vite's
build.minify: "terser":keep_fnames: true. In Vite, pass it throughbuild.terserOptions.Any other minifier: enable its option that keeps function names.
If you cannot change the bundler, there are two fallbacks. Removing displayName overrides from island components stops the error, but the marker still carries the minified name, so use it only when no client built from the original source hydrates that output. Otherwise, render the child directly without Island in the standalone build. A child rendered without Island is static HTML and never hydrates.
Hydrate against a zfb client
withIslandTestContext only satisfies the server-side checks. If the page is hydrated by a client bundle that zfb built, each wrapper's component name and data-zfb-build value must match that client's identity. A mismatch makes the client reject the island with ZR_IDENTITY: island <name> has invalid transport identity. For a client mount you own, see Mount from a host you own.