In this blog, we’ll walk through how our Angular application is architected to work with a dynamic CMS setup using a custom library that acts as the bridge between Optimizely CMS and frontend rendering. We’ll explore the folder structure, routing flow, component loading, and how the dynamic component architecture works behind the scenes.
GitHub Repository
You can access the full source code of this project on GitHub:
View On GitHub
Folder Structure
Our Angular application is organized into three main folders:
1. DropComponents
- Contains all components that are dropped via CMS into the PageComponents array or the Blocks drop in CMS Content area.
- These components are dynamically rendered using a custom directive provided by our Angular library.
2. GlobalComponents
Contains global UI components such as Header and NotFoundComponent (404 page) etc.
3. Pages
- Contains route-based page components.
- Includes components that render entire CMS pages such as PageLayoutComponent and detailed content pages like NewsDetailsComponent.
Structure Looks Like:
src
└──app
├──DropComponents
│ ├── eservice
│ │ └── eservice.component.ts
│ └── hero
│ └── hero.component.ts
│
├──GlobalComponents
│ ├── header
│ │ └── header.component.ts
│ └── not-found
│ └── not-found.component.ts
│
├──Pages
│ ├── page-layout
│ │ └── page-layout.component.ts
│ └── news-details
│ └── news-details.component.ts
│
└──app-routing.module.ts
Step By Step Guide:
1. Create a New Angular Application
Run the following command:
ng new demo-app
2. Create Project Folders:
create the following directories or folders inside the src --> app
- DropComponents
- GlobalComponents
- Pages
3. Generate the Layout Component
Inside the Global folder, generate a layoutComponent using the Angular CLI:
ng g c Global/layout
4. Generate the Page Layout Component
Inside the Pages folder, generate a pageLayoutComponent using the Angular CLI:
ng g c Pages/pageLayout
5. Now Setup Routing Architecture
Our Angular routing is designed such that any route ultimately resolves to the PageLayoutComponent, ensuring all CMS-driven pages go through a common entry point means whatever be the page route the hit will always come to the PageLayoutComponent.
In app-routing.module.ts:
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: '**', component: PageLayoutComponent } // nested catch-all
]
},
{ path: '**', redirectTo: '' }, // global catch-all
];
Response From Backend
we can receive two types of responses from the backend:
1️⃣ Response with [pageComponents] array:
- This array contains all the CMS dropped blocks.
- If it exists in the response, it means the page includes blocks that need to be rendered.
- These blocks are rendered using the library directive libDynamicComponent.
{
"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"
}
}
}
]
}
]
}
}
}
2️⃣ Response without [pageComponents] array:
- In this case, the page is loaded only through its page properties.
- There are no additional components or dropped blocks in the CMS.
- Such pages are rendered using ViewContainerRef[#dynamicContainer].
{
"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"
}
]
}
}
}
]
}
}
}
For a more detailed explanation about responses, you can check out the full blog here: Building a Dynamic CMS-Driven Angular App Using Optimizely & a Custom BFF Architecture
Install Angular Library In our Application
First, paste the dist folder of library into the project’s root directory and rename it to lib.
Now, Run the below command to install a Library
npm install .\lib\optimizely-cms-integration
⚠️Note: The library name is optimizely-cms-integration. When you build the library, the dist folder will contain a subfolder with your library’s name—in my case, it is optimizely-cms-integration.
Component Code and Responsibilities:
1️⃣ In layout.component.html inside the GlobalComponents folder:
The LayoutComponent provides a consistent layout for all pages by rendering a global and a :
<app-header ngSkipHydration></app-header>
<div class="content">
<router-outlet></router-outlet>
</div>
2️⃣ Create Drop Components
In the CMS content area, we now have two dropped components (as shown in the pageComponents array in the response above). Suppose the Start Page consists of these two components, each with its own design.
To set this up, generate two components inside the DropComponents folder using the following commands:
ng g c DropComponents/hero
ng g c DropComponents/eservice
- In hero.component.ts:
import { Component, Inject, Input, PLATFORM_ID, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-hero',
templateUrl: './hero.component.html',
styleUrls: ['./hero.component.css'],
encapsulation: ViewEncapsulation.None,
standalone: false
})
export class HeroComponent {
@Input() data: any; //CMS-response of this Block
}
- In e-services.component.ts:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-e-services',
templateUrl: './e-services.component.html',
styleUrls: ['./e-services.component.css'],
standalone: false
})
export class EServicesComponent {
@Input() data: any; //CMS-response of this Block
}
⚠️Note: The data variable here contains all the content for this component from the pageComponents array. After receiving the data in your component, you can design and structure it as you want.
3️⃣ Create a Page Component (Without pageComponents Array)
Next, we’ll create another component for a page type that doesn’t use the pageComponents array and instead depends only on its page properties. For example, a Leadership Details page.
Run the following command to generate a component inside the pages folder:
ng g c pages/leadership-details
- In leadership-details.component.ts:
import {Component, Input} from '@angular/core';
@Component({
selector: 'app-leadership-details',
standalone: false,
templateUrl: './leadership-details.component.html',
styleUrl: './leadership-details.component.css'
})
export class LeadershipDetailsComponent {
@Input() data: any //CMS-response of this Page
}
4️⃣ In page-layout.component.ts:
- First, we’ll set up the registries that will be used for CMS pages and the dropped blocks within the CMS content area.
//CMS Drop Block Registries:
export const COMP_REGISTRY: { [key: string]: () => Promise<any> } = {
HeroBlock: () =>
import('../../DropComponents/hero/hero.component').then((m) => m.HeroComponent),
EServiceBlock: () =>
import('../../DropComponents/e-services/e-services.component').then((m) => m.EServicesComponent)
};
//CMS page Registries:
export const PAGE_REGISTRY: { [key: string]: () => Promise<any> } = {
...COMP_REGISTRY,
LeadershipDetailPage: () =>
import('../leadership-details/leadership-details.component')
.then(m => m.LeadershipDetailsComponent)
};
- Secondly, register these registries in the library by using the functions: setPageRegistry(PAGE_REGISTRY) and setComponentRegistry(COMP_REGISTRY).”
import { OptimizelyCmsIntegrationService } from 'optimizely-cms-integration';
constructor(
private dynamicLoader: OptimizelyCmsIntegrationService, private router: Router
) {
// CMS Page registry mapping in library
this.dynamicLoader.setPageRegistry(PAGE_REGISTRY);
//CMS Drop Blocks registry mapping in library
this.dynamicLoader.setComponentRegistry(COMP_REGISTRY);
}
- Next, we’ll use the library method getPageContent(baseURL, router.url) to fetch the response and pass it to the renderPages() function along with our custom ViewContainerRef. After that, we’ll call the renderComponents() function, which uses the directive in the library as a container to handle the responses.
By calling these functions:
- If the response depends entirely on page properties, the library will look into the Pages folder and load the appropriate component from there.
- If the response contains dropped blocks in the CMS content area (via the pageComponents array), those blocks will be rendered accordingly from DropComponents folder.
import { Router, NavigationEnd } from '@angular/router';
import { Component, ViewChild, OnInit, AfterViewInit, OnDestroy, ViewContainerRef, Injector, NgZone } from '@angular/core';
import { OptimizelyCmsIntegrationService } from 'optimizely-cms-integration';
import { DynamicComponentDirective } from 'optimizely-cms-integration';
import { catchError, map, of, Subscription } from 'rxjs';
//Drop Components Directive
@ViewChild(DynamicComponentDirective, { static: true }) dynamicHost!: DynamicComponentDirective;
//Page Component ViewContainerRef
@ViewChild('dynamicContainer', { read: ViewContainerRef, static: true })
dynamicContainer!: ViewContainerRef;
getNewCompletePageData() {
const baseURL = "your Backend CMS Base URL"
this.dynamicLoader.getPageContent(baseURL,this.router.url).subscribe({
next: (data: any) => {
if (data) {
// Render a pages dynamically
this.dynamicLoader.renderPages(data.Type, data, this.dynamicContainer);
// Render a component dynamically
this.dynamicLoader.renderComponents(this.dynamicHost.viewContainerRef, data);
}
}
});
}
Overall Code snippet of "page-layout.component.ts"
import { Router, NavigationEnd } from '@angular/router';
import { Component, ViewChild, OnInit, AfterViewInit, OnDestroy, ViewContainerRef, Injector, NgZone } from '@angular/core';
import { OptimizelyCmsIntegrationService } from 'optimizely-cms-integration';
import { DynamicComponentDirective } from 'optimizely-cms-integration';
import { catchError, map, of, Subscription } from 'rxjs';
//CMS Drop Block Registries:
export const COMP_REGISTRY: { [key: string]: () => Promise<any> } = {
HeroBlock: () =>
import('../../DropComponents/hero/hero.component').then((m) => m.HeroComponent),
EServiceBlock: () =>
import('../../DropComponents/e-services/e-services.component').then((m) => m.EServicesComponent)
};
//CMS page Registries:
export const PAGE_REGISTRY: { [key: string]: () => Promise<any> } = {
...COMP_REGISTRY,
LeadershipDetailPage: () =>
import('../leadership-details/leadership-details.component')
.then(m => m.LeadershipDetailsComponent)
};
@Component({
selector: 'app-page-layout',
templateUrl: './page-layout.component.html',
styleUrls: ['./page-layout.component.css'],
standalone: false
})
export class PageLayoutComponent implements OnInit, AfterViewInit, OnDestroy {
//Drop Components Directive
@ViewChild(DynamicComponentDirective, { static: true }) dynamicHost!: DynamicComponentDirective;
//Page Component ViewContainerRef
@ViewChild('dynamicContainer', { read: ViewContainerRef, static: true })
dynamicContainer!: ViewContainerRef;
private routeSubscription!: Subscription;
constructor(private dynamicLoader: OptimizelyCmsIntegrationService, private router: Router)
{
// CMS Page registry mapping in library
this.dynamicLoader.setPageRegistry(PAGE_REGISTRY);
//CMS Drop Blocks registry mapping in library
this.dynamicLoader.setComponentRegistry(COMP_REGISTRY);
}
ngOnInit() {
this.routeSubscription = this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.getNewCompletePageData();
}
});
}
getNewCompletePageData() {
const baseURL = "your Backend CMS Base URL"
this.dynamicLoader.getPageContent(baseURL,this.router.url).subscribe({
next: (data: any) => {
if (data) {
// Render a pages dynamically
this.dynamicLoader.renderPages(data.Type, data, this.dynamicContainer);
// Render a component dynamically
this.dynamicLoader.renderComponents(this.dynamicHost.viewContainerRef, data);
}
}
});
}
//single layout so router subscription destroty
ngOnDestroy() {
if (this.routeSubscription) {
this.routeSubscription.unsubscribe();
}
}
}
5️⃣ In page-layout.component.html:
<!-- This dynamicContainer here is responsible to show component which will be load from page properties-->
<ng-container #dynamicContainer></ng-container>
<main className='Page-container'>
<div>
<section>
<!-- This libDynamicComponent here is responsible to show drop component on page-->
<ng-template libDynamicComponent></ng-template>
</section>
</div>
</main>
Explanation:
- #dynamicContainer: Used for rendering components based on page Properties.
- libDynamicComponent: A library directive responsible for rendering all DropComponents placed on the CMS page. It also supports dynamic component reordering—if a block is reordered or its position is changed in the CMS content area, the update will be reflected in the UI after a hard refresh. On the frontend, you don’t need to manually adjust or replace the order of dropped components; the library handles it automatically.
How the Page Rendering Flow Works
Step-by-Step
- User visits a URL → Routed to PageLayoutComponent via LayoutComponent.
- Call getNewCompletePageData() inside PageLayoutComponent:
- Uses library method: this.dynamicLoader.getPageContent(baseURL, this.router.url)
- This hits Backend CMS endpoint and gets CMS page data.
- Once data is received:
- First, the layout/page is rendered using the pageProperties via the library method renderPages(). The data is then passed to the component by checking the Type keyword in backend response and matching it against the PAGE_REGISTRY.
- Next, the dropped components on the CMS page are rendered using the library method renderComponents(). The data is passed to each component by checking the Type keyword in backend response and matching it against the COMP_REGISTRY.
⚠️Note: This is how you can render all types of pages and blocks on the frontend. Whatever pages or blocks you create in the CMS, you simply need to map them in the registries and design their markup on the frontend. The library methods will then handle passing the data to the correct components automatically.
Summary
By keeping all CMS logic and bindings encapsulated within a reusable Angular library, this architecture ensures a clean separation of concerns. All dynamic rendering decisions are based on CMS response keys (Type) and tied to Angular components via registries.
This results in a highly dynamic and CMS-author-driven site experience where frontend developers only need to map the component registry and design the components — the rendering is fully automated.
Refer to a previous blog "Building a Dynamic CMS-Driven Angular App Using Optimizely & a Custom BFF Architecture" for more on how the BFF integrates and delivers this structure.
Coming Soon
- How to Achieve OPE (On-Page Editing) in CMS like Optimizely
Follow Me
Connect with me on LinkedIn to stay updated with my latest blogs, tutorials, and projects:
Syed_Muhammad_Haris
Top comments (0)