Content Modeling in Angular
Learn how to handle different nestable and content type blocks, render rich text, and use story references to manage content globally.
In the existing space, create the following blocks:
- An
articlecontent type block with the following fields:title: Textcontent: Rich text
- An
article-overviewcontent type block with the following field:title: Text
- A
featured-articlesnestable block with the following field:articles: References
Next, create an Articles folder, open it, and create the following stories:
- A few stories that use the
articlecontent type. - An article overview story with a
article-overviewcontent type. Select the option Define as root for the folder.
Finally, add the featured-articles block to the body field of the Home story, and select articles to feature.
Fetch and list all articles
Section titled “Fetch and list all articles”Create a new src/app/services/article.service.ts file to fetch articles from the Storyblok API client.
import { inject, Injectable } from '@angular/core';import { StoryblokService, type Story } from '@storyblok/angular';
@Injectable({ providedIn: 'root' })export class ArticleService { private readonly storyblok = inject(StoryblokService);
async getArticles(): Promise<Story[]> { const client = this.storyblok.getClient(); const { data } = await client.stories.list({ query: { version: 'draft', starts_with: 'articles/', content_type: 'article' }, }); return (data?.stories as Story[]) ?? []; }}The client.stories.list() fetches multiple stories. The starts_with parameter fetches only stories from the “Articles” folder. The content_type parameter restricts the results to stories with the article content type.
Create a new src/app/components/article-overview/article-overview.component.ts component.
import { Component, ChangeDetectionStrategy, inject, OnInit, signal, input } from '@angular/core';import { RouterLink } from '@angular/router';import { type SbBlokData, type Story } from '@storyblok/angular';import { ArticleService } from '../../services/article.service';
export interface ArticleOverviewComponentBlok extends SbBlokData { title?: string;}
@Component({ selector: 'app-article-overview', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouterLink], template: ` <div> <h1>{{ blok().title }}</h1> @for (article of articles(); track article.uuid) { <div> <a [routerLink]="'/' + article.full_slug"> {{ article.content['title'] }} </a> </div> } </div> `,})export class ArticleOverviewComponent implements OnInit { readonly blok = input.required<ArticleOverviewComponentBlok>(); private readonly articleService = inject(ArticleService); readonly articles = signal<Story[]>([]);
async ngOnInit() { this.articles.set(await this.articleService.getArticles()); }}This component calls articleService.getArticles() to fetch all stories with content type article.
Create the article block
Section titled “Create the article block”Add a new src/app/components/article/article.component.ts component to render the new article content type.
import { Component, ChangeDetectionStrategy, input } from '@angular/core';import { type SbBlokData } from '@storyblok/angular';import { SbRichTextComponent, type StoryblokRichTextJson } from '@storyblok/angular';
export interface ArticleBlok extends SbBlokData { title?: string; content?: StoryblokRichTextJson}
@Component({ selector: 'app-article', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SbRichTextComponent], template: ` <div> <h2>{{ blok().title }}</h2> <sb-rich-text [sbDocument]="blok().content" /> </div> `,})export class ArticleComponent { readonly blok = input.required<ArticleBlok>();}To render rich text fields, use the SbRichTextComponent provided by the @storyblok/angular module. StoryblokRichTextJson defines the type for rich text nodes in Storyblok.
Register the article block and the article-overview block in the storyblok.components.ts file.
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), 'article-overview': () => import('./components/article-overview/article-overview.component').then((m) => m.ArticleOverviewComponent), article: () => import('./components/article/article.component').then((m) => m.ArticleComponent),};Now the articles in the “Articles” folder should render the content.
Handle referenced stories
Section titled “Handle referenced stories”In the app.routes.ts file, set the resolve_relations parameter to get the full object response of referenced stories.
import { inject } from '@angular/core';import { ActivatedRouteSnapshot, Routes } from '@angular/router';import { StoryblokService } from '@storyblok/angular';
export const routes: Routes = [ { path: '**', loadComponent: () => import('./routes/catch-all/catch-all.component').then((m) => m.CatchAllComponent), resolve: { story: async (route: ActivatedRouteSnapshot) => { const slug = route.url.map((s) => s.path).join('/') || 'home'; const client = inject(StoryblokService).getClient(); const { data } = await client.stories.get(slug, { query: { version: 'draft', resolve_relations: 'featured-articles.articles', }, }); return data?.story; }, }, },];Add inlineRelations: true to the Storyblok config in app.config.ts file to replace story UUIDs with resolved story objects automatically. To enable live preview for relations, pass resolveRelations option to the withLivePreview provider.
...export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes, withComponentInputBinding()), provideStoryblok( { accessToken: environment.accessToken, region: 'eu', // 'eu', 'us', 'ap', 'ca', or 'cn' inlineRelations: true, }, withStoryblokComponents(storyblokComponents), // Only required when using the live preview functionality withLivePreview({ resolveRelations: ['featured-articles.articles'], }), ), ],};Next, create a new src/app/components/featured-articles/featured-articles.component.ts component.
import { Component, ChangeDetectionStrategy, computed, input } from '@angular/core';import { RouterLink } from '@angular/router';import { type SbBlokData, type Story } from '@storyblok/angular';
export interface FeaturedArticlesComponentBlok extends SbBlokData { articles?: Story[];}
@Component({ selector: 'app-featured-articles', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouterLink], template: ` <div> <h1>Featured Articles</h1>
@for (article of articles(); track article.uuid) { <div> <a [routerLink]="'/' + article.full_slug"> {{ article.content['title'] }} </a> </div> } </div> `,})export class FeaturedArticlesComponent { readonly blok = input.required<FeaturedArticlesComponentBlok>(); readonly articles = computed(() => (this.blok().articles ?? []) as Story[]);}The FeaturedArticlesComponent renders each article title as a link. It uses an Angular computed signal to track the articles array and update the template automatically when the input changes.
Register this block in the storyblok.component.ts file.
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), 'article-overview': () => import('./components/article-overview/article-overview.component').then((m) => m.ArticleOverviewComponent), article: () => import('./components/article/article.component').then((m) => m.ArticleComponent), 'featured-articles': () => import('./components/featured-articles/featured-articles.component').then((m) => m.FeaturedArticlesComponent),};Now, this component will render links to the featured articles in the home page of the project.
Related 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