@contentful/experiences-angular

中文文档
0.8.9发布于 2026/11/07MITTypeScriptES Module近 30 天下载 4194

Angular adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan

原始使用文档
文档目录13 节
  1. Public API
  2. Authoring
  3. Fetching
  4. Resolver
  5. Live preview
  6. Renderers
  7. Styling + runtime context (helpers)
  8. Re-exported types and utilities
  9. Quick reference
  10. Slot children
  11. Parity table
  12. License
  13. Runtime Experience parameters

@contentful/experiences-angular

⚠️ Alpha. Published to npm. APIs are unstable and will change.

The Angular adapter for the Contentful Experiences SDK. You bring your own standalone Angular components; it renders Experience payloads from the Experience Delivery API (XDA) with them.

npm install @contentful/experiences-angular

Supports Angular 20, 21, and 22. The package ships partial-Ivy output compiled by the lowest supported compiler, so the Angular linker in your app can consume it on any of the three.

This is the only SDK package you install. It re-exports everything you need from @contentful/experiences-sdk-core and @contentful/experiences-design. The other packages are workspace-internal.

The public API mirrors @contentful/experiences-react and @contentful/experiences-svelte in shape; only the rendering primitives change (standalone Type<unknown> components, PortableRenderNode[] slot inputs instead of children: ReactNode, and injectDesignValues() instead of useDesignValues()). See Parity table for the full mapping.


Public API

Authoring

defineComponent<Props>(config); // Type-narrowing identity for component-type configs
defineExperienceTemplate<Props>(config); // Same shape, for coded Experience Template configs

Fetching

fetchExperience(experienceOptions, clientOptions, resolveOptions); // Async; fetches by Destination path/node id or Experience id and resolves in one call
fetchDestinationSitemap(destinationOptions, clientOptions, paginationOptions?); // Async; lists every routable path in a Destination
fetchPreviewSession(previewSessionOptions, clientOptions, resolveOptions); // Async; fetches and resolves a Preview Session Experience

Serve pages from a Destination: the URL path is the Destination path. A redirect node comes back as a value, not an error. Preview is not supported on Destinations (it throws DestinationPreviewNotSupportedError); use the by-id form for preview.

const result = await fetchExperience(
  { spaceId, destinationId, path: '/demo/landing' },
  { accessToken },
  { config: experienceConfig }
);
if ('redirect' in result) {
  // follow it: respond with a redirect to result.redirect.path
}
// otherwise `result` is a PortableRenderPlan

// Enumerate every routable path, for example to prerender
const sitemap = await fetchDestinationSitemap({ spaceId, destinationId }, { accessToken });
const paths = sitemap.items.map((item) => item.path); // follow sitemap.pages.next for more

Use fetchPreviewSession for the initial render when the Contentful app provides a preview_session_id. It fetches the current Preview Session snapshot and resolves it into a PortableRenderPlan. Use injectLivePreview to receive subsequent WebSocket updates:

const plan = await fetchPreviewSession(
  { spaceId, environmentId, sessionId },
  { previewToken },
  { config: experienceConfig }
);

If the Preview Session references resources from other spaces, pass the same encoded resourceResolution value in the previewSessionOptions passed to fetchPreviewSession and injectLivePreview.

Resolver

resolveExperience(payload, config, opts?); // Async; walks payload, runs resolveData, returns a PortableRenderPlan

Live preview

Use injectLivePreview() when the app should subscribe to Preview Session updates, resolve each payload, and render the resulting plan from data:

readonly livePreview = injectLivePreview(() => ({
  previewSessionOptions,
  initialPayload,
  initialPlan,
  resolveOptions: { config: experienceConfig },
}));

For separate access to the raw payload and rendered plan, use injectLivePreviewExperience() and injectExperiencePlan(). The first returns the raw Experience payload. The second passes that payload to resolveExperience() and returns the PortableRenderPlan consumed by the renderer. Keeping the values separate lets an app use the raw payload when it needs it.

Both helpers take a getter. Angular can call an injection helper before it has assigned component inputs, so the getter reads the current values when Angular runs the helper. Each helper returns an object with a data signal, matching the other framework adapters.

// page.component.ts
import { Component } from '@angular/core';
import {
  ClientExperienceRenderer,
  injectLivePreviewExperience,
  injectExperiencePlan,
  type PortableRenderPlan,
} from '@contentful/experiences-angular';
import { experienceConfig } from './experience-config';

@Component({
  selector: 'app-page',
  imports: [ClientExperienceRenderer],
  template: `<cf-experience [experience]="plan.data()" [config]="experienceConfig" />`,
})
export class PageComponent {
  readonly livePreviewOptions = {
    previewSessionOptions: {
      spaceId: 'space-id',
      environmentId: 'environment-id',
      previewToken: 'preview-token',
      sessionId: 'session-id',
    },
  };
  readonly initialPlan: PortableRenderPlan | undefined = undefined;

