Localess
TanStack Start

Getting Started

Use Localess in a TanStack Start app, in both server-rendered and prerendered output.

Package: @localess/react

The @localess/react package is the official React integration for the Localess headless CMS platform. In a TanStack Start app you wire it up through @localess/react/vite, which generates the localessInit() call for the SSR graph and the browser bundle alike — so the same setup serves both server-rendered and prerendered output.

⚠️ Security Notice: @localess/react/vite ships token to the browser bundle as well as the SSR graph — there is currently no secret/public split for this plugin. Treat it as a public, read-only value.

Requirements

  • Node.js >= 24.0.0
  • React 17, 18, or 19

Installation

# npm
npm install @localess/react@latest

# yarn
yarn add @localess/react@latest

# pnpm
pnpm add @localess/react@latest

Setup

For Vite-based SSR frameworks (TanStack Start, React Router v7 framework mode), @localess/react/vite replaces a manual localessInit() call with one Vite plugin, generating the same localessInit() call identically for every build graph (SSR and browser):

// 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!,
      enableSync: true,
      // Components are auto-discovered from this directory — no manual registry.
      componentsDir: 'src/components/localess',        // default: 'src'
      // Files are lowercase (page.tsx) while the schemas are PascalCase (Page),
      // so a case-insensitive naming strategy reconciles the two.
      componentNaming: 'camelCase',                     // default: 'exact'
      components: { 'HeroSection': './HeroOverride.tsx#HeroOverride' }, // optional, overrides auto-discovery
    }),
  ],
});

Every .tsx/.jsx file under componentsDir is auto-registered under its filename verbatimpage.tsx registers as page — and that key is matched against data._schema.

Because React filenames and CMS schema names rarely share a convention, 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. Both playgrounds use camelCase for exactly the reason in the comment above: lowercase files, PascalCase schemas.

componentNaming applies to auto-discovery only. components overrides take an exact key and a file path relative to componentsDir; a bare path assumes a default export, suffix it with #ExportName to import a named export instead.

TypeScript: if your tsconfig sets noUncheckedSideEffectImports: true, the bare import 'virtual:localess-init' below fails to resolve under tsc unless you add the plugin's ambient module declarations to types:

{ "compilerOptions": { "types": ["vite/client", "@localess/react/vite/virtual-modules"] } }

Then import the virtual module wherever you fetch. The bare import 'virtual:localess-init' is what runs the generated localessInit() call, so it must appear in the module doing the fetching — not only once at the app root:

// src/shared/server/get-page-content.ts
import 'virtual:localess-init';
import { getLocalessClient, LocalessApiError } from '@localess/react';

export async function getPageContent(slug: string, locale?: string) {
  try {
    return await getLocalessClient().getContentBySlug<Page>(slug, { locale });
  } catch (error) {
    if (error instanceof LocalessApiError && error.status === 404) return null;
    throw error;
  }
}

Known gap: unlike every other export, token here is shipped to the browser bundle as well as the SSR graph — there is currently no secret/public token split for this plugin. Treat token as a public value when using localess() from @localess/react/vite, until a scoped/public-token mechanism replaces this.

Playgrounds: playgrounds/react-router and playgrounds/tanstack-start are full SSR projects using @localess/react/vite with enableSync. playgrounds/react-router-static and playgrounds/tanstack-start-static mirror them under prerendered/static output.

Writing components

Components receive data, links, and references as props — type them with LocalessSchemaProps<T>, the contract every registered component accepts (there's no base class to extend, unlike @localess/angular's SchemaComponent<T>). Always spread localessEditable/localessEditableField so the Visual Editor can highlight and select the block and its fields:

import { localessEditable, localessEditableField } from "@localess/react";
import type { LocalessSchemaProps } from "@localess/react";
import type { HeroSection } from "./.localess/localess";

const Hero = ({ data, links, references }: LocalessSchemaProps<HeroSection>) => (
  <section {...localessEditable(data)}>
    <h1 {...localessEditableField<HeroSection>('title')}>{data.title}</h1>
    <p {...localessEditableField<HeroSection>('subtitle')}>{data.subtitle}</p>
  </section>
);

Pass links and references through the entire tree — child LocalessComponents need them.

localessEditable(content)

Marks a content block root element as editable.

import { localessEditable } from "@localess/react";

const Header = ({ data }) => (
  <nav {...localessEditable(data)}>
    {/* ... */}
  </nav>
);

localessEditableField<T>(fieldName)

Marks a specific field within a content block as editable, with type-safe field name inference when combined with generated types.

import { localessEditableField } from "@localess/react";

const Hero = ({ data }: { data: HeroBlock }) => (
  <section {...localessEditable(data)}>
    <h1 {...localessEditableField<HeroBlock>('title')}>{data.title}</h1>
    <p {...localessEditableField<HeroBlock>('subtitle')}>{data.subtitle}</p>
  </section>
);

Playground: playgrounds/tanstack-start and playgrounds/tanstack-start-static are the reference implementations for this guide.

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/react/SKILL.md

License

MIT

On this page