Skip to content

Dynamic Routing in Angular

Set up a catch-all route in the Angular project to render new stories dynamically.

Create a src/app/routes/catch-all/catch-all.component.ts file to fetch all stories in the space.

src/app/routes/catch-all/catch-all.component.ts
import { Component, ChangeDetectionStrategy, inject, computed, OnInit, linkedSignal, input, } from '@angular/core';
import { type Story, type SbBlokData, StoryblokComponent, LivePreviewService } from '@storyblok/angular';
@Component({
selector: 'app-catch-all',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [StoryblokComponent],
template: `
<div class="container">
<sb-component [sbBlok]="storyContent()" />
@if (!storyContent()) {
<div class="not-found">
<h2>Page not found</h2>
<p>The requested page could not be found.</p>
</div>
}
</div>
`,
})
export class CatchAllComponent implements OnInit {
private readonly livePreview = inject(LivePreviewService);
// Story data from route resolver
readonly storyInput = input<Story | null>(null, { alias: 'story' });
// Writable signal that can be updated by live preview
readonly story = linkedSignal(() => this.storyInput());
readonly storyContent = computed(() => this.story()?.content as SbBlokData | undefined);
ngOnInit(): void {
// Enable live preview updates
this.livePreview.listen((updatedStory) => {
this.story.set(updatedStory);
});
}
}

The CatchAllComponent renders any Storyblok story dynamically and supports live preview updates. It receives story data from a route resolver via the storyInput . It uses linkedSignal() to create a writable copy of storyInput that the live preview can update. storyContent extracts the content property from story and passes it to StoryblokComponent to render dynamic content. It subscribes to LivePreviewService when the component initializes to receive real-time updates from the Visual Editor. If there is no story data available, it shows a “Page not found” message.

Update app.routes.ts file to include the route resolver that fetches the story data.

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' },
});
return data?.story;
},
},
},
];

The route file defines a wildcard route (**) that catches all URLs and lazy loads the CatchAllComponent. Before the component renders, a route resolver fetches the story data. The resolver extracts the slug from the URL. If the route is the root path /, it uses the home slug. The resolver fetches the story from the Storyblok API client using the slug and passes the story data to the CatchAllComponent as the story input.

With this approach, your project can automatically handle new stories you add to your space.


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.