  readonly livePreview = injectLivePreviewExperience(() => this.livePreviewOptions);
  readonly plan = injectExperiencePlan(() => ({
    payload: this.livePreview.data(),
    initialPlan: this.initialPlan,
    resolveOptions: { config: experienceConfig },
  }));

  protected readonly experienceConfig = experienceConfig;
}

injectLivePreviewExperience() starts its subscription after the first browser render. Set both previewToken and sessionId to receive Preview Session updates. The initial payload remains available while updates arrive. The browser SDK sends the token as the WebSocket URL's access_token parameter.

injectExperiencePlan() calls resolveExperience() for each new raw payload. It updates plan.data() after the resolver returns a complete plan. The current plan stays in place until the new plan is ready.

The live-preview results also expose an error signal when the Preview Session connection fails. The last valid data remains available.

Renderers

Every renderer is standalone; add it to your own component's imports.

ServerExperienceRenderer; // <cf-server-experience>  SSR-safe renderer
ClientExperienceRenderer; // <cf-experience>         Client renderer
MissingComponent; // <cf-missing-component>  Default fallback for unregistered component types
NodesRenderer; // *cfNodes                Renders a slot's raw nodes (see Slot children)
NodeRenderer; // *cfNode                 Renders one node; NodesRenderer loops over it
DebugExperience; // <cf-debug-experience>   Auto-mounted by the renderers when debug is set
injectLivePreviewExperience; // Signal-backed raw Experience payload
injectExperiencePlan; // Signal-backed PortableRenderPlan

NodesRenderer and NodeRenderer are structural directives, not components, so they add no element of their own — see Slot children.

Each is also exported under its Angular-suffixed class name (ServerExperienceRendererComponent, NodesRendererDirective, and so on), and ExperienceRenderer is an alias for ClientExperienceRenderer.

Styling + runtime context (helpers)

All four are inject()-based: call them from a field initializer or a constructor, like any other Angular injection API.

injectDesignValues<T>(); // Signal of the resolved design record that auto-fills inputs
toCss(design, options?); // Turns a design record into a plain style object for [ngStyle]
injectExperience(); // Signal<RenderContext>: debug, metadata
injectContentfulComponent(); // Signal of the raw payload for the enclosing node (or undefined), including `attribution`
injectContentfulExperienceTemplate(); // Same, for an enclosing coded Experience Template node
getTrackingAttributes(attribution); // Attributes to bind on a node's outermost element for interaction tracking
type ToCssOptions;

Resolved design values are auto-filled onto your component's inputs by key, alongside content. Declaring an @Input() per design key you style with is the one recommended path — and in Angular it is also what makes the key arrive, since binding an undeclared input is an error. injectDesignValues() returns the same record as a Signal, as an escape hatch and as the way to read keys your component didn't declare, which are dropped rather than passed (see Parity table). Reach for it only for a nested child that isn't itself a registered component, or for design needed outside the render path (an effect, an imperative measurement) — see Styling components. Token resolution is configured with resolveToken on your Config (type ResolveToken).

Re-exported types and utilities

// From core
type Config, Components, ExperienceTemplates, Registration, ExperienceTemplateRegistration,
type ComponentConfig, ExperienceTemplateConfig,
type ContentfulComponent, ContentfulExperienceTemplate,
type RenderContext, RenderUnknown, ResolveToken, SlotNodes,
type ExperiencePayload, ExperienceNode, ComponentNode, ExperienceTemplateNode,
type ComponentRef, ExperienceTemplateRef, ExperienceSys,
type PortableRenderPlan, PortableRenderNode, PortableRegistration,
type DesignPropValue, ManualDesignValue, DesignToken, ExperienceContext, ResolveContext,
type ResolverConfig, ResolveExperienceOptions

// From live preview and the Angular adapter
type PreviewSessionOptions, LivePreviewClient,
type InjectLivePreviewExperienceOptions, InjectLivePreviewExperienceResult,
type ExperiencePlanResolveOptions, InjectExperiencePlanOptions,
type InjectExperiencePlanResult

// From design
isCssProperty, toCssKey, CSS_PROPERTIES

// From client
createClient, fetchExperience, fetchDestinationSitemap, fetchPreviewSession, ContentfulViewDelivery,
ContentfulViewDeliveryClient, NotFoundError, DestinationPreviewNotSupportedError, PreviewSessionFetchError,
LivePreviewConnectionError,
DELIVERY_HOST, PREVIEW_HOST, PREVIEW_WEBSOCKET_HOST

