Шаблоны сайтов
Руководство по управляемым шаблонам пока доступно только на английском.
A managed template is the design used to render a managed site. You make candidates, preview them privately, and accept one. Settings → Source & delivery → Site template provides these controls. It is part of the admin: there is no separate frontend setup. Making, uploading and previewing a candidate need an operator-run build host and a private R2 bucket. Delivering a release needs an operator-run deployment service (see Delivering releases).
Make a candidate
Заголовок раздела «Make a candidate»- Generate with AI. Describe the look. The model writes a template from the site’s accepted brief, theme and chrome.
- Upload template source (developers). A JSON file
{ "manifest": …, "source": [ … ] }.
Both go through the same checks: source rules, offline install from the pinned lockfile, type
check, build, page-contract fixtures, accessibility, and 390- and 1440-pixel viewport widths. The source rules
also refuse what generating the standalone backend could not export later: two file paths that differ only in letter case,
a file under a dist or node_modules directory, and text with an unpaired surrogate. A candidate is
stored only when every check passes. Its source, compiled output and check report go to template
storage, each under a key for this site and candidate, written once.
A candidate never serves public traffic.
Preview privately
Заголовок раздела «Preview privately»Choose a page: one of your saved pages, or a fixture (ordinary, long, sparse, no images, error).
- The preview service receives exactly the chosen page and its title and locale, plus the published entries below. It receives no other site content. A build request carries no customer content at all.
- A page with Latest entries sections also carries their published entries. The admin reads each section’s list through the public published projection, with the section’s exact mapping and limit, before it sends the request. The preview service serves only those answers to the candidate, at the exact public list path the candidate requests; any other request gets 404. The candidate has no network access or credentials. A refused list stays refused: the candidate draws that section’s error, never an empty list. Fixtures and pages without such a section carry no entries.
- One request carries at most 24 lists, each at most 256 KiB and all together at most 1 MiB. A larger request is rejected before anything is rendered.
- The page and its entries are used for that request only. They are not stored, logged, cached, put in a URL or repeated in an error, and the service removes its workspace after every result.
- The admin applies a strict display policy to the candidate’s HTML: no scripts, frames, forms or base links, images only from the page’s own recorded media origins and the recorded cover origins of the carried entries, no referrer. The preview frame is fully sandboxed.
A candidate built against a page contract different from the one embedded in this admin is not previewed
(template_contract_outdated): build a new candidate.
Accepting re-reads the candidate’s three stored files and checks their keys, lengths and digests,
re-runs the source rules, and requires a report bound to this site, candidate and build with every
check passed. The candidate must be built against the page contract this admin embeds; otherwise acceptance is
refused: rebuild it. If another template was accepted since the screen loaded, accepting refuses with
stale_document: reload first. Then it chooses the candidate for a later managed release.
Accepting is not activating. It deploys nothing and changes no public page.
Registered sections
Заголовок раздела «Registered sections»A developer can register typed page sections in an uploaded template. The
manifest lists them under registered_sections:
"registered_sections": [ { "definition": { "key": "team-grid", "label": "Team", "description": "People with a photo, a name and a role.", "variants": ["grid", "list"], "props": [ { "name": "intro", "kind": "multiline", "label": "Introduction", "required": false }, { "name": "members", "kind": "list", "label": "Members", "noun": "Member", "min": 1, "max": 12, "item": [{ "name": "name", "kind": "text", "label": "Name", "required": true, "max": 80 }] } ] }, "component": "src/template/registered/Team.astro" }]- A prop is
text,multiline,link,media,toggle,choice(2–12 options) or alistof those. The editor builds its fields from this;headingis reserved for each section’s own heading. - The component lives under
src/template/registered/and imports nothing but the page contract (../../lib/page-contract/…), so an exported site can carry it alone. It receives{ props, variant, layout, level, appearance }, wherepropsis{ heading, fields }. - It goes through every check a template does: source rules, offline build, type check and
accessibility. A form inside a section uses the platform form component, never
<form>. - Each registered section is identified by the SHA-256 of
{ definition, source }. The managed worker renders a section only for the exact identifier its page pins; any other pin is a page error.
Before you accept, GET /api/managed-templates/candidates/:versionId/sections (site:infra)
lists, for each key, the version accepting would give it, how it differs from the newest promoted
version (new, unchanged, compatible or breaking, with reasons), and how many saved or
published pages still pin an older version.
Accepting (site:deploy, nothing else) promotes every registered section to an immutable
version: a new address gets the next version number of its key, and an address the site already
holds keeps its version. Accepting refuses with section_pins_stranded when a saved or published page
pins a version the candidate no longer includes: keep that component in the template until those
pages move to a newer version (the editor’s Update section). Activating or rolling back a release
refuses with published_pages_pin_missing_sections for the same reason, and publishing a page whose
pin the active template does not draw refuses with renderer_capability_exceeded.
Promoted versions are never rewritten or deleted: a registered section’s record stays. The template manifest may list one definition twice when the component files differ, which is how a component keeps an older version beside its newer one. A component made from a page section is a candidate of this kind; its builds are cleaned up after 90 days when nothing references them (see Reusable components). Nothing else is removed by a clean-up.
Images in a template
Заголовок раздела «Images in a template»A template draws every recorded image through the platform component
src/runtime/Image.astro (<Image media={props.media} alt={…} />), never its own <img>. It
applies the slot’s crop and focal point, the image sizes the published read carries, and the
release’s transform hosts, with the same rule a generated site uses. The theme keeps its own box
shape: the component sets only width, height, srcset, sizes and the focal object-position.
An image whose size is unknown gets no dimensions and no srcset.
Transform hosts are an installation setting: MANAGED_IMAGE_TRANSFORM_ORIGINS, a comma-separated
list of https origins whose Cloudflare zone has Image Transformations enabled. Every release
prepared after it is set names them in its MANAGED_RELEASE variable. Without it every image keeps
its original URL. A portable export leaves the hosts out; add them to your own deployment.
Files in a template
Заголовок раздела «Files in a template»A media item that may be a document, audio or video is drawn through the platform component
src/runtime/MediaAsset.astro (<MediaAsset media={item} />). Audio and video play with their own
controls and a download link; a document is a link naming its type and size, for example
“Menu (PDF, 1.2 MB)”; an image is a plain <img>. The kind comes from the item’s mime_type. Pages
allow audio and video from the same hosts as images (media-src). A generated site carries the same
component at src/components/MediaAsset.astro.
Forms in a template
Заголовок раздела «Forms in a template»A template maps the form section type like any other. Its component
places the platform component src/runtime/Form.astro with the props it receives and the form view
the runtime passes (<RuntimeForm fields={props} view={Astro.props.form} />). The platform component
draws the fields and every state, and posts to the site’s intake at /v1/interactions/form.submit
as an ordinary form. The managed worker’s policy allows form-action 'self' for this; it still
allows no script. Previews and unsaved working-document renders draw forms disabled.
Menus in a template
Заголовок раздела «Menus in a template»A template’s shell places the platform components src/runtime/SiteNav.astro (the menu placed at
the top of every page) and src/runtime/SiteFooter.astro (the menu placed at the bottom, and the
footer note). They take no props: the page reads /v1/_chrome for its language on every request
and they render the returned menu data, so a menu change or a moved page needs no new release. One isolate
reuses an answer for at most 30 seconds. When the read fails, or nothing is placed, they draw
nothing and the page still renders. An item whose page is not live is left out; an item whose own
page is not live but has items under it is a heading. Each language has its own menu items; a
language without its own version shows the one the site’s fallback rule serves, and the components
mark it with that language’s lang. The built-in shell places both. Working document renders draw
no menus.
Regions in a template
Заголовок раздела «Regions in a template»A manifest may declare regions: "regions": [{ "key": "sidebar", "label": "Sidebar" }]. It declares
1 to 8, each with a unique key (letters, digits, _ and -, up to 64 characters) and a plain label
of up to 60 characters; anything else is refused with invalid_template_manifest. The shell places
the platform component src/runtime/SiteRegion.astro (<SiteRegion region="sidebar" />) where each
region belongs. The CMS invents no positions. The page reads /v1/_chrome on every request, so a
region change needs no new release. Items draw in order: a shared section (through the template’s
own section components, never with an h1), a menu, a search form, recent posts, categories, tags
or Posts by month. A shared section’s Latest entries render the entries /v1/_chrome contains for
them, using the same read rules as the public list; one that could not be read draws its refusal. Posts by
month links the 30 newest posts at most, in 12 months at most, and never a month address. A
region that shows nothing draws nothing. The built-in template declares page_start and
page_end. Saving a changed region the accepted template does not declare is refused with
unknown_region. Preview of a candidate draws a saved page with the candidate’s own declared
regions over the accepted Site Chrome; the accepted template and the live regions do not change.
A synthetic preview page carries no regions.
Use the built-in template
Заголовок раздела «Use the built-in template»Use built-in template makes the platform’s released built-in template live in one action. Nothing is built and nothing needs review: the platform release pipeline already built it in the pinned sandbox and recorded every check (see Releasing the built-in template).
- Accept. The admin re-reads the released source, compiled output and check report from template storage and checks their lengths, digests, the exact built-in source and the report. It copies them byte for byte under a new candidate of this site and accepts that version. Using it again reuses the site’s copy.
- Prepare and activate. When the site can be delivered (see What delivery needs), the same action prepares a release for the site’s address and activates it, through the ordinary Prepare release and Activate actions with all their checks. A release that already pins exactly this is reused; one already live is left alone.
When delivery is not set up, or the site has no active address, it stops after step 1 and names what is missing. When prepare or activate fails, the built-in stays accepted, the live site keeps its release (or, when the outcome is unknown, Check deployment settles it), and the screen says which step failed. Continue with Prepare release or Activate.
Before anything changes, it refuses with platform_template_unavailable when no built-in is released
in this admin, the release was made for another page contract, toolchain or built-in source, or its
files are not in template storage as released; with stale_document when another template was
accepted since the screen loaded; and with section_pins_stranded when saved or published pages pin
a custom section the built-in does not draw.
What each action needs
Заголовок раздела «What each action needs»| Action | Permission | Settings |
|---|---|---|
| Review | site:infra |
none; without TEMPLATE_ARTIFACTS, files show as unchecked |
| Preview | site:infra |
TEMPLATE_PREVIEW_SERVICE_URL, TEMPLATE_PREVIEW_SERVICE_KEY, TEMPLATE_SANDBOX_IMAGE, TEMPLATE_ARTIFACTS |
| Upload | site:deploy |
TEMPLATE_BUILD_SERVICE_URL, TEMPLATE_BUILD_SERVICE_KEY, TEMPLATE_SANDBOX_IMAGE, TEMPLATE_ARTIFACTS |
| Generate | site:deploy and ai:generate |
everything Upload needs, plus a configured inference connection (Inference provider) |
| Accept | site:deploy |
TEMPLATE_ARTIFACTS |
| Use built-in template | site:deploy |
TEMPLATE_ARTIFACTS holding the released built-in; going live also needs everything in What delivery needs |
A setting counts as unset when:
- a service key is shorter than 32 characters;
- a service URL is not a bare
httpsorigin (no path, query, fragment or credentials);httpis accepted only onlocalhost,127.0.0.1or[::1]; - the sandbox image is not pinned by digest.
A missing dependency disables only the actions that need it, and the card names what is missing
beside each one. Such an action refuses with 503 template_operation_unavailable and
{ operation, missing }, naming what is missing and never a value, before anything is read or
written. A build or preview service outage never blocks accepting.
Generating also needs the site’s generation model: generation is refused if the provider no longer offers that model (choose another in Settings → Generation). An unreadable model list does not block generating: the site’s model is used unchecked, and the candidate’s provenance records that it was not checked.
When the answer is lost
Заголовок раздела «When the answer is lost»If generating, uploading or accepting gets no answer, or an answer the screen cannot read, the action may have happened. The card says so, keeps what you see, and offers Reload. It never repeats the action by itself. Switching sites or closing a review drops any answer still on its way.
Releases
Заголовок раздела «Releases»A release pins an accepted template, the route map, theme and chrome, and the site’s canonical
origin. Its states are proven, active and retired.
The pinned chrome’s SEO defaults (title template, site description, default social image, who runs
the site and the search-engine verification codes) reach the renderer in MANAGED_RELEASE as seo,
only when they differ from the defaults. The Worker adds the page’s search and sharing tags
(description, Open Graph, Twitter card, robots, verification codes and one application/ld+json
structured data block) to the template shell’s <head>; the shell still draws the title and
canonical link it is given. The data block is never executed, so the release’s content security
policy neither blocks nor reports it. A page’s own overrides come from its published version at
request time.
Redirects are not pinned: when an address a route template (or the Blog post address) resolves has
no published item, the renderer reads the accepted rules from GET /v1/_redirects. A move answers
301, 302, 307 or 308 with its location; 410 and 451 answer the error page with that status. So does
a well-formed path no route template matches, such as an old URL from a previous site. Exact rules
win over patterns, and the pattern with the most fixed parts wins over the others. A malformed path
(one that does not decode, or holds an encoded slash, backslash, control character or dot segment)
gets no lookup. Each Worker instance keeps the list for up to 30 seconds.
/robots.txt, /sitemap.xml and /sitemap-<model>-<number>.xml are answered from
GET /v1/_sitemap and its chunks at request time, so a publication reaches the sitemap without a
release. A portable export carries the pinned Site Chrome as release/site-chrome.json;
both verifiers check it against the release pin and rebuild MANAGED_RELEASE.seo from it.
Site template prepares, activates and rolls back releases. See Delivering releases.
GET /api/sites/:siteId/managed-renderer/releases lists releases and their history. It needs
site:infra.
Accepting a template deploys nothing. Publishing content never rebuilds the renderer.
Canonical origin
Заголовок раздела «Canonical origin»A release’s origin is https:// plus the host of the site’s active control-plane binding. The
site’s domain setting, the request Host header and request bodies are not authorities. A site
without an active binding (shared mode) has no canonical origin, so it cannot have a production
release. The release digest covers that origin, so a host change needs a new release.
The product flow is Change address. It proves a candidate host before moving traffic, switches
the binding, route and release atomically, and keeps the prior binding for seven-day one-action rollback.
A failure leaves the old address active. A change is refused with provider_state_conflict when the provider
state no longer matches the recorded live site; do not tear down the working site.
Change address
Заголовок раздела «Change address»Settings → Source & delivery → Site template → Releases shows the site address. A live site (one
release active) offers Change address. Each step needs site:deploy; reading needs site:infra.
- Enter the new address. A bare host (
shop.example.com) orhttps://shop.example.com. It must be a public host that no other site uses as its domain, binding or previous address. - Check address (
POST /api/sites/:siteId/managed-renderer/address/prove,{ "host": … }). The deployment service reads the host’s routes; a foreign<host>/*or<host>/v1/*route refuses (provider_route_conflict), and so does a host outside the allowed zones (host_outside_allowed_zones). Then the accepted template is uploaded as a new version whose release is pinned tohttps://<new host>, and the identity its Version URL serves is proven. The result is a prepared release for the new address. No route is created and the live host keeps its release. - Switch address (
POST …/releases/:number/switch-address,{ "observed_active_number": … }). The deployment service (move-host) checks the current host still serves the live version on its own catch-all, creates<new host>/v1/*and<new host>/*when absent, gives the new version 100% and reads the identity athttps://<new host>. Dee Wan proves it, then writes one control-plane batch:site.domain,site_binding.host, the router’s completion marker and the renderer record (new host, zone, routes, live version) with the old host kept as the previous address. Then the release transition commits. A wrong identity, a lost race, or another site taking the host first puts the old version back, removes a catch-all the switch created, and keeps the old host active. - Go back (
POST …/address/rollback,{ "observed_active_number": … }) for seven days after the switch. The old host’s routes were kept, so it is the same move in reverse: the old release on the old host, one control-plane batch, one release transition. After seven days it refuses withaddress_rollback_expiredand its deadline; nothing changes. - Remove address (
POST …/address/remove-previous) deletes the previous host’s recorded<host>/*route through the deployment service, confirms none is left, and forgets the previous address. Its/v1/*route is the shared public Worker’s and stays. Use it after going back, after the seven days, or earlier to give up going back. A new Change address waits until the previous address is removed (address_change_retained).
During the seven days the old host still reaches the renderer. It serves the new release, whose
canonical links name the new host; its own /v1/* reads stop, because the public router follows the
binding.
A lost answer leaves the step unresolved, like any release change: Check deployment finishes it from what the provider shows. Once the control-plane batch has committed, Check deployment only goes forward (release transition, then the site row in the site’s own database); it never moves traffic back.
The site’s general settings no longer rename a site whose managed renderer serves or keeps a host:
PATCH /api/sites/:id with a new domain answers 409 managed_address_change_required.
Delivering releases
Заголовок раздела «Delivering releases»Site template prepares, activates and rolls back a site’s managed renderer. A separate, product-owned Node service does the Cloudflare work: the managed renderer deployment service. It holds the Cloudflare API token. The admin Worker never does.
What delivery needs
Заголовок раздела «What delivery needs»All of the following settings are required. Without any one, Prepare release, Activate and Roll back stay unavailable, and Settings lists what is missing.
MANAGED_RENDER_DEPLOY_SERVICE_URL(var): the service’shttpsorigin, no path.httponly onlocalhost,127.0.0.1or[::1].MANAGED_RENDER_DEPLOY_SERVICE_KEY(secret, 32+ characters): signs every request to the service and checks every receipt. The service holds the same value.MANAGED_RENDER_INSTALLATION_ID(var, not secret): lower-case letters, digits and hyphens, 3 to 63 characters. Each site’s renderer Worker name (dee-wan-r-<32 hex>) is derived from it and the site id. Never change it after a first release.MANAGED_RENDER_HMAC_KEY(secret): the admin derives a per-site key from it and sends the service only that site’s derived key with each prepare. The service never receives this master key.TEMPLATE_ARTIFACTS: the compiled artifact is read from here.- An active site binding (per-site database host) for the site.
- The admin Worker’s
global_fetch_strictly_publiccompatibility flag. The editor’s exact preview goes from the admin Worker to a*.workers.devVersion URL on the same account. Without the flag Cloudflare answers that request with error 1042. Setup adds the flag tobackend/wrangler.jsoncbesidenodejs_compatand keeps any other flags. The public Worker does not get it: its config is derived without it, because it makes no such request.
Do not set CLOUDFLARE_API_TOKEN on the admin Worker.
The deployment service
Заголовок раздела «The deployment service»npm run managed-deploy-serviceRun it on a trusted host. It refuses to start when any required setting is missing or invalid. It reads its secrets once and removes them from its environment. It never logs request bodies, artifacts, keys, tokens or receipts.
| Setting | What |
|---|---|
CLOUDFLARE_API_TOKEN |
Secret. Permissions below |
CLOUDFLARE_ACCOUNT_ID |
The Cloudflare account id |
MANAGED_RENDER_ALLOWED_ZONE_IDS |
Comma-separated zone IDs, 32 lower-case hex each, that the service may create routes in |
MANAGED_RENDER_PUBLIC_WORKER_SCRIPT |
Name of the shared public read Worker, e.g. dee-wan-cms-public. It must own https://<host>/v1/* |
MANAGED_RENDER_INTAKE_WORKER_SCRIPT |
Optional. Name of the form intake Worker, e.g. dee-wan-cms-intake. When set, activation also attaches https://<host>/v1/interactions/* to it; a route there owned by another script is refused (intake_route_conflict). Unset, website forms on managed sites take no submissions |
CLOUDFLARE_WORKERS_SUBDOMAIN |
The account’s workers.dev subdomain (<sub> in <sub>.workers.dev). A Version URL Cloudflare returns must sit under it, or the version is refused |
MANAGED_RENDER_DEPLOY_SERVICE_KEY |
Secret, 32+ characters. The same value as the admin’s |
MANAGED_RENDER_DEPLOY_SERVICE_AUDIENCE |
This service’s own origin, exactly as the admin’s MANAGED_RENDER_DEPLOY_SERVICE_URL names it |
MANAGED_RENDER_INSTALLATION_ID |
The same value as the admin’s |
MANAGED_RENDER_DEPLOY_SERVICE_HOST, MANAGED_RENDER_DEPLOY_SERVICE_PORT |
Optional. Default 127.0.0.1:8791 |
The service reads each allowed zone’s real name from Cloudflare. It picks the one zone whose name
equals the site host or is a dot-suffix of it (example.com for www.example.com). The operation is refused if no zone or
more than one zone matches.
Minimum token permissions:
- Account → Workers Scripts: Edit (covers Read);
- Zone → Workers Routes: Edit, for each allowed zone;
- Zone → Zone: Read, for each allowed zone.
The service must reach each site’s https://<host> and the *.workers.dev Version URLs over the
public internet. It reads renderer identities there itself.
A prepare can take up to 4 minutes and an activation up to 2. Do not put the service behind a proxy that ends requests sooner (Cloudflare’s own proxy ends them at 100 seconds).
Cloudflare API it uses
Заголовок раздела «Cloudflare API it uses»The service uploads with Cloudflare’s beta Workers API. Cloudflare’s direct-upload guide for
static-asset Workers uses it. Only cloudflare-provider.ts calls it.
- Beta Worker and version paths take the Worker’s immutable id, never its name. The service
finds or creates the Worker by name once, checks the name is the site’s
dee-wan-r-<32 hex>script, and from then on uses the id. Dee Wan stores the id in the control-plane provisioning record and with each release. Any mismatch between the stored id, the id Cloudflare answers, the script name and the site causes the operation to be refused. - Script settings, asset upload sessions, deployments, routes and deletion use the script name.
- Each upload is tagged with its operation id in the
workers/tagannotation, so a retried operation finds its version instead of uploading again. - Static assets are hashed as Cloudflare documents: SHA-256 of base64(file bytes) plus the file extension, first 32 hex characters.
- Each version gets a
PUBLIC_APIservice binding toMANAGED_RENDER_PUBLIC_WORKER_SCRIPT. The target comes only from the service’s own setting. The version gets no public-transport variable and no compatibility flag. - An operation-version lookup and a Worker lookup page through Cloudflare’s lists. Each answers, found or absent, only after an empty page. Reaching the page bound first (5 pages of versions, 50 of Workers) answers unknown, so a Worker with more versions than that cannot prove absence and reconciliation remains unresolved. Two matches are unknown too.
Version URLs
Заголовок раздела «Version URLs»Dee Wan never builds a Version URL. Cloudflare returns a version’s routable URLs; the service takes
exactly one that is https, has no credentials, port, path, query or fragment, sits under
CLOUDFLARE_WORKERS_SUBDOMAIN, and names the site’s script in its host. None or more than one
refuses the version.
The service names the account subdomain in its signed prepare answer. The admin stores it in the
control-plane provisioning record (provider_workers_subdomain). An existing record must name the
same subdomain, or the prepare stays unresolved. At render time the stored Version URL must be a
bare https origin that names the release’s script under exactly that recorded subdomain. A
subdomain read from the site’s release row alone is never trusted.
The URL is stored with the release as internal provider state. It is never shown in the browser, never in a release summary, and never used as the site’s public address.
Where identities are read
Заголовок раздела «Where identities are read»A Worker cannot call another Worker routed on its own zone with a plain fetch. So:
- The deployment service reads renderer identities. It fetches
/.well-known/managed-release.jsonfrom the version’s Version URL (prepare) or the site’s canonical address (activate, roll back, reconcile). Fixed path,https, no redirects, 5 s timeout, at most 64 KiB, exactly HTTP 200, exactly one JSON object. It retries for about half a minute while a new version or deployment reaches the edge, then answers the raw identity inside its signed, receipted answer. It does not decide whether the identity is right. - No answer is not a wrong answer. When no identity could be read, the operation stays
unresolved for Check deployment; nothing is rolled back for it. A wrong identity during
activate or roll back is rolled back. A prepare whose version reports a wrong identity closes as
refused (
renderer_proof_failed); that version stays uploaded and serves no traffic. - The admin decides. It compares the observed identity with the site, release digest and renderer-contract digest it derived from its own records. The proof always names the canonical address. The browser never supplies identity or proof.
- The renderer reads content through
PUBLIC_API. Published pages, previews and preview attestations go through the service binding to the public Worker. The request URL is still the canonicalhttps://<host>/v1/..., so host and site routing do not change. Plainfetchis used only for a local loopback build, or for a generated standalone backend that selects public transport (see Exported Worker configuration). A managed version without the binding never falls back tofetch: those reads answer 503. - Identity and the exact preview need no
PUBLIC_API./.well-known/managed-release.json, the renderer contract and the signed working-page render answer fromMANAGED_RELEASEalone. The signed render still checks audience, release digest, body digest, expiry, site, page, locale and baseline, persists nothing and calls no public API. - The editor’s exact preview goes to the Version URL. The admin sends the signed render request
to the active release’s stored Version URL. The envelope’s audience, the frame’s base and the
release identity stay the canonical address. It also checks that the release, the provisioning
record and the installation name the same script, Worker id and live version, and that the URL
sits under the recorded workers.dev subdomain. This request needs the admin Worker’s
global_fetch_strictly_publicflag. A release prepared before Version URLs were stored has no exact preview until a new release is prepared.
Routes on the site host
Заголовок раздела «Routes on the site host»https://<host>/v1/interactions/* -> shared form intake Worker (MANAGED_RENDER_INTAKE_WORKER_SCRIPT, when set)https://<host>/v1/* -> shared public Worker (MANAGED_RENDER_PUBLIC_WORKER_SCRIPT)https://<host>/* -> the site's renderer Worker (dee-wan-r-<32 hex>)These are Workers routes, not a Custom Domain. The more specific /v1/* route wins. The service
creates the /v1/* route when it is absent and refuses when it belongs to another script. It never
overwrites or deletes a route it does not own.
A host that the public Worker serves as a Custom Domain cannot also carry these routes. For a
managed site, attach the public Worker to the host by the /v1/* route instead. See the routes
comment in backend/wrangler.public.example.jsonc.
Every renderer Worker has workers.dev disabled and Version URLs (preview URLs) enabled.
After a Change address the previous host keeps its two routes until Remove address; the control plane records its catch-all as the previous address.
Prepare, activate, roll back
Заголовок раздела «Prepare, activate, roll back»- Prepare release uploads the accepted compiled template as a new Worker version with no traffic. The service reads the identity its Version URL serves; the admin checks it and stores a prepared release.
- Activate gives that version 100% of traffic. The first activation also creates the catch-all route. The service reads the identity at the site’s own address; the admin checks it, marks the release live, then records the live version in the control plane.
- Roll back re-deploys an earlier release’s existing version. Nothing is rebuilt.
Only one release operation runs per site at a time.
When the outcome is unknown
Заголовок раздела «When the outcome is unknown»If the identity check or the site database update fails after traffic moved, Dee Wan moves traffic back, or removes a route it just created, and verifies that.
Marking the release live and recording it in the control plane require writes to two databases. The site database
commits first and the operation stays open (tenant_committed). It closes only after the control
plane record reads back exactly the live version, deployment and routes. If that write fails, the
operation stays unresolved and release actions stay blocked. Check deployment then confirms the
release is active, the provider serves it on the owned routes and the site reports its identity,
repairs the control-plane record and closes the operation. It never moves the release again and
never moves traffic back for an operation in this state.
Every provisioning record write is read back. A write that changed nothing does not count.
If the outcome cannot be established, the operation stays unresolved and every release action for that site is blocked. Settings then offers Check deployment. It finishes the operation, closes it, repairs the record, or moves traffic back, as the provider state shows.
A signed prepare answer means a version exists. After the service answers a prepare, a script or
Worker mismatch, a bad Version URL or subdomain, a failed provisioning-record write or read-back, a
missing identity, or a failed release write leaves it unresolved. That keeps the next prepare from
uploading another version beside it. A version whose identity was read and is not this release closes
the operation as refused (renderer_proof_failed): the version stays uploaded, serves no traffic and
never becomes a release. Check deployment closes a prepare as refused when the service’s lookup
reached an empty page and found no version tagged with the operation (or no renderer Worker), or when
the version exists and reports a wrong identity. When the version exists but its identity, ownership
or storage cannot be confirmed, the operation stays unresolved. Dee Wan never deletes a single version
by itself.
Removing renderers
Заголовок раздела «Removing renderers»Installation teardown (npm run teardown) removes every recorded renderer before it deletes
the control database, using the operator’s own Cloudflare token:
- It reads
managed_renderer_provisionfrom the configured control database (theDBbinding, only after the D1 identity check proved it). If managed delivery is configured (MANAGED_RENDER_DEPLOY_SERVICE_URLorMANAGED_RENDER_INSTALLATION_IDset) and the table cannot be read,--applyrefuses. - For each record it re-derives the Worker name from the installation id and site id, checks the
Worker id still names that script, and checks the recorded route’s pattern and script. A name
prefix alone never counts. Any mismatch or unreadable answer refuses
--apply. - The dry run lists each recorded renderer route and Worker.
--applydeletes each recorded catch-all route (the live host’s and a previous address’s) and confirms it is gone, then each renderer Worker and confirms it is gone. Only then are the remaining resources deleted, with D1 last. A failed or unconfirmed renderer deletion stops the run before any D1 database is deleted.
It never deletes the shared public Worker or any /v1/* route.
Per-site removal. The admin API has no hard site deletion. The per-site renderer teardown
(teardownManagedRenderer, through the deployment service) is ready for that future caller: it
removes a previous address’s catch-all first, then deletes only the recorded catch-all route and the
recorded Worker, and keeps the provisioning record when a deletion fails or its result is unknown.
Admin settings
Заголовок раздела «Admin settings»All optional. Keys are secrets (wrangler secret put); the rest are vars or bindings.
| Setting | What |
|---|---|
TEMPLATE_ARTIFACTS |
Private R2 bucket for template storage |
TEMPLATE_BUILD_SERVICE_URL, TEMPLATE_BUILD_SERVICE_KEY |
Build service origin and its key (32+ characters) |
TEMPLATE_PREVIEW_SERVICE_URL, TEMPLATE_PREVIEW_SERVICE_KEY |
Preview service origin and its key (32+ characters) |
TEMPLATE_SANDBOX_IMAGE |
The sandbox image, repository@sha256:<64 hex> only |
| Inference connection | Needed only to generate: DeepInfra with DEEPINFRA_API_KEY, or the configured OpenAI-compatible connection with AI_API_KEY. See Inference provider. |
MANAGED_RENDER_DEPLOY_SERVICE_URL, MANAGED_RENDER_DEPLOY_SERVICE_KEY |
Deployment service origin and its key (32+ characters) |
MANAGED_RENDER_INSTALLATION_ID |
Stable installation id; see What delivery needs |
MANAGED_RENDER_HMAC_KEY |
Master render key (32+ characters); see Secrets and the exact working-page render |
Every build and preview request names the sandbox image; a service running another image refuses it. Use different keys for build and preview.
Build and preview service
Заголовок раздела «Build and preview service»npm run template-build-serviceOne host can serve builds, previews or both: each operation runs only when its key is set
(TEMPLATE_BUILD_SERVICE_KEY, TEMPLATE_PREVIEW_SERVICE_KEY, 32+ characters). With neither key it
does not start. TEMPLATE_SERVICE_AUDIENCE is the host’s own origin, the one the admin is
configured with. The host holds no deployment credential.
The service listens on plain HTTP at TEMPLATE_SERVICE_HOST:TEMPLATE_SERVICE_PORT (default
127.0.0.1:8790). The admin calls only https origins (except loopback), so put a TLS proxy at the
configured origin. The host runs one operation at a time; another request meanwhile is refused with
503 service_busy, not queued.
The host never runs generated code itself. Every build and every preview runs in a fresh container, and the service refuses to start without all of these:
| Setting | What |
|---|---|
TEMPLATE_SANDBOX_IMAGE |
Image by digest; a tag is refused |
TEMPLATE_SANDBOX_RUNTIME |
Absolute path to docker or podman |
TEMPLATE_SANDBOX_MEMORY_MB |
512 to 65536 |
TEMPLATE_SANDBOX_CPUS |
0.5 to 64 |
TEMPLATE_SANDBOX_PIDS |
64 to 65536 |
TEMPLATE_SANDBOX_TIMEOUT_SECONDS |
30 to 3600 |
TEMPLATE_SANDBOX_WORKSPACE_ROOT |
Absolute, writable directory for per-request workspaces |
Each container runs with:
- the service’s own non-root user (the service refuses to run as root);
- a read-only root filesystem, every Linux capability dropped,
no-new-privileges; - no network: install, type check, build, fixtures, browser checks and preview all talk over the container’s own loopback;
- bounded memory, CPU, processes, time and output;
- one fresh workspace directory and a bounded
/tmp; no home directory, repository or container socket mounted; - no host environment: the host sets only
HOMEandTMPDIR; - no container log; the answer comes back on stdout.
The runtime is started with an argument list, never a shell. After every run the host removes the
container (rm --force), confirms with a listing that no container of that name is left, and
deletes the workspace. Each cleanup call has a time limit. When a run times out or overflows its
output, the host kills the runtime client and waits for a limited grace period for it to close; a client that
does not close in time is not counted as stopped. A result, success or failure, is returned only
when the client has been observed closing, the container is confirmed gone and the workspace is deleted;
otherwise the request fails with sandbox_cleanup_failed.
The sandbox image
Заголовок раздела «The sandbox image»npm run template-sandbox:context -- --out <new dir> --node-image <node@sha256:…>docker build -t <registry>/<name> <new dir>docker push <registry>/<name>The context pins the base image by digest, the npm, wrangler, playwright-core and axe-core versions
this checkout uses, the headless Chromium those versions install, and an npm cache holding exactly
the managed worker lockfile. Builds install offline from that cache with install scripts disabled.
The image’s cache is read-only, and npm treats a cached package record as missing if it cannot write back to the cache, so
each build first copies the cache into its own workspace and installs from the copy. The copy is
deleted with the workspace. An image built before this change fails every build at install with
ENOTCACHED; build the image again from a fresh context.
Configure the service and the admin with the pushed digest, never the tag.
Proving an image on one machine. No provider is contacted. A local registry gives the image a digest:
docker run -d --rm --name dw-local-registry -p 127.0.0.1:5055:5000 registry:2docker tag <image> 127.0.0.1:5055/dee-wan-template-sandbox:localdocker push 127.0.0.1:5055/dee-wan-template-sandbox:localdocker inspect --format '{{json .RepoDigests}}' <image>cd backendDEEWAN_SANDBOX_IMAGE=127.0.0.1:5055/dee-wan-template-sandbox@sha256:<digest> \DEEWAN_SANDBOX_RUNTIME=$(command -v docker) \npx vitest run test/managed-sandbox-real.test.tsdocker stop dw-local-registryThe test runs the real service with the real runtime. It builds the built-in template in a
container and requires all seven checks passed and a stored bundle. Then it draws a saved page
whose Latest entries sections are populated, empty and refused. After each run no container may
be left and the workspace must be deleted. Without DEEWAN_SANDBOX_IMAGE the test is skipped. A
skipped run proves nothing.
Production does not rebuild the built-in from an admin route. The platform release pipeline runs the same pinned offline sandbox and checks, stores the immutable artifacts and evidence, and promotes that exact digest. A new site activates the preaccepted platform version in one action.
Releasing the built-in template
Заголовок раздела «Releasing the built-in template»TEMPLATE_SANDBOX_IMAGE=… TEMPLATE_SANDBOX_RUNTIME=… TEMPLATE_SANDBOX_MEMORY_MB=… \TEMPLATE_SANDBOX_CPUS=… TEMPLATE_SANDBOX_PIDS=… TEMPLATE_SANDBOX_TIMEOUT_SECONDS=… \TEMPLATE_SANDBOX_WORKSPACE_ROOT=… \npm run platform-template:release -- --out <new or empty directory>It takes the same sandbox settings as the build service and runs the build service in-process on loopback, with a key made for this one build. The built-in goes through the candidate build: source rules, the signed build request, the sandboxed build with every check, and the answer’s receipt and frame checks. Every sandboxed check must name the configured image.
On success it writes the three files once under <out>/managed/_platform/<candidate>/<role>.json,
reads them back verified, writes the release record to <out>/platform-template-release.json, and
promotes the build by rewriting backend/src/lib/managed-renderer/platform-template-release.ts.
Review and commit that file with the release; the admin embeds it. Then copy the three files into the
TEMPLATE_ARTIFACTS bucket at exactly their keys; the command prints them. It uploads and deploys
nothing itself.
_platform is a reserved partition: site ids never start with _. Until a build is promoted the
file holds null, and Use built-in template stays unavailable.
The build answer
Заголовок раздела «The build answer»A build answers one binary frame: small closed metadata (sandbox, toolchain, check results), then the compiled bundle exactly as template storage keeps it. The admin checks the receipt, the metadata and the bundle’s canonical form, then stores those bytes. Compiled files are not decoded and encoded again on the way.
The check results are an exact prefix of install, typecheck, build, contract_fixtures,
accessibility, viewport_390x844, viewport_1440x900: no repeat, no reorder, no gap, nothing
after a failed check. A shorter list must end on its failed check and carries an empty bundle. A
passed build has all seven at exit code 0 and a non-empty canonical bundle. A passed build with an
empty bundle (compiled output over its limit) is refused as compiled_too_large; anything else is
sandbox_output_invalid. Nothing is stored in either case. source_validation is the admin’s own check; the
sandbox never reports it.
Generating the standalone backend with an active managed release
Заголовок раздела «Generating the standalone backend with an active managed release»A site with no active managed release generates its standalone backend exactly as before.
Only whole-site generation for a site with an active release writes managed-renderer/ (format 2).
Generation reads that release from the CMS copy, re-verifies it, and refuses generation if anything is missing or unreadable, has changed, or belongs to another site:
- the release record, every pin, and the release digest;
- the pinned route map and template version;
- the template’s source, compiled output and check report, from template storage, by key, length and digest, with every check passed;
- the worker files, emitted again from that source, must match the build’s recorded input digest. A template built before input digests were recorded, or with another page contract or toolchain than this build pins, refuses: rebuild, accept and release it again.
Generation has no access to the R2 bucket. Copy the release’s three objects into one directory, each
at its key (managed/<site>/<candidate>/<role>.json), and pass it:
npm run backend:generate -- --site <id> --source <cms.sqlite> --dest <dir> --db <scratch.sqlite> --template-artifacts <copy>npm run backend:deliver -- … --template-artifacts <copy>Without it, generation refuses with managed_release_custody_unavailable and names the three keys.
The directory and every directory on the way to an object must be real directories, and each object
a regular file; links are not followed.
Limits:
- A
--modelrun exports no managed renderer. It still exports the selected model backend. npm run backend:generate:allcannot pass--template-artifacts, so generation for a site with an active release is refused by that command. Generate that site alone.- A verified managed-renderer export has its own 32 MiB total budget. It does not consume the ordinary 32 MiB GitHub source budget. Every exported file still stays below 4 MiB.
Exported files
Заголовок раздела «Exported files»Export format 2 only. There is no compatibility for older export shapes. Every file is stored as raw bytes, and every file is below the 4 MiB per-file limit of GitHub source delivery.
Path under managed-renderer/ |
What |
|---|---|
package.json, package-lock.json |
The pinned package manifest and lockfile |
astro.config.mjs, tsconfig.json, public/, worker/, src/env.d.ts, src/runtime/, src/pages/ |
The managed runtime |
src/lib/page-contract/ |
The page contract it was built against |
src/template/ |
The template source and assets |
wrangler.jsonc |
The release’s portable deploy configuration: the release as MANAGED_RELEASE, public transport and global_fetch_strictly_public |
release/release.json |
Closed record: release number, digest, origin, pins, template identity, chunk list, every other file’s length and digest |
release/route-map.json |
The complete route map, as pinned |
release/site-chrome.json |
The pinned Site Chrome, canonical; the source of MANAGED_RELEASE.seo |
release/template.json |
The pinned template version without storage keys: manifest, candidate and build identity, provenance, artifact digests and lengths |
release/evidence/compiled/NNNN.part |
The compiled artifact as evidence, in chunks |
release/evidence/report.json |
The check report, as stored |
release/managed-renderer.mjs |
The commands below |
The compiled artifact can be 24 MiB, so it is split in order into raw chunks of exactly 3 MiB (the
last one shorter), with no base64. release/release.json records each chunk’s order, length and
SHA-256, and the whole artifact’s length and digest. Joined in order, the chunks are the exact bytes
template storage holds.
release/template.json is not the stored template version: that version also holds storage keys,
which never leave the CMS. The release pins keep the stored version’s id and digest as recorded
identity; the repository cannot recompute that digest.
graduation.manifest.json pins the export: release number and digest, contract digest, and
exportDigest. release/release.json is canonical JSON followed by one newline, and its SHA-256 is
exportDigest. Every file is in the generated zone and the ownership file.
Never exported: storage keys, render or signing keys, service keys, tokens, environment values, preview or unpublished content, local paths in the release metadata. The check report goes out byte for byte as template storage holds it, so its check commands and outputs can name build-sandbox paths.
The CMS inspector
Заголовок раздела «The CMS inspector»The CMS checks an export from its bytes with one inspector. Generation runs it on the export it
composed, and on the staged tree (or, when a replay changes nothing, the destination) before any
database or destination write. On disk it skips only node_modules, dist, .astro and
.wrangler directly under managed-renderer/, like managed-renderer:verify; any other unlisted
file there, such as .dev.vars, causes rejection. Source assembly uses the same inspector. It checks format, manifest pin, exact file set with each
length and digest, chunk order, lengths and joined digest, input, source and contract digests, the
check report with exactly the eight checks in order and all passed, the template projection, the
route map, the release digest, wrangler.jsonc, and no storage key or local path anywhere in the
release metadata.
Among its refusals:
managed_renderer_export_cli_mismatch:release/managed-renderer.mjsis not byte for byte the command this product emits. A repository cannot replace its verifier and update the pin to accept that replacement;managed_renderer_export_pin_invalid: the manifest pin is open (not exactly the closed pin keys);managed_renderer_export_path_invalid: amanaged-rendererroot spelled in another case, or a path that is not a managed renderer path;managed_renderer_export_unpinned: files undermanaged-renderer/with no pin.
Regeneration and source delivery
Заголовок раздела «Regeneration and source delivery»Ordinary managed regeneration keeps the export. It carries the manifest pin into the new manifest
and never writes or deletes a file under managed-renderer/; a generator that returned one there
is refused.
In 1.0, source reaches your repository through local CLI generation and ordinary Git. Hosted source
preparation is not available: the CMS’s source-delivery build intents refuse with
source_build_runtime_unavailable before any provider write. No 1.0 command or screen calls source
assembly.
Source assembly (assembleSourceArtifact) takes the export as an explicit input with three states:
- omitted: a verified existing export and its pin are kept, or nothing when there is neither. It is kept only when it passes the inspector against the manifest pin and the ownership file records each file unedited. Anything else refuses;
- an export: it is installed after the inspector re-derives every identity it claims, in place of
the old one; old files it does not list are removed. The root
package.jsongains the scripts below; null: the export and its pin are removed, and only the canonical scripts below. On a repository that never had an export,nullchanges nothing. Passnullonly when the site has no active managed release.
Source assembly delivers the edited package.json.
Package scripts
Заголовок раздела «Package scripts»The root package.json carries three scripts: managed-renderer:verify, managed-renderer:build
and managed-renderer:prepare, each node managed-renderer/release/managed-renderer.mjs <command>.
- Adding writes only the scripts that are absent. A present script is never overwritten; one that holds another command refuses the add.
- Removing deletes a script only while its value is exactly the canonical one. A script you changed is kept.
- Edits are in place. Every other byte is kept: line endings, indentation, a byte order mark and the final newline.
The edit refuses with managed_renderer_scripts_unwritable (add) or
managed_renderer_scripts_unremovable (remove) when package.json is missing (add only), is not
UTF-8, is not strict JSON, its root is not an object, scripts is not an object, or it names
"scripts" or a managed-renderer:* script twice. Adding also refuses when one of the three
scripts holds another command; another managed-renderer:* script of yours is kept. A removal from
a package.json that never names a managed-renderer: script changes nothing.
Generation adds the scripts when it writes an export. It removes the canonical ones only when the
previous manifest pinned an export that this run no longer writes. A project that never had an
export keeps its package.json byte for byte.
Commands in the repository
Заголовок раздела «Commands in the repository»npm run managed-renderer:verifynpm run managed-renderer:buildnpm run managed-renderer:prepareRun them from the repository root. managed-renderer/ and managed-renderer/release/ must be real
directories, not links. None of them contacts the CMS.
-
verify checks: the manifest format and pin; a closed, canonical
release/release.jsonwith typed fields; that every file undermanaged-renderer/, except build output at its root, is a regular file the record lists, and every listed file is present at its length and digest, below the per-file limit, at a safe path, once even ignoring case; the chunks rejoining to the compiled digest; the input, source and contract digests; a closed check report bound to the release with every required check passed in order;release/template.jsonclosed, canonical and agreeing;release/route-map.jsoncanonical, shaped and pinned;release/site-chrome.jsoncanonical and pinned; the release digest;wrangler.jsoncbyte for byte; and no storage key or local path in the metadata. An export in another format refuses.Website or Blog comes from
graduation.manifest.json, never fromwrangler.jsonc. verify reads only site manifest format 9 and refuses any other format."blog": nullaccepts only the Website config (nobloginMANAGED_RELEASE)."blog": {"locales": [...]}(non-empty, sorted, unique) accepts only the Blog config, at the address the route map gives. Any otherblogvalue refuses. The CMS export inspector applies the same rule against the manifest it writes or keeps, and refuses a pinned export under a manifest that records neither.verify is narrower than the CMS inspector. It does not re-parse the template manifest, its provenance, the route map grammar, the Blog language spelling or the manifest digest; the CMS does that when it writes or keeps the export.
-
build: verify, then clear only
node_modules,dist,.astroand.wranglerdirectly undermanaged-renderer/(a link there is unlinked, never followed). Then it re-runs install (npm ci --ignore-scripts), type check and build only, on this machine, with network fornpm ci. It refuses a changed lockfile or another toolchain. It does not re-run the contract, accessibility or viewport checks; their evidence isrelease/evidence/report.json. The compiled evidence is never unpacked or run. -
prepare: build, then package the worker with
wrangler deploy --dry-runinto.wrangler/deploy. Nothing is deployed.
build and prepare run with ASTRO_TELEMETRY_DISABLED=1 and WRANGLER_SEND_METRICS=false.
Exported Worker configuration
Заголовок раздела «Exported Worker configuration»An export has no product-managed PUBLIC_API binding. Its wrangler.jsonc therefore selects public
transport explicitly:
"PUBLIC_API_TRANSPORT": "public"invars. The runtime accepts exactly this value. Any other value, or none, without a binding answers 503."global_fetch_strictly_public"incompatibility_flags. The renderer’s fetch tohttps://<host>/v1/...then leaves through the public internet and reaches the public Worker by its/v1/*route. Without the flag a same-zone fetch can skip that route.
The target is the canonical origin inside MANAGED_RELEASE. There is no other URL setting. The CMS
inspector and verify both check this file byte for byte, so edit neither setting by hand.
The exported wrangler.jsonc names no route. Whoever deploys it must serve it at
https://<the site's active binding host>, the origin the release was made for. The deploy
workflows still deploy apps/site, the detached renderer. They do not deploy, activate or roll back
the managed renderer. In the CMS, that is Delivering releases.
Publishing and freshness
Заголовок раздела «Publishing and freshness»The renderer reads the site’s public API on each request. Publishing needs no rebuild. Renderer
HTML is sent with cache-control: no-store.
Secrets and the exact working-page render
Заголовок раздела «Secrets and the exact working-page render»Public pages need no secret. The renderer reads its release from MANAGED_RELEASE in
wrangler.jsonc (not secret) and its content from the site’s public API. Deploying needs your own
Cloudflare credentials, as for the rest of the repository.
The editor’s Site design draws an unsaved working page through the release. It is offered only when all of these hold:
- the site has an active managed release;
- the CMS has
MANAGED_RENDER_HMAC_KEYof 32+ characters; - the site’s active binding host, as
https://<host>, is the origin the release was made for.
When any of these is missing, the editor shows the Structure preview.
The CMS’s MANAGED_RENDER_HMAC_KEY is a master key and never goes to a renderer. The CMS signs each
render with a per-site key: HMAC-SHA256, keyed by the master key, over
dee-wan:managed-render-site-key:1, a newline and the site id, encoded as unpadded base64url. A site
renderer’s MANAGED_RENDER_HMAC_KEY must hold that derived key, never the master. Without it the
renderer refuses the working-page render. With each prepare, the admin sends the deployment service
only that site’s derived key, never the master.
Стабильная версия ещё не выпущена (pre-MVP)
Коммит исходного кода: fe23db481c422b40e317b92fc98116b4c0c26a87 (с локальными изменениями)
Дата сборки (UTC): 2026-10-18T09:22:34.630Z
Коммит для обновления: не определён: в рабочей копии есть изменения. Примечания к выпуску