Localess
Nuxt

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. token is public — it goes into runtimeConfig.public and is part of the client bundle. serverToken is secret — it goes into runtimeConfig, which Nuxt never serializes to the client, and is read only by useLocalessServerClient(). Only ever put a token marked public in Localess into token.

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, ...) in plugins/localess.ts, delete that file. The module registers the plugin itself, and running both initializes the client twice.

Module Options

OptionTypeRequiredDefaultDescription
originstringYesFully qualified domain with protocol
spaceIdstringYesLocaless Space ID, found in Space settings
tokenstringPublic token. Enables client-side fetching and the Visual Editor. Omit to disable browser reads entirely
serverTokenstringSecret token. Required for draft/preview content, which a public token cannot read
componentsDirstringNo'~/components/localess'Directory scanned for components, relative to the Nuxt app directory
componentNamingstringNo'exact'How a _schema value is matched to a registered component — see Component registry
componentsRecord<string, string>No{}Explicit schema-key → path overrides, relative to componentsDir
enableSyncbooleanNofalseLoad the Visual Editor sync script
debugbooleanNofalseLog client requests and responses
devtoolsbooleanNotrueRegister the Localess tab in Nuxt DevTools. Dev only
cacheTTLnumber | falseNo300Cache 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 verbatimPage.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:

StrategyHeroBanner / hero-banner / hero_banner resolve as
exact (default)unchanged — only an identical spelling matches
camelCaseheroBanner
PascalCaseHeroBanner
kebab-casehero-banner
snake_casehero_banner
lowercaseherobanner — 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:

KindAvailable everywhere
ComponentsLocalessDocument, LocalessComponent, LocalessRichText
ComposablesuseLocaless, useLocalessSync, useLocalessRichText, useLocalessRichTextHtml
HelperslocalessEditable, 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/nuxt is 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

License

MIT

On this page