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 — 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,
})
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

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:

MemberSignatureDescription
assetUrl(asset)(asset: ContentAsset) => stringBuilds the full CDN URL for a Localess asset
findLink(link)(link: ContentLink) => stringResolves a CMS link to a path or URL
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="..." />

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);
  }
}

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, 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);
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

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
SchemaWithInputComponent<T>Abstract ClassBase component with @Input() data: T
SchemaWithSignalComponent<T>Abstract ClassBase component with data = input.required<T>()
SchemaComponentAbstract ClassBase component requiring content() override
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
LinkPipePipellLink — resolves a ContentLink
RichTextToHtmlPipePipellRtToHtml — Tiptap JSON to HTML
SafeHtmlPipePipellSafeHtml — bypasses DomSanitizer
BrowserAssetServiceServiceProgrammatic asset URL generation

@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
TranslationsFlat key–value map of translation strings
ContentFetchParamsParameters for content fetch requests
LinksFetchParamsParameters for links fetch requests

On this page