Noon Docs · Getting Started · Concepts · Guides · Reference

A block is one entry in a page's layout. Each block has a type and a props object, and the shape of props depends on type. This page lists every block type in the authoring vocabulary, with its exact props, so an agent can write a valid layout on the first try.

The layout shape

A page's layout is an object with one key:

{ "blocks": [ { "type": "HeroSection", "props": { "...": "..." } } ] }

blocks is an ordered array. blocks: [] is valid: an empty page is a real, storable state, not an error.

Each entry in blocks has:

  • type, a string naming the block (see the sections below for the exact spelling).
  • props, an object shaped by that block's schema.
  • presentation, optional. A small set of visual attributes any block can carry: background (background or surface), border (none, hairline, or rule), shadow (sm or md), and rotation (tilt-left or tilt-right, blocks only). At least one of these must be set if presentation is present at all; presentation: {} is rejected.
  • anchor, optional. A stable in-page id so another link can jump to this block, for example /#section-contact. It must match section- followed by lowercase letters, digits, and single hyphens (for example section-how-it-works), at most 64 characters. Two blocks on the same page cannot claim the same anchor.

There is no maximum on how many blocks a page may contain. Three narrower rules apply instead: a page may hold at most one PostList, at most one FAQ, and at most one FormBlock; every anchor on the page must be unique. Nothing else about block count or ordering is restricted.

This page describes blocks as used in a page's layout. Region and post-detail templates accept most of the same block types under their own rules; call describe_schema for those.

A worked example

{
  "slug": "about",
  "title": "About us",
  "layout": {
    "blocks": [
      {
        "type": "HeroSection",
        "props": {
          "headline": "We build small, fast things",
          "subhead": "A two-person team shipping since 2019.",
          "ctaLabel": "Get in touch",
          "ctaHref": "/#section-contact"
        }
      },
      {
        "type": "RichText",
        "props": {
          "markdown": "## What we do\n\nWe design and ship web products end to end."
        }
      },
      {
        "type": "Callout",
        "props": {
          "variant": "tip",
          "title": "Response time",
          "markdown": "We reply to every message within one business day."
        },
        "anchor": "section-contact"
      }
    ]
  },
  "upsert": true
}

Call this with create_page. It creates an unpublished draft; call publish to make it live. upsert: true means "create, or update if a page with this slug already exists."

HeroSection

type: "HeroSection". A headline banner with an optional lead sentence and one call-to-action link.

PropTypeRequiredNotes
headlinestring, max 200 charsYes
subheadstring, max 500 charsNo
ctaLabelstring, max 50 charsNo
ctaHrefstringNoMust be an absolute http:// or https:// URL, or a root-relative path starting with exactly one / (for example /pricing or /#section-contact). No other scheme (javascript:, data:, and so on), no // protocol-relative path, no backslash, no whitespace or control characters.

RichText

type: "RichText". A free-form Markdown (with MDX and GitHub-flavored tables) body.

PropTypeRequiredNotes
markdownstringYesNo length limit. Compiled and sanitized at render time.

ImageBlock

type: "ImageBlock". One image reference with optional alt text and caption.

PropTypeRequiredNotes
assetIdstringYesMust name an asset that exists. That is checked when the page is saved, not when the layout is parsed.
altstring, max 300 charsNo
captionstringNoNo length limit.

ProductGrid

type: "ProductGrid". A hand-picked grid of products. Needs the shop plugin: see "Blocks that need a plugin" below.

PropTypeRequiredNotes
productIdsarray of string, max 48 itemsNo
columnsinteger, 1 to 4No

props: {} is valid: every field is optional, so an empty grid is a legitimate placeholder.

Callout

type: "Callout". An admonition box (note, tip, warning, or danger) wrapping a Markdown body.

PropTypeRequiredNotes
variantenum: note, tip, warning, dangerYes
titlestring, max 120 charsNo
markdownstringYesNo length limit.

PostList

type: "PostList". One bounded, indexed feed of published blog posts. At most one PostList may appear on a page.

props.source selects the feed and decides which other props apply:

PropTypeRequiredNotes
sourceenum: latest, tagYes
tagstring, 1 to 80 characters, at most 191 UTF-8 bytesOnly when source: "tag"Must be NFC-normalized, trimmed, and free of control characters. Forbidden when source: "latest".
limitinteger, 1 to 12NoDefaults to 6.
columnsone of 1, 2, 3NoDefaults to 3.

There is no offset, arbitrary collection query, sort direction, category filter, or client-side pagination: PostList is one fixed feed shape, not a query builder.

SocialLinks

type: "SocialLinks". A row of social profile links, as icons or icons with labels.

PropTypeRequiredNotes
linksarray of {platform, href}, max 16 itemsNoWhen omitted, the block shows the site's own social profiles instead. An explicit list never merges with those; it replaces them.
sizeenum: sm, md, lgNo
variantenum: icons, icons-labelsNo

Each entry in links:

FieldTypeRequiredNotes
platformenum: website, github, linkedin, mastodon, bluesky, instagram, youtube, facebook, x, substack, telegramYesRepeated platforms are allowed (for example two website links).
hrefstringYesAbsolute HTTPS URL, at most 2048 characters, no embedded credentials, no #fragment. github, linkedin, bluesky, instagram, youtube, facebook, x, and telegram must live on that platform's recognized host (x accepts x.com or twitter.com; telegram accepts t.me or telegram.me; a www. prefix is accepted everywhere). mastodon and substack accept any HTTPS host. Two links that resolve to the same canonical URL are rejected.

FAQ

type: "FAQ". A page's question-and-answer disclosure list. It is also the one source the site reads to publish FAQPage or QAPage structured data for the page. Page-only: rejected inside a template. At most one FAQ may appear on a page.

