Skip to content

Integrate Angular with Storyblok

Use Storyblok to manage the content of your Angular application.

The fastest way to get started is using Storyblok’s Core Blueprint or CLI wizard:

Use Storyblok’s Core Blueprint to launch an Angular project, complete with a space, GitHub repository, and deployment environment.

Log in to Storyblok, select + Add SpaceStart from Blueprint, and follow the steps.

Alternatively, set up the project from scratch and configure your frontend code.

This guide has been tested with the following package versions:
  • @angular/core@21.2.0
  • @storyblok/angular
  • Node.js v22.13.0
  1. Create a new space

    Log in to Storyblok, select + Add SpaceNew Space, and follow the steps.

  2. Create an Angular project

    Install the Angular CLI, then create a new project with SSR support following the official installation guide.

    Terminal window
    npm install -g @angular/cli
    Terminal window
    ng new my-storyblok-app --ssr
    cd my-storyblok-app
  3. Install @storyblok/angular

    In the terminal, install the package.

    Terminal window
    npm install @storyblok/angular

Next, to use environment variables in your Angular app, generate environments files with the Angular CLI.

Terminal window
ng generate environments

Add the Storyblok access token to the environment files in the src/environments folder. Set production to false in environment.ts file and to true in environment.production.ts file.

src/environments/environment.ts
export const environment = {
production: true,
accessToken: "YOUR-ACCESS-TOKEN",
};
src/environments/environment.development.ts
export const environment = {
production: false,
accessToken: "YOUR-ACCESS-TOKEN",
};

Update the app.config.ts file to configure the Storyblok Angular package.

src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { environment } from '../environments/environment';
import { provideStoryblok } from '@storyblok/angular';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideStoryblok({
accessToken: environment.accessToken,
region: 'eu',
}),
],
};

The Storyblok Angular package provides features such as fetching content from the Content Delivery API, component registration, and real-time visual editing available across your project.

Create a new file src/app/routes/home.component.ts and add the following code.

src/app/routes/home.component.ts
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from "@angular/core";
import { StoryblokService, StoryblokComponent, type SbBlokData } from "@storyblok/angular";
@Component({
selector: "app-home",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [StoryblokComponent],
template: `
<div>
<sb-component [sbBlok]="storyContent()" />
</div>
`,
})
export class HomeComponent implements OnInit {
private readonly storyblok = inject(StoryblokService);
readonly storyContent = signal<SbBlokData | null>(null);
async ngOnInit() {
const client = this.storyblok.getClient();
const { data } = await client.stories.get("home", {
query: { version: "draft" },
});
this.storyContent.set(data?.story?.content ?? null);
}
}

StoryblokService provides access to the Storyblok API client to fetch story data. StoryblokComponent dynamically renders content type and nestable blocks. In this case, it looks for the content type block of the home story.

Create page.component.ts component to render all stories of the page content type, such as the home story.

src/app/components/page/page.component.ts
import { Component, ChangeDetectionStrategy, input } from "@angular/core";
import { type SbBlokData, StoryblokComponent } from "@storyblok/angular";
export interface PageBlok extends SbBlokData {
body?: SbBlokData[];
}
@Component({
selector: "app-page",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [StoryblokComponent],
template: `<sb-component class="page" [sbBlok]="blok().body" />`,
})
export class PageComponent {
// Angular signal
readonly blok = input.required<PageBlok>();
}

The PageComponent passes the array of blocks in the body block field to the StoryblokComponent, which dynamically renders each block. It uses Angular signal for its blok input, ensuring the template updates automatically when the input changes.

Stories can have a body field that contains an array of custom type blocks, such as Feature, Teaser, and Grid.

Create the code for these components as follows.

src/app/components/feature/feature.component.ts
import { Component, ChangeDetectionStrategy, input } from "@angular/core";
import { type SbBlokData } from "@storyblok/angular";
export interface FeatureBlok extends SbBlokData {
name?: string;
description?: string;
}
@Component({
selector: "app-feature",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="feature">
<h3>{{ blok().name }}</h3>
<p>{{ blok().description }}</p>
</div>
`,
})
export class FeatureComponent {
readonly blok = input.required<FeatureBlok>();
}
src/app/components/teaser/teaser.component.ts
import { Component, ChangeDetectionStrategy, input } from "@angular/core";
import { type SbBlokData } from "@storyblok/angular";
export interface TeaserBlok extends SbBlokData {
headline?: string;
}
@Component({
selector: "app-teaser",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="teaser">
<h1>{{ blok().headline }}</h1>
</div>
`,
})
export class TeaserComponent {
readonly blok = input.required<TeaserBlok>();
}
src/app/components/grid/grid.component.ts
import { Component, ChangeDetectionStrategy, input } from "@angular/core";
import { StoryblokComponent, type SbBlokData } from "@storyblok/angular";
export interface GridBlok extends SbBlokData {
columns?: SbBlokData[];
}
@Component({
selector: "app-grid",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [StoryblokComponent],
template: `<sb-component class="grid" [sbBlok]="blok().columns" />`,
})
export class GridComponent {
readonly blok = input.required<GridBlok>();
}

Similar to page.component.ts, grid.component.ts passes the array of blocks in the columns block field to the StoryblokComponent.

Create a component registry for Storyblok components in storyblok.components.ts and use lazy loading to reduce the initial bundle size.

src/app/storyblok.components.ts
import { type StoryblokComponentsMap } from "@storyblok/angular";
export const storyblokComponents: StoryblokComponentsMap = {
page: () => import("./components/page/page.component").then((m) => m.PageComponent),
teaser: () => import("./components/teaser/teaser.component").then((m) => m.TeaserComponent),
grid: () => import("./components/grid/grid.component").then((m) => m.GridComponent),
feature: () => import("./components/feature/feature.component").then((m) => m.FeatureComponent),
};

Update app.config.ts to register Storyblok components. Pass the storyblokComponents registry to the withStoryblokComponents provider.

By default, Angular does not bind route data to component inputs. Add withComponentInputBinding() to the app.config.ts file to allow resolved data to be passed directly as component inputs. withComponentInputBinding() automatically binds route resolver data to matching component inputs.

src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { environment } from '../environments/environment';
import { provideStoryblok, withStoryblokComponents } from '@storyblok/angular';
import { provideStoryblok } from '@storyblok/angular';
import { storyblokComponents } from './storyblok.components'
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withComponentInputBinding()),
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideStoryblok(
{
accessToken: environment.accessToken,
region: 'eu',
},
withStoryblokComponents(storyblokComponents),
),
],
};

Update src/app/app.routes.ts file to define the routes, so that the root path / renders the HomeComponent.

src/app/app.routes.ts
import { Routes } from "@angular/router";
import { HomeComponent } from "./routes/home.component";
export const routes: Routes = [{ path: "", component: HomeComponent }];

Finally, remove the contents in src/app/app.html and add the router-outlet. The router uses it to render the component that matches the current URL based on the defined routes.

src/app/app.html
<router-outlet></router-outlet>

Run the server and visit the site in your browser.

Terminal window
npm start

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.