In the era of content-rich web experiences, managing complex frontend logic with decoupled CMS data can be challenging.
In this post, we’ll explore a robust Back-End for Front-End (BFF) pattern using:
✅ Angular
✅ Optimizely Content Graph
✅ A custom Angular library for dynamic component rendering
⚠️Note: We've built custom backend APIs that interact with the Optimizely Content Graph and return frontend-friendly responses.
🌍 How It Works – End-to-End Flow
When a user visits any CMS page:
- ✅ The user navigates to a route in the Angular app.
- 📦 PageLayoutComponent is triggered — the shell component for rendering.
- 🔌 It calls a method from our Angular library, which:
- Hits our custom backend API.
- That in turn queries the Content Graph.
- 📄 The API response includes:
- Page metadata (title, subtitle, etc.)
- Page Type
- Drop components (Hero, Tabs, Cards, etc.)
- 🧠 The library:
- Determines if the page is rendered entirely via its type
- Or composed from drop components
🧱 Architecture at a Glance
⚙️ Key Components
- Optimizely CMS: Content authors manage content.
- Custom Backend APIs: Fetch from Content Graph and shape data.
- Angular Library: Dynamically renders components based on CMS type.
- PageLayoutComponent: The main entry point for CMS-driven routes.
🛍️ What Happens When a User Visits a Page?
Let’s walk through the interaction step-by-step:
- 🧑💻 User navigates to /leadership/john-doe
- ⚙️ Angular loads **PageLayoutComponent **via routing
- 🔄 getPageContent() is called with the current URL
- 🖙 Backend returns:
{
"Type": "LeadershipDetailPage",
"PageComponents": [
{ "Type": "HeroBlock", ... }
]
}
- 🧠 The Angular library:
- Loads LeadershipDetailPage from the Page Registry
- Injects it into the layout container
- If drop components exist:
- Injects them using the Component Registry via a directive.
✅ This approach supports both template-driven pages and CMS-composed pages using a single layout.
🔄 What Happens When a Page Loads
- In my Angular app, the router is configured to redirect all CMS paths or routes to the PageLayoutComponent, as shown in the routing setup below:
In app-routing.module.ts:
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: '**', component: PageLayoutComponent } // nested catch-all
]
},
{ path: '**', redirectTo: '' }, // global catch-all
];
- It calls:
getPageContent(baseURL, router.url) --> This library method calls the backend and retrieves the page response for the specified route.
In page-layout.component.ts:
ngOnInit(){
const baseURL = typeof window !== 'undefined' ? (window as any).env?.apiUrl : '';
this.dynamicLoader.getPageContent(baseURL,this.router.url).subscribe({
next: (data: any) => {
if (data) {
console.log('data',data);
}
}
});
}
- The backend returns:
- PageType: to determine which full-page component to load
- pageComponents: drop components like Hero, Cards, etc.
The response is shown below:
{
"data": {
"Content": {
"items": [
{
"Type": "StartPage",
"LastModifiedDate": "2025-06-03T09:44:34Z",
"breadcrumbs": {
"Content": {
"items": []
}
},
"PageModifiedDateTokenText": "Last Modified Date: {0} Saudi Arabia Time"
"BreadcrumbTitle": "Home",
"PageComponents": [
{
"ContentLink": {
"Expanded": {
"Type": "HeroBlock",
"OverHeadingText": "Hero",
"Heading": "Hero",
"Description": "Seamless tourism services, available"
}
}
},
{
"ContentLink": {
"Expanded": {
"Type": "EServiceBlock",
"OverHeadingText": "Smart Tourism Services",
"Heading": "E-services",
"Description": "Seamless tourism services, available anytime, anywhere. Access permits, licenses, and essential development tools instantly through our advanced digital platform—designed to streamline and elevate your tourism experience.",
"EServiceCTAUrl": {
"Title": "View all",
"Name": "View all",
"Target": "_self",
"Url": "/en/e-services"
},
"DetailCTAText": "Details",
"StartServiceCTAText": "Start Service"
}
}
}
]
}
]
}
}
}
🧠 Understanding Page Types
1️⃣ Pages Rendered from Page Properties
Some pages, such as LeadershipDetailPage, are rendered entirely based on their CMS type. This means that when a user visits the URL, these pages don’t include any drop components in the CMS content area means in the response their is no pageComponents[] array. Instead, all of their details are contained within the items[] array.
Response looks like:
{
"data": {
"Content": {
"items": [
{
"Type": "LeadershipDetailPage",
"LastModifiedDate": "2025-05-27T06:18:14Z",
"PageModifiedDateTokenText": "Last Modified Date: {0} Saudi Arabia Time",
"PageTitle": "About Tourism",
"SubTitle": "",
"PageShortDescription": ""
},
"HideSpotlight": false,
"HideBreadcrumb": false,
"HideShareButton": true,
"HidePrintButton": true,
"PageImage": {
"Expanded": {
"Url": "https://cms-url/siteassets/about-us/leadership/leaadership_banner.jpg?code=49b3af",
"AltText": "",
"ProportionsImages": [
{
"XXLImage": "https://cms-url/siteassets/about-us/leadership/leaadership_banner.jpg?code=49b3af&width=1400&height=408&rxy=0.5,0.5",
"XLImage": "https://cms-url/siteassets/about-us/leadership/leaadership_banner.jpg?code=49b3af&width=1200&height=350&rxy=0.5,0.5",
"LGImage": "https://cms-url/siteassets/about-us/leadership/leaadership_banner.jpg?code=49b3af&width=992&height=289&rxy=0.5,0.5",
"MDImage": "https://cms-url/siteassets/about-us/leadership/leaadership_banner.jpg?code=49b3af&width=768&height=224&rxy=0.5,0.5",
"SMImage": "https://cms-url/siteassets/about-us/leadership/leaadership_banner.jpg?code=49b3af&width=576&height=168&rxy=0.5,0.5"
}
]
}
},
"HideViewBioLink": false,
"LeadershipTitle": "Her Highness Princess Haifa Bint Mohammad Al Saud",
"LeadershipSubTitle": "Vice Minister of Tourism",
"SocialLinkData": [
{
"Name": "Linked in",
"IconCss": "hgi-stroke hgi-linkedin-02",
"Url": "https://linkedin.com/"
},
{
"Name": "X",
"IconCss": "hgi-stroke hgi-new-twitter",
"Url": "https://x.com/"
},
{
"Name": "Facebook",
"IconCss": "hgi-stroke hgi-facebook-02",
"Url": "https://facebook.com/"
},
{
"Name": "Youtube",
"IconCss": "hgi-stroke hgi-youtube",
"Url": "https://youtube.com/"
}
],
"LeadershipPageImage": {
"Expanded": {
"Url": "https://cms-url/contentassets/91bc22483f704dc6a2e244d247a44a0a/haifa-bint-mohammad-al-saud.jpg?code=49bcf1",
"AltText": "",
"ProportionsImages": [
{
"XXLImage": "https://cms-url/contentassets/91bc22483f704dc6a2e244d247a44a0a/haifa-bint-mohammad-al-saud.jpg?code=49bcf1&width=1400&height=408&rxy=0.55,0.46",
"XLImage": "https://cms-url/contentassets/91bc22483f704dc6a2e244d247a44a0a/haifa-bint-mohammad-al-saud.jpg?code=49bcf1&width=1200&height=350&rxy=0.55,0.46",
"LGImage": "https://cms-url/contentassets/91bc22483f704dc6a2e244d247a44a0a/haifa-bint-mohammad-al-saud.jpg?code=49bcf1&width=992&height=289&rxy=0.55,0.46",
"MDImage": "https://cms-url/contentassets/91bc22483f704dc6a2e244d247a44a0a/haifa-bint-mohammad-al-saud.jpg?code=49bcf1&width=768&height=224&rxy=0.55,0.46",
"SMImage": "https://cms-url/contentassets/91bc22483f704dc6a2e244d247a44a0a/haifa-bint-mohammad-al-saud.jpg?code=49bcf1&width=576&height=168&rxy=0.55,0.46"
}
]
}
}
}
]
}
}
}
2️⃣ Pages with Drop Components
Other pages, such as Home or StartPage, rely on dropped blocks in the CMS editor. This means the page details are defined through specific blocks in the CMS, which are then rendered as different components on the frontend:
- HeroBlock
- EServiceBlock
From the backend response, you can determine that there are two types of pages. The first type loads directly, meaning the page is fully dependent on its page properties, and the response should be passed to that specific component. The second type includes dropped components, where each part of the response is mapped to its corresponding Angular component—for example, a HeroBlock response maps to the HeroComponent, and an EServiceBlock response maps to the EServiceComponent.
The response here is the same as the one shown above under heading "What Happens When a Page Loads" point no. 3.
📦 Component Registries (The Bridge)
To implement this technique, you need a mapping mechanism that links the Type coming in the response to the component that should be rendered.
Next, we’ll create two registries in our Angular app to define which Angular component should be loaded based on the Type received in the response. The first registry will handle pages that are rendered entirely from page properties, while the second will handle pages composed of dropped components(pageComponents[] array in response).
🔹 Drop Component Registry
This registry ensures that the correct Angular component is rendered for each Type found in the pageComponents[] array (dropped components from the CMS).
In page-layout.component.ts [above ngOnInit()]:
export const COMP_REGISTRY = {
HeroBlock: () => import('../../DropComponents/hero/hero.component')
.then(m => m.HeroComponent),
EServiceBlock: () => import('../../DropComponents/eservice/eservice.component')
.then(m => m.EServiceComponent),
};
🔹 Page Registry (Pages directly load from properties)
This registry ensures that the correct Angular component is rendered for Type found in the items[] array in the main response.
In page-layout.component.ts [above ngOnInit()]:
export const PAGE_REGISTRY = {
...COMP_REGISTRY, // reuse block components
NewsDetailPage: () => import('../news-details/news-details.component')
.then(m => m.NewsDetailsComponent),
};
After retrieving the response from getPageContent(), we register the two registries in the library using the methods shown below.
//Properties render page registry mapping in library
this.dynamicLoader.setPageRegistry(PAGE_REGISTRY);
//Drop component registry mapping in library
this.dynamicLoader.setComponentRegistry(COMP_REGISTRY);
✅ The library exposes setPageRegistry() and setComponentRegistry() so the main app provides mappings.
In the future, our Angular library directive will use these registries to pass data to the appropriate components—whether it’s a direct page which renders from properties or dropped components within a page.
🧹 Dynamic Rendering Based on ViewContainerRef
For pages rendered directly from page properties, use a @ViewChild decorator to access the ViewContainerRef. This allows the Angular library to dynamically inject the appropriate components into it.
Once the container is initialized, we pass the data, its Type, and the containerRef to the library method renderPages(data.Type, data, this.dynamicContainer). The renderPages(type, response, containerRef) method then looks up the registry (PAGE_REGISTRY) to match the Type, injects the corresponding Angular component into the container, and provides it with the relevant data from the backend response.
In page-layout.component.ts
@ViewChild('dynamicContainer', { read: ViewContainerRef, static: true })
dynamicContainer!: ViewContainerRef;
ngOnInit(){
const baseURL = typeof window !== 'undefined' ? (window as any).env?.apiUrl : '';
this.dynamicLoader.getPageContent(baseURL,this.router.url).subscribe({
next: (data: any) => {
if (data) {
// Render a pages dynamically which is depend on page properties
this.dynamicLoader.renderPages(data.Type, data, this.dynamicContainer);
}
}
});
}
In page-layout.component.html
<ng-container #dynamicContainer></ng-container>
🧹 Directive-Based Dynamic Rendering
- Create a custom directive that exposes a ViewContainerRef.
- Use @ViewChild to get a reference to that directive in your component.
- Pass the directive’s viewContainerRef along with the backend data to the renderComponents() method.
- The library checks the COMP_REGISTRY to find which Angular component matches the Type from the response.
- The matched component is then injected into the container and receives the data.
In page-layout.component.ts
@ViewChild(DynamicComponentDirective, { static: true }) dynamicHost!: DynamicComponentDirective;
ngOnInit(){
const baseURL = typeof window !== 'undefined' ? (window as any).env?.apiUrl : '';
this.dynamicLoader.getPageContent(baseURL,this.router.url).subscribe({
next: (data: any) => {
if (data) {
// Render a component dynamically
this.dynamicLoader.renderComponents(this.dynamicHost.viewContainerRef, data);
}
}
});
}
In page-layout.component.html
<ng-template libDynamicComponent></ng-template>
🔍 Behind the Scenes:
- Components are extracted from PageComponents.
- Each block is matched by its Type (e.g., HeroBlock).
- The library uses the component registry to lazy-load and inject the corresponding Angular component.
- CMS data is passed into the component via @Input() bindings.
🔄 This directive approach allows editors to add, remove, or reorder blocks in the CMS — and the Angular app reflects changes automatically at runtime.
🌐 Real-Time SSR Adaptation (Server-Side Rendering with Angular Universal)
To make this system SEO-friendly and search engine discoverable, we use Angular Universal for server-side rendering (SSR).
🧠 Why Add SSR?
While Angular renders content in the browser by default, SSR enables:
- ✅ Search engine indexing of dynamic content
- ✅ Fast first-paint and Time-to-Interactive (TTI)
- ✅ Custom HTTP status codes (like 404s)
- ✅ Preview rendering for CMS editors
🔧 Library Used: @nguniversal/express-engine
Angular Universal provides an Express-based server adapter. We use it to inject HTTP-level logic inside Angular components.
Step-by-Step Setup for SSR: (Upcoming blog....)
✅ SSR + Dynamic CMS = Best of Both Worlds
With Angular Universal + Dynamic Component Directive:
- Your content loads instantly on first hit
- CMS changes reflect in real time
- Proper HTTP responses are delivered from the server
- Search engines can fully crawl and understand your content
⚡ This makes your Angular frontend fully compatible with modern CMS needs while staying SEO-compliant and dynamic.
🔍 Why This Pattern Works
✅ Separation of Concerns
Angular handles presentation, while APIs handle data shaping.
✅ Flexibility
Editors can build structured or composable pages.
✅ Scalability
New components? Just update the registry.
**
✅ SSR Ready**
This works with Angular Universal.
✅ Reusability
Drop components are modular and lazy-loaded.
🎯 Key Benefits
🧹 CMS Flexibility: Build pages freely with or without blocks
↻ Dynamic Rendering: Powered by simple backend mappings
⚙️ Maintainability: Registries make it easy to plug and play
⚡ Performance: Components load only when needed
🖥️ SSR Compatibility: Angular Universal plays nicely with dynamic rendering.
🔚 Conclusion
By combining:
- Angular’s dynamic loading
- A smart, registry-based Angular library
- A custom BFF layer
- And Optimizely CMS
…you get a scalable, maintainable, CMS-flexible frontend architecture.
Whether rendering full-page types or dynamic blocks — this system ensures your content is powerful, flexible, and consistent.
Top comments (0)