Vision Content OS rendering language

Reference for the XML-based templating language used to write Vision pages, blocks and page layouts.

Mental model

A page (or a block's or page layout's markup) is an XML document, so it must be well-formed XML with exactly one root element — several sibling elements at the top level are rejected on save. Wrap them in one root. Every element name is either:

  1. A literal HTML tag — rendered as-is (attributes resolved, children recursed).
  2. A block reference — the element's name matches a registered block or page-layout definition, and rendering substitutes that definition's own markup in its place, passing the reference element's attributes/children in as parameters and slot content.
  3. A rendering-language keyword — slot, parameter, if/else, switch/case/default, for-each, template, view, environment, random/choice, section-content/section-outlet, use-slot, data-source/filter — special forms handled directly by the renderer (documented below).
  4. A Blazor component reference — an element carrying namespace and assembly attributes instantiates a real .NET Razor component instead of emitting an HTML tag.

There is no separate "compile" step: this is walked and rendered directly from the parsed XElement tree on every request.

Predefined entities

Every markup fragment (page content, block markup, page-layout markup) is parsed with an internal <!DOCTYPE block [...]> prologue that predefines these named entities: &bull; &copy; &darr; &eacute; &frac12; &hellip; &laquo; &ldquo; &lrm; &lsquo; &mdash; &middot; &nbsp; &ndash; &ntilde; &rarr; &raquo; &rdquo; &reg; &rsaquo; &rsquo; &shy; &trade; &zwj;, plus XML's own &amp; &lt; &gt; &quot; &apos;. Any other named entity (&euro;, &times;, &aacute;) makes the markup fail to parse (VC1001); use a numeric reference (&#8364;) or the character itself. You never declare your own DOCTYPE.

Escaping

