React integration layer for Localess. Builds on @localess/client and adds a component registry, Visual Editor sync, rich text rendering, and asset resolution.
Peer dependencies: React 17, 18, or 19 + react-dom. next (13–16) is an optional peer, used only by /rsc's live-edit Server Action (next/cache).
@localess/react ships three runtime entry points plus two build-time ones. Choosing the wrong one is the most common mistake.
| Import path | Use case | Sync / hooks available |
|---|---|---|
@localess/react |
SPA or fully client-rendered app | Yes |
@localess/react/ssr |
SSR or Next.js output: 'export' (static) |
No |
@localess/react/rsc |
Next.js App Router (React Server Components) | Yes (Server-Action-driven LocalessDocument, plus client hooks) |
@localess/react/vite |
vite.config.ts of TanStack Start / React Router v7 / Remix Vite — see "Vite Plugin for SSR Frameworks" |
via generated localessInit({ enableSync }) |
@localess/react/vite/virtual-modules |
tsconfig compilerOptions.types — ambient declarations for virtual:localess-init / virtual:localess-components |
— |
@localess/react/rsc's LocalessDocument needs a live server at request time (its live sync is Server-Action-driven) and works under default/standalone, but not output: 'export' — use the default export's client-side LocalessDocument there instead (see "Client-Side Fallback for Static Export"). @localess/react/ssr remains the right choice when you deliberately want to exclude all sync code for the smallest bundle.
The smallest bundle. It exports LocalessServerComponent / LocalessServerDocument (server-safe, no sync attributes) in place of the default LocalessComponent / LocalessDocument, and does NOT include:
LocalessComponent/LocalessDocument(available from the default export and/rsc)useLocalesshook (requires'use client')isSyncEnabled,localessSyncOn,localessSyncOnChange,localessSyncOnDocument,localessSyncReady(not meaningful without live editing)
localessEditable, localessEditableField, isBrowser, isIframe, isServer, loadLocalessSync, buildAssetQueryString, LocalessRichText / renderRichText, LocalessApiError, and the sync event types (LocalessSync, EventToApp, EventToAppOf, EventCallback, EventToAppType) ARE included — they're cheap to bundle and harmless outside a client context. /ssr also re-exports localessClient, the raw @localess/client factory, for build-time scripts (see "Static Prerendering").
isSyncConfigured and getOrigin are only on the default export.
Re-exports everything from /ssr (including localessClient), plus:
LocalessComponent— server-safe (no'use client'), usable directly in a Server ComponentLocalessDocument— the live-editing entry point for App Router: a Server Component whose live sync is driven by a Server Action, with no client-side component registration needed. Requires a live server at request time (not usable underoutput: 'export'). This is a different implementation from the default export'sLocalessDocument, which/rscdoes not exportuseLocalesshook (requires'use client')isSyncEnabled,localessSyncOn,localessSyncOnChange,localessSyncOnDocument,localessSyncReady
npm install @localess/reactCall localessInit() once at app startup — in your root layout or _app.tsx. Safe in Server Components since it only sets up global state.
import { localessInit } from "@localess/react";
localessInit({
// Required
origin: process.env.LOCALESS_ORIGIN!,
spaceId: process.env.LOCALESS_SPACE_ID!,
token: process.env.LOCALESS_TOKEN!,
// Optional client settings
version: 'draft', // undefined = published, 'draft' for preview
cacheTTL: 300, // seconds; false to disable; default 300
debug: false,
// React-specific
enableSync: process.env.NODE_ENV !== 'production',
components: {
'page': PageComponent,
'hero-section': HeroSection,
'nav-menu': NavMenu,
'footer': Footer,
},
fallbackComponent: UnknownBlock,
});Security: a secret
tokenis safe here only because this runs server-side. Never expose it to the browser. The one exception is a Localess public token (read-only, published content and translations only), which may be passed to a second, client-sidelocalessInit()— see "Client-Side Fallback for Static Export".
localessInit returns the created LocalessClient. components / fallbackComponent are typed AnyLocalessComponent (React.ComponentType<LocalessSchemaProps<any>>) so that components typed per schema (LocalessSchemaProps<HeroSection>) are assignable; the registry itself is stored as React.ElementType.
Maps content _schema values to React components. Schema keys must match exactly what is configured in Localess CMS — use lowercase hyphenated names:
components: {
'page': Page,
'hero-section': Hero,
'card-grid': CardGrid,
'rich-text-block': RTE,
}The registry (and fallback component) are set via localessInit's components/fallbackComponent options — calling localessInit again replaces the registry entirely.
import { getComponent, getFallbackComponent } from "@localess/react";
const HeroComp = getComponent('hero');
const fallback = getFallbackComponent();Looks up data._schema in the registry and renders the matching component. Server-safe, no sync subscription; it always spreads localessEditable(data) (data-ll-id / data-ll-schema) onto the rendered component so the Visual Editor can target it. Props: data, links?, references?, assets?, ref, and any extra props (forwarded). Type: LocalessComponentProps<T>.
import { LocalessComponent } from "@localess/react";
<LocalessComponent
data={content.data}
links={content.links}
references={content.references}
assets={content.assets}
/>Rendering logic:
- Read
data._schemaas registry key - Render registered component with
data,links,references,assets, thedata-ll-*attributes, and any extra props - If not found → try
fallbackComponent(receives the same props, minus thedata-ll-*attributes) - If no fallback → render error message
Accepts the full Content<T> as document, delegates rendering to LocalessComponent (passing data, assets, links, references), and adds live sync. Does not fetch content — pass server-preloaded data as props. There are two implementations:
@localess/react/rsc— a Server Component. Live sync is driven by a Server Action (see "How/rscLive Sync Works"). Use this in Next.js App Router.@localess/react— a client-side component: holdsdocument.datainuseStateand subscribes toinput/changevialocalessSyncOnDocumentwhenenableSyncis active, applying only events whosedocumentIdmatches its owndocument.id. It calls React hooks, so render it inside a'use client'boundary.
import { getLocalessClient, LocalessDocument } from "@localess/react/rsc";
// Server Component
const content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
return <LocalessDocument document={content} />;Fetches content by slug on the client and subscribes to Visual Editor sync events, applying only those whose documentId matches the fetched document's id.
useLocaless<T extends ContentData = ContentData>(
slug: string | string[], // string[] joined with '/' — e.g. ['blog', 'post'] → 'blog/post'
options?: ContentFetchParams
): Content<T> | undefinedReturns undefined while the initial fetch is in flight.
LocalessComponent |
LocalessDocument |
useLocaless |
|
|---|---|---|---|
| Fetches content | No | No | Yes |
| Live sync | No | Yes | Yes |
| Works in RSC | Yes | /rsc version: yes (Server Component). Default-export version: requires a Client Component boundary |
No ('use client') |
| Best for | Static SSR | Server-preloaded + sync | SPA / client-rendered |
Components receive data, links, references, and assets as props (LocalessSchemaProps<T>). Always spread editable attributes for Visual Editor support:
import { localessEditable, localessEditableField, resolveAsset, LocalessComponent } from "@localess/react";
import type { LocalessSchemaProps } from "@localess/react";
import type { HeroSection } from "./.localess/localess";
const HeroSection = ({ data, links, references }: LocalessSchemaProps<HeroSection>) => (
<section {...localessEditable(data)}>
<h1 {...localessEditableField<HeroSection>('title')}>{data.title}</h1>
{data.image && <img src={resolveAsset(data.image)} alt={data.imageAlt} />}
{data.cta && <LocalessComponent data={data.cta} links={links} references={references} />}
</section>
);Pass links and references through the entire tree — child LocalessComponents need them.
Always preload data server-side and pass it as props — the page renders immediately with no loading flash, then sync activates on top.
LocalessDocument from /rsc is a Server Component. Live editing is driven by a Server Action shipped inside the SDK: a small 'use client' listener (rendered by LocalessDocument, given document.id, the configured origin, and the raw enableSync flag via isSyncConfigured()) loads the sync script when running inside the Visual Editor iframe and calls the action on every input, change, save, publish, and unpublish event whose documentId matches that document.id (events for other documents on the page are ignored). On input/change the action stashes the edited data in an in-process cache keyed by Content.id; on save/publish/unpublish it clears that entry (the API is the source of truth again). It then calls next/cache's revalidatePath (only when process.env.NEXT_RUNTIME is set), and Next.js refreshes the Server Component tree — LocalessDocument consumes the cached edit (one-shot) and re-renders using the server's own component registry. No client-side component registration is needed — localessInit({ components }) once, server-side, is enough:
import { getLocalessClient, LocalessDocument, localessInit } from "@localess/react/rsc";
localessInit({
origin: process.env.LOCALESS_ORIGIN!,
spaceId: process.env.LOCALESS_SPACE_ID!,
token: process.env.LOCALESS_TOKEN!,
enableSync: true,
components: { page: Page, hero: Hero },
});
export default async function HomePage({ params }) {
const { locale } = await params;
const content = await getLocalessClient().getContentBySlug('home', { locale });
return <LocalessDocument document={content} />;
}Security: a Server Action is a public endpoint, so localessLiveEditAction checks every call server-side and drops it unless (1) the server-side localessInit() set enableSync: true, (2) the id belongs to a document this server rendered via /rsc's LocalessDocument with sync enabled, (3) type is a known editor event, (4) path is a same-site absolute path (starts with /, not //, no \, at most 2048 characters), and (5) data is an object for input/change. When sync is off, LocalessDocument also ignores the live-edit cache entirely. Keep enableSync off in production (e.g. enableSync: process.env.NODE_ENV !== 'production' or a preview-only env var): anyone who can reach a sync-enabled deployment can call the action for an id it has rendered.
Known limitation: the live-edit cache is in-process memory (globalThis), matching how Storyblok's own React SDK implements the same mechanism. On a default-mode deployment that runs multiple serverless instances with no shared memory, a live edit may occasionally not appear until a subsequent edit lands on the same instance. This doesn't affect standalone deployments or local development (single process). If it matters for your setup, use a standalone deployment for live-editing sessions.
Requires a live server at request time — does not work under output: 'export'. Use the default export's client-side LocalessDocument instead there; see "Client-Side Fallback for Static Export" below.
The default export's LocalessDocument (import { LocalessDocument } from "@localess/react") is the client-side re-render fallback for output: 'export', where no server exists at request time to run a Server Action against. It is a client-side component holding its own state (useState) and re-rendering on window.localess input/change events. (Earlier releases re-exported it from /rsc under the alias LocalessClientDocument; that alias no longer exists — import from @localess/react directly.)
Because Next.js App Router bundles Server and Client Components into separate module graphs, a localessInit({ enableSync: true, components }) call made only in a Server Component populates neither the component registry nor the enableSync flag in the Client Component module graph that this LocalessDocument actually runs in — both live in module-scope state, and that state is a separate instance per graph. This holds under output: 'export' exactly as much as under default/standalone: static-export Client Components still hydrate and run their effects normally in the browser (see the Next.js static export docs) — the module-graph split, not the output mode, is what makes a second registration necessary.
Call localessInit a second time, from inside the Client Component boundary that renders LocalessDocument, using a public token — read-only, scoped to published content and translations only, safe to expose client-side (unlike the secret token used for the server-side call above). See docs/decisions/001-server-side-only.md for the full token-secrecy policy this exception is documented under.
// app/[locale]/page-client.tsx
'use client';
import { localessInit, LocalessDocument } from "@localess/react";
import { components } from "@/localess.config"; // the same map passed to localessInit server-side
localessInit({
origin: process.env.NEXT_PUBLIC_LOCALESS_ORIGIN!, // same origin as the server-side call
spaceId: process.env.NEXT_PUBLIC_LOCALESS_SPACE_ID!, // same spaceId as the server-side call
token: process.env.NEXT_PUBLIC_LOCALESS_PUBLIC_TOKEN!, // a public token — never the secret one
enableSync: true,
components,
});
export default function PageClient({ content }) {
return <LocalessDocument document={content} />;
}This is a plain second call to the same localessInit you already use server-side — no separate function to learn. The server-side call keeps using the secret token (for build-time data fetching); this client-side call uses the public token, and its components/enableSync populate the client module graph that this LocalessDocument actually reads from. All entry points share the same core/client module, so localessInit from @localess/react and getLocalessClient from @localess/react/ssr read the same state within one module graph.
Use this only when you specifically need live editing on a statically-exported build. For default/standalone, prefer /rsc's LocalessDocument above — it needs no client-side registration at all.
For Vite-based SSR frameworks (TanStack Start, React Router v7 framework
mode, Remix Vite), @localess/react/vite automates the two-localessInit-calls
pattern above: one Vite plugin, one config object, one localessInit() call
generated identically for every build graph.
Known gap:
tokenhere is shipped to the browser bundle as well as the SSR graph — there is currently no secret/public token split for this plugin (unlike the manual two-call pattern above, where the client-sidelocalessInit()receives a public token and the server-side one keeps the secret). Use a public token withlocaless(), until a scoped/public-token mechanism replaces this.
// vite.config.ts
import { defineConfig } from 'vite';
import { localess } from '@localess/react/vite';
export default defineConfig({
plugins: [
localess({
origin: process.env.LOCALESS_ORIGIN!,
spaceId: process.env.LOCALESS_SPACE_ID!,
token: process.env.LOCALESS_TOKEN!, // shipped to the SSR graph AND the browser bundle
enableSync: true,
componentsDir: 'src/components/localess', // default: 'src'
components: { 'hero-section': './HeroOverride.tsx#HeroOverride' }, // optional, overrides auto-registration
}),
],
});Then, anywhere in your app:
import 'virtual:localess-init';
import { getLocalessClient } from '@localess/react';
const content = await getLocalessClient().getContentBySlug('home', { locale });For that bare import to type-check, add the ambient declarations to your tsconfig (the same way you add vite/client):
{ "compilerOptions": { "types": ["vite/client", "@localess/react/vite/virtual-modules"] } }localess(options) takes LocalessOptions: origin, spaceId, token
(required — it throws if any is missing), optional version: 'draft',
cacheTTL, debug, enableSync (all forwarded verbatim to the generated
localessInit()), componentsDir (default 'src'), components
(Record<schemaKey, path>), and componentNaming (default 'exact'). It returns two Vite plugins
(vite-plugin-localess-components, vite-plugin-localess-init).
virtual:localess-init resolves to the same localessInit() call regardless
of which Vite build graph imports it — both the SSR/server graph and the
client graph get token. Every .tsx/.jsx file under componentsDir
is auto-registered under its filename verbatim (Page.tsx -> 'Page'). How that
key is matched to data._schema is the componentNaming option — see
Component naming strategies below. components
overrides take an exact key (no case transformation) and a file path relative
to componentsDir, for cases where the schema key doesn't match the
filename, or the file lives outside componentsDir. A bare path
assumes a default export; suffix it with #ExportName (e.g.
'./HeroOverride.tsx#HeroOverride') to import a named export instead.
Only use the manual components two-localessInit-calls pattern above
directly if you're not on a Vite-based framework, or need control the plugin
doesn't expose.
When a Vite-based framework prerenders pages ahead of time (React Router v7's
ssr: false + prerender, TanStack Start's prerender: { enabled: true }),
you end up constructing a localessClient twice, in two different
places, and that's correct — not a bug to dedupe into one instance.
- Config-resolution time. Before any page can be prerendered, the build
tool needs the list of paths to render. For a static/dynamic route (a
splat/catch-all matching arbitrary CMS slugs), that list doesn't exist
until you fetch it — so the framework's config file constructs its own
localessClient(from@localess/react/ssr— never@localess/clientdirectly, see below) and callsgetLinks()to build the path list. This runs as plain Node code before any Vite plugin or virtual module exists;virtual:localess-initisn't resolvable yet, so there's no singleton to reuse. - Module-graph execution time. Once the path list is known, the build
tool renders each path by executing your actual route/loader code inside
the SSR module graph — where
import 'virtual:localess-init'runs, callinglocalessInit(), which builds a secondlocalessClientand stores it as the singletongetLocalessClient()reads from.
These are separate Node module instantiations in separate lifecycle stages —
there's no reference from stage 1 that could be handed to stage 2, since
stage 2's module graph is built fresh by the framework's SSR compiler. This
mirrors Next.js's generateStaticParams() running independently of the
page's own data fetch. It's also not wasteful in requests: getLinks()
(path enumeration) and getContentBySlug() (per-page content) are different
endpoints, so nothing is fetched twice — only the { origin, spaceId, token }
literal is duplicated between the two call sites, which you can factor into
one shared constant if you want a single source of truth.
React Router v7 (framework mode) — the path-enumeration client lives in
react-router.config.ts's prerender():
// react-router.config.ts
import type { Config } from '@react-router/dev/config';
import { localessClient } from '@localess/react/ssr'; // not @localess/client directly
async function getPrerenderPaths(): Promise<string[]> {
const client = localessClient({ origin: '...', spaceId: '...', token: '...' });
const links = await client.getLinks({ kind: 'DOCUMENT' });
return Object.values(links).map(link => `/${link.fullSlug}`);
}
export default {
ssr: false,
async prerender() {
return getPrerenderPaths();
},
} satisfies Config;The second localessClient is built by localess() in vite.config.ts,
as shown above.
TanStack Start — both the path-enumeration client and localess()
live in the same vite.config.ts, since TanStack Start's prerender.pages
option is passed directly to defineConfig's plugins array:
// vite.config.ts
import { defineConfig } from 'vite';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { localessClient } from '@localess/react/ssr'; // not @localess/client directly
import { localess } from '@localess/react/vite';
async function getPrerenderPaths(): Promise<string[]> {
const client = localessClient({ origin: '...', spaceId: '...', token: '...' });
const links = await client.getLinks({ kind: 'DOCUMENT' });
return Object.values(links).map(link => `/${link.fullSlug}`);
}
export default defineConfig(async () => {
const pages = await getPrerenderPaths();
return {
plugins: [
localess({ origin: '...', spaceId: '...', token: '...' }),
tanstackStart({ prerender: { enabled: true }, pages: pages.map(path => ({ path })) }),
],
};
});Next.js output: 'export' doesn't use @localess/react/vite at all (it's
not Vite-based), and — unlike the two Vite frameworks above — it genuinely
can share one client instance between path enumeration and page
rendering. generateStaticParams() and the page component are both part of
the same route module, evaluated once in the same Node process by Next's
build, so a plain module-level constant works:
// shared/utils/locales.ts
export const localessClient = localessInit({ origin: '...', spaceId: '...', token: '...' });// app/[[...path]]/page.tsx
import { localessClient } from '@/shared/utils/locales';
export async function generateStaticParams() {
const links = await localessClient.getLinks({ kind: 'DOCUMENT' });
// ...build path list from links
}
async function fetchData(locale: string | undefined, slug: string) {
return localessClient.getContentBySlug(slug, { locale });
}The reason this works for Next but not for React Router v7 / TanStack Start is Next's module-caching model: importing the same module twice within one build process returns the same evaluated instance. The Vite frameworks' config-resolution phase and SSR module-graph phase are not the same module graph, so no import can bridge them. See "Client-Side Fallback for Static Export" above for the separate client-side registration piece that's specific to Next's static export (live editing in the browser).
Re-fetches on client, falls back to server data until ready, auto-syncs.
// app/[locale]/page.tsx — Server Component
import { getLocalessClient } from "@localess/react/rsc";
export default async function HomePage({ params }) {
const { locale } = await params;
const content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
return <PageClient initialContent={content} locale={locale} />;
}
// app/[locale]/page-client.tsx
'use client';
import { useLocaless, LocalessComponent, localessEditable } from "@localess/react/rsc";
export function PageClient({ initialContent, locale }) {
const content = useLocaless<Page>('home', { locale }) ?? initialContent;
return (
<main {...localessEditable(content.data)}>
{content.data?.body?.map(item => (
<LocalessComponent key={item._id} data={item} links={content.links} references={content.references} />
))}
</main>
);
}No client re-fetch. Uses server data directly. Auto-syncs with editor.
// app/[locale]/page.tsx — no separate client file needed
import { getLocalessClient, LocalessDocument } from "@localess/react/rsc";
export default async function HomePage({ params }) {
const { locale } = await params;
const content = await getLocalessClient().getContentBySlug<Page>('home', { locale });
return <LocalessDocument document={content} />;
}Full control. Initialise state with server data; subscribe to sync events yourself.
'use client';
import { useEffect, useState } from "react";
import { LocalessComponent, localessEditable, localessSyncOnDocument } from "@localess/react/rsc";
export function PageClient({ initialContent }) {
const [pageData, setPageData] = useState(initialContent.data);
useEffect(() => {
// No-op unless isSyncEnabled(); waits for localessSyncReady() internally.
// Fires on 'input' / 'change' for this document only, with the edited data.
// Returns the unsubscribe function, so React removes the listener on unmount.
return localessSyncOnDocument(initialContent.id, (data) => setPageData(data));
}, [initialContent.id]);
return (
<main {...localessEditable(pageData)}>
{pageData?.body?.map(item => (
<LocalessComponent key={item._id} data={item} links={initialContent.links} references={initialContent.references} />
))}
</main>
);
}The patterns above re-render on the client using React state (they're for the default export's client-side LocalessDocument / useLocaless and the static-export fallback, not /rsc's LocalessDocument, which is already server-driven — see "How /rsc Live Sync Works"). An alternative for the SPA/client-side case — useful if you want to avoid registering components in the client bundle at all (see "Client-Side Fallback for Static Export") — is to re-render server-side and patch the DOM, the same approach @localess/astro uses for its livePreview tier:
- On a Visual Editor
input/changeevent,POSTthe updateddatato a Next.js Route Handler (e.g.app/api/localess-preview/route.ts). - In that Route Handler, re-render your page's content server-side with the new data (via RSC) and return the resulting HTML.
- On the client, debounce the event,
fetchthe Route Handler, and use a DOM-diffing library such asmorphdomto patchdocument.bodyin place, keyed by thedata-ll-idattribute (matching howlocalessEditablemarks elements):
import morphdom from 'morphdom';
async function patchWithUpdatedContent(data: unknown) {
const response = await fetch(location.href, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data }),
});
const html = await response.text();
const newBody = new DOMParser().parseFromString(html, 'text/html').body;
morphdom(document.body, newBody, {
getNodeKey(node) {
return node.nodeType === 1 ? (node as Element).getAttribute('data-ll-id') ?? undefined : undefined;
},
});
}This requires a server running at request time and does not work under output: 'export' — there is no server to handle the POST in a fully static build. Most apps don't need this: the client-side re-render patterns above already work under all three output modes. Reach for this only if avoiding a client-side component registry specifically matters for your build.
Resolves a ContentLink to a URL string.
import { findLink } from "@localess/react";
const href = findLink(content.links, data.ctaLink);
// type: 'content' → '/' + fullSlug, or '/not-found' if absent from links map
// type: 'url' → raw URIConverts a ContentAsset to a full URL ({origin}/api/v1/spaces/{spaceId}/assets/{uri}, plus a query string built by buildAssetQueryString(params)). Configured automatically from origin + spaceId in localessInit.
import { resolveAsset } from "@localess/react";
const imageUrl = resolveAsset(data.heroImage);
const imageUrl = resolveAsset(data.heroImage, { w: 800, h: 600, f: 'webp', q: 90 });
const thumb = resolveAsset(data.video, { w: 400, thumbnail: true });See AssetTransformParams table in docs/client.md.
The stored bytes, inline (resolveAssetOriginal) or as an attachment (resolveAssetDownload). Takes no params: the response never enters the image pipeline,
and a transform parameter on that route is rejected by the API with a 400.
import { resolveAssetDownload, resolveAssetOriginal } from "@localess/react";
const uploaded = resolveAssetOriginal(data.heroImage);
const saveFile = resolveAssetDownload(data.brochure);resolveAssetOriginal(asset) returns the uploaded bytes inline. resolveAsset(asset) does not —
a still raster is re-encoded at its format's default quality even with no params, so a bare asset
URL is a rendition.
Removed in v4.
resolveAsset(asset, { download: true })andresolveAsset(asset, { f: 'original' })were the old spellings; both are now rejected with a400. To resize without converting, name the source format —{ w: 400, f: 'jpeg' }.
Renders Localess ContentRichText (Tiptap JSON) to a native React node tree — no TipTap at runtime, safe in SPA, SSR, and RSC. Built on @localess/richtext (see docs/richtext.md).
import { LocalessRichText, renderRichText } from "@localess/react";
<LocalessRichText content={data.body} />
<article>{renderRichText(data.body)}</article>Per-node/per-mark overrides are React components receiving the node's fields plus children:
<LocalessRichText
content={data.body}
renderers={{ link: ({ attrs, children }) => <Link href={attrs.href}>{children}</Link> }}
/>Supported elements: headings (h1–h6), paragraphs, bold, italic, strikethrough, underline, ordered/unordered lists, code, code blocks, blockquotes, horizontal rules, links. Link hrefs pass a protocol allowlist (javascript:/data: are stripped) — also the attrs.href a custom link renderer receives. sanitizeUrl applies the same allowlist to URLs you build yourself. Unknown node types are skipped with a dev-only warning unless a renderer for that type is provided.
import { getLocalessClient } from "@localess/react";
const client = getLocalessClient(); // throws if localessInit() not calledFor a standalone client outside the singleton (build-time path enumeration), use localessClient from @localess/react/ssr — see "Static Prerendering" above.
isSyncEnabled()—trueonly whenenableSync: truewas passed tolocalessInit, code is running in the browser, and the page is inside the Visual Editor iframe.isSyncConfigured()(default export only) — the rawenableSyncflag without that gating. Read it server-side and pass it down as a prop when a Client Component (a separate module graph) needs to know.localessSyncReady()— resolves once the sync script has loaded (immediately when sync is off); never rejects.localessSyncOn(event | event[], callback)/localessSyncOnChange(callback)— subscribe towindow.localessevents; both no-op unlessisSyncEnabled()and awaitlocalessSyncReady()internally, and return an unsubscribe function (also safe before the script has loaded) to return fromuseEffect. Event types:input,change,save,publish,unpublish,pong,enterSchema,hoverSchema,leaveSchema. Neither filters by document.localessSyncOnDocument(documentId, callback)— subscribes toinput/changefor one document only (event.documentId === documentId) and callscallback(data, event)with the edited content data; same gating and unsubscribe return as above.
// Default export (@localess/react)
export { localessInit, getLocalessClient, getOrigin }
export { getComponent, getFallbackComponent }
export { isSyncEnabled, isSyncConfigured, localessSyncReady, localessSyncOn, localessSyncOnChange, localessSyncOnDocument }
export { LocalessComponent, LocalessDocument, LocalessRichText } // LocalessDocument here = client-side, useState-based
export { renderRichText, sanitizeUrl } // sanitizeUrl re-exported from @localess/richtext
export { resolveAsset, resolveAssetOriginal, resolveAssetDownload }
export { useLocaless }
export { findLink, buildAssetQueryString } // re-exported from @localess/client
export { normalizeComponentKey, createComponentIndex, formatComponentKeyCollisions } // re-exported from @localess/client
export { localessEditable, localessEditableField, loadLocalessSync } // re-exported from @localess/live-preview
export { isBrowser, isServer, isIframe, createSyncController } // re-exported from @localess/live-preview
export { LocalessApiError } // re-exported from @localess/client; thrown by getContentBySlug/getContentById on a non-2xx response
export type { LocalessClient, LocalessClientOptions, LocalessOptions, AnyLocalessComponent }
export type { LocalessComponentProps, LocalessDocumentProps, LocalessRichTextProps, LocalessSchemaProps, UseLocalessOptions }
export type { LocalessReactRichTextRenderers, LocalessReactRichTextOptions }
export type { ContentFetchParams, LinksFetchParams, TranslationFetchParams, AssetTransformParams }
export type { Content, ContentData, ContentMetadata, ContentDataSchema, ContentDataField }
export type { ContentAsset, ContentRichText, ContentLink, ContentReference }
export type { Assets, Links, References, Translations }
export type { LocalessRichTextDocument, LocalessRichTextInput, LocalessRichTextMark, LocalessRichTextNode }
export type { LocalessSync, EventToApp, EventToAppOf, EventCallback, EventToAppType }
// @localess/react/ssr — no sync functions, no hooks, no client-side LocalessDocument
export { localessInit, getLocalessClient, localessClient, getComponent, getFallbackComponent }
export { resolveAsset, resolveAssetOriginal, resolveAssetDownload }
export { LocalessServerComponent, LocalessServerDocument, LocalessRichText, renderRichText, sanitizeUrl }
export { findLink, loadLocalessSync, buildAssetQueryString, localessEditable, localessEditableField, isBrowser, isServer, isIframe }
export { normalizeComponentKey, createComponentIndex, formatComponentKeyCollisions, createSyncController }
export { LocalessApiError }
export type { LocalessServerComponentProps, LocalessServerDocumentProps /* + all shared types above */ }
// @localess/react/rsc — everything from /ssr, plus:
export { LocalessComponent, LocalessDocument /* Server Component, Server-Action sync */, useLocaless }
export { isSyncEnabled, localessSyncOn, localessSyncOnChange, localessSyncOnDocument, localessSyncReady }
export type { LocalessDocumentProps }
// @localess/react/vite — build-time only
export { localess } // (options: LocalessOptions) => Plugin[]
export type { LocalessOptions, LocalessInitOptions }
// @localess/react/vite/virtual-modules — ambient declarations only
declare module 'virtual:localess-init' {}
declare module 'virtual:localess-components' {}- Wrong import path. Rendering the default export's
LocalessDocument(or callinguseLocaless) from a Server Component fails — they call React hooks. Use/rscfor App Router; itsLocalessDocumentis a Server Component. - Using
@localess/react/ssrwhen you need sync. The/ssrexport deliberately excludes all sync and browser-only code. If you need live Visual Editor editing, use/rsc. - Using the client-side
LocalessDocumentwithout registering components client-side. Only relevant if you deliberately opted into theoutput: 'export'fallback —/rsc'sLocalessDocumentneeds no client-side registration at all. See "Client-Side Fallback for Static Export" above. - Calling
localessInit()with the secret token in a Client Component. Call it once in the root layout (Server Component). The only client-side call allowed is the static-export fallback, and it must use a public token. - Not passing
links/referencesdown the tree. ChildLocalessComponents need them for resolved data. Always pass them through every level. - Enabling sync in production.
enableSync: process.env.NODE_ENV !== 'production'— the sync script is only useful inside the Localess editor iframe.
A Localess schema can be named anything; every framework has its own file-naming convention. The
componentNaming option reconciles them by normalizing both the registry key and the incoming
data._schema before they are compared.
| Strategy | HeroBanner / hero-banner / hero_banner -> |
|---|---|
exact (default) |
unchanged — matches only an identical spelling |
camelCase |
heroBanner |
PascalCase |
HeroBanner |
kebab-case |
hero-banner |
snake_case |
hero_banner |
lowercase |
herobanner — separators dropped entirely |
Every strategy except exact is case- and separator-insensitive, so they differ only in the shape of
the key they produce, not in what they match.
Name your component files after your schemas and the default exact works with no configuration.
Reach for another strategy when the two conventions genuinely differ — e.g. schemas named
hero-banner and files named HeroBanner.
Under any strategy other than exact, two files that normalize to the same key (HeroBanner and
hero-banner in one directory) collide; the SDK logs a warning naming both and keeps the first.
See ADR 012.
componentNaming is an option of the Vite plugin only — it sits next to componentsDir,
because auto-discovery is the only thing that has to reconcile two naming conventions:
// vite.config.ts
localess({
origin, spaceId, token,
componentsDir: 'src/components/localess',
componentNaming: 'camelCase',
})It is not an option on localessInit(). When you pass a components map by hand — the usual
path in Next.js, which does not use this plugin — you choose the keys, so use the schema name as the
key and matching is a plain exact lookup.
The plugin implements this without adding anything to the core API: when a non-exact strategy is
configured, the generated virtual:localess-components registry resolves keys through the strategy
itself. Under the default exact it emits an ordinary object with no wrapper at all.
Because the plugin serializes its options, it accepts the strategy names only — there is no custom-function form in React.