---
title: Reports
slug: cms-11/dev-docs/creating-custom-reports
docTags: 
createdAt: 2026-07-22T19:45:17.666Z
---

```asp
@model ExistingPagesReportViewModel

@using EPiServer.DataAbstraction
@using EPiServer.Framework.Web.Resources

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />

    <!-- Shell -->
    @Html.Raw(ClientResources.RenderResources("ShellCore"))

    <!-- LightTheme -->
    @Html.Raw(ClientResources.RenderResources("ShellCoreLightTheme"))

    <link href="/EPiServer/CMS/App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet">
    <link href="/EPiServer/CMS/App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet">
</head>
<body>
    @Html.Raw(Html.ShellInitializationScript())

    <div class="epi-contentContainer epi-padding">
        <div class="epi-contentArea">
            <div class="EP-systemImage" style="background-image: url('/App_Themes/Default/Images/ReportCenter/PublishedPages.gif');">
                <h1 class="EP-prefix">
                    Existing Pages
                </h1>
                <p class="EP-systemInfo">
                    This report displays pages that exists on the site.
                </p>
            </div>
            <div id="FullRegion_ValidationSummary" class="EP-validationSummary" style="color: Black; display: none;">
            </div>
        </div>

        @using (Html.BeginForm("ListPages", "ExistingPagesReport", FormMethod.Post))
        {
            <script src="/Util/javascript/episerverscriptmanager.js" type="text/javascript"></script>
            <script src="/EPiServer/CMS/javascript/system.js" type="text/javascript"></script>
            <script src="/EPiServer/CMS/javascript/dialog.js" type="text/javascript"></script>
            <script src="/EPiServer/CMS/javascript/system.aspx" type="text/javascript"></script>

            <input type="hidden" id="doExport" name="do
```