The XML parser decodes entities once. After that:

  • Text (an element's text, or a parameter value used as @block.x in text) is written into the page as HTML, unencoded — whatever the parameter's type, plain-text included. So &lt;b&gt; becomes a real <b> tag. That is how formatted-text and html values are stored: as escaped text, e.g. <parameter name="intro">Welcome &lt;b&gt;home&lt;/b&gt;</parameter>.
  • Attributes (title="@block.x") are HTML-encoded on output, so any decoded character is safe there.
To show In text In an attribute
& &amp; &amp;
< &amp;lt; (&lt; makes a tag) &lt;
> > or &gt; > or &gt;
" / ' as is &quot; / &apos;, or the other quote style
the text &amp; &amp;amp;amp; &amp;amp;
  • Child elements in a supplied <parameter> are dropped: <parameter name="heading"><b>Big</b></parameter> renders just "Big". Escape the markup instead (above). Only <image-parameter>, <anchor-parameter> and responsive <target>s are structural.
  • @ has no escape. @block.x, @obj.prop and @fn(...) are expressions anywhere in text and attributes, values included. Inside a block, a bare @name matching one of its parameters is replaced by that value, and an unknown @word(...) renders as false. An email address (joe@example.com) or @ 5pm is left alone unless the word after @ names a data object.
  • CDATA is text like any other: rendered unencoded, expressions resolved.
  • Whitespace in values is kept (not trimmed), except image values and built-ins like extraClasses.

Block definitions and page layouts

A block or page layout has a name, a type (block or page-layout), markup (the XML fragment), styles (raw CSS, scoped by convention to .{name}), and optionally static asset files (see below).

A block/layout's markup must have a single root element, and it can use any root element name — it's rendered literally as that tag (commonly <block>, but it could just as well be <div> or <section>; <block> is just a naming convention, not a keyword):

<!-- blocks/block/has-block-class.xml -->
<block>
  <p>content</p>
</block>
<!-- referencing it in page/parent content -->
<has-block-class></has-block-class>

renders:

<has-block-class class="block" data-block-id="<generated-guid>">
  <p>content</p>
</has-block-class>

Split pages into blocks

Don't put a whole page in one block. Break it into one block per section (e.g. site-header, hero-banner, feature-grid, cta-section, site-footer) and compose them in the page content. Reuse an existing block when one fits, and make repeated patterns (cards, buttons) their own blocks with parameters. Smaller blocks are easier to edit, reorder and reuse across pages.

A page's content (its root and every slot, the layout's or a block's) should hold only block references. HTML there is a warning (VC2004): people can only edit it as markup, with no fields or checks. Put it in a block whose parameters carry the editable values.

Naming convention: always two or more kebab-case words

A block or page-layout's registered Name is looked up purely as an XML element-name string — there's no separate namespace distinguishing a block reference from a literal HTML tag, so a block named e.g. header or table would silently shadow (or be confused with) the literal HTML element of the same name. Every standard HTML element name is a single word; hyphenated names are reserved (the same convention the web platform itself enforces for custom elements, for exactly this future-proofing reason).

Always name blocks and page layouts with two or more kebab-case words (site-header, hero-section, plane-card, admin-shell), never a bare single word (header, hero, card, shell). Don't start a name with core-: that prefix is for system blocks. This guarantees no collision with any current or future standard HTML tag, and makes a block reference visually unmistakable from a literal element wherever it's used.

Give each one a human-readable display text too ("Hero banner"): it's what people see in Insert Block and Block Properties. One that's missing or repeats the name is a warning (VC2005).

Gotcha: the check the renderer uses to decide whether a reference is "directly in editable content" (see class="block" / data-block-id just below) — tests whether the element's tag name contains a hyphen at all, as its proxy for "already a marked non-editable child-block reference". Since this naming convention mandates a hyphen in every block/layout name, class="block" and data-block-id never get auto-injected for a fresh (never-before-attributed) reference, regardless of nesting — confirmed empirically (rendering a page of hyphenated-name block references adds neither attribute anywhere). In practice this only matters if something depends on @block.id, or an editor UI that overlays on class="block"/data-block-id — pick a static id instead when a block is only ever used once per page, or supply data-block-id explicitly on the reference yourself (the injection only fires when the attribute is missing, but nothing stops an author from setting it up front).

System blocks (core-*)

Every site has the system blocks: the basics for building a page, named core-*. Use one before making a look-alike block. A site can customize one for its design by saving a block of the same name (Customize in the block editor or the Blocks list). Its markup and styles can change freely, but it has to keep the system block's standard parameters (same name and type, not newly required) and named slots, because pages, other sites and agents rely on them (VC2091-VC2094). Removing the customization goes back to the built-in default. Customizations export and import with the site's other blocks. Other core- names are reserved (VC2090).

Group Block Display text Standard parameters Slots
Site chrome core-site-header Site header logo (image), logoLink (url), siteName (plain-text) nav, actions
Site chrome core-site-footer Site footer copyright (plain-text) content
Site chrome core-nav-menu Navigation menu menuName (plain-text, required), label (plain-text), orientation (single-select) —
Site chrome core-breadcrumbs Breadcrumbs separator (plain-text), includeHome (boolean), homeLabel (plain-text) —
Site chrome core-skip-link Skip to content link target (plain-text, required), label (plain-text, required) —
Primitives core-button Button label (plain-text, required), href (url), variant (single-select), size (single-select), openInNewTab (boolean) —
Primitives core-link Link label (plain-text, required), href (url), openInNewTab (boolean) —
Primitives core-heading Heading text (formatted-text, required), level (single-select) —
Primitives core-text Text text (html: every text toolbar action) —
Primitives core-image Image image (image), aspectRatio (single-select), href (url), caption (plain-text) —
Primitives core-icon Icon icon (single-select, required), label (plain-text) —
Primitives core-divider Divider size (single-select) —
Primitives core-spacer Spacer size (single-select) —
Layout core-section Section width (single-select), backgroundColor (color), backgroundImage (image) content
Layout core-columns Columns layout (single-select), gap (single-select) column-1, column-2, column-3
Layout core-grid Grid columns (integer), gap (single-select) items
Layout core-card Card image (image), heading (plain-text, required), text (formatted-text), href (url) footer

get_block (MCP) shows a system block's options and its standard parameters and slots.

<core-section width="contained">
  <slot name="content">
    <core-heading text="Our services" level="h2" />
    <core-columns layout="1-1">
      <slot name="column-1"><core-text text="..." /></slot>
      <slot name="column-2"><core-image><parameter name="image"><image-parameter target="img" src="/team.jpg" alt="Our team" /></parameter></core-image></slot>
    </core-columns>
    <core-button label="Get in touch" href="/contact" />
  </slot>
</core-section>

class="block" / data-block-id

When a block reference sits directly in editable content (not nested inside another block's own markup, and not itself a page layout), the renderer auto-injects class="block" and a generated data-block-id GUID onto the reference element (unless already present) before substituting the definition's markup inside it. This is how the block editor identifies which DOM subtree corresponds to which content-authored block instance. Reference @block.id inside the definition to get at this id (see Expressions below):

<container data-block-id="03d89e57-...">
  <div id="c_@block.id"> <!-- becomes id="c_03d89e57-..." -->
    <slot name="slot1"></slot>
  </div>
</container>

Child blocks that appear inside a parent block's own markup (not inside a <slot>) are rendered but are not made editable — no class="block", no data-block-id is added, and they still receive parameters normally:

<!-- parent-block definition -->
<block>
  <p>parent block</p>
  <child-block></child-block>          <!-- rendered, but not editable -->
</block>

Templates (local, inline block definitions)

<template name="..."> inside a block or page's markup registers a throwaway block definition scoped to the current render, usable exactly like any other block reference for the rest of that markup (typically paired with a <slot> acting as a repeater, since it isn't discoverable outside this render):

<ul class="fancy-list">
  <template name="list-item">
    <parameter name="value" type="formatted-text"></parameter>
    <li>@block.value</li>
  </template>
  <slot name="list-items"></slot>   <!-- filled with <list-item> references from page content -->
</ul>

Slots

<slot name="..."> marks an insertion point. In a block/layout's own definition markup, a slot is a placeholder to be filled by whatever the consuming content puts in the matching-named <slot> inside the reference element. If the reference supplies no matching slot, the slot renders empty (or, in the simple case of a leaf value, static default children in the definition markup are not supported — slots don't have defaults).

