Angular SDK for the Localess headless CMS. Ships as a single unified package — no /browser or /server split — providing content delivery, rich text rendering, asset management, and Visual Editor integration for both client-side and server-side rendered Angular applications.
Security note:
provideLocaless()takes a singletoken. Use a public token (read-only, published content and translations only) where the configuration is bundled into the browser (app.config.ts). Use a secret token only where it stays server-side (app.config.server.ts, which takes precedence during server rendering).
- Installation
- Quick Start
- Setup
- Component Registry & Dynamic Rendering
- Content Service
- Asset Service
- Translation Service
- Schema Components
- Directives
- Pipes
- Visual Editor Integration
- Angular Image Optimization
- Other Exports
# 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, @angular/platform-browser — versions >=21.0.0 <23.0.0. Requires Node.js >= 24.
1. Register the provider in app.config.ts, with a public (read-only) token — this configuration is bundled into the browser:
// app.config.ts
import { provideLocaless } from '@localess/angular';
export const appConfig: ApplicationConfig = {
providers: [
provideLocaless({
origin: 'https://my-localess.web.app',
spaceId: 'YOUR_SPACE_ID',
token: 'YOUR_PUBLIC_TOKEN',
}),
],
};2. For SSR apps, register it again in app.config.server.ts, with a secret token — this registration takes precedence during server rendering (last provider for a given token wins):
// app.config.server.ts
import { mergeApplicationConfig } from '@angular/core';
import { provideLocaless } from '@localess/angular';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideLocaless({
origin: 'https://my-localess.web.app',
spaceId: 'YOUR_SPACE_ID',
token: 'YOUR_SECRET_TOKEN',
}),
],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);3. Fetch content with LocalessContentService, e.g. in a route resolver:
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { Content, LocalessContentService } from '@localess/angular';
const contentResolver: ResolveFn<Content> = route => {
return inject(LocalessContentService).contentBySlug(route.paramMap.get('slug')!);
};On the server, the fetched content is written to TransferState; on the browser, the same call reads it back out instead of re-fetching (or, in a pure client-side-rendered app with no SSR, falls back to fetching directly using the public token).
provideLocaless() registers everything: LocalessClientService, LocalessContentService, LocalessAssetService, LocalessTranslationService, LocalessSyncService, LocalessComponentResolver, the LOCALESS_CONFIG and LOCALESS_SYNC_READY injection tokens, and Angular's IMAGE_LOADER. It throws at startup if origin, spaceId, or token is missing or empty.
import { provideLocaless } from '@localess/angular';
provideLocaless({
origin: 'https://my-localess.web.app', // Required. Localess instance URL (no trailing slash)
spaceId: 'YOUR_SPACE_ID', // Required. Found in Localess Space settings
token: 'YOUR_TOKEN', // Required. Public token if bundled into the browser, secret token if server-only
version: 'draft', // Optional. Omit for published content
enableSync: true, // Optional. Loads the Visual Editor sync script
cacheTTL: 300, // Optional. Seconds; false disables caching
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 |
token |
string |
✅ | Public token where bundled into the browser, secret token where server-only |
version |
'draft' |
— | Fetch draft content; omit for published |
enableSync |
boolean |
— | When true, injects the Visual Editor sync script into the page |
cacheTTL |
number | false |
— | Response cache TTL in seconds (default 300); false disables caching |
debug |
boolean |
— | When true, logs internal activity to the console |
provideLocaless() also registers Angular's built-in IMAGE_LOADER provider so that NgOptimizedImage automatically appends ?w=<width> to Localess asset URLs for responsive image optimization, plus any AssetTransformParams passed via [loaderParams] (Angular's derived height is never sent — pass h via loaderParams to request a box).
provideLocaless() accepts optional trailing features, the same pattern as provideRouter()/provideHttpClient(). Today there's one: withLocalessComponents().
Register a map of content _schema keys to Angular components, then render content without writing a switch statement over schema types yourself.
Pass to provideLocaless() as a feature. Entries can be an eager component reference or a lazy loader (LocalessComponentLoader, i.e. () => Promise<AnySchemaComponent>) — mix both in the same map. Every entry, including the optional fallback, must be a class that extends SchemaComponent — LocalessComponentsMap values and the fallback parameter are typed AnySchemaComponent (Type<SchemaComponent<any>>), so anything else is a compile error:
import { provideLocaless, withLocalessComponents } from '@localess/angular';
import { HeroSectionComponent } from './components/hero-section.component';
import { UnknownBlockComponent } from './components/unknown-block.component';
provideLocaless(
{ origin: 'https://my-localess.web.app', spaceId: 'YOUR_SPACE_ID', token: 'YOUR_TOKEN' },
withLocalessComponents(
{
hero: HeroSectionComponent, // eager — bundled immediately
teaser: () => import('./components/teaser.component').then(m => m.TeaserComponent), // lazy — loaded on demand
},
UnknownBlockComponent // optional fallback, rendered when a `_schema` has no match — also a SchemaComponent
)
);Because every registered component extends SchemaComponent, data/links/references/assets are always set unconditionally — no need to conditionally declare inputs. The fallback commonly only reads data()._schema (e.g. to log or display the unmatched key) and ignores links/references/assets, but it still must extend SchemaComponent to be accepted by withLocalessComponents():
import { SchemaComponent } from '@localess/angular';
@Component({ selector: 'app-unknown-block', template: `Unknown block: {{ data()._schema }}` })
export class UnknownBlockComponent extends SchemaComponent {}LocalessDocument is the top-level entry point for a fetched page. It renders document().data through [llComponent] (passing the document's links/references/assets) and keeps it live: it subscribes to LocalessSyncService.onDocument internally, so input/change events from the Visual Editor for its own document().id (matched against event.documentId) update the page without a full reload — no manual sync wiring needed. When document().data is missing it renders a placeholder message and logs a console error.
import { Component, input } from '@angular/core';
import { Content, LocalessDocument } from '@localess/angular';
@Component({
selector: 'app-slug',
imports: [LocalessDocument],
template: `<ll-document [document]="content()" />`,
})
export class SlugComponent {
content = input.required<Content>();
}LocalessComponentDirective is used by <ll-document> internally, and directly useful inside your own schema components to render a nested schema item. Apply it to a plain ng-container; inputs are llComponent (ContentData | null | undefined), links, references, and assets:
import { LocalessComponentDirective } from '@localess/angular';
@Component({ imports: [LocalessComponentDirective] })<!-- inside a schema component's own template -->
<ng-container [llComponent]="data().hero" [links]="links()" [references]="references()" [assets]="assets()" />It resolves the item's _schema against the registry (via LocalessComponentResolver, which caches resolved components so a lazy loader runs once per key) and creates the matching component with ViewContainerRef.createComponent, directly at the ng-container's position — no wrapper element is inserted, so the component renders as a direct sibling of whatever its parent's CSS (e.g. Grid/Flexbox) expects.
Recreates the rendered component only when _schema changes; otherwise the existing instance is reused and just gets updated data/links/references/assets inputs, so unrelated content edits don't tear down component state. A null/undefined value clears the rendered component.
For an array field (e.g. a page's body), loop it yourself with @for — @for already handles keyed add/remove/reorder, and neither it nor ng-container produce a DOM element, so nesting stays wrapper-free at any depth:
@for (item of data().body; track item._id) {
<ng-container [llComponent]="item" [links]="links()" [references]="references()" [assets]="assets()" />
}When a _schema key has no match in the registry: the fallback component (if configured) is rendered, and a console error is logged either way. With no fallback configured, nothing is rendered for that item.
LocalessContentService fetches content and hydrates it from server to browser via TransferState. All methods return a Promise — call from anywhere async/await works, e.g. a route resolver or an event handler; wrap in Angular's resource() yourself if you need reactive re-fetching.
import { LocalessContentService } from '@localess/angular';
import { inject } from '@angular/core';
const contentService = inject(LocalessContentService);content = await contentService.contentBySlug<HeroSection>('home');
// With params
content = await contentService.contentBySlug<HeroSection>('home', {
version: 'draft',
locale: 'en',
resolveReference: true,
resolveLink: true,
});content = await contentService.contentById<ArticlePage>('abc123', { locale: 'fr' });links = await contentService.links({ kind: 'DOCUMENT', parentSlug: 'blog', excludeChildren: false });| Parameter | Type | Description |
|---|---|---|
version |
'draft' |
Override the global version for this request |
locale |
string |
Locale code, e.g. 'en', 'fr' |
resolveReference |
boolean |
Populate references — one level only |
resolveLink |
boolean |
Populate links with content metadata |
resolveAsset |
boolean |
Populate assets with asset metadata |
Resolution is all-or-nothing with no depth option. A resolved reference carries metadata, locale
and data, with none of its own collections; follow a further reference via the uri on the
REFERENCE field value in data. A deleted target is silently omitted from the map and the
request still succeeds. See docs/client.md.
| Parameter | Type | Description |
|---|---|---|
kind |
'DOCUMENT' | 'FOLDER' |
Filter links by content kind; omit for all |
parentSlug |
string |
Return only links under this parent slug |
excludeChildren |
boolean |
Exclude descendant slugs |
Each method returns a Promise that rejects on a failed fetch (e.g. LocalessApiError for a 404) — handle it wherever you call it, such as a route resolver:
const contentResolver: ResolveFn<Content | undefined> = async () => {
try {
return await contentService.contentBySlug('home');
} catch (error) {
if (error instanceof LocalessApiError && error.status === 404) return undefined;
throw error;
}
};LocalessAssetService generates asset URLs. Its API is identical whether called server-side or in the browser.
import { LocalessAssetService } from '@localess/angular';
@Injectable()
export class MyService {
private assetService = inject(LocalessAssetService);
getUrl(asset: ContentAsset): string {
return this.assetService.link(asset);
// or: this.assetService.link('path/to/asset.jpg')
}
}SchemaComponent.assetUrl() and the llAsset pipe use the same underlying logic (LocalessClientService.assetLink()) — all three are equivalent.
link() always returns a rendition — a still raster is re-encoded at its format's default
quality even with no transform parameters. Two further methods serve the uploaded bytes untouched,
and neither takes transform parameters:
this.assetService.originalLink(asset); // served inline — archival, print, downstream processing
this.assetService.downloadLink(asset); // served as an attachment, so the browser saves itLocalessClientService.assetOriginalLink() / assetDownloadLink() are the equivalent methods on
the client service.
The
downloadtransform parameter was removed in v4; usedownloadLink()instead. It also carries a non-ASCII asset name in an RFC 5987filename*parameter, so the file saves under its real name.
Pass an AssetTransformParams object as the second argument to request a resized image or a different output format:
assetService.link(asset, { w: 400, f: 'webp' });
assetService.link(asset, { w: 800, h: 600, q: 70, f: 'avif' });| Param | Type | Description |
|---|---|---|
w |
number |
Target width in pixels |
h |
number |
Target height in pixels (combined with w, crops to cover the box) |
q |
number |
Output quality 1–100 (default 85; ignored for PNG) |
f |
'webp' | 'jpeg' | 'png' | 'avif' |
Converts the output format |
fit |
'cover' | 'contain' | 'inside' | 'outside' | 'fill' |
Fit mode, applied only when both w and h are set. The API default is cover, which crops; inside shrinks to fit without cropping |
thumbnail |
boolean |
Extracts the first frame of an animated/video asset before resizing |
LocalessTranslationService fetches translation strings for a given locale.
import { LocalessTranslationService } from '@localess/angular';
@Injectable()
export class MyService {
private translationService = inject(LocalessTranslationService);
async getTranslations(locale: string) {
return this.translationService.fetch(locale);
}
}The returned Translations object is a flat key–value map (Record<string, string>).
SchemaComponent<T> is the abstract base class you extend to render a Localess content schema. It automatically sets 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.
The base class declares four signal inputs:
| Input | Type | Description |
|---|---|---|
data |
input.required<T>() |
The schema data object (required) |
links |
input<Links>() |
Map of content ID → ContentMetadata (incl. fullSlug), used by findLink() |
references |
input<References>() |
Map of content ID → resolved Content |
assets |
input<Assets>() |
Map of asset metadata |
import { Component } from '@angular/core';
import { SchemaComponent } from '@localess/angular';
import type { ContentAsset, ContentLink } from '@localess/angular';
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 SchemaComponent<HeroSection> {}<!-- 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>Normally <ll-document> / [llComponent] instantiate schema components for you via the registry. To render one directly, bind its inputs yourself (Content.data is optional, so guard it — data is a required input):
@if (content().data; as data) {
<app-schema-hero-section [data]="data" [links]="content().links" [references]="content().references" [assets]="content().assets" />
}| Member | Signature | Description |
|---|---|---|
assetUrl(asset, params?) |
(asset: ContentAsset, params?: AssetTransformParams) => string |
Builds the full CDN URL for a Localess asset, with optional transform params |
findLink(link) |
(link: ContentLink) => string |
Resolves a CMS link to a path or URL, using the links input |
findLink(link) resolves a ContentLink field: internal content links are looked up in the links input (a map of content ID → slug) and resolve to /<fullSlug> (or /not-found if the ID isn't found); url links are returned as-is.
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 (ContentIdDirective, ContentSchemaDirective) — empty classes; the Visual Editor reads the attributes themselves. 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>Marker directive (ContentFieldDirective). 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:
import { ContentDirective } from '@localess/angular';
@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.
import { AssetPipe } from '@localess/angular';
@Component({ imports: [AssetPipe] })<img [src]="data.image | llAsset" alt="..." />
<img [src]="data.image | llAsset:{ w: 400, f: 'webp' }" alt="..." />See Requesting a transformed asset above for the full AssetTransformParams field reference.
import { LinkPipe } from '@localess/angular';<a [href]="links | llLink: data.ctaLink">Visit</a>The piped value is the Links map; the argument is the ContentLink to resolve.
ContentLink.type |
Result |
|---|---|
"content" |
Looks up link.uri in the links map and returns /<fullSlug> (/not-found if missing) |
"url" |
Returns link.uri as-is |
Converts a Localess RichText field (Tiptap JSON) to sanitizer-trusted HTML, synchronously — built on @localess/richtext, no TipTap at runtime, no | async, no | llSafeHtml:
import { LocalessRichTextPipe } from '@localess/angular';
@Component({ imports: [LocalessRichTextPipe] })<div [innerHTML]="data.body | llRichText"></div>The pipe accepts LocalessRichTextInput (a doc, node, node array, ContentRichText, or null/undefined → empty). An optional argument passes per-node string renderers (LocalessRichTextRenderers<string>): data.body | llRichText:renderers. Supports paragraphs, headings (H1–H6), bold, italic, strike, underline, bullet lists, ordered lists, code, code blocks, blockquotes, horizontal rules, and links; link hrefs are sanitized (javascript:/data: stripped), including the attrs.href a custom link renderer receives. Renderers return HTML that is trusted as-is, so escape what you interpolate: escapeAttr for attribute values, escapeHtml for text (both exported, with sanitizeUrl).
LocalessRichText renders the field into its host element via [innerHTML]; re-renders on signal changes. Inputs: content (required, LocalessRichTextInput) and renderers (optional, LocalessRichTextRenderers<string>):
import { LocalessRichText } from '@localess/angular';
@Component({ imports: [LocalessRichText] })<ll-rich-text [content]="data.body" />
<ll-rich-text [content]="data.body" [renderers]="myRenderers" />Bypasses Angular's DomSanitizer for a trusted HTML string. Accepts string | null | undefined (treated as empty HTML).
<div [innerHTML]="trustedHtmlString | llSafeHtml"></div>Security:
llSafeHtmlcallsDomSanitizer.bypassSecurityTrustHtml(). Only use it with HTML that comes directly from your trusted Localess space.
The Localess Visual Editor enables live in-browser content editing. Set enableSync: true in provideLocaless() to automatically inject the sync script.
The script is injected once the application is stable (ApplicationRef.whenStable()), not during bootstrap. The sync script hooks every [data-ll-id] element it can see the moment the editor answers its ping, so loading it eagerly raced the first render: a lazily registered schema component destroys and recreates its server-rendered DOM when its loader resolves, and elements recreated after that handshake stayed unclickable in the editor. LocalessComponentResolver registers each lazy loader as a PendingTasks task so stability accounts for it under zoneless change detection too.
Inject LocalessSyncService and use onDocument() — it already covers the enabled() check (browser + Visual Editor iframe) and the ready() wait. A subscriber that attaches after the document was already edited is called once straight away with the latest edit (recorded by provideLocaless from the moment the sync script loads), so a component that mounts late still shows what the editor shows.
import { Component, DestroyRef, inject, input, signal } from '@angular/core';
import { Content, ContentData, LocalessSyncService } from '@localess/angular';
@Component({
selector: 'app-slug',
standalone: true,
templateUrl: './slug.component.html',
})
export class SlugComponent {
private sync = inject(LocalessSyncService);
readonly content = input.required<Content>();
liveContent = signal<ContentData | undefined>(undefined);
constructor() {
// Subscribed in an injection context, so it is removed when the component is destroyed.
// Only `input`/`change` events for this document, with the edited data.
this.sync.onDocument(this.content().id, data => this.liveContent.set(data));
}
}Cleanup. A subscription made in an injection context (constructor or field initializer) is removed automatically when that component, directive or service is destroyed. Elsewhere — ngOnInit, a callback — pass a DestroyRef, or call the function on()/onChange()/onDocument() return:
private readonly destroyRef = inject(DestroyRef);
ngOnInit(): void {
this.sync.onDocument(this.content().id, data => this.liveContent.set(data), this.destroyRef);
}For just the latest event as a signal, use localessSyncEvent(event) in an injection context — it cleans up the same way:
readonly saved = localessSyncEvent(['save', 'publish']); // Signal<EventToAppOf<'save' | 'publish'> | undefined>onChange(callback) is shorthand for on(['input', 'change'], callback): 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. on/onChange don't filter by document; onDocument(documentId, callback, destroyRef?) keeps only input/change events whose documentId (the edited Content.id) matches and calls callback(data, event) with the edited data.
For other event types (save, publish, unpublish, pong, enterSchema, hoverSchema, leaveSchema), use on(event, callback) — event is a single EventToAppType or an array, and the callback is narrowed to the matching variant(s) (EventToAppOf<T>):
this.sync.on(['save', 'publish'], event => console.info(`Content ${event.type}d`));All three methods are no-ops if sync isn't enabled or usable in the current context — no need to check enabled() yourself. The service also exposes enabled(): boolean (enableSync: true + running in the browser + inside the Visual Editor iframe) and ready(): Promise<void> (resolves once the sync script has loaded and window.localess exists; resolves immediately when sync is disabled, and never rejects — a failed script load is logged instead). ready() returns the LOCALESS_SYNC_READY injection token's value, which provideLocaless() sets when enableSync: true.
If you render with <ll-document>, none of this is needed — it subscribes to onDocument for you.
provideLocaless() 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. Other AssetTransformParams (q, f, fit, thumbnail, h) can be passed per image via [loaderParams]; Angular's own width wins over a w there, and its derived height is deliberately not sent (sending both w and h would switch to a fit crop):
<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. The loader only rewrites URLs under <origin>/api/v1/spaces/<spaceId>/assets/; other src values pass through unchanged.
Everything below is exported from @localess/angular alongside the APIs above.
| Export | Kind | Description |
|---|---|---|
LocalessClientService |
service | Thin DI wrapper around localessClient() built from LOCALESS_CONFIG: getLinks(), getContentBySlug(), getContentById(), getTranslations(), assetLink(), assetOriginalLink(), assetDownloadLink(). No TransferState hydration — prefer LocalessContentService for content. |
LocalessComponentResolver |
service | Resolves _schema keys against the registry: has(key), resolve(key): Promise<Type<SchemaComponent> | null> (cached; falls back to the fallback component). Used by [llComponent]. |
LOCALESS_CONFIG, LocalessConfig |
token / type | The resolved provider configuration (same shape as LocalessOptions). |
LocalessOptions |
type | The provideLocaless() options object. |
LOCALESS_SYNC_READY |
token | Promise<void> that resolves when the sync script has loaded (already-resolved when sync is disabled). |
LOCALESS_COMPONENTS, LOCALESS_FALLBACK_COMPONENT |
tokens | Registry and fallback tokens populated by withLocalessComponents(). |
LocalessComponentsMap, LocalessComponentLoader, AnySchemaComponent |
types | Registry map, lazy loader, and Type<SchemaComponent<any>> component type. |
isComponentLoader(entry) |
function | Type guard distinguishing a lazy loader from a component class. |
findLink, buildAssetQueryString, isBrowser, isIframe, loadLocalessSync |
functions | Utilities re-exported from @localess/client (findLink, buildAssetQueryString) and @localess/live-preview (isBrowser, isIframe, loadLocalessSync). |
escapeHtml, escapeAttr, sanitizeUrl |
functions | Escaping and link-href allowlist for custom rich text string renderers, re-exported from @localess/richtext. |
Content, ContentData, ContentDataSchema, ContentAsset, ContentLink, ContentReference, ContentRichText, Links, References, Assets, Translations, AssetTransformParams, ContentFetchParams, LinksFetchParams, TranslationFetchParams, EventToAppType, EventToAppOf, LocalessRichTextInput, LocalessRichTextDocument, LocalessRichTextNode, LocalessRichTextMark, … |
types | Domain-model types re-exported from @localess/model, @localess/client, @localess/live-preview (EventToAppType, EventToAppOf), and @localess/richtext. |
The package also re-exports the full @localess/client surface (export * from '@localess/client'), so LocalessApiError, localessClient, and every client type are importable from @localess/angular without adding @localess/client as a direct dependency.