Localess
SDK

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 browser entry point requires no API token and is safe to use in client-side code. The server entry 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@latest

Peer dependencies: @angular/core, @angular/common, @angular/compiler, @angular/platform-browser — versions >=21.0.0 <23.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'));

Playground: playgrounds/angular-ssr is a full working Angular SSR project wired up with provideLocalessBrowser/provideLocalessServer and the LocalessService TransferState pattern shown below.


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,
})
OptionTypeRequiredDescription
originstringFully qualified Localess URL, e.g. https://my-localess.web.app
spaceIdstringSpace ID from the Localess Space settings
enableSyncbooleanWhen true, injects the Visual Editor sync script into the page
debugbooleanWhen 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

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:

InputTypeDescription
datainput.required<T>()The schema data object (required)
linksinput<Links>()Map of content ID → slug, used by findLink()
referencesinput<References>()Map of resolved ContentReference objects
assetsinput<Assets>()Map of asset metadata
import { Component } from '@angular/core';
import { SchemaComponent } from '@localess/angular/browser';

@Component({
  selector: 'app-hero-section',
  standalone: true,
  templateUrl: './hero-section.component.html',
})
export class HeroSectionComponent extends SchemaComponent<HeroSection> {}

In the template, read inputs with function-call syntax and use the assetUrl() and findLink() helpers provided by the base class:

<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 in a parent template by passing the schema object and maps from the CMS:

<app-hero-section [data]="content.data" [links]="content.links" [references]="content.references" [assets]="content.assets" />

Resolving an asset with assetUrl()

assetUrl(asset, params?) builds the fully qualified CDN URL for a ContentAsset. Pass an AssetTransformParams object as the second argument to request a resized image or a different output format — see AssetTransformParams in docs/client.md:

<!-- Base URL, no transform -->
<img [src]="assetUrl(data().backgroundImage)" [alt]="data().title" />

<!-- Smaller, WebP thumbnail for a card -->
<img [src]="assetUrl(data().backgroundImage, { w: 400, f: 'webp' })" [alt]="data().title" />

<!-- Fixed box crop + quality control -->
<img [src]="assetUrl(data().backgroundImage, { w: 800, h: 600, q: 70, f: 'avif' })" [alt]="data().title" />

The same params argument works identically on the llAsset pipe (see below) and the standalone BrowserAssetService.link().


Base class helpers

SchemaComponent<T> exposes:

MemberSignatureDescription
assetUrl(asset, params?)(asset: ContentAsset, params?: AssetTransformParams) => stringBuilds the full CDN URL for a Localess asset, with optional transform params
findLink(link)(link: ContentLink) => stringResolves a CMS link to a path or URL, using the links input
configLocalessBrowserConfigInjected 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="..." />

Pass AssetTransformParams as a pipe argument to resize or convert the format:

<!-- Smaller WebP thumbnail -->
<img [src]="data.image | llAsset:{ w: 400, f: 'webp' }" alt="..." />

<!-- Fixed box crop + quality control -->
<img [src]="data.image | llAsset:{ w: 800, h: 600, q: 70, f: 'avif' }" alt="..." />

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.typeResult
"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: llSafeHtml calls DomSanitizer.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);
  }

  getThumbnailUrl(asset: ContentAsset): string {
    return this.assetService.link(asset, { w: 400, f: 'webp' });
  }
}

This service is browser-only. Use ServerAssetService on the server.


Visual Editor Integration

Set enableSync: true in provideLocalessBrowser() to automatically inject the Visual Editor sync script.

To receive real-time content updates, inject LocalessSyncService and use onChange() — it already covers the enabled() check (browser + Visual Editor iframe) and the ready() wait (avoiding a race where the listener is attached before the sync script has loaded):

import { Component, inject, OnInit, signal } from '@angular/core';
import { LocalessSyncService } from '@localess/angular/browser';

@Component({
  selector: 'app-slug',
  standalone: true,
  templateUrl: './slug.component.html',
})
export default class SlugComponent implements OnInit {
  private readonly sync = inject(LocalessSyncService);
  liveContent = signal<ContentData | undefined>(undefined);

  ngOnInit(): void {
    this.sync.onChange(event => this.liveContent.set(event.data));
  }
}

onChange(callback) is shorthand for on(['input', 'change'], callback): the input event fires on every keystroke, change fires when the editor saves, and callback is narrowed to that variant. Render liveContent() instead of the server-fetched data when it is set to give authors a live preview.

For other event types (save, publish, unpublish, pong, enterSchema, hoverSchema, leaveSchema), use on(event, callback):

this.sync.on(['save', 'publish'], event => console.info(`Content ${event.type}d`));

Both methods are no-ops if sync isn't enabled or usable in the current context — no need to check enabled() yourself.


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);
OptionTypeRequiredDescription
originstringFully qualified Localess URL
spaceIdstringSpace ID from Localess Space settings
tokenstringAPI token — keep this secret, never expose it to the browser
version'draft' | stringSet to 'draft' to fetch unpublished content
debugbooleanWhen 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

ParameterTypeDescription
version'draft' | stringOverride the global version for this request
localestringLocale code, e.g. 'en', 'fr'
resolveReferencebooleanInline referenced content objects
resolveLinkbooleanInline link objects
resolveAssetbooleanInline referenced asset metadata

LinksFetchParams

ParameterTypeDescription
kindstringFilter links by content kind
parentSlugstringReturn only links under this parent slug
excludeChildrenbooleanExclude 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

ExportKindDescription
provideLocalessBrowser(options)FunctionRegisters all browser-side providers
SchemaComponent<T>Abstract ClassBase component with data, links, references, assets signal inputs
ContentIdDirectiveDirective[data-ll-id] marker
ContentSchemaDirectiveDirective[data-ll-schema] marker
ContentFieldDirectiveDirective[data-ll-field] marker
ContentDirectiveDirective[llContent] — sets both id and schema attributes
AssetPipePipellAsset — asset to URL, with optional transform params
LinkPipePipellLink — resolves a ContentLink
RichTextToHtmlPipePipellRtToHtml — Tiptap JSON to HTML
SafeHtmlPipePipellSafeHtml — bypasses DomSanitizer
BrowserAssetServiceServiceProgrammatic asset URL generation
LocalessSyncServiceServiceVisual Editor sync — on() / onChange()
buildAssetQueryString(params?)FunctionStandalone asset transform query-string builder
findLink(links, link)FunctionStandalone link resolution utility

@localess/angular/server

ExportKindDescription
provideLocalessServer(options)FunctionRegisters all server-side providers
ServerContentServiceServiceFetches content by slug, ID, or links
ServerAssetServiceServiceProgrammatic asset URL generation
ServerTranslationServiceServiceFetches translations by locale

@localess/angular

Re-exports all types from @localess/client:

TypeDescription
Content<T>CMS document with metadata and typed data payload
ContentDataBase type for schema data objects
ContentDataSchemaSchema data with _id and _schema fields
ContentAssetAsset reference { uri: string }
ContentLinkLink reference { type: 'content' | 'url', uri: string }
ContentRichTextTiptap JSON rich text
LinksMap of content ID → { fullSlug: string }
ReferencesMap of referenced content objects
AssetsMap of asset ID → asset metadata
TranslationsFlat key–value map of translation strings
ContentFetchParamsParameters for content fetch requests
LinksFetchParamsParameters for links fetch requests
AssetTransformParamsAsset transform parameters (w, h, f, q, thumbnail, download)
LocalessSyncVisual Editor sync event types
EventToApp / EventToAppOf / EventCallback / EventToAppTypeVisual Editor sync event payload types

On this page