Built-in blocks
Nine functions every block map gets for free (lazy, multivariate, always, never, date, page, redirect, cms-settings and secret), and how to replace or change one.
Almost every site needs pages, redirects and an if/else. Instead of writing those functions yourself, Deco CMS adds nine of them to your block map. You don't import or list them, and they're always in the schema and the registry, so a built-in never fails with UNKNOWN_BLOCK.
This page lists the built-ins, what each one takes and returns, and how to replace one or change the fields editors see. It's a quick list: each built-in is explained on the page where you'll use it, linked in the last column.
| Name | Takes | Returns | More in |
|---|---|---|---|
lazy | value | A function that resolves value when called | Lazy blocks |
multivariate | variants: a list of { rule, value } | The first variant whose rule is true | Matchers and variants |
always | nothing | true | Matchers and variants |
never | nothing | false | Matchers and variants |
date | start, end | true from start until end | Matchers and variants |
page | name, path, seo, sections | The page, fully resolved | Pages and routing |
redirect | from, to, permanent, status, discardQueryParameters | Its arguments, as saved | Pages and routing |
cms-settings | preview, telemetry, analytics | Its arguments, with defaults filled in | CMS settings below |
secret | ciphertext | The decrypted string, on the server only | Secrets below |
Language built-ins
These five are part of how blocks evaluate, so every site gets the same ones:
lazytakesvalueand returns aLazy<T>function that resolves it when called, at most once. It's the one block whose argument doesn't resolve first (see Lazy blocks).multivariatetakesvariants, a list of entries that each pair arule(aboolean) with avalue(aLazy<T>), and returns the first variant whose rule istrue, orundefinedif none is. Only that variant runs. It also takes an optionalexperiment, the stable ID of an A/B test.alwaysreturnstrue, so the variant paired with it is the fallback.neverreturnsfalse, so the variant paired with it is never picked. That's how the site editor hides a block: it resolves toundefined, and a list leaves it out (see Hide a block).datetakesstartandend, both optional ISO 8601 strings, and returnstruefromstartuntilend.
Matchers and variants shows how they work together, and how to write matchers of your own.
Pages and redirects
pagetakesname,path, an optionalseoandsections(the page's blocks, in order), and returns the page withseoand every block insectionsresolved. Its type isinterface Page extends Route { seo?: Seo; sections: ReactNode[] }. A page withoutseouses your site's defaults.redirecttakesfrom,to,permanent(truefor a 301,falsefor a 302), and two optional fields:status, one of301,302,307or308, which wins overpermanent, anddiscardQueryParameters, which drops the request's query string instead of carrying it over. It returns them as saved.
Pages and routing shows how to find the page for a URL and turn redirects into responses.
CMS settings
Marketing wants page views to go to their own collector, and the team wants drafts to open only on the staging host. Both are site-wide settings editors can see and change, so they live in one place: a saved block named CMS, whose type is the built-in cms-settings.
{
"__resolveType": "cms-settings",
"preview": { "hosts": ["staging.example.com"] },
"telemetry": { "enabled": true, "metrics": true, "errorSampleRate": 0.05, "traceSampleRate": 0 },
"analytics": { "enabled": true, "collector": "https://stats.example.com/events" }
}Settings are split between code and content:
- Code says where things go, and how far content may go.
createCMSholds destinations and secrets, such as your telemetry collector and its token, and the caps: the highest sample rates content may set, and the hosts content may allow previews on. Code changes with a deploy. - Content says what's on, and how much. The
CMSblock holds the switches, the sample rates, the analytics collector and the preview hosts, within those caps. It ships like any other content, and editors change it in the site editor.
The block has three sections, each with its own page:
| Section | Fields | Defaults | More in |
|---|---|---|---|
preview | hosts | Every host, or the hosts code allows | Allow previews per host |
telemetry | enabled, metrics, errorSampleRate, traceSampleRate | true, true, 0.05, 0 | Telemetry settings are content |
analytics | enabled, collector | true, the hosted Deco CMS collector | Analytics |
Every field is optional, and so is the block: without it, every default applies. deco content doesn't create it; save it from the site editor, or add the file, when you want to change something. Like any field, a section or a single setting can have variants, such as analytics switched on only during a campaign.
Your code reads the settings with cms.settings(), which returns every section with the defaults filled in and the caps applied. It always reads the release, never a draft, so a draft can't allow its own preview host or change what telemetry sends. Telemetry, analytics and the draft helpers all read the settings this way.
Secrets
A newsletter form needs its email service's API key, and editors should be able to change it without a deploy. As plain JSON, the key would sit in your repository for anyone who can read it. A secret block keeps only an encrypted copy there:
{ "__resolveType": "secret", "ciphertext": "v1.AbX3…" }On the server, it resolves to the decrypted string.
Type a field as a secret
Type the field as Secret, from @decocms/blocks. It's a string with a type tag, so your function uses it like any string:
import type { Secret } from "@decocms/blocks";
export interface NewsletterProps {
listId: string;
/** @title API key */
apiKey: Secret;
}The site editor shows a Secret field as a write-only password box: editors can set or replace the value, never read it back. It encrypts the value in the browser and saves a secret block, so the plain text never reaches your repository.
Create the keys
Secrets use a key pair, so encrypting needs no secret at all:
- The public key, committed at
.deco/secrets.pub, encrypts. The site editor gets it fromdeco serveor the hosted backend, and an AI agent reads the file. In a script,encryptSecret(publicKey, value)from@decocms/blocks/secretsreturns a readysecretblock. - The private key decrypts. It stays on your servers, in an environment variable, and you pass it to
createCMS:
export const cms = createCMS({ blocks, content, secrets: { key: process.env.DECO_SECRETS_KEY } });Create the pair once, with OpenSSL, from your app root:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out deco-secrets.key
openssl pkey -in deco-secrets.key -pubout -out .deco/secrets.pubCommit .deco/secrets.pub. Store the whole of deco-secrets.key, line breaks included, as DECO_SECRETS_KEY wherever you keep server secrets (on Workers, npx wrangler secret put DECO_SECRETS_KEY < deco-secrets.key; in .env.local or .dev.vars, wrap it in double quotes). Then delete the file, or keep it in a password manager. Never commit it.
Each value is encrypted with a fresh AES-256-GCM key, which is wrapped with the public key using RSA-OAEP and SHA-256. Both are in the Web Crypto API, so it works the same in browsers, on Node and on Workers.
To rotate keys by hand, create a new pair, replace .deco/secrets.pub, save each secret again on a branch, and deploy that branch with the new DECO_SECRETS_KEY.
Guardrails
- Server only. A
secretblock fails if it's resolved in the browser, so its value only exists where your server code runs. It's a plain string there: use it in server code, and don't pass it to a Client Component as a prop, or it ends up in the page. - Reads stay encrypted.
{ run: false }andclient.listreturn the block as saved, with itsciphertext. - No plain text in content. The content protocol refuses a plain string in a
Secretfield, so the site editor and agents can't save one by mistake. - A missing key fails that block. Without a key, or with the wrong one, a
secretblock fails withBLOCK_FAILED. Resolve the page's blocks one by one (see Errors, streaming, and cancellation) and the rest of the page still renders. - Telemetry redacts. Decrypted values never appear in spans or logs.
deco checkneeds no key. It checks that eachciphertextis well formed, but can't decrypt it.
A v7 site re-encrypts its secrets once when it migrates (see Secrets).
Change a built-in
Your block map is spread over the built-ins (see the lookup rule), so declaring a built-in's key in your block map replaces it. For example, declare page to wrap every page in your layout, or declare multivariate to pick variants your way (type each value as Lazy<T> to keep running only the chosen one).
To change only the fields editors see, declare the key with a function that returns its input. Page is exported from @decocms/blocks; extend it to add fields:
import type { Blocks, Page } from "@decocms/blocks";
import seo from "../src/seo";
import hero from "../src/hero";
interface StorePage extends Page { theme: "light" | "dark" }
const page = (props: StorePage) => props;
export default { seo, hero, page } satisfies Blocks;Nothing is lost: arguments resolve before the function runs, so returning props gives the same resolved page as the built-in, seo and sections included, plus theme. You read it in the result of client.resolve, as page.theme, and deco schema gives editors a theme select on every page.
No saved block can be called page, redirect, cms-settings, always, never, date, multivariate, lazy or secret (see Names).