Skip to content

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 article content type block with the following fields:
    • title: Text
    • content: Rich text
  • An article-overview content type block with the following field:
    • title: Text
  • A featured-articles nestable 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 article content type.
  • An article overview story with a article-overview content 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.

Create a new src/app/services/article.service.ts file to fetch articles from the Storyblok API client.

src/app/services/article.service.ts
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.

src/app/components/article-overview/article-overview.component.ts
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.

Add a new src/app/components/article/article.component.ts component to render the new article content type.

src/app/components/article/article.component.ts
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.

src/app/storyblok.component.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),
'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.

In the app.routes.ts file, set the resolve_relations parameter to get the full object response of referenced stories.

src/app/app.routes.ts
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.

src/app/app.config.ts
...
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.

src/app/components/featured-articles/featured-articles.component.ts
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.

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),
'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.


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.