【ITニュース解説】🚀 Building a Dynamic CMS-Driven Angular App Using Optimizely & a Custom BFF Architecture
2025年09月16日に「Dev.to」が公開したITニュース「🚀 Building a Dynamic CMS-Driven Angular App Using Optimizely & a Custom BFF Architecture」について初心者にもわかりやすく解説しています。
ITニュース概要
Optimizely CMSとカスタムBFFを使い、Angularアプリで動的なWebサイトを構築する。CMSで管理されたコンテンツデータはBFFで整形され、Angularのカスタムライブラリが「レジストリ」を活用し、必要なコンポーネントを動的に読み込み表示する。SSRにも対応し、柔軟でパフォーマンスの良いシステムを実現する。
ITニュース解説
現代のWebサイトでは、多くの情報をユーザーに届けるため、コンテンツの量が非常に増えている。このような状況で、Webサイトの見た目(フロントエンド)と、その元となる情報(CMSデータ)が分離されて管理されている場合、それらを連携させて複雑なWebサイトを作るのはなかなか難しい課題だ。この記事では、この課題を解決するための堅牢なWebサイト構築の仕組みを紹介している。
この仕組みでは、AngularというWebアプリケーション開発のフレームワークを使い、Optimizelyというコンテンツ管理システム(CMS)のコンテンツグラフという機能から情報を取得する。さらに、ページの表示内容を柔軟に切り替えるための専用のAngularライブラリと、フロントエンド専用のバックエンドAPI(BFF: Back-End for Front-End)を組み合わせる。ここで注目すべきは、単にCMSからデータを受け取るだけでなく、フロントエンドのニーズに合わせてデータを整形してから返す、カスタムのバックエンドAPIを用意している点である。
ユーザーがCMSで管理されているページにアクセスした際の一連の流れを見てみよう。まず、ユーザーがWebサイト上のリンクをクリックすると、Angularアプリケーション内の特定のURLに移動する。すると、PageLayoutComponentという、ページの全体的な骨組みを表示するコンポーネントが起動する。このPageLayoutComponentは、私たちが作成したAngularライブラリのメソッドを呼び出す。このライブラリのメソッドは、カスタムのバックエンドAPIに問い合わせを行い、そのAPIがさらにOptimizelyのコンテンツグラフにアクセスして、必要なコンテンツ情報を取得する。
バックエンドAPIからの応答には、ページのタイトルやサブタイトルといった基本的な情報(ページメタデータ)や、そのページがどのような種類のページであるかを示す情報(ページタイプ)、そしてページ内に配置される各部品(ヒーローセクション、タブ、カードなど)の情報が含まれている。Angularライブラリは、これらの応答データをもとに、ページ全体が特定のページタイプとしてレンダリングされるべきか、それとも複数の部品を組み合わせて構成されるべきかを判断し、適切な方法でページを表示する。
このアーキテクチャの主要な構成要素はいくつかある。第一に、Optimizely CMSは、コンテンツ作成者がWebサイトのテキストや画像などのコンテンツを管理する場所である。第二に、カスタムバックエンドAPIは、Optimizelyのコンテンツグラフから生のデータを取得し、それをフロントエンドで使いやすい形に整形して返す役割を担う。第三に、Angularライブラリは、CMSから受け取ったページタイプや部品の情報に基づいて、どのAngularコンポーネントを表示すべきかを動的に決定し、レンダリングする。最後に、PageLayoutComponentは、CMSによって制御されるすべてのルート(URL)に対する主要な入り口となるコンポーネントである。
では、ユーザーが例えば「/leadership/john-doe」というURLにアクセスした場合に何が起こるか、具体的に追ってみよう。まず、Angularのルーティング機能によってPageLayoutComponentが読み込まれる。次に、PageLayoutComponent内でgetPageContent()というメソッドが現在のURLとともに呼び出される。このメソッドはバックエンドAPIにリクエストを送信し、APIは以下のような応答を返す。応答には、ページの全体像を定義する「Type」として「LeadershipDetailPage」が示され、ページを構成する部品のリストとして「PageComponents」配列が含まれている。この例では「HeroBlock」などの情報が含まれる。
その後、Angularライブラリがこの応答を受け取り、ページの登録情報(Page Registry)から「LeadershipDetailPage」に対応するAngularコンポーネントを特定し、それをレイアウトの表示領域に挿入する。もし応答データに複数の部品(ドロップコンポーネント)が含まれていれば、ライブラリは部品の登録情報(Component Registry)を使って、それぞれの部品に対応するAngularコンポーネントを、特別な指示(ディレクティブ)を通じて動的に挿入していく。このアプローチにより、ページ全体が特定のテンプレートによって作られるページも、CMS上で部品を組み合わせて作られるページも、たった一つの共通のレイアウトで柔軟に表示できるようになる。
Angularアプリケーションでは、すべてのCMS関連のパス(URL)がPageLayoutComponentにリダイレクトされるようにルーティングが設定されている。これは、app-routing.module.tsというファイルで、path: '**'(あらゆるパス)をPageLayoutComponentに紐づけることで実現される。PageLayoutComponentの初期化時に、getPageContent()メソッドが呼び出され、バックエンドAPIに現在のURLに対応するコンテンツを要求する。バックエンドからの応答には、表示すべきページ全体のコンポーネントを決定するためのPageTypeと、ページ内に配置される個別の部品(ヒーロー、カードなど)を示すpageComponentsが含まれる。
ページには大きく分けて二つの種類があることを理解する必要がある。一つは「ページプロパティからレンダリングされるページ」である。例えばLeadershipDetailPageのようなページは、そのページのCMSタイプに基づいて全体がレンダリングされる。これは、CMSのコンテンツエリアに特定の部品が配置されることはなく、ページに関するすべての詳細情報が、API応答のitems[]配列の中に直接含まれていることを意味する。もう一つは「ドロップコンポーネントを持つページ」である。例えばホームページ(StartPage)のように、CMSエディターで配置された様々なブロック(HeroBlock、EServiceBlockなど)によってページの表示内容が構成されるタイプである。バックエンドからの応答を見て、ページ全体が特定のコンポーネントで表示されるべきか、あるいは複数の部品が組み合わされて表示されるべきかを判断できるようになっている。
この動的な表示を実現するために、「コンポーネントレジストリ」というマッピングの仕組みが必要になる。これは、バックエンドから送られてくるデータの「Type」という情報と、それを表示すべきAngularコンポーネントを結びつける橋渡しのようなものだ。Angularアプリケーション内には、二つのレジストリ(登録簿)が作られる。一つは「ドロップコンポーネントレジストリ」で、API応答のpageComponents[]配列に含まれるType(例えばHeroBlock)に対して、どのAngularコンポーネント(例えばHeroComponent)をレンダリングすべきかを定義する。もう一つは「ページレジストリ」で、API応答のメインのitems[]配列に含まれるType(例えばNewsDetailPage)に対応するAngularコンポーネントを定義する。これらのレジストリは、アプリケーションのpage-layout.component.tsファイル内で定義され、ライブラリのメソッドを使ってシステムに登録される。これにより、Angularライブラリは、将来的にこれらのレジストリを参照して、適切なデータとコンポーネントを対応付け、ページを構築する。
実際に動的なレンダリングを行う方法も二通りある。まず、ページプロパティから直接レンダリングされるページの場合、@ViewChildデコレーターを使ってViewContainerRefという特別な場所を取得する。これは、Angularライブラリが動的にコンポーネントを挿入するための「容器」のようなものだと考えると良い。この容器が準備できたら、renderPages()というライブラリメソッドに、受け取ったデータ、そのデータのType、そしてこのViewContainerRefを渡す。renderPages()メソッドは、先に登録したページレジストリを参照し、Typeに一致するAngularコンポーネントをこの容器に注入し、バックエンドからの関連データをそのコンポーネントに提供する。
次に、ドロップコンポーネントのような部品をレンダリングする場合、カスタムディレクティブを使う。このディレクティブは、それ自体がViewContainerRefを公開している。コンポーネントの中で@ViewChildを使ってこのディレクティブへの参照を取得し、そのディレクティブが持つviewContainerRefとバックエンドデータをrenderComponents()というライブラリメソッドに渡す。ライブラリは、部品レジストリ(COMP_REGISTRY)を調べて、応答データに含まれるTypeに合致するAngularコンポーネントを見つけ、それを容器に挿入する。CMSからのデータは、@Input()という仕組みを使って、この挿入されたコンポーネントに渡される。このディレクティブを使ったアプローチにより、CMSの編集者がコンテンツブロックを自由に追加したり、削除したり、並べ替えたりしても、Angularアプリケーションは実行時にこれらの変更を自動的に反映できる。
さらに、このシステムを検索エンジンに優しくし、多くの人に見つけてもらえるようにするため、サーバーサイドレンダリング(SSR)という技術をAngular Universalというライブラリを使って導入している。通常、AngularはWebブラウザでコンテンツを生成するが、SSRを使うことで、Webサーバー側で事前にHTMLページを生成できるようになる。これにより、検索エンジンが動的なコンテンツも正しくインデックスできるようになり、ユーザーは最初のページ表示が非常に速く感じられ、操作可能になるまでの時間も短縮される。また、ページが見つからない場合に適切なエラーコード(404など)を返すことも可能になる。Angular UniversalはExpressというWebサーバーフレームワークをベースにしており、Angularコンポーネント内でHTTPレベルのロジックを注入できるようになっている。
Angular Universalと動的コンポーネントディレクティブを組み合わせることで、まさに最高のWeb体験を提供できるようになる。ユーザーが初めてWebサイトにアクセスした際、コンテンツは即座に読み込まれる。CMSで行われた変更はリアルタイムでフロントエンドに反映される。サーバーからは適切なHTTP応答が返され、検索エンジンはWebサイトのコンテンツを完全にクロールして理解できる。これにより、Angularで構築されたフロントエンドは、現代のCMSの要求に完全に適合しつつ、SEOにも強く、常に動的なコンテンツを提供できるものとなる。
この構築パターンが優れているのは、いくつかの理由がある。一つは「関心の分離」である。Angularは見た目の表示に専念し、APIはデータの整形と提供に専念する。次に「柔軟性」だ。コンテンツ編集者は、構造化されたページも、部品を組み合わせて作るページも自由に構築できる。そして「スケーラビリティ」。新しいコンポーネントを追加する必要がある場合でも、レジストリを更新するだけで対応できるため、システムの拡張が容易である。また「SSR対応」であり、Angular Universalと連携して動作する。さらに「再利用性」も高い。ドロップコンポーネントはモジュール化されており、必要なときにだけ読み込まれるため、効率的に再利用できる。これらの利点により、CMSの柔軟性、動的なレンダリング、システムの保守性、高いパフォーマンス、そしてSSRとの互換性が確保される。
結論として、Angularの動的な読み込み機能、スマートなレジストリベースのAngularライブラリ、カスタムのフロントエンド専用バックエンド層、そしてOptimizely CMSを組み合わせることで、非常にスケーラブルで保守が容易であり、CMSに対して高い柔軟性を持つフロントエンドアーキテクチャが実現できる。ページ全体としてレンダリングされるタイプであっても、動的なブロックを組み合わせるタイプであっても、このシステムはコンテンツが強力で柔軟、そして一貫性のある方法で提供されることを保証する。