See Agility CMS in action. Watch a product demo
How Page Models Work in .NET
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.
What is a Page Model?
A Page Model is a template that defines:
- Name - A descriptive identifier (e.g., "Main Template", "Two Column Layout", "Blog Page")
- Zones - Areas where editors can add, edit, and remove components
Think of Page Models as the skeleton of a page. They define the layout structure, while components fill in the actual content.
Example Page Models
Single Column Layout
┌─────────────────────────────┐
│ Header Zone │
├─────────────────────────────┤
│ │
│ Main Content Zone │
│ │
├─────────────────────────────┤
│ Footer Zone │
└─────────────────────────────┘
Two Column Layout
┌─────────────────────────────┐
│ Header Zone │
├──────────────┬──────────────┤
│ │ │
│ Left Zone │ Right Zone │
│ │ │
├──────────────┴──────────────┤
│ Footer Zone │
└─────────────────────────────┘
Blog Post Layout
┌─────────────────────────────┐
│ Hero Zone │
├─────────────────────────────┤
│ Article Content Zone │
├─────────────────────────────┤
│ Related Posts Zone │
└─────────────────────────────┘
Creating Page Models in Agility CMS
- Go to Settings > Page Models in Agility CMS
- Click New to create a Page Model
- Enter a Name (e.g., "Main Template")
- Add Zones - these become the areas where editors place components
Each zone you create will be available in the code for rendering.
Implementing Page Models in .NET
Blazor Implementation
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;
}
MVC Implementation
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 |
Main Template Example
@* 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>
Two Column Layout Example
@* 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>
The RenderZoneAsync Helper
Both starters include a helper for rendering zones. It:
- Gets the zone from the page data
- Iterates through each module in the zone
- Invokes the appropriate component for each module
// 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);
}
}
Dynamic Page Model Selection
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";
}
Zone Data Structure
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"
}
}
]
}
}
}
Best Practices
1. Use Descriptive Zone Names
Choose zone names that clearly indicate their purpose:
| Good | Avoid |
|---|---|
| MainContentZone | Zone1 |
| HeroSection | Top |
| SidebarWidgets | Right |
| FooterLinks | Bottom |
2. Keep Page Models Flexible
Design Page Models that work for multiple page types. A "Main Template" with a single content zone is more flexible than many specialized templates.
3. Document Zone Purposes
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")
4. Handle Missing Zones Gracefully
Not all pages use all zones. Check for null zones:
@if (PageData.Zones.ContainsKey("SidebarZone"))
{
<aside>
@await Html.RenderZoneAsync("SidebarZone")
</aside>
}
5. Consider Mobile Layouts
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>
Shared Components
Some components appear on every page regardless of Page Model (like headers and footers). These are typically:
- Hard-coded in the Page Model template
- Fetched from a shared content item in Agility
@* Always include header and footer *@
@await Component.InvokeAsync("SiteHeader")
<main>
@await Html.RenderZoneAsync("MainContentZone")
</main>
@await Component.InvokeAsync("SiteFooter")
Next Steps
- How Components Work - Build the components that fill zones
- How Pages Work - Understand page routing
- Configuration - Set up caching and locales