Getting Started
Nuxt module for Localess — one config block for content delivery, component auto-registration, a server-only client for draft content, and Visual Editor integration.
Package: @localess/nuxt
The @localess/nuxt package is the official Nuxt module for the Localess headless CMS platform. It wraps @localess/vue and adds the three things a Nuxt app needs on top: a public/secret token split that matches Nuxt's two runtimes, component auto-registration from a directory, and a server-only client that can read draft content.
The components, composables, and rich text renderer are @localess/vue's, and the module installs its plugin for you — so everything you need is documented across these four pages. Content covers rendering and Reference covers the full API.
⚠️ Security Notice: Nuxt feeds one config object to both runtimes, so the module splits the token in two rather than guessing which you meant.
tokenis public — it goes intoruntimeConfig.publicand is part of the client bundle.serverTokenis secret — it goes intoruntimeConfig, which Nuxt never serializes to the client, and is read only byuseLocalessServerClient(). Only ever put a token marked public in Localess intotoken.
Requirements
- Node.js >= 24.0.0
- Nuxt
^4.0.0
Installation
# npm
npm install @localess/nuxt@latest
# yarn
yarn add @localess/nuxt@latest
# pnpm
pnpm add @localess/nuxt@latest@localess/vue comes along as a dependency — you don't install it separately.
Setup
Add the module and one localess block to nuxt.config.ts. There is no plugin file to write:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@localess/nuxt'],
localess: {
origin: 'https://my-localess.web.app',
spaceId: 'YOUR_SPACE_ID',
// Public token — reaches the client bundle.
token: process.env.LOCALESS_PUBLIC_TOKEN,
// Secret token — stays on the server, read only by useLocalessServerClient().
serverToken: process.env.LOCALESS_TOKEN,
// Components under this directory are registered automatically.
componentsDir: '~/components/localess',
enableSync: true,
},
});Migrating from a hand-written plugin? If you previously called
nuxtApp.vueApp.use(Localess, ...)inplugins/localess.ts, delete that file. The module registers the plugin itself, and running both initializes the client twice.
Module Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
origin | string | Yes | — | Fully qualified domain with protocol |
spaceId | string | Yes | — | Localess Space ID, found in Space settings |
token | string | — | — | Public token. Enables client-side fetching and the Visual Editor. Omit to disable browser reads entirely |
serverToken | string | — | — | Secret token. Required for draft/preview content, which a public token cannot read |
componentsDir | string | No | '~/components/localess' | Directory scanned for components, relative to the Nuxt app directory |
componentNaming | string | No | 'exact' | How a _schema value is matched to a registered component — see Component registry |
components | Record<string, string> | No | {} | Explicit schema-key → path overrides, relative to componentsDir |
enableSync | boolean | No | false | Load the Visual Editor sync script |
debug | boolean | No | false | Log client requests and responses |
devtools | boolean | No | true | Register the Localess tab in Nuxt DevTools. Dev only |
cacheTTL | number | false | No | 300 | Cache TTL in seconds for the server client. Applies to useLocalessServerClient() only |
At least one of token or serverToken must be set — the module throws at build time otherwise. Setting only serverToken is a valid, fully server-rendered setup; the module warns that useLocaless() will throw in the browser, and all fetching must go through useLocalessServerClient().
Component registry
Every .vue file under componentsDir is registered automatically, keyed by its filename verbatim — Page.vue registers as Page, and that key is matched against data._schema. Name your component files exactly after your schemas and no configuration is needed.
The directory is watched, so adding or removing a component during nuxt dev updates the registry without a restart.
When file names and schema names differ
Set componentNaming to normalize both sides before they're compared:
| Strategy | HeroBanner / hero-banner / hero_banner resolve as |
|---|---|
exact (default) | unchanged — only an identical spelling matches |
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 key shape they produce, not in what they match. Reach for one when the two conventions genuinely differ — schemas named hero-banner against files named HeroBanner.vue.
Overriding one component
components takes an exact schema key and a path relative to componentsDir. Suffix the path with #ExportName for a named export. Overrides win over auto-discovered entries on a key collision:
localess: {
componentsDir: '~/components/localess',
components: {
'hero-section': './HeroOverride.vue',
'teaser': './blocks.vue#Teaser',
},
}Auto-imports
The module registers @localess/vue's API with Nuxt's auto-import system, so none of it needs an explicit import in your app code:
| Kind | Available everywhere |
|---|---|
| Components | LocalessDocument, LocalessComponent, LocalessRichText |
| Composables | useLocaless, useLocalessSync, useLocalessRichText, useLocalessRichTextHtml |
| Helpers | localessEditable, localessEditableField, renderRichText, renderRichTextToHtml |
Anything else — LocalessApiError, the LocalessSchemaProps type — is imported from @localess/vue as usual.
Writing components
Components receive data, links, references, and assets as props — type them with LocalessSchemaProps<T>. Bind localessEditable/localessEditableField so the Visual Editor can highlight and select the block and its fields:
<!-- app/components/localess/Page.vue -->
<script setup lang="ts">
import { type LocalessSchemaProps } from '@localess/vue';
import type { Page } from '#shared/models/localess';
const props = defineProps<LocalessSchemaProps<Page>>();
</script>
<template>
<main v-bind="localessEditable(props.data)" class="flex flex-col gap-4">
<h1 v-bind="localessEditableField<Page>('title')">{{ props.data.title }}</h1>
<div v-if="props.data.buttons?.length" class="flex gap-2">
<LocalessComponent v-for="button in props.data.buttons" :key="button._id" :data="button" />
</div>
<div v-if="props.data.content" v-bind="localessEditableField<Page>('content')">
<LocalessRichText :content="props.data.content" />
</div>
</main>
</template>LocalessComponent, LocalessRichText, localessEditable, and localessEditableField are all auto-imported — only the type needs importing. Generate #shared/models/localess with the CLI.
Full Example
Server route — server/api/content.ts
import { LocalessApiError } from '@localess/vue';
import { useLocalessServerClient } from '#localess/server';
export default defineEventHandler(async event => {
const client = useLocalessServerClient();
const slug = (getQuery(event).slug as string) || 'home';
try {
return await client.getContentBySlug(slug);
} catch (error) {
if (error instanceof LocalessApiError && error.status === 404) {
throw createError({ statusCode: 404, statusMessage: `Content not found for slug "${slug}".` });
}
throw error;
}
});useLocalessServerClient() reads the secret serverToken from runtimeConfig, so it must only run on the server. It throws with an explanatory message if called in the browser.
Page — app/pages/[...slug].vue
<script setup lang="ts">
import { type Content } from '@localess/vue';
import type { Page } from '#shared/models/localess';
const route = useRoute();
const slug = Array.isArray(route.params.slug) ? route.params.slug.join('/') : route.params.slug || '';
const { data: content, error } = await useAsyncData(`content-${slug}`, () =>
$fetch<Content<Page>>('/api/content', { query: { slug } })
);
if (error.value) {
throw createError({ statusCode: error.value.statusCode ?? 500, statusMessage: error.value.statusMessage, fatal: true });
}
</script>
<template>
<LocalessDocument v-if="content" :document="content" />
</template>Nuxt's own payload transfer hydrates the server-fetched result to the client, so nothing re-fetches on hydration. <LocalessDocument> then picks up Visual Editor edits automatically once enableSync is on.
Playground:
playgrounds/nuxtis a full working Nuxt project built on exactly this pattern, including locale-prefixed slug resolution and a light/dark theme toggle.
Nuxt DevTools
In development the module adds a Localess tab to Nuxt DevTools showing the resolved configuration, which token kinds are set (never their values), and the discovered component registry with the key each entry resolves as — the fastest way to diagnose a block that renders as an "unknown component" error.
The registry is re-scanned per request, so the panel reflects components added or removed while the dev server runs. Set devtools: false to opt out; it's never registered in a production build regardless.
AI Coding Agents
This package ships a SKILL.md file that provides AI coding agents (GitHub Copilot, Claude Code, Cursor, and others) with accurate, up-to-date APIs, patterns, and best practices.
Reference it from your project's AGENTS.md:
## Localess
@node_modules/@localess/nuxt/SKILL.md