PropTypeRequiredNotes
itemsarray of {question, answer}, 1 to 12 itemsYes
schemaTypeenum: FAQPage, QAPage, noneYesNo default; you must choose. none renders the disclosure UI but publishes no structured-data entry.

Each entry in items:

FieldTypeRequiredNotes
questionstring, 1 to 300 charsYesNFC-normalized, trimmed, single line, no control characters. Must be unique (case-sensitively) within the block.
answerstring, 1 to 4000 charsYesNFC-normalized, not blank. Markdown/MDX body; its rendered plain text must also come out non-empty (a bare image with no alt text, or a link with no label, does not count as content).

If schemaType is QAPage, items must contain exactly one entry.

FormBlock

type: "FormBlock". An anonymous-write contact or lead form, submitted through the site's public form endpoint. Page-only: rejected inside a template. At most one FormBlock may appear on a page.

PropTypeRequiredNotes
fieldsarray of field objects, 1 to 20 itemsYesSee field shape below. Exactly one field must have type: "email", and that field must be required: true.
submitstring, max 80 charsYesThe submit button label.
successstring, max 300 charsYesShown after a successful submission.
errorstring, max 300 charsYesShown after a failed submission.
espConnectionIdstring, max 64 charsNoAn email-service-provider connection id.
mailerConnectionIdstring, max 64 charsNoAn SMTP connection id.
captchaConnectionIdstring, max 64 charsNoA captcha connection id.
honeypotstring, matches ^[a-z][a-z0-9_]{0,31}$YesA spam-trap field name. Must not equal any declared field name.
actionsarray of action objects, 1 to 8 itemsNoSee action shapes below.

At least one of espConnectionId or mailerConnectionId must be set. This rule applies even when actions is used: it is not relaxed by the presence of actions.

Each entry in fields:

FieldTypeRequiredNotes
namestring, matches ^[a-z][a-z0-9_]{0,31}$YesUnique among the form's fields. Cannot equal formId or cf-turnstile-response (reserved).
labelstring, max 120 charsYes
typeenum: text, email, textarea, checkbox, selectYes
requiredbooleanYes
placeholderstring, max 160 charsNoOnly allowed on text, email, or textarea.
optionsarray of string, max 100 chars eachRequired on select, forbidden otherwiseA select field needs 2 to 20 unique options.
widthenum: 20, 25, 33, 50, 66, 75, 100NoPercent width. Omitted means full width.

actions, when present, is what actually delivers a submission: it must contain at least one email, esp, or webhook action (a captcha action alone verifies a visitor and delivers nothing). Every entry has a type and a connectionId (string, max 64 chars); an email action also carries to, subject, and body:

Action typeExtra fieldsNotes
emailto (string, max 254 chars), subject (string, max 200 chars), body (string, max 8000 chars)to is either the exact placeholder {submitter} (sends to the address the visitor typed) or one plain-ASCII address. subject and body may reference {{formRef}}, {{submissionId}}, {{submittedAt}}, {{email}}, {{fields}}, or {{field.<declared-field-name>}}; any other token, or a stray brace, is rejected. An action addressed to {submitter} may not reference any field token or {{fields}} at all.
espnone
webhooknone
captchanoneA precondition, checked before anything is stored, regardless of where it sits in the array.

Count limits inside actions: at most 1 captcha, at most 1 esp, at most 4 webhook, at most 2 email (at most 1 of those addressed to {submitter}). If any action is an email addressed to {submitter}, the array must contain exactly one captcha action. Duplicate actions (same type, connection, and recipient) are rejected.

Actions do not run before the response. The captcha precondition is checked first, then the submission is stored and each remaining action becomes a queued unit of work that a later background pass executes with its own retry and backoff. The visitor sees the success message as soon as the submission is stored, not when the email or the webhook has gone out. Array order is stable identity for retries, not an execution order.

One caveat worth knowing today: the form endpoint currently only accepts the visitor's captcha response when captchaConnectionId is also set on the block, even if a captcha action is present. Set captchaConnectionId if the form should verify a captcha.

Poll

type: "Poll". A poll owned by the vote plugin, addressed by its slug. Needs the vote plugin: see "Blocks that need a plugin" below. Page-only: rejected inside a template.

PropTypeRequiredNotes
slugstring, 1 to 191 charsYesThe slug of an existing poll, created and managed through manage_poll.

The block carries only the address. The question, options, deadline, and whether results are shown all live on the poll itself, so there is no second copy to fall out of sync. A slug naming a poll that does not exist renders nothing.

Countdown

type: "Countdown". A deadline, counted down, with what to show once it passes.

PropTypeRequiredNotes
endsAtstring, ISO-8601 datetime with an explicit offsetYesFor example 2026-08-21T18:00:00Z. A value with no offset (2026-08-21T18:00:00) is rejected, because it would mean a different moment in every timezone that reads it.
headingstring, 1 to 120 charsYesShown above the clock.
descriptionstring, max 500 charsNoShown under the heading while the clock runs.
completedMessagestring, max 500 charsNoShown instead of everything else once endsAt has passed. If omitted, the block renders nothing at all after the deadline.

Blocks that need a plugin

ProductGrid and Poll are always part of the block vocabulary, and describe_schema always lists both. But each only does anything useful when its owning plugin is enabled on the server:

  • ProductGrid needs the shop plugin.
  • Poll needs the vote plugin.

Plugin enablement is a server-level setting, not something a page author changes per page. If you are not sure which plugins are enabled on a given install, ask the operator.

The vocabulary is closed

The block types on this page are the complete set. A create_page or update_page call with an unknown type, or with a prop that is not listed for its block, is rejected: nothing is silently dropped or ignored. Call describe_schema to get the live JSON Schema for every block, generated from the same validators the write path enforces, so it can never drift from what this page describes.