Angular SDK for the Localess headless CMS. Provides two independent entry points — browser and server — for content delivery, rich text rendering, asset management, and Visual Editor integration in both client-side and server-side rendered Angular applications.
Security note: The
browserentry point requires no API token and is safe to use in client-side code. Theserverentry point requires your Localess API token and must only be used in server-side code to keep the token secret.
- Installation
- Quick Start
- Browser Module
- Server Module
- SSR with TransferState
- Angular Image Optimization
- API Reference
# npm
npm install @localess/angular@latest
# yarn
yarn add @localess/angular@latest
# pnpm
pnpm add @localess/angular@latestPeer dependencies: @angular/core, @angular/common, @angular/compiler — versions ^19.0.0 || ^20.0.0 || ^21.0.0.
1. Register the browser provider in app.config.ts:
// app.config.ts
import { provideLocalessBrowser } from '@localess/angular/browser';
export const appConfig: ApplicationConfig = {
providers: [
provideLocalessBrowser({
origin: 'https://my-localess.web.app',
spaceId: 'YOUR_SPACE_ID',
}),
],
};2. Register the server provider in app.config.server.ts:
// app.config.server.ts
import { mergeApplicationConfig } from '@angular/core';
import { provideLocalessServer } from '@localess/angular/server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideLocalessServer({
origin: 'https://my-localess.web.app',
spaceId: 'YOUR_SPACE_ID',
token: 'YOUR_SECRET_TOKEN',
}),
],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);3. Fetch content on the server and render it on the client:
// In a server-side resolver or component
import { ServerContentService } from '@localess/angular/server';
const contentService = inject(ServerContentService);
const content = await firstValueFrom(contentService.getContentBySlug('home'));Import from @localess/angular/browser.
Register provideLocalessBrowser() once in your root ApplicationConfig. It configures all browser-side services and optionally loads the Visual Editor sync script.
import { provideLocalessBrowser } from '@localess/angular/browser';
provideLocalessBrowser({
origin: 'https://my-localess.web.app', // Required. Localess instance URL (no trailing slash)
spaceId: 'YOUR_SPACE_ID', // Required. Found in Localess Space settings
enableSync: true, // Optional. Loads the Visual Editor sync script
debug: false, // Optional. Enables console logging
})| Option | Type | Required | Description |
|---|---|---|---|
origin |
string |
✅ | Fully qualified Localess URL, e.g. https://my-localess.web.app |
spaceId |
string |
✅ | Space ID from the Localess Space settings |
enableSync |
boolean |
— | When true, injects the Visual Editor sync script into the page |
debug |
boolean |
— | When true, logs internal activity to the browser console |
provideLocalessBrowser() also registers Angular's built-in IMAGE_LOADER provider so that NgOptimizedImage automatically appends ?w=<width> to Localess asset URLs for responsive image optimization.
Schema components are abstract base classes you extend to render CMS content. They automatically set the data-ll-id and data-ll-schema attributes on the host element so the Localess Visual Editor can highlight and select components on the page.
Three variants are available depending on whether you prefer @Input() decorators, signal inputs, or a custom data binding approach.
Extend this class when you want to receive the schema data via a traditional @Input(). The base class declares the required data input and optional links / references inputs.
import { Component } from '@angular/core';
import { SchemaWithInputComponent } from '@localess/angular/browser';
// Define a TypeScript interface matching your Localess schema
interface HeroSection {
_id: string;
_schema: string;
title: string;
subtitle: string;
backgroundImage: ContentAsset;
ctaLink: ContentLink;
}
@Component({
selector: 'app-schema-hero-section',
standalone: true,
templateUrl: './hero-section.component.html',
})
export class HeroSectionComponent extends SchemaWithInputComponent<HeroSection> {}In the template, data, links, and references are available directly. Use the assetUrl() and findLink() helpers provided by the base class:
<!-- hero-section.component.html -->
<section>
<h1>{{ data.title }}</h1>
<p>{{ data.subtitle }}</p>
<img [src]="assetUrl(data.backgroundImage)" [alt]="data.title" />
<a [href]="findLink(data.ctaLink)">Learn more</a>
</section>Use the component in a parent template by passing the schema object from the CMS:
<app-schema-hero-section [data]="content.data" [links]="links" />Extend this class when you prefer Angular signal inputs. The base class declares data = input.required<T>(), links = input<Links>(), and references = input<References>() as signals.
import { Component } from '@angular/core';
import { SchemaWithSignalComponent } from '@localess/angular/browser';
@Component({
selector: 'app-schema-hero-section',
standalone: true,
templateUrl: './hero-section.component.html',
})
export class HeroSectionComponent extends SchemaWithSignalComponent<HeroSection> {}In the template, read inputs with function-call syntax:
<section>
<h1>{{ data().title }}</h1>
<img [src]="assetUrl(data().backgroundImage)" />
<a [href]="findLink(data().ctaLink)">Learn more</a>
</section>Extend this class when you want to define your own inputs but still benefit from Visual Editor host bindings. Implement the required content() method to return the active schema object — the base class uses it to set data-ll-id and data-ll-schema on the host element.
import { Component, input } from '@angular/core';
import { SchemaComponent } from '@localess/angular/browser';
import type { ContentDataSchema, Links } from '@localess/angular/browser';
@Component({
selector: 'app-schema-hero-section',
standalone: true,
templateUrl: './hero-section.component.html',
})
export class HeroSectionComponent extends SchemaComponent {
data = input.required<HeroSection>();
links = input<Links>();
override content(): ContentDataSchema {
return this.data();
}
}All three schema base classes expose:
| Member | Signature | Description |
|---|---|---|
assetUrl(asset) |
(asset: ContentAsset) => string |
Builds the full CDN URL for a Localess asset |
findLink(link) |
(link: ContentLink) => string |
Resolves a CMS link to a path or URL |
config |
LocalessBrowserConfig |
Injected browser configuration |
Use these directives when you have a component or element that is not a schema component but should still be selectable in the Visual Editor.
Marker directives. Apply both together to any element to make it recognizable in the Visual Editor. Set the attribute values manually:
<div [attr.data-ll-id]="item._id" [attr.data-ll-schema]="item._schema">
<!-- content -->
</div>Marks an individual field within a schema for field-level selection in the Visual Editor:
<p data-ll-field="subtitle">{{ data.subtitle }}</p>A convenience directive that sets both data-ll-id and data-ll-schema on the host element from a single ContentDataSchema input binding. Useful for sub-schemas rendered without a dedicated component:
import { ContentDirective } from '@localess/angular/browser';
@Component({
imports: [ContentDirective],
})
export class PageComponent {}<div [llContent]="subSchema">
<!-- sub-schema content -->
</div>Import individual pipes into the imports array of any standalone component that uses them.
Transforms a ContentAsset object into a fully qualified CDN URL. Equivalent to SchemaComponent.assetUrl().
import { AssetPipe } from '@localess/angular/browser';
@Component({
imports: [AssetPipe],
})<img [src]="data.image | llAsset" alt="..." />Resolves a ContentLink from the links map to a navigable path or URL. The pipe takes the links map as the first argument and the ContentLink object as the value:
import { LinkPipe } from '@localess/angular/browser';<a [href]="links | llLink: data.ctaLink">Visit</a>Link resolution behavior:
ContentLink.type |
Result |
|---|---|
"content" |
Looks up link.uri in the links map and returns /<fullSlug> |
"url" |
Returns link.uri as-is |
Converts a Localess RichText field (Tiptap JSON) to an HTML string. Supports headings (H1–H6), bold, italic, strike, underline, bullet lists, ordered lists, code, code blocks, and links.
import { RichTextToHtmlPipe } from '@localess/angular/browser';<div [innerHTML]="data.body | llRtToHtml"></div>The pipe accepts JSONContent | ContentRichText | string | null | undefined. If passed a plain string it returns it unchanged; null and undefined return an empty string.
Bypasses Angular's DomSanitizer for a trusted HTML string. Always apply this after llRtToHtml when binding to [innerHTML] to avoid Angular stripping elements:
import { RichTextToHtmlPipe, SafeHtmlPipe } from '@localess/angular/browser';
@Component({
imports: [RichTextToHtmlPipe, SafeHtmlPipe],
})<div [innerHTML]="data.body | llRtToHtml | llSafeHtml"></div>Security:
llSafeHtmlcallsDomSanitizer.bypassSecurityTrustHtml(). Only use it with HTML that comes directly from your trusted Localess space.
BrowserAssetService is an injectable service that generates asset URLs programmatically. It is equivalent to the assetUrl() method on schema components.
import { BrowserAssetService } from '@localess/angular/browser';
@Component({ ... })
export class MyComponent {
private assetService = inject(BrowserAssetService);
getImageUrl(asset: ContentAsset): string {
return this.assetService.link(asset);
}
// Also accepts a raw URI string
getImageUrlByUri(uri: string): string {
return this.assetService.link(uri);
}
}This service is browser-only. It logs an error if instantiated during SSR. Use
ServerAssetServiceon the server.
The Localess Visual Editor enables live in-browser content editing. Set enableSync: true in provideLocalessBrowser() to automatically inject the sync script.
To receive real-time content updates from the Visual Editor, subscribe to its events in any component. Guard the subscription with isPlatformBrowser to avoid errors during SSR:
import { Component, inject, OnInit, signal, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { LocalessSync } from '@localess/angular/browser';
@Component({
selector: 'app-slug',
standalone: true,
templateUrl: './slug.component.html',
})
export class SlugComponent implements OnInit {
private platformId = inject(PLATFORM_ID);
liveContent = signal<ContentData | undefined>(undefined);
ngOnInit(): void {
if (isPlatformBrowser(this.platformId)) {
// Only subscribe when running inside the Visual Editor
if (window.localess) {
window.localess.on(['input', 'change'], (event) => {
this.liveContent.set(event.data);
});
}
}
}
}The input event fires on every keystroke; the change event fires when the editor saves. Render liveContent() instead of the server-fetched data when it is set to give authors a live preview.
Import from @localess/angular/server.
The server module provides services that call the Localess REST API using a secret API token. All services are SSR-only and log an error if instantiated in the browser. They must be registered via provideLocalessServer() in the server application config.
// app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { provideLocalessServer } from '@localess/angular/server';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(withRoutes(serverRoutes)),
provideLocalessServer({
origin: 'https://my-localess.web.app', // Required
spaceId: 'YOUR_SPACE_ID', // Required
token: 'YOUR_SECRET_TOKEN', // Required
version: 'draft', // Optional. Omit for published content
debug: false, // Optional
}),
],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);| Option | Type | Required | Description |
|---|---|---|---|
origin |
string |
✅ | Fully qualified Localess URL |
spaceId |
string |
✅ | Space ID from Localess Space settings |
token |
string |
✅ | API token from Localess Space settings. Keep this secret — never expose it to the browser |
version |
'draft' | string |
— | Set to 'draft' to fetch unpublished content. Omit to fetch published content |
debug |
boolean |
— | When true, logs API calls and cache activity to the server console |
ServerContentService fetches CMS content from the Localess API. All results are cached in-memory for the lifetime of the server request using Map-based caches keyed by slug, ID, or link params. This prevents redundant network requests when the same content is resolved multiple times during SSR.
import { ServerContentService } from '@localess/angular/server';
@Injectable()
export class MyServerService {
private contentService = inject(ServerContentService);
}Fetches a content document by its full slug path.
const content = await firstValueFrom(
contentService.getContentBySlug<HeroSection>('home')
);
// With params
const draftContent = await firstValueFrom(
contentService.getContentBySlug<HeroSection>('home', {
version: 'draft',
locale: 'en',
resolveReference: true,
resolveLink: true,
})
);Fetches a content document by its unique ID.
const content = await firstValueFrom(
contentService.getContentById<ArticlePage>('abc123', { locale: 'fr' })
);Fetches the full links map — a dictionary of content IDs to their slug paths. Pass this to browser-side schema components to enable link resolution.
const links = await firstValueFrom(
contentService.getLinks()
);
// Filter by content kind or parent
const blogLinks = await firstValueFrom(
contentService.getLinks({
kind: 'DOCUMENT',
parentSlug: 'blog',
excludeChildren: false,
})
);| Parameter | Type | Description |
|---|---|---|
version |
'draft' | string |
Override the global version for this request |
locale |
string |
Locale code, e.g. 'en', 'fr' |
resolveReference |
boolean |
Inline referenced content objects |
resolveLink |
boolean |
Inline link objects |
| Parameter | Type | Description |
|---|---|---|
kind |
string |
Filter links by content kind |
parentSlug |
string |
Return only links under this parent slug |
excludeChildren |
boolean |
Exclude descendant slugs |
ServerAssetService generates asset URLs on the server. Its API is identical to BrowserAssetService.
import { ServerAssetService } from '@localess/angular/server';
@Injectable()
export class MyService {
private assetService = inject(ServerAssetService);
getUrl(asset: ContentAsset): string {
return this.assetService.link(asset);
// or: this.assetService.link('path/to/asset.jpg')
}
}ServerTranslationService fetches all translation strings for a given locale. Results are cached by locale.
import { ServerTranslationService } from '@localess/angular/server';
@Injectable()
export class MyService {
private translationService = inject(ServerTranslationService);
getTranslations(locale: string): Observable<Translations> {
return this.translationService.fetch(locale);
}
}The returned Translations object is a flat key–value map (Record<string, string>).
In an SSR application, content is fetched on the server and must be transferred to the browser to avoid a duplicate fetch on hydration. Use Angular's TransferState to store the server response, then read it on the client.
The pattern below uses an abstract service with two implementations — one for the server, one for the browser — and swaps them via the DI system.
Abstract service (localess.service.ts):
import { Injectable, makeStateKey } from '@angular/core';
import { Content, Links, ContentData } from '@localess/angular';
import { Observable } from 'rxjs';
@Injectable()
export abstract class LocalessService {
LINKS_KEY = makeStateKey<Links>('ll:links');
abstract getLinks(): Observable<Links>;
abstract getContentBySlug<T extends ContentData>(slug: string | string[], locale?: string): Observable<Content<T>>;
abstract getContentById<T extends ContentData>(id: string, locale?: string): Observable<Content<T>>;
}Server implementation (localess-server.service.ts):
import { inject, Injectable, makeStateKey, TransferState } from '@angular/core';
import { tap } from 'rxjs/operators';
import { ServerContentService } from '@localess/angular/server';
import { LocalessService } from './localess.service';
@Injectable()
export class LocalessServerService extends LocalessService {
private state = inject(TransferState);
private contentService = inject(ServerContentService);
getLinks() {
return this.contentService.getLinks().pipe(
tap(links => this.state.set(this.LINKS_KEY, links))
);
}
getContentBySlug<T extends ContentData>(slug: string | string[], locale?: string) {
const normalizedSlug = Array.isArray(slug) ? slug.join('/') : slug;
const key = makeStateKey<Content<T>>(`ll:content:slug:${normalizedSlug}`);
return this.contentService.getContentBySlug<T>(normalizedSlug, { locale }).pipe(
tap(content => this.state.set(key, content))
);
}
getContentById<T extends ContentData>(id: string, locale?: string) {
const key = makeStateKey<Content<T>>(`ll:content:id:${id}`);
return this.contentService.getContentById<T>(id, { locale }).pipe(
tap(content => this.state.set(key, content))
);
}
}Browser implementation (localess-browser.service.ts):
import { inject, Injectable, makeStateKey, TransferState } from '@angular/core';
import { of } from 'rxjs';
import { LocalessService } from './localess.service';
@Injectable()
export class LocalessBrowserService extends LocalessService {
private state = inject(TransferState);
getLinks() {
return of(this.state.get(this.LINKS_KEY, {}));
}
getContentBySlug<T extends ContentData>(slug: string | string[], locale?: string) {
const normalizedSlug = Array.isArray(slug) ? slug.join('/') : slug;
const key = makeStateKey<Content<T>>(`ll:content:slug:${normalizedSlug}`);
return of(this.state.get(key, {} as Content<T>));
}
getContentById<T extends ContentData>(id: string, locale?: string) {
const key = makeStateKey<Content<T>>(`ll:content:id:${id}`);
return of(this.state.get(key, {} as Content<T>));
}
}Wire them up:
// app.config.ts — browser uses browser implementation
providers: [
{ provide: LocalessService, useClass: LocalessBrowserService },
]
// app.config.server.ts — server uses server implementation
providers: [
{ provide: LocalessService, useClass: LocalessServerService },
]Use the abstract service anywhere without worrying about the platform:
@Component({ ... })
export class SlugComponent {
private localess = inject(LocalessService);
content = toSignal(this.localess.getContentBySlug('home'));
}provideLocalessBrowser() automatically registers Angular's IMAGE_LOADER provider. When you use NgOptimizedImage (ngSrc) with a Localess asset URL, Angular appends ?w=<requested-width> to the URL, enabling server-side image resizing:
<img
ngSrc="{{ data.image | llAsset }}"
width="800"
height="600"
alt="Hero image"
/>
<!-- Rendered src: https://my-localess.web.app/api/v1/spaces/.../assets/image.jpg?w=800 -->This works automatically — no additional configuration required.
| Export | Kind | Description |
|---|---|---|
provideLocalessBrowser(options) |
Function | Registers all browser-side providers |
SchemaWithInputComponent<T> |
Abstract Class | Base component with @Input() data: T |
SchemaWithSignalComponent<T> |
Abstract Class | Base component with data = input.required<T>() |
SchemaComponent<T> |
Abstract Class | Base component requiring content() override |
ContentIdDirective |
Directive | [data-ll-id] marker |
ContentSchemaDirective |
Directive | [data-ll-schema] marker |
ContentFieldDirective |
Directive | [data-ll-field] marker |
ContentDirective |
Directive | [llContent] — sets both id and schema attributes |
AssetPipe |
Pipe | llAsset — asset to URL |
LinkPipe |
Pipe | llLink — resolves a ContentLink |
RichTextToHtmlPipe |
Pipe | llRtToHtml — Tiptap JSON to HTML |
SafeHtmlPipe |
Pipe | llSafeHtml — bypasses DomSanitizer |
BrowserAssetService |
Service | Programmatic asset URL generation |
LOCALESS_BROWSER_CONFIG |
InjectionToken | Browser configuration token |
LocalessBrowserConfig |
Type | Browser config shape |
LocalessBrowserOptions |
Type | Options for provideLocalessBrowser() |
findLink(links, link) |
Function | Standalone link resolution utility |
LocalessSync |
Type | Visual Editor sync event types |
| Export | Kind | Description |
|---|---|---|
provideLocalessServer(options) |
Function | Registers all server-side providers |
ServerContentService |
Service | Fetches content by slug, ID, or links |
ServerAssetService |
Service | Programmatic asset URL generation |
ServerTranslationService |
Service | Fetches translations by locale |
LOCALESS_SERVER_CONFIG |
InjectionToken | Server configuration token |
LocalessServerConfig |
Type | Server config shape |
LocalessServerOptions |
Type | Options for provideLocalessServer() |
Re-exports all types from @localess/client:
| Type | Description |
|---|---|
Content<T> |
CMS document with metadata and typed data payload |
ContentData |
Base type for schema data objects |
ContentDataSchema |
Schema data with _id and _schema fields |
ContentAsset |
Asset reference { uri: string } |
ContentLink |
Link reference { type: 'content' | 'url', uri: string } |
ContentRichText |
Tiptap JSON rich text |
ContentReference |
Reference to another content document |
Links |
Map of content ID → { fullSlug: string } |
References |
Map of referenced content objects |
Translations |
Flat key–value map of translation strings |
ContentFetchParams |
Parameters for content fetch requests |
LinksFetchParams |
Parameters for links fetch requests |
MIT © Lessify