Documentation

Widgets

Content zones

A content zone is a named place on a VitNode page where widgets render. Render one with ContentZone, name it, choose its wrapper and see what an empty zone does.

A content zone is a named place on a page where widgets render. ContentZone takes a list of widgets and renders them through ContentRenderer. What it adds is the name, which the visual editor and editable pages use to place and save widgets in the right spot.

Before you begin

  • Widgets to render, read by your route's loader: a record with a blocks() field, or an editable page, which hands its zones their widgets for you.
  • A widget registry: blocksRegistry from the app's generated src/blocks.gen.ts, or one a plugin page builds with createBlockRegistry. See Where the registry comes from.

Render a zone

The newsroom plugin's page route renders a record's content field in a zone named content:

plugins/newsroom/src/pages/site-page.tsx (excerpt)
import { ContentZone } from '@vitnode/core/widgets/zone'

<ContentZone
  allowedBlocks={PAGE_BLOCKS_ALLOWED}
  blocks={page.content}
  id="content"
  registry={blocksRegistry}
/>

page is the record the route's loader returned and blocksRegistry the registry built in the same file; First widget shows the whole page. allowedBlocks is optional; see Restrict widgets.

A zone never fetches. It renders the list it is handed, so every zone on a page shares the loader's one request. A widget that needs its own data, such as "the six newest articles", fetches it inside its own component.

Name the zone

A zone id is one or more :-separated segments of lowercase letters, digits and single hyphens, up to 64 characters:

<ContentZone id="main" />
<ContentZone id="before-profile" />
<ContentZone id="homepage:hero" />

Write the id by hand and never change it. A saved layout addresses zones by these strings, so a renamed zone is a zone that lost its content. An id that ContentZone could not address later throws the first time the page renders:

<ContentZone id="Before Profile" blocks={content} />
// BlockError: Content zone id "Before Profile" is not usable. …

A zone id says where widgets render. It is unrelated to the id of each widget instance inside it, which says which widget it is and is created once with createBlockInstanceId().

Add a wrapper

By default a zone renders nothing but the widgets, with no wrapper element or class around them. A zone often sits inside a flex or grid container, and an uninvited <div> there would break its gap. Ask for a wrapper when you want one:

<ContentZone
  as="aside"
  className="flex w-full flex-col gap-4 md:w-64"
  id="sidebar"
  blocks={sidebar}
  registry={blocksRegistry}
/>
PropsRenders
neither as nor classNamethe widgets, and nothing around them
className onlya <div> with that class
as (with or without className)that element

as takes an HTML element name, such as "section", "aside" or "ul", not a component. The wrapper carries the zone's identity in data-* attributes, and a component could drop them. To put a zone inside your own component, nest it instead:

<Card>
  <ContentZone id="sidebar" blocks={sidebar} registry={blocksRegistry} />
</Card>

A wrapper carries the zone's id and allowlist:

<aside data-vitnode-zone="sidebar" data-vitnode-zone-allowed="core:text">…</aside>

A zone without a wrapper leaves no trace in the HTML, so do not look for zones by scanning the DOM. The editor doesn't either. Each ContentZone registers itself while edit mode is on.

Empty zones

An empty zone renders nothing at all, wrapper included. null, undefined and [] behave the same, so a page whose zones are all empty is the same markup as a page without zones. Most zones are empty most of the time, and visitors should not pay for placeholders nobody is looking at.

In edit mode the same zone shows Drag and drop widgets here and an Add widget button, so a moderator can fill it. See Visual editor.

Props

PropDefault
id-The zone's stable name. Required.
blocks-The widgets to render. null, undefined and [] all render nothing. Leave it out on an editable page, which supplies them.
registrythe process defaultThe registry to resolve widgets against.
allowedBlocksnoneWhat may be placed here, in the allowlist format. On an editable page it may only narrow the page's list.
min / maxnoneHow many widgets the editor lets the zone hold. On an editable page they may only tighten the page's own bounds.
asnoneAn HTML element name to wrap in. Components are not accepted.
classNamenoneA class for that wrapper. Implies as="div".
validate"development"Passed to the renderer; decides when allowedBlocks is checked. See Validation.
fallbacka development-only noticeWhat to render for a widget that is skipped.

ContentZone comes from @vitnode/core/widgets/zone, which pulls in React and the renderer and nothing else, so no editor code reaches a page that only shows zones. For code without React, @vitnode/core/widgets exports isContentZoneId, assertContentZoneId, parseContentZoneId and contentZoneAttributes.

Check the result

Open the page and view its source. The zone's widgets are in the server-rendered HTML, in stored order, with no element around them unless you asked for a wrapper. On /newsroom/zones from the newsroom plugin, only the sidebar zone, rendered as="aside", carries data-vitnode-zone="sidebar" data-vitnode-zone-allowed="core:text"; its three unwrapped zones add no element.