A template is a Razor page named after the type it renders, with CmsPageModel<T> as its model.
@model CmsPageModel<StandardPage>
<h1>@Model.Content.Heading</h1>
@Cms.Html(Model.Content.Body)
@await Html.RenderZoneAsync(Model.Content.BodyBlocks)
Model.Content is your class, typed. Nothing casts, and nothing looks a field up by string.
Escaping is your decision, and the type tells you which
string and TextArea are plain text: render them normally and Razor escapes them. HtmlString
is author markup and must go through Cms.Html(...), which writes it unescaped.
The type of the property is the whole signal. If you find yourself calling Html.Raw on a
string field, the field is the wrong type.
Content zones
A ContentZone is not rendered field by field. It is a list of blocks an author composed, in the
order they chose:
@await Html.RenderZoneAsync(Model.Content.BodyBlocks)
Each block renders through its own partial — StandardBlock through
/Pages/Shared/Blocks/StandardBlock.cshtml — by the same class-name convention page types use.
The zone is the load-bearing part of a page type. It is what makes every block feature reachable at all, forms included. A page type without one can render its own fields and nothing else, which is why the Starter ships one from the start.
Blocks that hold blocks
A block type can have its own zone. That makes it a container: an accordion that holds accordion
items, or a row that holds columns. Name what the zone accepts with [AllowedContentTypes]:
[ContentTypeId("cendia.block.accordioncontainer")]
[ContentTypeDisplay("Accordion Container", "This is a container for accordion items", "block")]
public sealed class AccordionBlock : BlockType
{
[FieldId("accordioncontainer.zone")]
[FieldDisplay("Accordion items", "Content", "The items stacked inside this accordion.")]
[AllowedContentTypes(typeof(AccordionItemBlock))]
public ContentZone? Items { get; set; }
}
The container's New Block dialog now offers only accordion items. Publishing is refused if the zone holds anything else. That restriction stops something else getting into the container. It does not stop an accordion item turning up somewhere else.
Mark the item [ContainerOnly] when it has no meaning outside its container:
[ContentTypeId("cendia.block.accordionitem")]
[ContentTypeDisplay("Accordion Item", "This is an accordion item that goes into an accordion container", "block")]
[ContainerOnly]
public sealed class AccordionItemBlock : BlockType { … }
A [ContainerOnly] type is offered only by a zone whose [AllowedContentTypes] names it:
- The New Block modal in Shared and Local never lists it.
- A page's "any block" zone does not list it.
- Publishing is refused when the type is placed anywhere else, including by paste or by dragging it in from the panel. The error names the container it belongs in.
Authors therefore create the item from inside the container, and it lands in that container's Local folder.
The attribute is not implied by an allow list. Arena's Content Container accepts Content Items
and Media Items, and both also render on their own. Neither is [ContainerOnly]. Only the block's
developer knows whether it can stand alone, so the attribute states it.
Design Blocks have the same switch. Under Settings > Designer > Blocks, the block's ··· menu has a Container only option, and the block's header shows container only while it is on. When the AI plans the child blocks for a container's zone (the items of an accordion, say), it creates them with the option already on. Turn it off for a child that should also be usable on its own.
Items that already exist are not changed, and wherever they are filed, they still work inside their container. The exception is an item that was placed directly on a page before the type was marked. It still renders, but the page cannot be published until the item is moved into a container.
Handling absence
Three fields can point at nothing, and a template that assumes otherwise breaks the day an author clears one:
MediaReference— the target may have been deletedContentSelector— likewise, and it may point at an unpublished pageLink— has three shapes, and one of them is empty
Resolve, check, and have something to render when the answer is nothing.
Images that are cropped: the focal point
A template rarely shows a picture whole. It gives it a box, such as a banner strip, a square card
or a 16:9 tile, and the stylesheet fills the box with object-fit: cover, trimming what does not
fit. Left alone the browser trims evenly from every side, which is how a portrait ends up as a
forehead in a banner.
An author can say which part of an image has to stay in frame: Focal Point… on the image's menu in the Media panel. It is stored on the image, once, as two fractions (0 to 1 across, 0 to 1 down), so one decision covers every place the image is used.
A reference hands it to the template with the image's address. Put it on any image your stylesheet crops:
<img src="@Model.Hero?.Url" alt="" class="banner__image" style="@Model.Hero?.FocalStyle" />
.banner__image { width: 100%; height: 100%; object-fit: cover; }
FocalStyle is object-position: 54% 31%; when the image has a focal point and null when it
does not. Razor leaves a null attribute off, so an image without one is centred as before, or
positioned however your stylesheet says.
| On a reference | Gives |
|---|---|
FocalStyle |
object-position: 54% 31%; for an <img>'s style, or null |
ObjectPosition |
54% 31% on its own, for background-position or a style you are composing, or null |
FocalPoint |
the two numbers, X and Y, if you need to calculate with them |
The same three are on what cms.Resolve(...) returns, for a template that resolves a reference
itself.
Two things to know:
- Use it where you crop, and not where you fit. On an image fitted whole into a box
(
object-fit: contain, an icon or a logo) there is nothing to trim, and the same property would slide the picture toward a corner of its box. - It costs nothing to read. It comes from the lookup that already finds the image's address, so a page with a dozen cropped images makes no extra queries for them.
The focal point is cleared when an image's file is replaced or edited, because a point chosen on one picture means nothing on another. Images placed inside a rich-text body are not cropped by a template, so the focal point does not apply to them.
Layouts
Pages/_ViewStart.cshtml sets the layout once, so a new content type needs a .cshtml and
nothing else. Override it in a template that wants different chrome:
@{ Layout = "_BareLayout"; }
A layout has no typed model, so what the request resolved to is reached through the context:
Context.CmsCulture(), Context.CmsPath(), Context.CmsCanonical(), Context.CmsAlternates()
and Context.CmsRobots(). All are empty on a request that is not a rendered page, which is what
makes them safe to call from a layout shared with pages the CMS never routed.
Site Scripts
Administrators can paste scripts for each website under Settings > Websites: analytics, a tag manager, a chat widget. The CMS stores them. Your layout decides where they render, with one call per box:
<head>
…your own tags…
@await Html.CmsHeadScriptsAsync()
</head>
<body>
@await Html.CmsBodyStartScriptsAsync()
…
@await Html.CmsBodyEndScriptsAsync()
</body>
The website is chosen by host name, not by route, so a layout shared with pages the CMS never routed (sign-in, search results) renders them too. Each call renders nothing when its box is empty and nothing in preview. The first call on a request makes one database query; the others reuse it.
A layout without these calls never renders the boxes, and an administrator's saved script silently goes nowhere. The starter layout includes all three, so keep them unless you mean to take the feature away.
The markup is written verbatim. If your site asks for cookie consent before loading third-party
scripts, gate these calls on it. That decision belongs to the site, not the CMS. A site that sends
a strict Content-Security-Policy needs to allow the vendor's origins and inline snippets. The
calls do not add a nonce.