Angular
Angular SDK for Localess — content delivery, rich text rendering, asset management, and Visual Editor integration for client-side and server-side rendered Angular applications.
@localess/angular provides two independent entry points — browser and server — for integrating Localess into Angular applications.
Security: 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.
Installation
# 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.
Quick Start
1. Register the browser provider in 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:
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:
import { ServerContentService } from '@localess/angular/server';
const contentService = inject(ServerContentService);
const content = await firstValueFrom(contentService.getContentBySlug('home'));Browser Module
Import from @localess/angular/browser.
Setup
Register provideLocalessBrowser() once in your root ApplicationConfig:
import { provideLocalessBrowser } from '@localess/angular/browser';
provideLocalessBrowser({
origin: 'https://my-localess.web.app',
spaceId: 'YOUR_SPACE_ID',
enableSync: true,
debug: false,
})| 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. See Angular Image Optimization.
Schema Components
Schema components are abstract base classes you extend to render CMS content. They automatically set 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.
SchemaWithInputComponent<T> — Decorator Input
Extend this class to receive schema data via a traditional @Input(). The base class declares the required data input and optional links and references inputs.
import { Component } from '@angular/core';
import { SchemaWithInputComponent } from '@localess/angular/browser';
@Component({
selector: 'app-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:
<section>
<h1>{{ data.title }}</h1>
<img [src]="assetUrl(data.backgroundImage)" [alt]="data.title" />
<a [href]="findLink(data.ctaLink)">Learn more</a>
</section>Use the component by passing the schema object from the CMS:
<app-hero-section [data]="content.data" [links]="links" />SchemaWithSignalComponent<T> — Signal Input (Angular 17+)
Extend this class to use 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-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>SchemaComponent — Custom Binding
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-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();
}
}Base class helpers
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 |
Directives
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.
[data-ll-id] and [data-ll-schema]
Apply both together to any element to make it recognizable in the Visual Editor:
<div [attr.data-ll-id]="item._id" [attr.data-ll-schema]="item._schema">
<!-- content -->
</div>[data-ll-field]
Marks an individual field within a schema for field-level selection in the Visual Editor:
<p data-ll-field="subtitle">{{ data.subtitle }}</p>[llContent]
A convenience directive that sets both data-ll-id and data-ll-schema on the host element from a single ContentDataSchema input. 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>Pipes
Import individual pipes into the imports array of any standalone component that uses them.
llAsset — Asset URL
Transforms a ContentAsset object into a fully qualified CDN URL. Equivalent to assetUrl() on schema components.
import { AssetPipe } from '@localess/angular/browser';
@Component({ imports: [AssetPipe] })<img [src]="data.image | llAsset" alt="..." />llLink — Link Resolution
Resolves a ContentLink from the links map to a navigable path or URL. Pass the links map as the piped value and the ContentLink as the argument:
import { LinkPipe } from '@localess/angular/browser';<a [href]="links | llLink: data.ctaLink">Visit</a>ContentLink.type | Result |
|---|---|
"content" | Looks up link.uri in the links map and returns /<fullSlug> |
"url" | Returns link.uri as-is |
llRtToHtml — Rich Text to HTML
Converts a Localess RichText field (Tiptap JSON) to an HTML string. Supports headings, bold, italic, strike, underline, bullet lists, ordered lists, code blocks, and links.
import { RichTextToHtmlPipe } from '@localess/angular/browser';<div [innerHTML]="data.body | llRtToHtml"></div>llSafeHtml — Safe HTML
Bypasses Angular's DomSanitizer for a trusted HTML string. Always apply after llRtToHtml when binding to [innerHTML]:
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 sourced directly from your trusted Localess space.
Browser Asset Service
BrowserAssetService generates asset URLs programmatically. It is equivalent to assetUrl() 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);
}
}This service is browser-only. Use
ServerAssetServiceon the server.
Visual Editor Integration
Set enableSync: true in provideLocalessBrowser() to automatically inject the Visual Editor sync script.
To receive real-time content updates, subscribe to Visual Editor events. 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';
@Component({
selector: 'app-slug',
standalone: true,
templateUrl: './slug.component.html',
})
export default class SlugComponent implements OnInit {
private platformId = inject(PLATFORM_ID);
liveContent = signal<ContentData | undefined>(undefined);
ngOnInit(): void {
if (isPlatformBrowser(this.platformId)) {
if (window.localess) {
window.localess.on(['input', 'change'], (event) => {
this.liveContent.set(event.data);
});
}
}
}
}The input event fires on every keystroke; change fires when the editor saves. Render liveContent() instead of the server-fetched data when it is set to give authors a live preview.
Server Module
Import from @localess/angular/server.
All server services call the Localess REST API using a secret API token. They must be registered via provideLocalessServer() in the server application config and must never be used in browser code.
Setup
// 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',
spaceId: 'YOUR_SPACE_ID',
token: 'YOUR_SECRET_TOKEN',
version: 'draft', // omit for published content
}),
],
};
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 — keep this secret, never expose it to the browser |
version | 'draft' | string | — | Set to 'draft' to fetch unpublished content |
debug | boolean | — | When true, logs API calls to the server console |
Content Service
ServerContentService fetches CMS content from the Localess API. Results are cached in-memory per server request to prevent redundant network calls.
import { ServerContentService } from '@localess/angular/server';
@Injectable()
export class MyService {
private contentService = inject(ServerContentService);
}getContentBySlug<T>(slug, params?)
const content = await firstValueFrom(
contentService.getContentBySlug<HeroSection>('home', {
locale: 'en',
resolveReference: true,
resolveLink: true,
})
);getContentById<T>(id, params?)
const content = await firstValueFrom(
contentService.getContentById<ArticlePage>('abc123', { locale: 'fr' })
);getLinks(params?)
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' })
);ContentFetchParams
| 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 |
LinksFetchParams
| Parameter | Type | Description |
|---|---|---|
kind | string | Filter links by content kind |
parentSlug | string | Return only links under this parent slug |
excludeChildren | boolean | Exclude descendant slugs |
Server Asset Service
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);
}
}Translation Service
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>).
SSR with TransferState
In an SSR application, content fetched on the server must be transferred to the browser to avoid a duplicate network request on hydration. The recommended pattern uses an abstract service with two implementations swapped via Angular's 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
providers: [{ provide: LocalessService, useClass: LocalessBrowserService }]
// app.config.server.ts
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'));
}Angular Image Optimization
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 for 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 -->No additional configuration is required.
API Reference
@localess/angular/browser
| 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 | 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/angular/server
| 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/angular
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 |
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 |