Integrate Angular with Storyblok
Use Storyblok to manage the content of your Angular application.
In the terminal, install the Angular CLI.
npm install -g @angular/cliCreate a new Angular project with SSR support, following the official installation page.
ng new my-storyblok-app --ssrcd my-storyblok-appCreate a new blank space (opens in a new window) to follow the tutorial from scratch, or start from the core blueprint.
Installation
Section titled “Installation”In the terminal, install the @storyblok/angular package.
npm install @storyblok/angularTo use environment variables in your Angular app, generate environments files with the Angular CLI.
ng generate environmentsAdd 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.
export const environment = { production: true, accessToken: 'YOUR-ACCESS-TOKEN'};export const environment = { production: false, accessToken: 'YOUR-ACCESS-TOKEN'};Update the app.config.ts file to configure the Storyblok Angular package.
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.
Fetch a single story
Section titled “Fetch a single story”Create a new file src/app/routes/home.component.ts and add the following code.
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 and register blocks
Section titled “Create and register blocks”Create page.component.ts component to render all stories of the page content type, such as the home story.
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: ` <div class="page"> <sb-component [sbBlok]="blok().body" /> </div> `,})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 might contain a body or similar field that consists of an array with several blocks of custom types (for example, Feature, Teaser, Grid) in it.
Create the code for these components as follows.
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>();}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>();}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.
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.
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.
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.
<router-outlet></router-outlet>Run the server and visit the site in your browser.
npm startRelated Resources
Section titled “Related Resources”Was this page helpful?
This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window) . Terms of Service (opens in a new window) apply.
Get in touch with the Storyblok community