Introduction
Page Models define the structure and layout of pages in Agility CMS. They determine where editors can place components and how the page is visually organized.
Page Models define the structure and layout of pages in Agility CMS. They determine where editors can place components and how the page is visually organized.
A Page Model is a template that defines:
Think of Page Models as the skeleton of a page. They define the layout structure, while components fill in the actual content.
┌─────────────────────────────┐
│ Header Zone │
├─────────────────────────────┤
│ │
│ Main Content Zone │
│ │
├─────────────────────────────┤
│ Footer Zone │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Header Zone │
├──────────────┬──────────────┤
│ │ │
│ Left Zone │ Right Zone │
│ │ │
├──────────────┴──────────────┤
│ Footer Zone │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Hero Zone │
├─────────────────────────────┤
│ Article Content Zone │
├─────────────────────────────┤
│ Related Posts Zone │
└─────────────────────────────┘
Each zone you create will be available in the code for rendering.
In Blazor, Page Models are implemented as layouts or conditionally rendered sections:
@* Components/Pages/AgilityPage.razor *@
@page "/{*slug}"
<MainLayout>
@foreach (var zone in PageData.Zones)
{
<section class="zone zone-@zone.Key.ToLower().Replace(" ", "-")">
@foreach (var module in zone.Value.Modules)
{
<AgilityComponent
ComponentName="@module.ModuleName"
Fields="@module.Fields"
CustomData="@module.CustomData" />
}
</section>
}
</MainLayout>
For different layouts based on Page Model name:
@switch (PageData.TemplateName)
{
case "Main Template":
<MainLayout>
@RenderZones()
</MainLayout>
break;
case "Two Column Layout":
<TwoColumnLayout LeftZone="@GetZone("LeftZone")"
RightZone="@GetZone("RightZone")" />
break;
case "Blog Page":
<BlogLayout HeroZone="@GetZone("HeroZone")"
ContentZone="@GetZone("ContentZone")" />
break;
}
In MVC, Page Models map to Razor view files in the Views/PageTemplates/ folder:
Views/
└── PageTemplates/
├── MainTemplate.cshtml
├── TwoColumnLayout.cshtml
└── BlogPage.cshtml
The Page Model name is converted to a file path by removing spaces:
| Page Model Name | View File |
|---|---|
| Main Template | MainTemplate.cshtml |
| Two Column Layout | TwoColumnLayout.cshtml |
| Blog Page | BlogPage.cshtml |
@* Views/PageTemplates/MainTemplate.cshtml *@
@model AgilityPageModel
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@await Component.InvokeAsync("SEO", Model)
<link href="~/css/output.css" rel="stylesheet" />
</head>
<body>
@await Component.InvokeAsync("SiteHeader")
<main>
@* Render the MainContentZone *@
@await Html.RenderZoneAsync("MainContentZone")
</main>
@await Component.InvokeAsync("SiteFooter")
</body>
</html>
@* Views/PageTemplates/TwoColumnLayout.cshtml *@
@model AgilityPageModel
<!DOCTYPE html>
<html lang="en">
<head>
@await Component.InvokeAsync("SEO", Model)
<link href="~/css/output.css" rel="stylesheet" />
</head>
<body>
@await Component.InvokeAsync("SiteHeader")
<main class="container mx-auto">
<div class="flex flex-wrap -mx-4">
<div class="w-full lg:w-2/3 px-4">
@await Html.RenderZoneAsync("LeftZone")
</div>
<aside class="w-full lg:w-1/3 px-4">
@await Html.RenderZoneAsync("RightZone")
</aside>
</div>
</main>
@await Component.InvokeAsync("SiteFooter")
</body>
</html>
Both starters include a helper for rendering zones. It:
// Simplified implementation
public static async Task RenderZoneAsync(this IHtmlHelper html, string zoneName)
{
var page = html.ViewData["PageData"] as PageResponse;
var zone = page?.Zones?.GetValueOrDefault(zoneName);
if (zone?.Modules == null) return;
foreach (var module in zone.Modules)
{
await html.RenderComponentAsync(module);
}
}
The page rendering system automatically selects the correct Page Model based on the TemplateName property from Agility:
// Get the template path
var templatePath = PageHelpers.GetPageTemplatePath(page.TemplateName);
// Returns: "/Views/PageTemplates/MainTemplate.cshtml"
// PageHelpers.cs
public static string GetPageTemplatePath(string templateName)
{
// Remove spaces from template name
var fileName = templateName.Replace(" ", "");
return $"/Views/PageTemplates/{fileName}.cshtml";
}
When you fetch a page, zones are returned as a dictionary:
{
"zones": {
"MainContentZone": {
"modules": [
{
"moduleName": "RichTextArea",
"fields": {
"textBlob": "<p>Hello world</p>"
}
},
{
"moduleName": "TextBlockWithImage",
"fields": {
"title": "Our Services",
"content": "<p>We offer...</p>",
"image": { "url": "...", "label": "..." }
}
}
]
},
"SidebarZone": {
"modules": [
{
"moduleName": "Newsletter",
"fields": {
"heading": "Subscribe",
"buttonText": "Sign Up"
}
}
]
}
}
}
Choose zone names that clearly indicate their purpose:
| Good | Avoid |
|---|---|
| MainContentZone | Zone1 |
| HeroSection | Top |
| SidebarWidgets | Right |
| FooterLinks | Bottom |
Design Page Models that work for multiple page types. A "Main Template" with a single content zone is more flexible than many specialized templates.
Add comments or documentation so editors know what types of components work best in each zone:
@* MainContentZone: Full-width content modules *@
@await Html.RenderZoneAsync("MainContentZone")
@* SidebarZone: Narrow widgets, CTAs, navigation *@
@await Html.RenderZoneAsync("SidebarZone")
Not all pages use all zones. Check for null zones:
@if (PageData.Zones.ContainsKey("SidebarZone"))
{
<aside>
@await Html.RenderZoneAsync("SidebarZone")
</aside>
}
Design zones that work well on all screen sizes:
<div class="flex flex-col lg:flex-row">
<main class="w-full lg:w-2/3">
@await Html.RenderZoneAsync("MainContentZone")
</main>
<aside class="w-full lg:w-1/3 mt-8 lg:mt-0">
@await Html.RenderZoneAsync("SidebarZone")
</aside>
</div>
Some components appear on every page regardless of Page Model (like headers and footers). These are typically:
@* Always include header and footer *@
@await Component.InvokeAsync("SiteHeader")
<main>
@await Html.RenderZoneAsync("MainContentZone")
</main>
@await Component.InvokeAsync("SiteFooter")