Storefront → Blocks → Referência de engenharia
Design decisions
Esta página ainda não foi traduzida para o português — o conteúdo abaixo está em inglês.
You're about to add a feature and wonder why Deco CMS doesn't just fetch your data or route your URLs for you. Each of these choices was argued over.
This page lists each decision in one line, and why it won, grouped by the part of Deco CMS it shapes: the Blocks syntax, the tools built on it, and how content and telemetry leave your servers.
The syntax
| Decision | Why |
|---|---|
A block is either a function (code) or saved content (JSON). One namespace: a saved block is merged in and looked up again; a function gets its inputs resolved, then runs, and its result is returned as is. Built-in functions such as page share the same namespace, under yours. | Saved blocks are named calls, not a second concept. A function that returns its input, like seo, can't loop, because results are never looked up again. The only recursion is over saved blocks, and a cycle check stops it. |
| Block functions are pure. A block function gets its inputs and nothing else. | No context object means block functions are ordinary functions you can call and test, and the same code runs in a website, a mobile app, or a background job. Request state comes from your framework's own request scope, where it already lives. |
Laziness is written in the data, one special case. Every input resolves first, from the inside out, except a lazy block, which becomes a Lazy<T> function. | One exception is easy to keep in your head, and the saved JSON shows exactly where evaluation is deferred. A function asks for it in its types, so deco check catches a mismatch, and multivariate stays an ordinary function that runs only the chosen variant. |
page, redirect, cms-settings, always, never, date, multivariate, lazy and secret are built-in blocks; everything else, data included, is a function in your block map. deco schema reads only the default export, and every key gets its schema from its function's first parameter. | One list of names for the CLI and the runtime, so a type that has a form always resolves. Data-only types such as posts use a function that returns its input ((props: Post) => props), so there's no second, type-level map to keep in sync. The framework spreads its built-ins under your block map, so pages and redirects are always in the schema and always resolve. Almost every website needs them, and the site editor's page list and redirects screen look for them. A key in your map replaces one. |
Three matchers and multivariate are built in; the rest is app code. | always, never and date depend on nothing but the clock, and multivariate only runs the first variant whose rule is true. A device or cookie matcher is an opinion about the request, and the app owns those. Declare multivariate yourself to change how it picks. |
| Lazy loading code belongs to your web framework, not to Blocks. | What's heavy is the component, and React and Next.js already load components lazily. The block map stays a plain object the CLI can read. |
| Short type names; aliases for the old file-path names. | A type name is a column name, not a file path. Content saved under the Fresh and Deno and v7 names, like website/pages/Page.tsx, keeps working through aliases, while new code uses names that survive a refactor. |
The tools
| Decision | Why |
|---|---|
The CLI ships inside @decocms/blocks. It's the package's single bin, deco. | One install, and the CLI's version can't drift from the runtime's. The CLI never reaches an app bundle: apps don't import its subpath, and typescript loads only when a command runs (see How the CLI ships). |
| Four CLI commands, no dev wrapper. | deco schema and deco content each do one job and run from predev and prebuild; deco check runs in CI and prebuild. Every command works on one folder, .deco/, in your app root, found by walking up from the current folder (or passed as --root), so there are no paths to configure; deco schema and deco content take --watch. Generated files carry .gen. in their names (schema.gen.json, blocks.gen.ts), so it's clear which files you don't edit. deco serve is the only server, and you run it only while you want the site editor on your working tree. |
The site editor needs only the schema and the files. It reads the schema, reads and writes .deco/blocks through a four-method content protocol, and never runs site code. | Editing doesn't depend on a deployed, reachable site, an app with its .deco folder can live anywhere in a monorepo (the site editor's "app root" setting points at it), and one editor works on GitHub and on a developer's machine. Features that ran site code (previews in the site editor's gallery, dynamic pickers, Run) degrade, while secrets are encrypted in the browser with the committed public key; the running site remains the preview: your dev server, a preview deploy, or, with the hosted Deco CMS, a ?__draft= link. See What works without your code. |
| Compatibility is a CI check on the content, not a runtime gate. | Content and code merge independently, and with the hosted Deco CMS content goes live without a deploy, so code that's already live must render new content; holding content back until a deploy would undo that. deco check validates every saved block against the schema the code generates, so a code change that breaks saved content and content that uses something the code lacks both fail before they merge. On GitHub, the site editor reads the schema committed on the branch it edits, falling back to your default branch when that branch has none. See Backward compatibility. |
| Poll with conditional reads; no event stream. | One batched request that answers "not modified" when nothing changed costs a few bytes, needs no reconnect logic, and works through any proxy. Two seconds locally and thirty on GitHub is fast enough for an editor watching their own edits. |
Delivery and telemetry
| Decision | Why |
|---|---|
| Git is the source of truth for content. Publishing is committing: a deploy ships the commit, or the hosted Deco CMS serves it as a release without one. | History, review, branches, and rollback come for free, and agents can edit content with the tools they already have. There is no publish command because there is nothing to publish that a commit doesn't already express. deco publish exists only as a signpost for AI agents: it prints this explanation and exits with code 1, so an agent that tries it learns to commit instead. It isn't listed in the CLI's help. |
| The CMS at module scope, a client per request, pinned to one revision. | Everything a response renders agrees with itself, a page's blocks can be resolved and streamed to the browser one at a time, and the next request sees the next revision. Clients are cheap because the content cache lives in the CMS, shared by every client. |
content is a snapshot or one Loader interface: load(pointer?), plus an optional update(). | Production, drafts, and staying current are uses of the same interface, so the content module and remoteLoader replace each other in one option, and the rest of your code never knows which one it has. |
| A production request never waits for the network. | Requests read content from memory. A content source that can change is checked with update() at idle moments, never in front of a request. |
Deco CMS doesn't route. matchRoute is a helper over entries you list. | Which types have URLs is an app decision, a non-website has none, and the framework's file-system router still owns code routes. Editors get pages at arbitrary paths because the path is a field. |
The framework doesn't fetch data. Platform integrations are thin, instrumented API clients; there's no /deco/invoke and no cachedLoader. | Data fetching is the part of a site that differs most between platforms and frameworks, and frameworks already have server functions and route handlers for it. Thin clients keep every call measured the same way while converters, hooks and flows live in templates the site owns. Caching depends on the platform, so it lives in your app, as a fetch you pass to a client (see Upstream data). |
| Packages export TypeScript source and depend on each other in one direction. | Bundling one package into another can leave two copies of a module that must be one, like the block registry. Importing source keeps one copy, and the one-way graph keeps the core free of framework code. Instances also live on globalThis, so a module loaded twice still shares one cache and one poller. |
| The SDK never touches the filesystem. | One setup for every runtime: Node, Workers, Deno, Bun, React Native and browsers. The CLI turns files into a module, and your framework's hot module replacement gives hot reload. |
| Hosted releases: poll a channel manifest once a minute, at idle moments; no push. | A small manifest per server per interval, no infrastructure to reach every server, and never in front of rendering. Content lives in immutable storage/CDN assets; production never reads GitHub. New promotion generations support rollback to older revisions. Until the first release arrives, or if the Deco API is unreachable, a server keeps serving the content the build shipped with or the newest release it has. Eventual consistency for visitors. Draft pointers fix overlays; each preview inherits its server's local production, captured once per client. |
Telemetry is open source; code says where it goes, content says how much. Destination and credentials live in createCMS; the telemetry section of the CMS block holds switches and sample rates, capped by code. | Secrets never go into content, which editors change and releases ship. Switching telemetry off is a content edit anyone can see in Git, while an editor can't raise what leaves your servers past the limits code sets. Speaking OTLP means any backend works; the hosted Deco CMS collector is one destination. Sampling and aggregation keep the cost flat as traffic grows. |
One settings block; code holds destinations, secrets and caps. Site-wide settings are one saved block, CMS, of the built-in type cms-settings, with a section per feature (preview, telemetry, analytics). Code caps what each section may allow, and cms.settings() reads it from the release in memory, never from a draft. | One form for editors and one file to review, with no list of special blocks to keep in sync: the schema already describes the type, and the site editor finds it by its one well-known name. Reading the release keeps a draft from allowing its own preview host or changing what's sent, and reading memory keeps a server able to start and serve with no network. |
Analytics is separate from telemetry and speaks the One Dollar Stats wire format. Its settings are the analytics section of the CMS block, and the site renders AnalyticsScript with them from cms.settings(), the same way on every framework. | Page views are a browser concern that editors switch and configure, so they belong in content, not in the server-side telemetry option. An existing, simple, cookie-free format means it works with One Dollar Stats' collector and their tracker works with yours, with no new format to learn or maintain. |