<!-- two-column block definition -->
<block>
  <div class="container">
    <div><slot name="col-1"></slot></div>
    <div><slot name="col-2"></slot></div>
  </div>
</block>
<!-- reference in content -->
<two-column>
  <slot name="col-1">column 1 content</slot>
  <slot name="col-2"><page-title><parameter name="title">Hi</parameter></page-title></slot>
</two-column>

Slots nest arbitrarily deep through multiple levels of block-inside-block without any special authoring — each level's <slot name="X"> in a definition is filled from the reference element's own <slot name="X"> children, regardless of how many block layers are in between.

An alias attribute on a slot appears to suppress the normal parent-content auto-fill — treat it as an advanced/legacy escape hatch rather than the default pattern.

use-slot

<use-slot name="..."> re-renders whatever was placed in the page's top-level, document-wide slot of that name (tracked separately from per-block slot contents), letting a layout or block pull page-level slot content into a second location on the page.

Parameters (block properties)

A block definition declares its inputs with <parameter>, directly in its root (or a <template>). The declaration says what's stored (type), what's valid, and how Block Properties edits it:

<section class="feature">
  <parameter name="heading" type="formatted-text" required="true" max-length="80" display-text="Heading">Our feature</parameter>
  <parameter name="layout" type="single-select" default="stacked" group="Layout">
    <option value="stacked">Stacked</option>
    <option value="split">Image beside text</option>
  </parameter>
  <parameter name="imageSide" type="single-select" default="left" group="Layout" visibility="eq(@block.layout, 'split')">
    <option value="left">Left</option>
    <option value="right">Right</option>
  </parameter>
  <parameter name="photo" type="image" alt-required="true" />

  <h2>@block.heading</h2>
  <img src="@block.photo" alt="@block.photo.alt" />
</section>

A parameter's value is used as @block.<name> anywhere in the definition (attribute values or text). An element whose whole content is one @block.<name> of a text type is edited inline on the page; inline="false" turns that off.

Make every value someone might change a parameter, and declare it fully: the narrowest type that fits (single-select over free text, image, anchor, url, color, number), a display-text, a default, and the checks that keep values valid (required, max-length, min/max, pattern, alt-required, link-types). Use formatted-text for rich text; keep html for genuinely free-form content, since only its well-formedness is checked. Parameter examples has common patterns.

Types

type Stored Widget
text (default) string text field (editor="textarea")
plain-text string, no markup text field / inline
formatted-text text with b i u s sub sup strong em del a br span only inline, with the toolbar
html any HTML inline, or the HTML editor
number, integer number number field
boolean (alias switch) true-value / false-value switch
attribute-boolean true/false switch; see below
url link link editor
color CSS color swatches (palette) and picker
date yyyy-mm-dd date picker
image image, per target image picker
anchor link with its text link editor
single-select one option select, or segmented buttons
multi-select (alias space-delimited-string) options joined by separator (a space) checkbox-list, multi-select or tags
css declarations (margin-top: 0) CSS editor
responsive-class-set a class per target (breakpoint) segmented buttons or select; see Responsive classes

editor picks another widget the type allows. format is a deprecated alias for type.