type ExperienceOptions, ByIdExperienceOptions, ByDestinationPathExperienceOptions,
  ByDestinationNodeIdExperienceOptions, DestinationRedirectResult, DestinationSitemapResult,
  FetchDestinationSitemapOptions, FetchDestinationSitemapPaginationOptions, SitemapPathItem,
  PreviewSessionExperienceOptions, PreviewSessionClientOptions,
  PreviewSessionResolveOptions, ClientOptions, ResolveOptions, CreateClientOptions

Quick reference

// button.component.ts — content + resolved design both arrive as inputs
import { Component, Input, signal } from '@angular/core';

@Component({
  selector: 'app-button',
  template: `
    @if (urlValue()) {
      <a [href]="urlValue()" [style.background]="bgValue()" [style.color]="colorValue()">
        {{ labelValue() }}
      </a>
    } @else {
      <button type="button" [style.background]="bgValue()" [style.color]="colorValue()">
        {{ labelValue() }}
      </button>
    }
  `,
})
export class ButtonComponent {
  protected readonly labelValue = signal('Button');
  protected readonly urlValue = signal<string | undefined>(undefined);
  protected readonly bgValue = signal<string | undefined>(undefined);
  protected readonly colorValue = signal<string | undefined>(undefined);

  // Decorator inputs bridged into signals, not signal input(): see the parity
  // table. The input name is what the payload key binds to; the readable signal
  // needs a different name, and must be `protected`, not `private`, to stay
  // template-readable under strictTemplates.
  @Input() set label(value: string | undefined) {
    this.labelValue.set(value ?? 'Button');
  }
  @Input() set url(value: string | undefined) {
    this.urlValue.set(value);
  }
  @Input() set backgroundColor(value: string | undefined) {
    this.bgValue.set(value);
  }
  @Input() set color(value: string | undefined) {
    this.colorValue.set(value);
  }
}
// experience-config.ts
import {
  defineComponent,
  type Components,
  type Config,
  type ResolveToken,
} from '@contentful/experiences-angular';
import { ButtonComponent } from './components/button.component';

interface ButtonProps {
  label?: string;
  url?: string;
  backgroundColor?: string; // resolved design, auto-filled
  color?: string;
}

const components: Components = {
  // Bare component class, or defineComponent({...}) when you need defaults/resolveData.
  Button: defineComponent<ButtonProps>({
    resolveData: ({ content }) => ({ url: ensureScheme(content.url) }),
    component: ButtonComponent,
  }),
};

const resolveToken: ResolveToken = (token) => designTokens[token.value];

export const experienceConfig: Config = { components, resolveToken };
// page.component.ts
import { Component, Input } from '@angular/core';
import { ServerExperienceRenderer, type PortableRenderPlan } from '@contentful/experiences-angular';
import { experienceConfig } from './experience-config';

@Component({
  selector: 'app-page',
  imports: [ServerExperienceRenderer],
  template: `<cf-server-experience [experience]="experience" [config]="config" />`,
})
export class PageComponent {
  @Input() experience!: PortableRenderPlan;
  protected readonly config = experienceConfig;
}

Slot children

Every slot arrives as an input named after the slot, holding an array of nodes (PortableRenderNode[], aliased SlotNodes) — not renderable children. Hand the array to *cfNodes to render it; to wrap, reorder, or drop children individually, loop the array yourself and render each with *cfNode.

// section.component.ts
import { Component, Input, signal } from '@angular/core';
import { NodesRenderer, type SlotNodes } from '@contentful/experiences-angular';

@Component({
  selector: 'app-section',
  imports: [NodesRenderer],
  // Common case — render them all:
  template: `<div><ng-container *cfNodes="nodes()"></ng-container></div>`,
  // Or take control of each child, with NodeRenderer in imports instead:
  //   <div>
  //     @for (child of nodes() ?? []; track $index) {
  //       <div class="cell"><ng-container *cfNode="child"></ng-container></div>
  //     }
  //   </div>
})
export class SectionComponent {
  protected readonly nodes = signal<SlotNodes | undefined>(undefined);

  @Input() set children(value: SlotNodes | undefined) {
    this.nodes.set(value);
  }
}

Slot children stay lazy: a component that never renders a slot input never instantiates those subtrees.

Both are structural directives, so the adapter puts no element of its own between you and your children: they render as direct children of the element you wrapped them in, exactly as in React and Svelte. display: grid with gap on the <div> above lays out the slot children; > .card, :nth-child(2), :first-child, and the +/~ combinators all work. (Each directive leaves a comment anchor, as Svelte does — comments are not elements, so they affect neither layout nor any of those selectors.)

children is not special — it is simply the conventional name for the default slot. Every slot in the payload becomes a same-named SlotNodes input, so a component with a header slot just declares header and renders it the same way. This applies identically to coded Experience Templates: a template with a content slot receives a content input.

The same nodes are also on the payload at injectContentfulComponent()().slots (a Record<string, PortableRenderNode[]>).


Parity table

