# Management SDK - Pages

> Source: https://agilitycms.com/docs/dotNet/management-sdk-dotnet-pages

The pages client works with pages, the sitemap and page models. You reach it through `client.Pages` on an `AgilityManagementClient`.

> **Note:** This page covers version 2.0 of the .NET Management SDK. Upgrading from 1.x? See [Migrating to 2.0](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-migrating-to-2).

## Before you start

The examples assume a `client`, an instance GUID (`guid`) and a locale (`locale`). To set up the client, see the [Intro](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-intro).

```csharp
using System.Net;
using Agility.Management.Sdk;
using Agility.Management.Sdk.Clients;
using Agility.Management.Sdk.Models;
```

Every method takes the instance GUID first, then the locale, then IDs, then optional settings. Every method also takes an optional `CancellationToken cancellationToken` as its last parameter. The signatures below leave it out.

Page saves, deletes and workflow actions run as batches and return a `BatchResult`, just like content. See [Batches](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-intro#batches). A page save always lands in Staging, so publish the page for the change to go live.

Page model saves are different: they aren't batches, and they return the saved template.

## Method list

| Method | Description |
|---|---|
| `GetSitemapAsync` | Get the sitemap for a locale |
| `GetPageAsync` | Get a page by ID |
| `SavePageAsync` | Create or update a page |
| `PublishPageAsync` | Publish a page |
| `UnpublishPageAsync` | Unpublish a page |
| `RequestApprovalPageAsync` | Request approval for a page |
| `ApprovePageAsync` | Approve a page |
| `DeclinePageAsync` | Decline a page |
| `DeletePageAsync` | Delete a page |
| `BatchWorkflowPagesAsync` | Run one workflow action on many pages in one batch |
| `GetCascadeItemsAsync` | List what a cascade publish of a page would include |
| `PublishPageCascadeAsync` | Publish a page and the content its components use |
| `GetPageHistoryAsync` | Get a page's version history |
| `GetPageCommentsAsync` | Get a page's comments |
| `GetPageTemplatesAsync` | List page models |
| `GetPageTemplateAsync` | Get a page model by ID |
| `GetPageTemplateByNameAsync` | Get a page model by name |
| `GetPageTemplateZonesAsync` | Get a page model's zones |
| `SavePageTemplateAsync` | Create or update a page model |
| `DeletePageTemplateAsync` | Delete a page model |

---

## Sitemap

### GetSitemapAsync

Get the sitemap for a locale. The result has one entry per channel, and each channel has its pages.

```csharp
List<Sitemap> sitemap = await client.Pages.GetSitemapAsync(guid, locale);

foreach (var channel in sitemap)
{
    Console.WriteLine($"Channel {channel.Name}");
    foreach (var node in channel.Pages ?? [])
    {
        Console.WriteLine($"  {node.Url} - Page ID: {node.PageID}");
    }
}
```

Each `SitemapItem` lists its children in `ChildPages`.

**Signature:** `Task<List<Sitemap>> GetSitemapAsync(string instanceGuid, string locale)`

---

## Pages

### GetPageAsync

Get a page by its page ID. `Zones` holds the components on the page, keyed by zone name.

```csharp
PageItem page = await client.Pages.GetPageAsync(guid, locale, 7);
Console.WriteLine($"Page: {page.Name}");

foreach (var (zone, components) in page.Zones ?? [])
{
    Console.WriteLine($"{zone}: {components.Count} components");
}
```

**Signature:** `Task<PageItem> GetPageAsync(string instanceGuid, string locale, int pageId)`

---

### SavePageAsync

Create or update a page. A save replaces the whole page, so read it first, change it, and send it all back:

```csharp
var page = await client.Pages.GetPageAsync(guid, locale, 7);
page.Title = "About us";

await client.Pages.SavePageAsync(guid, locale, page);
await client.Pages.PublishPageAsync(guid, locale, 7);
```

To create a page, use a `PageID` of `-1` and fill in the page's properties (for example `Name`, `Title`, `MenuText` and `TemplateName`). Say where the page goes with `SavePageOptions`:

```csharp
BatchResult created = await client.Pages.SavePageAsync(guid, locale, newPage,
    new SavePageOptions { ParentPageId = 7 });

int newPageId = created.ItemId!.Value;
```

| `SavePageOptions` | |
|---|---|
| `ParentPageId` | The parent page. Leave it out for the root. |
| `PlaceBeforePageId` | The sibling page to put this page before. Leave it out to put the page last. |
| `OtherLocale`, `PageIdInOtherLocale` | Link the page to the same page in another locale |
| `LinkExistingComponents` | Link the page's components to existing content instead of copying it |

Settings you leave out use the API's defaults.

**Signature:** `Task<BatchResult> SavePageAsync(string instanceGuid, string locale, PageItem page, SavePageOptions? options = null, bool waitForBatch = true)`

**Returns:** A `BatchResult`. `ItemId` is the page ID.

---

## Page workflow

Each workflow method takes an optional `comments` string for the page's history, and returns a `BatchResult`.

### PublishPageAsync

```csharp
await client.Pages.PublishPageAsync(guid, locale, pageId, comments: "Publishing page");
```

**Signature:** `Task<BatchResult> PublishPageAsync(string instanceGuid, string locale, int pageId, string? comments = null, bool waitForBatch = true)`

---

### UnpublishPageAsync

```csharp
await client.Pages.UnpublishPageAsync(guid, locale, pageId, comments: "Taking down temporarily");
```

**Signature:** `Task<BatchResult> UnpublishPageAsync(string instanceGuid, string locale, int pageId, string? comments = null, bool waitForBatch = true)`

---

### RequestApprovalPageAsync

```csharp
await client.Pages.RequestApprovalPageAsync(guid, locale, pageId, comments: "Ready for review");
```

**Signature:** `Task<BatchResult> RequestApprovalPageAsync(string instanceGuid, string locale, int pageId, string? comments = null, bool waitForBatch = true)`

---

### ApprovePageAsync

```csharp
await client.Pages.ApprovePageAsync(guid, locale, pageId, comments: "Approved for publication");
```

**Signature:** `Task<BatchResult> ApprovePageAsync(string instanceGuid, string locale, int pageId, string? comments = null, bool waitForBatch = true)`

---

### DeclinePageAsync

```csharp
await client.Pages.DeclinePageAsync(guid, locale, pageId, comments: "Needs revision");
```

**Signature:** `Task<BatchResult> DeclinePageAsync(string instanceGuid, string locale, int pageId, string? comments = null, bool waitForBatch = true)`

---

### DeletePageAsync

```csharp
await client.Pages.DeletePageAsync(guid, locale, pageId, comments: "Removing page");
```

**Signature:** `Task<BatchResult> DeletePageAsync(string instanceGuid, string locale, int pageId, string? comments = null, bool waitForBatch = true)`

---

### BatchWorkflowPagesAsync

Run one workflow action on many pages in a single batch. The action is a `WorkflowOperationType`: `Publish`, `Unpublish`, `Approve`, `Decline` or `RequestApproval`.

```csharp
await client.Pages.BatchWorkflowPagesAsync(guid, locale, [7, 8, 9], WorkflowOperationType.Publish);
```

Pass at least one page ID.

**Signature:** `Task<BatchResult> BatchWorkflowPagesAsync(string instanceGuid, string locale, IEnumerable<int> pageIds, WorkflowOperationType operation, bool waitForBatch = true)`

---

### GetCascadeItemsAsync

List what publishing a page would also publish: the content its components use, and nested lists.

```csharp
CascadeItem tree = await client.Pages.GetCascadeItemsAsync(guid, locale, pageId);
```

**Signature:** `Task<CascadeItem> GetCascadeItemsAsync(string instanceGuid, string locale, int pageId)`

---

### PublishPageCascadeAsync

Publish a page and the content it depends on. The API can create more than one batch, and this method doesn't wait for them. Wait for each one with `client.Batches.WaitForBatchAsync`:

```csharp
BatchCreateResult created = await client.Pages.PublishPageCascadeAsync(guid, locale, pageId);

foreach (var batchId in created.BatchIDs ?? [])
{
    await client.Batches.WaitForBatchAsync(guid, batchId);
}
```

**Signature:** `Task<BatchCreateResult> PublishPageCascadeAsync(string instanceGuid, string locale, int pageId, string? comments = null)`

---

## Page history and comments

### GetPageHistoryAsync

Get a page's version history, a page of results at a time.

```csharp
PageHistoryResponse history = await client.Pages.GetPageHistoryAsync(guid, locale, pageId, take: 20);

foreach (var version in history.Items ?? [])
{
    Console.WriteLine($"v{version.VersionNumber} {version.State} by {version.CreatedBy}");
}
```

**Signature:** `Task<PageHistoryResponse> GetPageHistoryAsync(string instanceGuid, string locale, int pageId, int? take = null, int? skip = null)`

---

### GetPageCommentsAsync

Get a page's comments.

```csharp
ItemCommentsResponse comments = await client.Pages.GetPageCommentsAsync(guid, locale, pageId);
```

**Signature:** `Task<ItemCommentsResponse> GetPageCommentsAsync(string instanceGuid, string locale, int pageId, int? take = null, int? skip = null)`

---

## Page models

A page model (called a page template in the SDK and API) defines a page's zones. In the SDK, a template is a `PageModel`, and its zones are its `ContentSectionDefinitions`. Each zone (`ContentSectionDefinition`) can have default components (`DefaultModules`) that new pages get automatically.

### GetPageTemplatesAsync

List page models. Set `includeModuleZones: true` to include each template's zones.

```csharp
List<PageModel> templates = await client.Pages.GetPageTemplatesAsync(guid, locale, includeModuleZones: true);

foreach (var template in templates)
{
    Console.WriteLine($"{template.PageTemplateName} (ID: {template.PageTemplateID})");
}
```

`searchFilter` returns only templates whose name contains that text.

**Signature:** `Task<List<PageModel>> GetPageTemplatesAsync(string instanceGuid, string locale, bool includeModuleZones = false, string? searchFilter = null)`

---

### GetPageTemplateAsync

Get a page model by ID, with every zone and each zone's default components. Use this method to read a template you plan to save back.

```csharp
PageModel template = await client.Pages.GetPageTemplateAsync(guid, locale, pageTemplateId);
Console.WriteLine($"Template: {template.PageTemplateName}");
```

**Signature:** `Task<PageModel> GetPageTemplateAsync(string instanceGuid, string locale, int pageTemplateId)`

---

### GetPageTemplateByNameAsync

```csharp
PageModel template = await client.Pages.GetPageTemplateByNameAsync(guid, locale, "Main Template");
Console.WriteLine($"Template ID: {template.PageTemplateID}");
```

**Signature:** `Task<PageModel> GetPageTemplateByNameAsync(string instanceGuid, string locale, string templateName)`

---

### GetPageTemplateZonesAsync

Get just a template's zones.

```csharp
List<ContentSectionDefinition> zones = await client.Pages.GetPageTemplateZonesAsync(guid, locale, pageTemplateId);

foreach (var zone in zones)
{
    Console.WriteLine($"Zone: {zone.PageItemTemplateName} ({zone.PageItemTemplateReferenceName})");
}
```

> ⚠️ **Don't save zones read with this method.** It always returns `DefaultModules` as empty. If you put these zones in a template and save it, you clear every zone's default components. Read the template with `GetPageTemplateAsync` instead.

**Signature:** `Task<List<ContentSectionDefinition>> GetPageTemplateZonesAsync(string instanceGuid, string locale, int pageTemplateId)`

---

### SavePageTemplateAsync

Create or update a page model. Read the save rules below before your first template save.

**Signature:** `Task<PageModel> SavePageTemplateAsync(string instanceGuid, string locale, PageModel pageTemplate)`

**Returns:** The saved template.

#### How a template save treats zones and default components

The SDK leaves any property you don't set (`null`) out of the request, and the API reads what you send like this:

| You send | The API |
|---|---|
| `ContentSectionDefinitions` left `null` | Keeps the zones as they are |
| A `ContentSectionDefinitions` list | Makes it the **complete** zone list. Stored zones you leave out are removed. |
| A zone with `DefaultModules` left `null` | Keeps that zone's default components |
| A zone with a `DefaultModules` list | Replaces that zone's default components with the list |
| A zone with `DefaultModules = []` | Clears that zone's default components |

**How zones are matched.** The API matches each zone you send to a stored zone by its ID (`PageItemTemplateID`). A zone sent with a `PageItemTemplateID` of `-1` is matched by its reference name (`PageItemTemplateReferenceName`) instead, as long as no other zone in the same request already claims that stored zone by ID. If no stored zone matches, the API creates a new zone.

> ⚠️ **Removing a zone removes its components from pages.** When a save leaves a zone out, the components placed in that zone disappear from every page that uses the template. The content items themselves aren't deleted. Don't send a partial zone list by accident: start from `GetPageTemplateAsync`, which returns every zone.

**Restoring a removed zone.** Send a zone with a `PageItemTemplateID` of `-1` and the removed zone's reference name through the Management API, for example with `SavePageTemplateAsync`. The API restores the zone along with the components that were placed in it. Re-adding a zone with the same reference name in Web Studio doesn't do this: it creates a new, empty zone.

**Channel.** `DigitalChannelTypeID` is the template's channel. When you create a template without one, it's created for the Website channel. When you update a template, it keeps its channel.

**Collections start as `null`.** On a new object, create a collection before you add to it, for example `zone.DefaultModules ??= [];`. Leaving it `null` means "don't change it".

#### Rename a template without touching its zones

```csharp
var template = await client.Pages.GetPageTemplateAsync(guid, locale, pageTemplateId);
template.PageTemplateName = "Landing Page";
template.ContentSectionDefinitions = null;   // keep the zones as they are

await client.Pages.SavePageTemplateAsync(guid, locale, template);
```

#### Add a zone and keep the others

```csharp
var template = await client.Pages.GetPageTemplateAsync(guid, locale, pageTemplateId);   // every zone and its defaults

template.ContentSectionDefinitions!.Add(new ContentSectionDefinition
{
    PageItemTemplateID = -1,
    PageItemTemplateName = "Sidebar",
    PageItemTemplateReferenceName = "Sidebar",
});

await client.Pages.SavePageTemplateAsync(guid, locale, template);
```

#### Add a default component to a zone

```csharp
var hero = (await client.Models.GetComponentModelsAsync(guid)).Single(m => m.ReferenceName == "Hero");

var template = await client.Pages.GetPageTemplateAsync(guid, locale, pageTemplateId);
var zone = template.ContentSectionDefinitions!.Single(z => z.PageItemTemplateReferenceName == "MainContentZone");

zone.DefaultModules ??= [];
zone.DefaultModules.Add(new ContentSectionDefaultModule { ContentDefinitionID = hero.Id, AutoCreate = true });

await client.Pages.SavePageTemplateAsync(guid, locale, template);
```

The list you send replaces the zone's default components, so it must include the existing ones. Starting from `GetPageTemplateAsync` keeps them. Only the component model (`ContentDefinitionID`) and `AutoCreate` are stored: a read returns the component model's own title, whatever `Title` you send.

#### Clear one zone's default components

```csharp
var template = await client.Pages.GetPageTemplateAsync(guid, locale, pageTemplateId);
template.ContentSectionDefinitions!.Single(z => z.PageItemTemplateReferenceName == "Sidebar").DefaultModules = [];

await client.Pages.SavePageTemplateAsync(guid, locale, template);
```

#### Create a template

```csharp
PageModel created = await client.Pages.SavePageTemplateAsync(guid, locale, new PageModel
{
    PageTemplateID = -1,
    PageTemplateName = "Campaign Page",
    ContentSectionDefinitions =
    [
        new ContentSectionDefinition
        {
            PageItemTemplateID = -1,
            PageItemTemplateName = "Main",
            PageItemTemplateReferenceName = "Main",
            ItemOrder = 0,
        },
    ],
});

Console.WriteLine($"Created template {created.PageTemplateID}");
```


---

### DeletePageTemplateAsync

Delete a page model. The method returns nothing. A failure throws an exception.

```csharp
await client.Pages.DeletePageTemplateAsync(guid, locale, pageTemplateId);
```

**Signature:** `Task DeletePageTemplateAsync(string instanceGuid, string locale, int pageTemplateId)`

---

## Error handling

```csharp
try
{
    var page = await client.Pages.GetPageAsync(guid, locale, pageId);
}
catch (AgilityManagementException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    Console.Error.WriteLine($"Page not found: {ex.ApiMessage}");
}
```

Page saves and workflow actions throw an `AgilityBatchException` when their batch is processed with failures, and an `AgilityBatchTimeoutException` when it doesn't finish in time. See [Errors](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-intro#errors) for the full list of exceptions.
