Localess
AnalogJS

Getting Started

AnalogJS integration for Localess — file-based routes, route resolvers, TransferState hydration, dynamic component rendering, and Visual Editor support on @localess/angular.

Package: @localess/angular

AnalogJS is the Vite-powered Angular meta-framework. Localess integrates through @localess/angular, exactly as in a plain Angular app — the same provideLocaless() setup works unchanged. This guide covers what Analog changes: the Vite config, file-based routes, and route resolvers. The shared API — services, directives, pipes, and SchemaComponent — is documented in the Angular guide.

Security: provideLocaless() takes a single token, bundled into the browser JS. Only a public token (read-only, published content and translations only) should ever be used here — never a secret token. LocalessContentService fetches once on the server and hydrates the result to the browser via Angular's TransferState, so the browser normally never makes its own API call. See SSR and content fetching.

Requirements

  • Node.js >= 24.0.0
  • AnalogJS 2
  • @angular/core, @angular/common, @angular/compiler, @angular/platform-browser — versions >=21.0.0 <23.0.0

Installation

# npm
npm install @localess/angular@latest

# yarn
yarn add @localess/angular@latest

# pnpm
pnpm add @localess/angular@latest

Setup

vite.config.ts

@localess/angular ships as an untranspiled FESM bundle, so it has to be bundled at SSR time rather than loaded as an external CommonJS dependency:

// vite.config.ts
import analog from '@analogjs/platform';
import { defineConfig } from 'vite';

export default defineConfig(() => ({
  resolve: { mainFields: ['module'] },
  ssr: { noExternal: ['@localess/angular'] },
  plugins: [analog()],
}));

app.config.ts

Register Analog's file router and HTTP client alongside provideLocaless(). withLocalessComponents() maps content _schema keys to components — eagerly, or lazily with a dynamic import:

// src/app/app.config.ts
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { withComponentInputBinding } from '@angular/router';
import { provideFileRouter, requestContextInterceptor } from '@analogjs/router';
import { provideLocaless, withLocalessComponents } from '@localess/angular';
import { PageComponent } from './shared/components/localess/page/page.component';

export const appConfig: ApplicationConfig = {
  providers: [
    provideFileRouter(withComponentInputBinding()),
    provideHttpClient(withFetch(), withInterceptors([requestContextInterceptor])),
    provideClientHydration(withEventReplay()),
    provideLocaless(
      {
        origin: 'https://my-localess.web.app',
        spaceId: 'YOUR_SPACE_ID',
        token: 'YOUR_PUBLIC_TOKEN',
        enableSync: true,
      },
      withLocalessComponents({
        Page: PageComponent, // eager — always needed, it's the page root
        Button: () => import('./shared/components/localess/button/button.component').then(m => m.ButtonComponent), // lazy
      })
    ),
  ],
};

withComponentInputBinding() is what lets a route resolver's result arrive as a component input() — see the Full Example. The provideLocaless() options are the same as in Angular — see Setup.

Rendering modes

Server-rendered is Analog's default: pages render per request on its Nitro server, and LocalessContentService hydrates the fetched content to the browser.

Static output drops the server and prerenders every page to HTML at build time. Enable it in vite.config.ts and give Analog the list of routes. The config runs in plain Node, with no Angular injector, so LocalessContentService isn't available there — use localessClient from @localess/client (a build-time dev dependency) to enumerate the pages:

// vite.config.ts
import analog from '@analogjs/platform';
import { localessClient } from '@localess/client';
import { defineConfig } from 'vite';

async function prerenderRoutes(): Promise<string[]> {
  const client = localessClient({ origin: 'https://my-localess.web.app', spaceId: 'YOUR_SPACE_ID', token: 'YOUR_PUBLIC_TOKEN' });
  const links = await client.getLinks({ kind: 'DOCUMENT' });
  return ['/', ...Object.values(links).map((link) => `/${link.fullSlug}`)];
}

export default defineConfig(() => ({
  resolve: { mainFields: ['module'] },
  ssr: { noExternal: ['@localess/angular'] },
  plugins: [analog({ static: true, prerender: { routes: prerenderRoutes } })],
}));

The application code is identical in both modes — only vite.config.ts changes.

Writing components

Extend SchemaComponent<T> with the generated content type. It provides data(), links(), references() and assets() as signals. Mark editable fields with data-ll-field, and render nested blocks with [llComponent]:

// src/app/shared/components/localess/page/page.component.ts
import { Component } from '@angular/core';
import { LocalessComponentDirective, LocalessRichText, SchemaComponent } from '@localess/angular';
import { Page } from '../../../models/localess';

@Component({
  selector: 'app-page',
  imports: [LocalessComponentDirective, LocalessRichText],
  templateUrl: './page.component.html',
})
export class PageComponent extends SchemaComponent<Page> {}
<!-- page.component.html -->
@if (data(); as data) {
  <main>
    <h1 data-ll-field="title">{{ data.title }}</h1>
    @for (button of data.buttons; track button._id) {
      <ng-container [llComponent]="button" [links]="links()" [references]="references()" [assets]="assets()" />
    }
    @if (data.content) {
      <ll-rich-text data-ll-field="content" [content]="data.content" />
    }
  </main>
}

See Schema Components for the base-class helpers.

Full Example

A catch-all file route renders every page. Fetch in the route's routeMeta resolver, so the content is loaded — and transferred to the browser — before the page renders:

// src/app/pages/[...slug].page.ts
import { Component, inject, input } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { RouteMeta } from '@analogjs/router';
import { Content, LocalessApiError, LocalessContentService, LocalessDocument } from '@localess/angular';
import { Page } from '../shared/models/localess';

const contentResolver: ResolveFn<Content | undefined> = async (_route, state) => {
  // The catch-all route leaves ActivatedRouteSnapshot.url empty — read the path from the router state
  const slug = state.url.split('?')[0].split('/').filter(Boolean).join('/') || 'home';
  try {
    return await inject(LocalessContentService).contentBySlug(slug);
  } catch (error) {
    if (error instanceof LocalessApiError && error.status === 404) return undefined;
    throw error;
  }
};

export const routeMeta: RouteMeta = {
  resolve: { content: contentResolver },
};

@Component({
  selector: 'app-slug',
  imports: [LocalessDocument],
  template: `
    @if (content(); as content) {
      <ll-document [document]="content" />
    }
  `,
})
export default class SlugPage {
  readonly content = input<Content<Page>>();
}

The page component must be the file's default export. <ll-document> looks up the component registered for document.data._schema and renders it.

Playground: playgrounds/analog (server-rendered) and playgrounds/analog-static (static) are the reference implementations for this guide, including locale-prefixed routes.

AI Coding Agents

@localess/angular 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/angular/SKILL.md

License

MIT

On this page