Everything below is a deliberate divergence from React and Svelte, forced by an Angular primitive. Semantics — merge precedence, degradation behaviour, and context walk-up — are identical across all three adapters and covered by the same ported test suite.

ConcernReactSvelteAngularWhy
Runtime accessorsuseExperience(), useDesignValues()getExperience(), getDesignValues()injectExperience(), injectDesignValues()Angular's DI idiom. Must be called from an injection context.
Accessor return typeplain value (re-renders)plain value (read in $derived)Signal<T> — call it to readSignals are Angular's reactive primitive.
Renderer usage<ServerExperienceRenderer …/><ServerExperienceRenderer …/><cf-server-experience …/> after adding ServerExperienceRenderer to importsAngular components are referenced by selector, imported by class.
Slot childrenchildren: ReactNode — render directlySnippet[] — {@render child()}PortableRenderNode[] — render with *cfNodesAngular has no lazy named-slot primitive; projectableNodes is positional and eager.
NodesRenderernot exportedexported (escape hatch)exported and load-bearingIt is the only way to render a slot.
Undeclared merged keyspassed through as propspassed through as propsdroppedBinding an input a component does not declare is an error, so the merged record is filtered via reflectComponentType. Still readable through injectDesignValues().
Reading dropped keysn/an/ainjectDesignValues()The full resolved design record is always available regardless of declared inputs.
Component inputsprops$props()@Input() setter → signalSignal input() is AOT-only; a JIT consumer reports zero declared inputs, which would break reflectComponentType filtering.
Input naminganyanysetter takes the payload key; the readable signal needs a distinct nameA class cannot declare a field and an accessor under one name, and under useDefineForClassFields: false the field initializer would assign straight through the setter.
Missing-component warningeffecteffectngOnInitSo the diagnostic also fires during server rendering.
Prop-shape typesinferredseparate *.ts per componentnot neededAngular components are .ts, so tsc --noEmit already resolves them.
Style helper outputCSSPropertiesplain recordplain record for [ngStyle]Scalar-only, same as Svelte.
component-render-error under SSRcaught (internal <Suspense> degrades gracefully under both legacy and streaming renderers)not caught — <svelte:boundary> doesn't run server-sidecaught, identically to CSRAngular has no separate server renderer — createComponent is the same call either way. See the root README's error-handling section.
component-render-error after a later re-rendercaught (standard class boundary)caught (standard <svelte:boundary>)not caught — creation-time onlyApplicationRef resolves ErrorHandler once from the root injector, so a per-node override can't catch a later change-detection throw. Documented gap; see the root README.

Not a divergence: the DOM around slot children. React renders them through a fragment, Svelte through no element, and Angular through structural directives — no adapter element in any of the three. Dispatch deliberately does not use components, because an Angular component always has a host element and no configuration removes it; display: contents would hide such a wrapper from layout but not from > .card, :nth-child(n), or the sibling combinators.

For the full getting-started walkthrough, merge-precedence rules, and design rationale, see the root README and AGENTS.md.


License

MIT. See the repository LICENSE and NOTICE for full attribution.

Runtime Experience parameters

By-ID fetchExperience accepts parameters, for example { featuredPosts: { limit: 6, order: '-fields.title' } }. Scalars are strings or numbers; Record leaves have one level. Order is one signed fields.<apiName> or sys.<id> path. Refetch with changed values, then replace the renderer's plan. Exports include ExperienceParameters, ExperienceParameterValue, ExperienceParameterRecord, and ExperienceParameterOrder. See the collection/refetch example.

版本与发布记录

@contentful/experiences-angular 共发布 34 个版本,首次发布于 2026/09/29;过去 12 个月发布了 34 个版本。

版本发布日期距今
0.8.9latest2026/11/074 天前
0.8.82026/11/055 天前
0.8.72026/11/055 天前
0.8.62026/11/056 天前
0.8.52026/11/046 天前
0.8.42026/10/3111 天前

关于 @contentful/experiences-angular 的常见问题

@contentful/experiences-angular 是什么?

Angular adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan

如何安装 @contentful/experiences-angular?

在项目目录运行 npm install @contentful/experiences-angular,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@angular/core(^20.0.0 || ^21.0.0 || ^22.0.0)、@angular/common(^20.0.0 || ^21.0.0 || ^22.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@contentful/experiences-angular 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@contentful/experiences-angular 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@contentful/experiences-angular 可以商用吗?

@contentful/experiences-angular 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@contentful/experiences-angular 的下载量有多少?

根据 npm 官方下载统计,@contentful/experiences-angular 在 2026/10/11 – 2026/11/09 这 30 天内被下载 4,194 次,日均约 140 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@contentful/experiences-angular 还在维护吗?

最新版本 0.8.9 发布于 2026/11/07(4 天前);至今共发布 34 个版本,其中过去 12 个月发布了 34 个;首次发布于 2026/09/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。