The top bar of the CMS is extensible. A section you write gets a tab beside Edit, Audiences, Releases, Tasks and Reports, and its pages open inside the CMS shell with the same header as Cendia's own.
Use a section for screens your authors work in: a translation queue, an order list, a review board. Configuration an administrator sets once belongs on the Settings menu instead.
There is no registration call and no menu file to edit. The page's location and one attribute are all the CMS needs.
What the CMS does for you
For every Razor Page under your site's Extensions/Sections/ folder, the CMS:
- serves it at
/cms/sections/<folder>/<name>, in lowercase; - draws it inside the CMS shell: top bar, page header, and a sidebar when the section has one;
- lets in Authors and Administrators only. Somebody signed in to your public site is not signed in to the CMS and is turned away. You cannot forget this and you cannot turn it off.
A page that also carries [SectionPage] puts its section on the top bar.
Step 1: create the page
Give the section a folder under Extensions/Sections/ and add a Razor Page to it. A folder's
Index page is served at the folder's address.
YourSite/
Extensions/
Sections/
_ViewImports.cshtml ← ships with the Starter
Translations/
Index.cshtml → /cms/sections/translations
Index.cshtml.cs
Vendors.cshtml → /cms/sections/translations/vendors
The Starter already includes Extensions/Sections/_ViewImports.cshtml. If your site predates it,
create it:
@using Cendia.Extensions
@namespace YourSite.Extensions.Sections
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Do not add a _ViewStart.cshtml or set Layout. The CMS supplies the layout for this folder.
Step 2: put it on the top bar
Give the PageModel [SectionPage]. It takes three things: the section (the tab), the menu
name, and a subtitle.
using Cendia.Extensions;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace YourSite.Extensions.Sections.Translations;
[SectionPage("Translations", "Queue", "Pages waiting for a translator.",
Title = "Translation Queue", Order = 10)]
public class QueueModel : PageModel
{
public void OnGet() { }
}
| Argument | Required | What it does |
|---|---|---|
section |
yes | The tab the page belongs to, as the top bar should read. Matching is case-insensitive. |
menuName |
yes | What the section's sidebar calls the page. |
subtitle |
yes | One line under the page title saying what the page is for. |
Title |
no | The page heading, when it should say more than the menu name. Defaults to the menu name. |
Order |
no | Where the page sits in its section's sidebar. The lowest is the page the tab opens. |
TabOrder |
no | Where the section's tab sits on the top bar. See step 3. |
AdministratorOnly |
no | true keeps the page, and its row in the sidebar, to Administrators. |
A page without a PageModel carries the attribute in the .cshtml instead, with
@attribute [SectionPage(...)]. Use one place, not both.
Step 3: choose where the tab sits
Cendia numbers its own tabs 10, 20, 30… from the left. Your TabOrder places your tab among
them:
| Tab | Number |
|---|---|
| Edit | 10 |
| Audiences | 20 |
| Releases | 30 |
| Tasks | 40 |
| Reports | 50 |
| Settings | 60 |
TabOrder |
Result |
|---|---|
25 |
Between Audiences and Releases |
45 |
Between Tasks and Reports |
| (left out) | After Reports, before Settings |
65 |
After Settings |
TabOrder belongs to the section, so set it on one page. If several pages of a section set it,
the lowest is used. If your tab has the same number as a Cendia tab, Cendia's comes first. Two tabs
of your own with the same number are sorted by name.
The tab is in the same place for everyone who sees it, whichever of its pages they may open.
TabOrder is not Order. Order arranges the pages inside the section; TabOrder arranges
the section on the top bar.
Step 4: add more pages
Every page naming the same section shares its tab.
- One listed page: the tab opens it, and there is no sidebar.
- Two or more: the section gets a sidebar listing them by
Order, then by menu name.
Step 5: write the page body
Do not write a title or subtitle in the page. The CMS draws both from [SectionPage]. Write only
what goes beneath them.
@page
@model YourSite.Extensions.Sections.Translations.QueueModel
@section PageActions {
<button type="button" class="btn btn-primary btn-sm">Send All</button>
}
<table class="table table-sm">…</table>
@section PageActions, ViewData["CmsWidth"], forms, handlers and tag helpers all work as they
do on a Settings page.
Who sees what
- The tab is drawn for anyone who can open at least one of the section's pages.
- An
AdministratorOnlypage is refused to an Author and left out of their sidebar. If that leaves them one page, they get no sidebar. - Content rights are yours to apply. The CMS decides who may open the page. What the page
lists about pages, blocks or media should be filtered by what the author may read, with
IAccessControl.
Detail pages
A page in the section's folder without [SectionPage] is still served and still protected,
but it is not listed. Use this for a screen reached from a listed page. It keeps the section's
tab lit, and sets its own header:
@{
ViewData["Title"] = Model.Job.Name;
ViewData["Subtitle"] = "One translation job and the pages in it.";
}
Rules to know
- Addresses are lowercase and under
/cms/sections/. Link to a section page by that address rather than withasp-page, since the page is not in thecmsarea. Nothing of Cendia's is served under/cms/sections/, so an upgrade cannot take an address you are using. - Name the folder after the section. Two sections cannot share a folder name, including one that arrives in a package.
- It lives in Extensions, not Pages.
Pages/is your public site. - The Starter has a worked example.
Extensions/Sections/Translations/SectionPage.Example.csand.cshtmlship commented out.
From a package
A NuGet package adds a section the same way: a Razor Class Library with its pages under
Extensions/Sections/<Folder>/, and one call from the site so the package's pages are served.
public static IServiceCollection AddAcmeTranslations(this IServiceCollection services) =>
services.AddCendiaExtension<AcmeTranslationsMarker>();
The same call is where a package names its editor scripts. See Adding tree actions.