This blog site is an Angular build time pre-rendered static site. I chose to develop this blog as a static site as I don't foresee I need dynamic contents, helps with Search Engine Optimization (SEO), and the greatest factor, I can just host this blog site with firebase for free :D
During development of this blog site with Angular SSR, I ran into a situation where the HTML returned from the dev server lacked blog content. Although the site was rendering on the server side, the actual content wasn’t visible in the HTML sent from the server; it was only populated on the client side. Here's how my initial setup looked:
blog.component.ts
:@Component({
selector: 'app-blog',
templateUrl: './blog.component.html',
styleUrl: './blog.component.scss',
})
export class BlogComponent implements OnInit, OnDestroy {
blog$ = new BehaviorSubject<Blog | undefined>(undefined);
id$: Observable<string>;
private subscriptions: Subscription[] = [];
constructor(
private readonly blogService: BlogService,
route: ActivatedRoute
) {
this.id$ = route.params.pipe(map((p) => p['id']));
}
ngOnInit(): void {
this.id$
.pipe(
mergeMap((id) => {
return this.blogService.getBlogById(id).pipe(
tap((blog) => {
this.blog$.next(blog);
})
);
})
)
.subscribe();
}
}
blog.component.html
:<h1 class="blog-title">{{ (blog$ | async)?.title }}</h1>
<div class="blog-content" [innerHTML]="(blog$ | async)?.content"></div>
Upon viewing the rendered HTML from the server, I saw this:
<h1 class="blog-title"><!--ngEnt--></h1><div class="blog-content"><!--ngEnt--></div>
The missing content meant that the async
pipe was not rendering on the server side. After investigating, I found this helpful article, which explained that ngOnInit
is rendered synchronously. The server completes rendering before the data is retrieved, causing the async pipe to return undefined
.
The solution to this issue is to leverage an Angular Resolver. Resolvers fetch the necessary data before Angular activates a route, allowing SSR to render the content directly without waiting for client-side data retrieval.
With the resolver implemented, the updated component code looks like this:
blog.component.ts
:@Component({
selector: 'app-blog',
templateUrl: './blog.component.html',
styleUrl: './blog.component.scss',
})
export class BlogComponent implements OnInit, OnDestroy {
blog$ = new BehaviorSubject<BlogDto | undefined>(undefined);
constructor(private readonly route: ActivatedRoute) {}
ngOnInit(): void {
this.subscriptions.push(
this.route.data.subscribe((data) => {
const blog: BlogDto = data['blog'];
this.blog$.next(blog);
})
);
}
}
The Blog Resolver ensures that the blog content is loaded before the component is activated. Here’s the resolver code:
blog-resolver.service.ts
:export class BlogResolverService implements Resolve<BlogDto | undefined> {
constructor(private readonly blogService: BlogService) {}
resolve(
route: ActivatedRouteSnapshot,
_: RouterStateSnapshot
): MaybeAsync<BlogDto | undefined> {
const blogId = route.params['id'];
return this.blogService.getBlogById(blogId);
}
}
To apply the resolver to your routes, add it to your Angular router configuration. Here’s an example:
const routes: Routes = [
{
path: 'blog/:id',
component: BlogComponent,
resolve: { blog: BlogResolverService },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class BlogRoutingModule {}
Using Angular Resolver is an effective way to make sure that your SSR-rendered content is loaded and ready to be displayed. This not only improves the user experience but also ensures search engines can index your pre-rendered content. When implemented correctly, a resolver minimizes the need for post-rendering data fetching, making your application more efficient and SEO-friendly.
For further reading, check out the official Angular documentation on resolvers to deepen your understanding.