Attributes

  • All: name, type, required, default, display-text, help, placeholder, group (fields sharing a panel), order, advanced="true" (the collapsed Advanced group), visibility (an <if> condition on the block's values; a hidden field keeps its value), editor.
  • Text types: min-length, max-length (markup not counted), pattern (.NET regex, whole value) with pattern-message, inline; toolbar (bold italic link nowrap ...) for formatted-text and html; rows.
  • number, integer, date: min, max, step.
  • boolean: true-value, false-value.
  • single-select, multi-select: <option value="v">Label</option> children, source (choices from the site, e.g. articleTags), allow-custom (values that aren't options). multi-select also takes separator, min-count, max-count. With editor="segmented", an option's icon shows instead of its label; see Option icons.
  • image: targets="img desktop tablet mobile", and picker filters: tags / upload-tags (comma-separated; {target} is the target being picked), search, folder, extensions, min-width, min-height, aspect-ratio="16:9", alt-required. focal-point="true" lets editors choose a focal point; see Images.
  • color: palette (comma-separated), allow-custom, allow-alpha.
  • url, anchor: link-types="Page Article External Email Phone Fragment".
  • responsive-class-set: <target> children, and separator (what joins the targets' classes, a space by default); see Responsive classes.

Option icons

A segmented editor (single-select, or a responsive-class-set's targets) shows an option's icon instead of its label. An option with an icon needs a label (VC2070): it's the icon's tooltip and what screen readers say.

<parameter name="headlineAlignment" type="single-select" editor="segmented" default="left" display-text="Headline alignment">
  <option value="text-start" icon="align-left">Left</option>
  <option value="text-center" icon="align-center">Center</option>
  <option value="text-end" icon="align-right">Right</option>
</parameter>
<h2 class="@block.headlineAlignment">...</h2>

An unknown icon shows the label, with a warning (VC2078). The icons:

Kind Icons
Text alignment align-left, align-center, align-right, align-justify
Vertical alignment align-top, align-middle, align-bottom
Horizontal alignment justify-start, justify-center, justify-end, justify-between
Media placement media-left, media-right, media-top, media-bottom, media-background
Layout columns-1, columns-2, columns-3, columns-4, rows, grid
Width width-contained, width-full
Shape shape-square, shape-rounded, shape-circle
Aspect ratio ratio-landscape, ratio-square, ratio-portrait
Theme theme-light, theme-dark
Direction arrow-up, arrow-down, arrow-left, arrow-right
Visibility visible, hidden, none
Devices phone, phone-landscape, tablet, laptop, desktop, wide

Parameter groups: parameter-group

<parameter-group> puts the declarations inside it in one group, named by its name. A group or advanced on a parameter inside it is ignored, with a warning (VC2073). It sits where declarations do, and its attributes say how the group shows in Block Properties: expanded="false" starts it collapsed (it's true by default). Two groups with the same name are a warning (VC2074): their parameters share the group, with the first's attributes.

<parameter-group name="Text Fields" expanded="false">
  <parameter name="eyebrow" display-text="Eyebrow" />
  <parameter name="subheading" display-text="Subheading" />
</parameter-group>

A misused attribute or value is an error (VC2060-VC2071); a parameter without display-text is a warning (VC2072). The old <option description="Label">value</option> form still works but is deprecated (VC2081).

Values and fallback

A reference supplies a value two ways:

<!-- as an attribute (a declared parameter, or a built-in) -->
<my-block extraClasses="foo"></my-block>

<!-- as a child <parameter> element (works even for XML-valued parameters) -->
<my-block>
  <parameter name="extraClasses">foo</parameter>
</my-block>

Values are checked against the declaration in Block Properties, in markup and in MCP saves. A value with an expression (@...) isn't type-checked.

Fallback order for @block.X when the reference doesn't supply a value:

  1. If the definition's <parameter> has a default attribute, or has <option> children, use the default value (empty string if no default given but options exist).
  2. Otherwise, fall back to the <parameter> element's own text content in the definition (its "example"/default body).

Parameters pass straight through nested block boundaries: a parent block can forward its own @block.X value into a child block reference's parameter, including the special id parameter (@block.id) and same-named parameters at different nesting levels (each level's @block. scope is independent, so reusing a parameter name like extraClasses at 3 nested levels does not collide).

Built-ins: extraClasses, extraCss, layoutClasses, layoutStyles, anchorId

Every block has these, in Block Properties' Advanced group (anchorId first):

Name Type Goes on
extraClasses multi-select (tags) the definition root's class
extraCss css the definition root's style
layoutClasses multi-select (tags) the block's own element's class
layoutStyles css the block's own element's style
anchorId text (letters, digits, -, _) the block's own element's id

The block's own element is the reference's element (<my-block class="block">), the one its container lays out: grid columns, flex sizing, order. anchorId gives in-page links (#pricing) somewhere to scroll to.

Unless the definition uses @block.<name> itself, the renderer adds each one for it: classes and styles on the end of the existing class and style, and anchorId as the id. When the root is <if>, <switch>, <for-each> or <view>, extraClasses and extraCss go on the reference's element too.

A declaration of any of them is merged onto the default: only what's different, and its type can't change.

<parameter name="extraClasses">
  <option value="pt-0">No padding top</option>
</parameter>
<parameter name="extraCss" advanced="false" />

Boolean attributes

HTML boolean attributes (open, disabled, checked, ...) need special handling: their presence — with any value — means true; only their total absence means false. Declare the parameter with type="attribute-boolean":

<!-- definition -->
<parameter name="open" type="attribute-boolean" required="false"></parameter>
<details name="test" open="@block.open"></details>

If the resolved value parses as boolean true, the renderer emits the bare attribute (open="open"); otherwise it omits the attribute entirely.

Deprecated: data-parameter

data-parameter="name" (with data-format) still shows a parameter without a declaration, but is deprecated (VC2082), with a fix that converts it:

<!-- before -->
<h1 data-parameter="title">Welcome</h1>

<!-- after -->
<parameter name="title" type="formatted-text">Welcome</parameter>
<h1>@block.title</h1>

Expressions

Two evaluation contexts exist:

  • Value context (ordinary text/attribute content, switch/case/filter value=). Plain @object.property references are substituted directly; a function call written inline needs a leading @, e.g. @if(cond, 'a', 'b'), @coalesce(...).
  • Condition context (<if condition="...">, and the arguments of and/or/not). The whole string is the function call, so no leading @ on the function name itself — e.g. condition="not(empty(@page.X))" — though @-prefixed data references inside the arguments still need their @.

Data references (@object.property)

Prefix Source Examples
@block.X The current block's own parameters @block.extraClasses, @block.id
@page.X The current page @page.Title, @page.Url, @page.BrowserTitle, @page.BreadcrumbCategoryDisplayText, @page.BreadcrumbCategoryUrl, @page.FeaturedImageUrl, @page.MenuName, @page.MenuDisplayText, @page.LastModifiedDate, @page.Platform
@site.X Site settings @site.CdnUrl, @site.FranchiseCdnUrl, @site.SiteName, @site.CommitSha
@franchise.X Franchise data object @franchise.PhoneNumber
@now.X Current time @now.Year
@route.X Current ASP.NET route values @route.pageRoute
@me.X The signed-in visitor (blank strings if anonymous) @me.DisplayName, @me.EmailAddress, @me.IsAuthenticated
@<contentType>.X Per-content-type CDN URL override (registered in site settings) @articles.CdnUrl
bare @Field (no dot) The current for-each loop item's field, falling back to a same-named block parameter if not in a loop @Title, @PermaLink, @href
@subCollection (bare, referenced as in="@subCollection") A nested child collection on the current loop item <for-each in="@subMenus">

All data objects are flat string dictionaries — there is no nested object graph beyond the child collections used by nested for-each.

Any resolved value that looks like an <img>/CSS url() reference pointing at a configured CDN host is automatically rewritten (converted to .webp, given a CDN transformation segment) — this happens transparently, so authors never manually invoke it.

String functions (value context, @fn(args))

Function Behavior
@coalesce(a, b, ...) First non-blank resolved arg
@if(cond, whenTrue, whenFalse) Ternary; cond resolved then parsed as boolean
@lower(x) / @upper(x) Case conversion
@replace(src, old, new[, ignoreCase]) String replace

Unknown functions fall back to resolving the whole raw expression as a plain value. Arguments can be quoted literals ('a', "a"), nested function calls, or @-prefixed data references.

Boolean functions (condition context)

Function Behavior
not(x) Negate
empty(x) True if blank/whitespace; if x is a bare @childKey and there's current loop data, checks that field or child collection
and(a, b, ...) / or(a, b, ...) All/any true
eq / ne,neq Equality (numeric if both parse as numbers, else boolean, else case-sensitive string)
gt / lt / gte,ge / lte,le Numeric if possible, else case-insensitive string compare
contains / startswith / endswith Case-insensitive substring checks
in(x, a, b, ...) / notin(...) Membership test (numeric if possible, else boolean, else case-insensitive string)

String functions can be nested as arguments, without the @: eq(lower(@route.code), 'ct') for a case-insensitive comparison.

<if condition="eq('a', 'a')"> ... </if>
<if condition="not(empty(@page.BreadcrumbCategoryDisplayText))"> ... </if>
<if condition="endsWith(@PermaLink, @page.Url)"> ... </if>

Control flow

if / else

<if condition="empty(@block.featuredImage)">
  <p>fallback</p>
  <else>
    <p class="@block.featuredImage">@block.featuredImage</p>
  </else>
</if>

<if> with no matching <else> simply renders nothing when false.

switch / case / default

<switch value="@page.BreadcrumbCategoryDisplayText">
  <case value="blah"><p>case: blah</p></case>
  <case value="Unit Test"><p>case: Unit Test</p></case>
  <default><p>case: default</p></default>
</switch>

Case values match the switch value exactly (case-sensitive). No match and no <default> renders nothing.

environment

<environment include="dev,Development">
  <p>This is content for the dev environment</p>
</environment>

Renders its children only if the current environment name is in the comma-separated include list (trimmed, case-sensitive).

view

<view platform="editor"><h1>editor view</h1></view>
<view platform="frontend"><h1>frontend view</h1></view>

Renders only the <view> whose platform attribute exactly matches (case-sensitive) the current render's platform parameter ("frontend" normally; "editor" inside the block editor). Any <view> for a non-matching platform renders nothing.

random / choice

<random variable="randomImage">
  <choice>img/CTA1.webp</choice>
  <choice>img/CTA2.webp</choice>
  <choice>img/CTA3.webp</choice>
</random>
<img src="@site.CdnUrl/@block.randomImage" />

Picks one <choice> at render time and stores it as if it were a block parameter of that name (@block.randomImage).

Repetition: for-each

<for-each in="pages">
  <data-source page-size="100">
    <filter column="Tags" operator="contains" value="@block.tags" />
  </data-source>
  <li><a href="@PermaLink">@Title</a></li>
</for-each>

in="<name>" looks up a registered data source (built-in names: pages — all published page content; staticFiles — a block/layout's own static asset files, see below; sites can register their own). Inside the loop body, bare @Field references bind to the current item's fields.

Filters can be supplied three ways, all equivalent, and are combined:

  • Unknown attributes directly on <for-each> become implicit eq filters (<for-each in="menus" name="@name"> filters where name = the resolved value).
  • <filter column="..." operator="..." value="..." /> elements directly inside <for-each>.
  • <filter> elements inside a nested <data-source page-size="N"> element (also where you set page size).

Nested loops work by referencing a child collection with a leading @:

<for-each in="menus">
  <a href="@href">@title</a>
  <if condition="not(empty(@subMenus))">
    <ul>
      <for-each in="@subMenus">
        <li><a href="@href">@title</a></li>
      </for-each>
    </ul>
  </if>
</for-each>

in="@subMenus" reads subMenus off the current loop item, rather than looking up a registered data source by name.

Sections (section-content / section-outlet)

Independent of the slot/block system — used for things like a page layout's <head> needing content contributed from elsewhere (block styles, per-page styles, meta tags, the preview banner). A page-layout or block declares an outlet; content elsewhere on the page contributes into it by name:

<!-- page-layout markup -->
<html>
  <head><section-outlet name="styles"></section-outlet></head>
  <body><slot name="body"></slot></body>
</html>
<!-- page content -->
<section-content name="styles">
  <!-- arbitrary markup, comments included -->
</section-content>
<slot name="body">...</slot>

This is how the renderer itself injects block/page-layout CSS (block-styles), meta tags (meta-tags), and the preview banner (preview-banner).

Page layouts

A page's root element name is looked up against registered page-layout definitions. If found, that layout's own markup is rendered as the page's outer shell, with the page's own <slot name="..."> content filling the layout's matching-named slots (same mechanism as block slots) and the page's own root-element attributes/parameters available as @block.X inside the layout. If no layout matches, a synthetic default layout consisting of a single <slot name="body"></slot> is used — meaning any page-root element name with no matching layout still renders, with all of its content expected inside one <slot name="body">.

<!-- page content, root name "classic-page" -->
<classic-page>
  <slot name="body"><p>page content</p></slot>
</classic-page>
<!-- page-layout "classic-page" -->
<div class="layout">
  <image-banner></image-banner>
  <slot name="body"></slot>
</div>

Layout inheritance: a page-layout's own root element name can itself match another page-layout, chaining indefinitely. The child layout's matching-named <slot> elements override/replace the parent layout's slots of the same name; anything the child doesn't override falls through to the parent's markup. Both layouts' styles are concatenated (parent, then child).

Legacy <page> root: the renderer rewrites a literal <page> root element to <default> and, if there's no explicit <slot name="body"> already, wraps all of the page's existing content into one implicit <slot name="body">, resolving against the synthetic default layout described above. New pages should name an explicit page-layout with an explicit <slot name="body"> instead.

Images

Declare type="image" and use its members: @block.photo is the img target's src, @block.hero.<target> a target's src, @block.hero.alt its alt text. An empty src, or background-image: url(''), is dropped.

<parameter name="photo" type="image" alt-required="true" />
<img src="@block.photo" alt="@block.photo.alt" />

<parameter name="hero" type="image" targets="desktop mobile" tags="Homepage Hero, {target}" />
<div class="hero-desktop" style="background-image: url('@block.hero.desktop')"></div>
<div class="hero-mobile" style="background-image: url('@block.hero.mobile')"></div>

A value is one <image-parameter> per target:

<parameter name="hero">
  <image-parameter target="desktop" src="/img/a.jpg" />
  <image-parameter target="mobile" src="/img/b.png" alt="..." />
</parameter>

With focal-point="true" on the declaration, editors click the image to choose the part kept in view when it's cropped. It's stored per target as focal="30% 60%", and the renderer adds object-position to an <img src="@block.photo"> and background-position to an element styled with url('@block.hero.desktop'). Without focal-point, a stored focal is ignored.

The legacy free-text form (desktop: /img/a.jpg; mobile: /img/b.png;, or a bare URL for img) still works.

Responsive classes

A responsive-class-set chooses a class per target, so a block can, say, set its headline size per screen size. Its targets are the site's breakpoints (Settings > Breakpoints: mobile, tablet and desktop unless the site changes them, or Bootstrap's or Tailwind's). Each <target> has its own options; default is the choice when none is made, or inherit:

<parameter name="headlineSize" type="responsive-class-set" display-text="Headline size">
  <target name="mobile" default="s">
    <option key="s" value="fs-4">S</option>
    <option key="m" value="fs-3">M</option>
    <option key="l" value="fs-2">L</option>
  </target>
  <target name="tablet" default="inherit">
    <option key="s" value="fs-md-4">S</option>
    <option key="m" value="fs-md-3">M</option>
    <option key="l" value="fs-md-2">L</option>
  </target>
  <target name="desktop" default="inherit">
    <option key="s" value="fs-lg-4">S</option>
    <option key="m" value="fs-lg-3">M</option>
    <option key="l" value="fs-lg-2">L</option>
  </target>
</parameter>
<h2 class="@block.headlineSize fw-bold">...</h2>

An option's value is the class (or classes) it renders. key is optional; when one option has a key every option needs one, and the key is what's stored, so classes can change without changing pages. Without keys the value is stored.

@block.headlineSize is every target's class joined by separator, so it belongs in a class attribute (VC2077 elsewhere). @block.headlineSize.tablet is one target's class. A target set to inherit renders no class: with mobile-first CSS, the target before it applies.

A value holds only the targets changed from their defaults:

<parameter name="headlineSize">
  <target name="tablet">l</target>
  <target name="desktop">inherit</target>
</parameter>

renders class="fs-4 fs-md-2 fw-bold". As an attribute, it's headlineSize="tablet: l; desktop: inherit".

Anchors

Declare type="anchor" and use @block.cta.href, .target, .text and .link-id:

<parameter name="cta" type="anchor">
  <anchor-parameter href="/contact" target="_self">Get in touch</anchor-parameter>
</parameter>
<a class="butn" href="@block.cta.href" target="@block.cta.target" data-link-id="@block.cta.link-id">@block.cta.text</a>

An empty href or target is dropped.

Deprecated: data-image-parameter, data-anchor-parameter

data-image-parameter (with data-image-target) and data-anchor-parameter still work but are deprecated (VC2083, VC2084), with fixes that convert them, one at a time or the whole block. The old value element names <data-image-parameter> and <data-anchor-parameter> are still read; saving renames them (VC2085).

Blazor components

An element with namespace/assembly attributes bypasses the whole block system and directly instantiates a Razor component , passing every other attribute through as a component parameter (still resolved through the expression evaluator, so @block.X etc. work as component parameter values):

<TestComponent Title="@block.pageName"
  assembly="MyAssembly" namespace="My.Namespace" />

Editor-only variable spans

When rendering for the block editor (platform="editor"), a parameter value containing <span class="variable">@object.Property</span> is left unresolved (rendered verbatim) instead of substituted, so the editor can re-detect and re-edit the inserted-variable token; on the normal frontend platform it resolves like any other expression.

Static asset files (for-each in="staticFiles")

A block or page layout can carry files, usually JS and CSS, each in an optional section (SectionName, e.g. styles-top, scripts-top, scripts-bottom). Files are never loaded automatically: markup has to emit their <script>/<link> tags by looping the staticFiles data source. It lists the current block's own files plus those of every page layout in the current inheritance chain, each with FileName, ContentType, SectionName, BlockName and href.

Put scripts and stylesheets on the page layout, in sections, and loop each section where its files belong. Sections control where and in what order files load — e.g. a library in scripts-top before the page renders, code that reads the DOM in scripts-bottom. Within a section, files load in file-name order (prefix them 01-, 02- when order matters).

<!-- the page layout: the site's own <head> takes section-content name="head" -->
<block>
  <section-content name="head">
    <for-each in="staticFiles">
      <filter column="SectionName" value="styles-top" />
      <link rel="stylesheet" href="/@href" />
    </for-each>
    <for-each in="staticFiles">
      <filter column="SectionName" value="scripts-top" />
      <script src="/@href"></script>
    </for-each>
  </section-content>

  <main><slot name="body"></slot></main>

  <for-each in="staticFiles">
    <filter column="SectionName" value="scripts-bottom" />
    <script src="/@href"></script>
  </for-each>
</block>

A loop in the layout sees only the layout chain's files, not the files of blocks used on the page, so a script attached to a block does nothing unless that block loops staticFiles itself. A file with no section matches no SectionName filter.

href is relative (static/{design}/{kind}/{name}/js/{version}/{file}), so prefix it with /. It changes with every new version of the file, so browsers never run a stale copy.

Practical authoring gotchas

Each of these is a silent failure (a blank/broken page, or a script that just never runs) with no obvious error pointing back at the cause.

This is XML, not HTML — void elements must self-close. <br>, <img>, <input>, <hr>, <meta>, <link> etc. need <br/> (or <br></br>); a bare <br> fails with a parser error like "The 'br' start tag ... does not match the end tag of 'h1'". Same reasoning for boolean attributes with no CMS parameter behind them (hidden, disabled, readonly, novalidate) — write hidden="hidden", not a bare hidden (valid HTML5, invalid XML).

XML comments can't contain --. A decorative <!-- ---------------- Section ---------------- --> divider is invalid XML ('--' cannot appear inside a comment, nor can a comment end in '-') and fails the same way as the void-element issue above. Use <!-- Section -->.

Inline <script> content is still XML text — any literal </>/& inside a JS string (very common when a script builds HTML via template literals, e.g. `<span>${x}</span>`) is parsed as real markup/entities, not JS syntax, unless it happens to still be well-formed by coincidence. The usual break: a bare, unquoted conditional attribute spliced into a template literal, e.g. `<input ${cond ? "checked" : ""} />` — the parser hits $ where it expects an attribute name and fails with "Name cannot begin with the '$' character". Fix: always put the interpolation inside a real, quoted attribute value (e.g. data-checked="${cond}", synced onto the real DOM property in JS afterward) — never let a template literal splice in a whole bare attribute.

An interpolated value inside an attribute gets \uXXXX-escaped if it contains a quote character — confirmed for both ' and ", regardless of which quote style delimits the attribute itself. data-checked="${x !== 'Disabled'}" renders as data-checked="${x !== 'Disabled'}", which is not valid JS syntax at that point (' outside a string/template literal) and throws "Invalid or unexpected token" — this aborts the whole <script>, silently, with no console error since it's a parse-time failure, not a caught runtime exception. Keep quote characters out of any expression that lands inside an HTML attribute value: compute the comparison as a plain variable in ordinary script text first (const enabled = x !== "Disabled";), then interpolate only that variable (data-checked="${enabled}") into the attribute.

Wrap page-content inline scripts in DOMContentLoaded. A page's content is rendered asynchronously, and an inline <script> that runs immediately at its parse position — rather than waiting for DOMContentLoaded — can end up running before, or against a since-replaced version of, the DOM it expects; observed as the script's own later code (event listeners, DOM writes) having no visible effect even though the script element itself is present with valid content and no console error. A block/layout's own static asset files (loaded via <for-each in="staticFiles">, i.e. real <script src> tags — see below) don't have this problem and can run immediately; it's specifically inline <script> text content in page/block markup that needs the wrapper:

<script>
  document.addEventListener('DOMContentLoaded', function () {
    // ... anything that reads or writes the DOM ...
  });
</script>

A block/layout's own root markup tag name is irrelevant to the naming convention above (see "Naming convention" under Block definitions) — e.g. <block> as a wrapper doesn't need to be hyphenated, since it's discarded during substitution (only its children survive) and is never looked up as a reference name itself.