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:
- A literal HTML tag — rendered as-is (attributes resolved, children recursed).
- 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.
- 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). - A Blazor component reference — an element carrying
namespaceandassemblyattributes 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: • © ↓ é ½
… « “ ‎ ‘ — ·
– ñ → » ” ®
› ’ ­ ™ ‍, plus XML's own &
< > " '. Any other named entity (€,
×, á) makes the markup fail to parse (VC1001); use a
numeric reference (€) 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.xin text) is written into the page as HTML, unencoded — whatever the parameter'stype,plain-textincluded. So<b>becomes a real<b>tag. That is howformatted-textandhtmlvalues are stored: as escaped text, e.g.<parameter name="intro">Welcome <b>home</b></parameter>. - Attributes (
title="@block.x") are HTML-encoded on output, so any decoded character is safe there.
| To show | In text | In an attribute |
|---|---|---|
& |
& |
& |
< |
&lt; (< makes a tag) |
< |
> |
> or > |
> or > |
" / ' |
as is | " / ', or the other quote style |
the text & |
&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.propand@fn(...)are expressions anywhere in text and attributes, values included. Inside a block, a bare@namematching one of its parameters is replaced by that value, and an unknown@word(...)renders asfalse. An email address (joe@example.com) or@ 5pmis 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) withpattern-message,inline;toolbar(bold italic link nowrap ...) forformatted-textandhtml;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-selectalso takesseparator,min-count,max-count. Witheditor="segmented", an option'siconshows 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, andseparator(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:
- If the definition's
<parameter>has adefaultattribute, or has<option>children, use thedefaultvalue (empty string if nodefaultgiven but options exist). - 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/filtervalue=). Plain@object.propertyreferences 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 ofand/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 impliciteqfilters (<for-each in="menus" name="@name">filters wherename= 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.