Vue 3 integration for Localess. Rendering-only: component registry, editable attributes, Visual Editor sync, rich-text rendering, and a Vite plugin for component auto-registration. Does not fetch data — see "SSR with Nuxt" for how server-side fetching fits in.
Peer dependency: Vue >=3.4.
npm install @localess/vue vue// main.ts
import { createApp } from 'vue';
import { Localess } from '@localess/vue';
import App from './App.vue';
const app = createApp(App);
app.use(Localess, {
origin: import.meta.env.VITE_LOCALESS_ORIGIN,
spaceId: import.meta.env.VITE_LOCALESS_SPACE_ID,
token: import.meta.env.VITE_LOCALESS_TOKEN, // public token only
components: { page: PageComponent, button: ButtonComponent },
enableSync: true,
});
app.mount('#app');<script setup lang="ts">
import { LocalessComponent, useLocaless } from '@localess/vue';
const client = useLocaless();
const content = await client.getContentBySlug('home');
</script>
<template>
<LocalessComponent :data="content.data" />
</template>Registration always goes through the components: { schemaKey: Component } option of app.use(Localess, {...}). Two ways to build that map:
- Manual — import your components and pass the map yourself.
- Vite plugin —
@localess/vue/vite'slocaless({ componentsDir, components })auto-globs a folder's.vuefiles intovirtual:localess-vue-components, keyed by filename verbatim (Page.vue->Page). Matching that key todata._schemais governed by this plugin'scomponentNamingoption (see Component naming strategies below). Itscomponentsoption adds explicit path overrides (relative tocomponentsDir,#ExportNamesuffix for a named export); those win over globbed entries on key collision.
// vite.config.ts
import { localess } from '@localess/vue/vite';
export default defineConfig({
plugins: [localess({ componentsDir: 'src/components/localess' })],
});// main.ts
// @ts-expect-error -- generated at build time
import { localessComponents } from 'virtual:localess-vue-components';
app.use(Localess, { origin, spaceId, token, components: localessComponents });<LocalessComponent :data="blok" /> <!-- applies data-ll-id/data-ll-schema automatically -->
<section v-bind="localessEditable(blok)">...</section> <!-- manual application -->
<h1 v-bind="localessEditableField('title')">{{ blok.title }}</h1> <!-- field-level -->enableSync: true (in app.use(Localess, {...})) injects the sync script and activates useLocalessSync. It's a no-op outside the Localess Visual Editor iframe.
<script setup lang="ts">
import { useLocalessSync } from '@localess/vue';
const latest = useLocalessSync(['input', 'change']);
</script>LocalessDocument applies only events whose documentId matches its document.id (via localessSyncOnDocument); useLocalessSync doesn't filter. For one document's edits, use localessSyncOnDocument(documentId, callback) — it calls callback(data, event) for input/change events of that document and returns an unsubscribe function:
import { onMounted, onScopeDispose, ref } from 'vue';
import { localessSyncOnDocument } from '@localess/vue';
const live = ref(props.document.data);
let unsubscribe: (() => void) | undefined;
onMounted(() => { unsubscribe = localessSyncOnDocument(props.document.id, data => (live.value = data)); });
onScopeDispose(() => unsubscribe?.());Built on @localess/richtext (see docs/richtext.md) — no TipTap at runtime. The <LocalessRichText> component renders native VNodes:
<script setup lang="ts">
import { LocalessRichText, type LocalessSchemaProps } from '@localess/vue';
import type { Article } from './models/localess'; // your content types — `body` is a `ContentRichText` field
const props = defineProps<LocalessSchemaProps<Article>>();
</script>
<template>
<LocalessRichText :content="props.data.body" />
</template>The content prop accepts a ContentRichText field value, a rich text document/node/node array, or null/undefined. Composables: useLocalessRichText(doc, options?) returns a reactive ComputedRef<VNodeChild>; useLocalessRichTextHtml(doc, options?) returns ComputedRef<string> for v-html bindings. Per-node overrides are Vue components receiving children as the default slot (declare the props you consume, or set inheritAttrs: false, to avoid attribute fallthrough):
<LocalessRichText :content="data.body" :renderers="{ link: AppLink }" />A custom link component receives an already-sanitized attrs.href (javascript:/data: become ""). String renderers passed to useLocalessRichTextHtml/renderRichTextToHtml must still escape what they interpolate — escapeAttr, escapeHtml, and sanitizeUrl are re-exported for that.
@localess/vue is rendering-only — SSR data-fetching goes through Nuxt's own server conventions, calling localessClient (re-exported from @localess/vue, never @localess/client directly) with a secret token in a server-only file:
// server/api/content.ts
import { localessClient } from '@localess/vue';
export default defineEventHandler(async event => {
const client = localessClient({
origin: useRuntimeConfig(event).localessOrigin,
spaceId: useRuntimeConfig(event).localessSpaceId,
token: useRuntimeConfig(event).localessToken, // secret, server-only
});
return client.getContentBySlug(getQuery(event).slug as string);
});<!-- pages/[...slug].vue -->
<script setup lang="ts">
import { LocalessDocument } from '@localess/vue';
const { data: content } = await useAsyncData('content', () => $fetch('/api/content', { query: { slug: 'home' } }));
</script>
<template>
<LocalessDocument v-if="content" :document="content" />
</template>Nuxt's own payload transfer hydrates the server-fetched result to the client — @localess/vue needs no hydration mechanism of its own. Register the Localess plugin in a Nuxt plugin (e.g. app/plugins/localess.ts, via nuxtApp.vueApp.use(Localess, {...})) with a public token only if you also want Visual Editor sync on top — LocalessDocument picks up live input/change events automatically; use LocalessComponent instead if you don't need sync.
| Export | Kind | Description |
|---|---|---|
Localess |
Vue plugin | app.use(Localess, options) — installs the client + component registry |
LOCALESS_INJECTION_KEY |
InjectionKey |
provide/inject key, for advanced use |
LocalessComponent |
Component | Dynamic schema-to-component renderer |
LocalessDocument |
Component | Wraps LocalessComponent and re-renders on Visual Editor sync events |
localessEditable(data) |
Function | Applies data-ll-id/data-ll-schema, bound onto an element |
localessEditableField(name) |
Function | Applies data-ll-field, bound onto an element |
normalizeComponentKey(name, naming?) |
Function | Applies a component naming strategy to a key, re-exported from @localess/client |
useLocaless() |
Composable | Returns the injected LocalessClient |
useLocalessSync(event) |
Composable | Visual Editor bridge event subscription, returns a Ref |
localessSyncOnDocument(documentId, callback) |
Function | Subscribes to input/change for one document, calls callback(data, event); returns an unsubscribe function |
LocalessRichText |
Component | Renders a rich text field to native VNodes (content, renderers? props) |
useLocalessRichText(doc, options?) |
Composable | Tiptap JSON → VNodes, returns a reactive ComputedRef<VNodeChild> |
useLocalessRichTextHtml(doc, options?) |
Composable | Tiptap JSON → HTML string for v-html, returns ComputedRef<string> |
renderRichText(content, options?) |
Function | One-shot Tiptap JSON → VNodes |
renderRichTextToHtml(content, options?) |
Function | One-shot Tiptap JSON → HTML string, re-exported from @localess/richtext |
escapeHtml, escapeAttr, sanitizeUrl |
Functions | Escaping and link-href allowlist for custom string renderers, re-exported from @localess/richtext |
LocalessApiError |
Class | Re-exported from @localess/client |
localessClient(options) |
Function | Re-exported from @localess/client — raw client factory for server-only SSR use, outside the Localess plugin's singleton lifecycle |
LocalessComponentProps, LocalessDocumentProps, LocalessSchemaProps, LocalessVueRichTextOptions, LocalessVueRichTextRenderers |
Types | Local prop/option types |
LocalessClient, LocalessClientOptions, EventToAppOf, EventToAppType, ComponentNaming, ComponentNamingStrategy |
Types | Re-exported from @localess/client |
Content, ContentData, ContentDataSchema, Assets, Links, References |
Types | Re-exported from @localess/model |
@localess/vue/vite's localess(options) |
Vite plugin factory | Component auto-registration |
@localess/vue/vite's LocalessOptions |
Type | localess() options: componentsDir? (default 'src'), components?, componentNaming? (default 'exact') |
@localess/vue/vite's VIRTUAL_LOCALESS_VUE_COMPONENTS_MODULE_ID |
Constant | 'virtual:localess-vue-components' |
See packages/vue/SKILL.md for the full usage guide (also shipped inside the npm package).
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 — localess({ componentsDir, componentNaming })
— because auto-discovery is the only thing that has to reconcile two naming conventions:
// vite.config.ts
import { localess } from '@localess/vue/vite';
localess({ componentsDir: 'src/components/localess', componentNaming: 'camelCase' })It is not an option on the Localess plugin / localessInit(). When you pass a components map
by hand you choose the keys, so use the schema name as the key and matching is a plain exact lookup.
The plugin needs nothing from the core API to do this: when a non-exact strategy is configured, the
generated virtual:localess-vue-components registry resolves keys through the strategy itself, and
you pass it to app.use(Localess, { components: localessComponents }) exactly as before. Under the
default exact it emits an ordinary object with no wrapper.
Strategy names only — there is no custom-function form.