AngularでJamstackなブログサイトをつくる
これはAngularアドベントカレンダー 6日目の記事です。
はじめに
Angularがv19アップデートでJamstackなサイトを構築しやすくなったので、試しにブログサイトをつくってみました。ヘッドレスCMSにmicroCMS、ホスティングサービスにVercelを使いました。
完成したもの
記事の一覧ページと詳細ページのみ。今回styleは設定しません。
microCMSの準備
事前にAPIを構築して、いくつか記事を追加しておきます。
また、エンドポイントURLとX-MICROCMS-API-KEY
をメモっておきます。
実装
バージョン
% ng version (git)-[main]
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 19.0.2
Node: 20.17.0
Package Manager: npm 10.8.2
OS: darwin arm64
Angular: 19.0.0
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, platform-server
... router
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1900.1
@angular-devkit/build-angular 19.0.2
@angular-devkit/core 19.0.1
@angular-devkit/schematics 19.0.1
@angular/cli 19.0.2
@angular/ssr 19.0.2
@schematics/angular 19.0.1
rxjs 7.8.1
typescript 5.6.3
zone.js 0.15.0
プロジェクト作成
v19で追加されたハイブリッドレンダリングを有効化して、新しいプロジェクトを作成します。
ng new ng19-ssr-hybrid --ssr --server-routing
サービスの実装
BlogService
を実装します。microCMSのAPIを呼び出します。
- fetchBlogs:記事一覧を取得する
- fetchBlog:記事詳細を取得する
- fetchBlogIds:記事ID一覧を取得する
interface
は適当に必要なものだけを。
blog.service.ts
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
export interface Blog {
id: string;
title: string;
content: string;
}
export interface MicroCMSGetBlogsResponse {
contents: Blog[];
}
@Injectable({
providedIn: 'root',
})
export class BlogService {
private readonly httpClient = inject(HttpClient);
private readonly microCMSEndpoint = 'https://**********.microcms.io/api/v1'; // microCMSのエンドポイント
private readonly microCMSApiKey = '**********'; // microCMSのAPI-KEY
private readonly headers = new HttpHeaders({
'X-MICROCMS-API-KEY': this.microCMSApiKey,
});
fetchBlogs(): Observable<MicroCMSGetBlogsResponse> {
return this.httpClient.get<MicroCMSGetBlogsResponse>(
`${this.microCMSEndpoint}/blogs`,
{
headers: this.headers,
}
);
}
fetchBlog(id: string): Observable<Blog> {
return this.httpClient.get<Blog>(`${this.microCMSEndpoint}/blogs/${id}`, {
headers: this.headers,
});
}
fetchBlogIds(): Observable<string[]> {
return this.fetchBlogs().pipe(
map((response) => response.contents.map((blog) => blog.id))
);
}
}
記事一覧の実装
BlogPageComponent
を実装します。
v19で追加されたresource API
を使うと、ローディングやエラーをシンプルに記述できます。
blog-page.component.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
import { BlogService } from '../../services/blog.service';
@Component({
selector: 'app-blog-page',
imports: [RouterLink],
templateUrl: './blog-page.component.html',
styleUrl: './blog-page.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlogPageComponent {
private readonly blogService = inject(BlogService);
blogsResource = rxResource({
loader: () => this.blogService.fetchBlogs(),
});
}
blog-page.component.html
<h2>BLOG</h2>
@if(blogsResource.value(); as blogs) {
@for(blog of blogs.contents; track blog.id) {
<div>
<a [routerLink]="['/blog', blog.id]">{{ blog.title }}</a>
</div>
}
} @else if (blogsResource.error()) {
<div>Load failed</div>
} @else {
<div>Loading...</div>
}
記事詳細の実装
PostPageComponent
を実装します。
APIレスポンスをそのままinnerHTML
にバインドすると警告が出るので、DomSanitizer.bypassSecurityTrustHtml
でサニタイズします。
post-page.component.ts
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { DomSanitizer } from '@angular/platform-browser';
import { map } from 'rxjs';
import { BlogService } from '../../services/blog.service';
@Component({
selector: 'app-post-page',
templateUrl: './post-page.component.html',
styleUrl: './post-page.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PostPageComponent {
private readonly blogService = inject(BlogService);
private readonly sanitizer = inject(DomSanitizer);
id = input.required<string>();
postResource = rxResource({
request: () => this.id(),
loader: ({ request: id }) =>
this.blogService.fetchBlog(id).pipe(
map((blog) => ({
...blog,
content: this.sanitizer.bypassSecurityTrustHtml(blog.content),
}))
),
});
}
post-page.component.html
@if(postResource.value(); as post) {
<h3>{{ post.title }}</h3>
<article [innerHTML]="post.content"></article>
} @else if (postResource.error()) {
<div>Load failed</div>
} @else {
<div>Loading...</div>
}
ルーティングの設定
app.routes.ts
を実装します。
app.routes.ts
import { Routes } from '@angular/router';
import { BlogPageComponent } from './pages/blog-page/blog-page.component';
import { PostPageComponent } from './pages/post-page/post-page.component';
export const routes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: '/blog',
},
{
path: 'blog',
children: [
{
path: '',
component: BlogPageComponent,
},
{
path: ':id',
component: PostPageComponent,
},
],
},
];
サーバールーティングの実装
ハイブリッドレンダリングで追加されたapp.routes.server.ts
を実装します。
app.routes.ts
に設定された各Routeに対するRenderMode
を設定するファイルです。今回はJamstackなので全てRenderMode.Prerender
を設定します。
さらに、記事詳細画面(blog/:id
)にgetPrerenderPrams
関数を設定します。この関数で記事ID一覧を返すことによって、各記事詳細画面をプリレンダリングすることができます。
app.routes.server.ts
import { inject } from '@angular/core';
import { RenderMode, ServerRoute } from '@angular/ssr';
import { firstValueFrom } from 'rxjs';
import { BlogService } from './services/blog.service';
export const serverRoutes: ServerRoute[] = [
{
path: 'blog',
renderMode: RenderMode.Prerender,
},
{
path: 'blog/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const blogService = inject(BlogService);
const ids = await firstValueFrom(blogService.fetchBlogIds());
return ids.map((id) => ({ id }));
},
},
{
path: '**',
renderMode: RenderMode.Prerender,
},
];
ビルド
ng build
すると各記事IDごとにindex.html
が生成されていることが確認できます。
% tree dist/ng19-ssr-hybrid/browser
dist/ng19-ssr-hybrid/browser
├── blog
│ ├── 4q-rm66b576v
│ │ └── index.html
│ ├── index.html
│ ├── kukv_0rvfqf
│ │ └── index.html
│ └── zbptd_3j6i
│ └── index.html
├── favicon.ico
├── index.csr.html
├── main-4YZ5U62D.js
├── polyfills-SC4UBBZS.js
└── styles-5INURTSO.css
5 directories, 9 files
Vercelにデプロイする
ホスティングするだけならGitHub PagesやFirebase Hostingで十分ですが、今後SSRを追加する可能性を考慮してVercelにデプロイします。
Add New Project
から実装したAngularのGitHubリポジトリを選択して、Framework Preset
でAngular
を選択するだけです。v17のときに必要だったvercel.json
は不要です。
まとめ
これまではScullyやAnalogなどのMeta Frameworkがないと難しかったSSR/SSGやJamstackが、Angular単体で構築できる時代がやってきました。
今回の構成(Angular+microCMS+Vercel)は無料なので、ぜひ自分のブログサイトをつくってみてはいかがでしょうか?
参考
Discussion