(Thanks to [Henrik Fransas at NetRelations](https://www.netrelations.com) for this example.)

In Optimizely, you can create custom reports and use the built-in reports in the admin view. The following example shows how to create a custom report of existing pages on a website and export the report to Excel using EPPlus.

1. Start by creating a Controller that looks like this:

```csharp
namespace AlloyExample.Controllers {
  [EPiServer.PlugIn.GuiPlugIn(
    Area = EPiServer.PlugIn.PlugInArea.ReportMenu,
    Url = "~/existingpagesreport",
    Category = "Existing Pages",
    DisplayName = "Pages By PageType")]
  [Authorize(Roles = "Administrators, WebAdmins")]
  public class ExistingPagesReportController: Controller {
    public ActionResult Index() {
      return View();
    }
  }
}
```

This controller is a GuiPlugin, which is shown in the report menu and uses the URL `/existingpagesreport`. The controller is protected, so you have to be part of the administrator group to use it.

2. For the URL to work, add a route to it in `global.asax.cs`:

```csharp
protected override void RegisterRoutes(RouteCollection routes) {
  base.RegisterRoutes(routes);
  routes.MapRoute(
    "ExistingPagesReport",
    "existingpagesreport/{action}",
    new {
      controller = "ExistingPagesReport", action = "Index"
    });
}
```

Create a simple view and ensure it displays in the report center.

3. Add a `ViewModel` with the necessary properties:

```csharp
public class ExistingPagesReportViewModel {
  public IEnumerable<PageType> PageTypes {
    get;
    set;
  }
  public PageDataCollection Pages {
    get;
    set;
  }
  public string SelectedPageType {
    get;
    set;
  }
}
```

4. Create a helper class to make a request against Optimizely like this:

```csharp
public static class ExistingPagesHelper {
  public static IEnumerable<PageType> GetAllPageTypes() {
    var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
    return contentTypeRepository.List().OfType<PageType>();
  }

  public static void SetPagesForPageTypeName(ExistingPagesReportViewModel model) {
    var criterias = new PropertyCriteriaCollection();
    var criteria = new PropertyCriteria();
    criteria.Condition = CompareCondition.Equal;
    criteria.Name = "PageTypeID";
    criteria.Type = PropertyDataType.PageType;
    criteria.Value = model.SelectedPageType;
    criteria.Required = true;
    criterias.Add(criteria);
    var pages = DataFactory.Instance.FindPagesWithCriteria(ContentReference.RootPage, criterias);
    model.Pages = pages;
  }
}
```

5. Update your index action to create an instance of the view model and assign all page types to it:

```csharp
public ActionResult Index() {
  var model = new ExistingPagesReportViewModel {
    PageTypes = ExistingPagesHelper.GetAllPageTypes()
  };
  return View(model);
}
```

6. Create a view and add Optimizely's CSS and JavaScript to it to have the same appearance as other built-in reports in Optimizely. The complete view looks like this:

```asp
@model ExistingPagesReportViewModel

@using EPiServer.DataAbstraction
@using EPiServer.Framework.Web.Resources

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    <!-- Shell -->
    @Html.Raw(ClientResources.RenderResources("ShellCore"))
    <!-- LightTheme -->
    @Html.Raw(ClientResources.RenderResources("ShellCoreLightTheme"))

    <link href="/EPiServer/CMS/App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet">
    <link href="/EPiServer/CMS/App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet">
</head>
<body>
    @Html.Raw(Html.ShellInitializationScript())

    <div class="epi-contentContainer epi-padding">
        <div class="epi-contentArea">
            <div class="EP-systemImage" style="background-image: url('/App_Themes/Default/Images/ReportCenter/PublishedPages.gif');">
                <h1 class="EP-prefix">Existing Pages</h1>
                <p class="EP-systemInfo">This report displays pages that exist on the site.</p>
            </div>
            <div id="FullRegion_ValidationSummary" class="EP-validationSummary" style="color: Black; display: none;"></div>
        </div>

        @using (Html.BeginForm("ListPages", "ExistingPagesReport", FormMethod.Post))
        {
            <script src="/Util/javascript/episerverscriptmanager.js" type="text/javascript"></script>
            <script src="/EPiServer/CMS/javascript/system.js" type="text/javascript"></script>
            <script src="/EPiServer/CMS/javascript/dialog.js" type="text/javascript"></script>
            <script src="/EPiServer/CMS/javascript/system.aspx" type="text/javascript"></script>

            <input type="hidden" id="doExport" name="doExport" value="False">
            <div class="epi-formArea">
                <fieldset>
                    <legend>Report Criteria</legend>
                    <div class="epi-size10">
                        <label for="pageTypes">Select PageType</label>
                        <select name="pageType" id="pageType">
                            @foreach (var type in Model.PageTypes.Where(w => w.ID != 1).OrderBy(o => o.Name))
                            {
                                <option value="@type.ID" @(type.ID.ToString() == Model.SelectedPageType ? "selected=selected" : "")>
                                    @type.Name
                                </option>
                            }
                        </select>
                    </div>
                </fieldset>

                <div class="epitoolbuttonrow">
                    <span class="epi-cmsButton">
                        <input class="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Report" 
                               type="submit" 
                               name="showReport" 
                               id="showReport" 
                               value="Show Report" 
                               onmouseover="EPi.ToolButton.MouseDownHandler(this)" 
                               onmouseout="EPi.ToolButton.ResetMouseDownHandler(this)" />
                    </span>
                    <span class="epi-cmsButton">
                        <input class="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Report" 
                               type="submit" 
                               name="exportReport" 
                               id="exportReport" 
                               value="Export Report" 
                               onmouseover="EPi.ToolButton.MouseDownHandler(this)" 
                               onmouseout="EPi.ToolButton.ResetMouseDownHandler(this)" />
                    </span>
                </div>
            </div>
        }

        @if (Model.Pages != null && Model.Pages.Count > 0)
        {
            <div class="epi-floatLeft epi-marginVertical-small">
                Number of Hits: @Model.Pages.Count
            </div>
            <div class="epi-contentArea epi-clear">
                <div>
                    <table class="epi-default epi-default-legacy" 
                           cellspacing="0" 
                           id="FullRegion_MainRegion_ReportView" 
                           style="border-style: None; width: 100%; border-collapse: collapse;">
                        <tr>
                            <th scope="col">Page Id</th>
                            <th scope="col">Page Name</th>
                            <th scope="col">Page Url</th>
                            <th scope="col">Published Date</th>
                        </tr>
                        @foreach (var page in Model.Pages)
                        {
                            <tr>
                                <td style="width: 27%;">@page.ContentLink.ID</td>
                                <td>@page.PageName</td>
                                <td>@Url.ContentUrl(page.ContentLink)</td>
                                <td>@(page.StartPublish.HasValue ? page.StartPublish.Value.ToString("yyyy-MM-dd HH:mm") : "Not published")</td>
                            </tr>
                        }
                    </table>
                </div>
            </div>
        }
    </div>
    <script type="text/javascript">
        document.getElementById("exportReport").onclick = function () {
            document.getElementById("doExport").value = "True";
        };
        document.getElementById("showReport").onclick = function () {
            document.getElementById("doExport").value = "False";
        };
    </script>
</body>
</html>
```

This code has two **Submit** buttons but only one form action; the JavaScript function connected to the click event on the export and search submit buttons updates a hidden value telling the controller to do an export of the data or not.
The action in the controller that handles this looks like this:

```csharp
[HttpPost]
public ActionResult ListPages(FormCollection form) {
  var model = new ExistingPagesReportViewModel {
    PageTypes = ExistingPagesHelper.GetAllPageTypes(),
      SelectedPageType = form["pageType"]
  };

  ExistingPagesHelper.SetPagesForPageTypeName(model);

  var doExport = false;

  if (bool.TryParse(form["doExport"], out doExport) && doExport && model.Pages != null && model.Pages.Count > 0) {
    Export(model.Pages, System.Web.HttpContext.Current.Response);
  }

  return View("Index", model);
}
```

It returns the view if the export values are not **true**.

7. To export the report to Excel, use **EPPlus** and create a function in the same class like this:

```csharp
public void Export(PageDataCollection pagesToExport, HttpResponse response) {
  using(var package = new ExcelPackage()) {
    ExcelWorksheet ws = package.Workbook.Worksheets.Add("pages");

    ws.Cells[1, 1].Value = "PageId";
    ws.Cells[1, 2].Value = "PageName";
    ws.Cells[1, 3].Value = "PageUrl";
    ws.Cells[1, 4].Value = "Published Date";

    ws.Row(1).Style.Font.Bold = true;
    ws.Row(1).Style.Locked = true;

    int row = 2;

    foreach(var page in pagesToExport) {
      ws.Cells[row, 1].Value = page.ContentLink.ID;
      ws.Cells[row, 2].Value = page.PageName;
      ws.Cells[row, 3].Value = Url.ContentUrl(page.ContentLink);
      ws.Cells[row, 4].Value = page.StartPublish.HasValue ? page.StartPublish.Value.ToString("yyyy-MM-dd HH:mm") : "Not published";

      ++row;
    }

    response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    response.AddHeader("content-disposition", string.Format("attachment; filename=pages{0}.xlsx", DateTime.Now.ToString("yyyyMMdd")));
    response.BinaryWrite(package.GetAsByteArray());
    response.Flush();
    response.End();
  }
}
```
