Preview and the generated TypeScript client
The generated public client talks to the CMS public /v1 API. It is not a client for the detached backend’s different API. The standalone backend API reference describes that API.
Generate the client
Section titled “Generate the client”Open Settings → API → TypeScript client. The card shows the schema identity of the client for the models your role can read and lets you copy or download the file. The file starts with a @dee-wan/public-client comment that records the site, public URL, contract version, schema identity and the contract itself. The schema identity is a SHA-256 digest of the locales, the locale policy, and each model’s visibility and field types. Labels, names and field order do not change it.
To refresh the client from your repository, download dee-wan-client.mjs from the same card and commit it. It needs Node 18 or later and no packages. It reads a site API token from DEE_WAN_SITE_TOKEN. Create the token under Settings → API and keep it in a server or CI secret. Private models must be in its scope. Substitute your site’s public origin; the values below are placeholders.
DEE_WAN_SITE_TOKEN=dw_site_… node dee-wan-client.mjs pull --url https://www.example.com --out src/dee-wan-public.tsThe command reads GET /v1/_client on the public origin. It writes the file only when the response is complete and its header matches the served identity. The pulled client covers public models and the private models in the token’s scope. Its identity matches the one in Settings only when both cover the same models.
Before each deploy, for example in CI, check the committed file:
DEE_WAN_SITE_TOKEN=dw_site_… node dee-wan-client.mjs check --file src/dee-wan-public.tscheck exits 0 when the identity matches, or when every change is compatible: an added model, field or locale, a field made required, a model made public, or a removed select value. It lists those changes. It exits 1 on a breaking change: a removed model, field or locale, a changed field type or list shape, a field made optional, a new select value, a model made private, or a changed site, default locale or locale policy. It exits 2 when it cannot run: bad arguments, no token, a network or HTTP error, or a file without the header. After a change, run pull, fix what no longer compiles, and commit.
An admin session can still fetch the same file by hand:
curl --fail --silent --show-error -H "X-Site-Id: $DEE_WAN_SITE_ID" -b "$DEE_WAN_SESSION_COOKIE" 'https://admin.example.com/api/models/public-client.ts' -o /tmp/dee-wan-public.tsThe file contains exact model fields, locale and query types, response envelopes and model methods. It is dependency-free TypeScript, not a package to install. Your application never imports the command, and Dee Wan never changes your repository.
Use a model method
Section titled “Use a model method”This example assumes you actually created a public article model and an en locale. Use your site’s public hostname, not the admin origin and not a base ending in /v1.
import { createClient } from './dee-wan-public';
const client = createClient({ baseUrl: 'https://www.example.com' });const first = await client.article.list({ locale: 'en', limit: 20 });const item = await client.article.get('hello', { locale: 'en' });Server-side and build-time reads need no CORS setup. For a browser app on another origin, allow it under Settings → API → Browser origins; see browser origins. Private models require token in createClient options, kept in a server secret. Never publish it in a browser bundle. Inspect data and the list’s page metadata. Forward opaque cursors unchanged and check the returned locale under fallback. See the public API for error and pagination contracts.
Activate a reported Astro origin
Section titled “Activate a reported Astro origin”After managed source delivery, Source & delivery shows the repository owner and name, exact deployed commit and reported Astro origin. Completion labels that origin Preview unverified and leaves the existing preview target active. While signed in with site:settings, choose a readable saved item and locale and select Verify and activate preview. The server loads the recorded origin and its renderer contract, then runs the existing draft preview proof. The browser cannot substitute another origin in this action.
A successful proof marks that candidate active. A failed proof, unreachable origin, wrong-site contract or concurrent settings change leaves the previous active target unchanged and displays an actionable refusal. No build callback or system principal activates a renderer. The Advanced — custom renderer flow remains available for explicitly configured renderers.
Open an actual rendered draft preview
Section titled “Open an actual rendered draft preview”The editor’s preview action creates a one-item capability valid for 30 minutes. It reads that draft and its authorized related saved drafts, not the whole site, and does not bypass private-model access. New links recheck the creator’s current account, site membership and read permissions on every request. The root fields and languages remain limited by the permissions recorded when the link was created. Older links without this related-draft grant keep published-only relationships. Never log, index or share the capability as a public URL. Invalid capabilities are refused. Request a new preview rather than reusing an expired link.
Configure the site’s preview renderer origin and per-model route template under Settings → Source & delivery → Advanced — custom renderer. The contract uses an HTTPS origin (plain HTTP only for a loopback address: localhost, 127.0.0.1 or [::1]). It uses route placeholders {slug} and {locale}, and a token query-parameter name. The renderer must pass verification for that site before the CMS reports rendered: true. Configuration alone is insufficient. Without a verified renderer, Preview opens the public API JSON and reports rendered: false.
For a generated Astro site, explicitly set previewApiOrigin in apps/site/src/owned/extensions.ts to the CMS public API origin and verify the renderer contract. It is null by default. Only /preview uses that runtime bridge. Normal published pages use their generated snapshot, except pages with a Latest entries section and a Blog’s /blog routes: those read published entries at request time from /v1 on the site’s own host (the Dee Wan public Worker in the reference topology), or from the owned publicApiOrigin in local development, and answer no-store. Latest entries in a shared section shown in a region are pinned at generation instead. Preview responses are no-store and must not enter search or public caches. A detached site should disable this bridge when it no longer uses the CMS.
Edit from the preview
Section titled “Edit from the preview”In the entry editor, Preview here opens the saved render beside the fields. It is offered when the site’s verified renderer routes the model and speaks an editor bridge. Wide screens (1280 pixels and up) show fields and render side by side. Narrower screens show one pane at a time, with Fields and Preview tabs. Preview on site still opens the render in a new tab.
The render shows the saved version unless the site supports previewing unsaved changes (see Preview unsaved changes below). Without that support, unsaved changes do not appear until you save, and the pane says so.
The current managed and generated Blog renderers let you edit ordinary text and rich text at the rendered value. Double-click the value, choose its Edit badge, or focus its field link and press Enter to open the normal field control over the preview; format rich text there, then choose Done. Changes remain in the editor draft until Save. Other fields and older renderers open the matching form field. A custom-model renderer can opt in with data-dw-editor-inplace on its bridge identity marker; it must draw field links and the verified draft. The frame only identifies the field and its position; the editor checks the current field and permissions and owns all changes.
With a field-linked renderer (editor bridge 3, which generated sites declare):
- Double-click a rendered value, Alt-, Ctrl- or Cmd-click it, press its Edit badge, or focus it and press Enter. Where in-place editing is unavailable, the editor switches to the fields, opens the component, block or panel that holds the field, and focuses it.
- Focus a field in the editor. The render outlines every element that shows it and scrolls to the first.
- On a page model the sections, the title and the facts are edited in the page editor. Selecting one of these values offers Open page editor.
- A plain click in the render follows no link, so the preview stays open.
With a renderer that speaks only editor bridge 1, the preview works and the editor says that field links are not available for this renderer. In the pages workspace, Site preview uses the same frame: selecting a value focuses the section field it names.
The built-in renderers (a generated site and the managed renderer) link page models and Blog posts.
On a post, the title, the date, the cover, the author, category and tag links, and each body block
(including text in plugin blocks) link to their fields. Their renderer contracts route
the post model, so verify and activate the renderer again after you regenerate or redeploy. Other
ordinary models get field links only from a custom renderer that speaks bridge 3. See
the page-document contract.
A managed preview may be framed only by the editor origin its link names, and only with the
editor’s nonce. It then allows that one ancestor and the bridge script by hash. Any other preview
or published page keeps frame-ancestors 'none'.
Preview unsaved changes
Section titled “Preview unsaved changes”When the site’s verified renderer declares that it draws a model from a signed draft, Preview here shows what you have typed and not saved. Text, components, relationships, the images you picked and the language-specific address update after you pause typing; one update is in flight at a time. The managed renderer declares this for the Blog post model. Any other model, and any renderer that declared nothing, shows the saved version and the pane says why. Verify and activate the renderer again after you update the site design.
- Nothing is saved. The values undergo the same validation as a save and are then rendered; they never change the stored draft, the published version, a revision or a public read. The server does not log them. A request is refused when it is over 1 MiB.
- Previewing unsaved changes requires permission to edit the entry in the language shown. A reader sees the saved version.
- Relationships use each target’s latest saved draft, including entries never published. Your unsaved relationship choices are included; edits still unsaved in another entry are not. Ordinary and component relationships follow the same current item, field and language permissions and private-field projection. Expansion follows the site’s language fallback policy, stops after four levels or 128 targets, and truncates cycles to stable identities. Related items arrive under
item.expanded; public reads stay published-only. - Images are the files you picked in each image field, with the crop and focal point you set, rendered as they would be in a saved entry. The server looks every file up by its id in this site’s library with your current permissions; it never uses an address the browser sends. A file from another site, a file that no longer exists and a private file placed where a saved entry would refuse it (anything but a field of a private model) are refused, and the pane identifies the rejected file. A private file in a private model’s field shows its facts and no address, as a saved entry does. The language’s address is the one you typed, drawn at that address. Nothing is reserved: no address is claimed, no redirect is written, and the preview does not change the live site’s published address.
- The same field links apply: click a rendered value to open its field, and focus a field to outline its value.
- A page model is previewed in the page editor, not here.
- If the entry changed elsewhere, its language has no saved content, or the site’s design cannot draw it, the pane keeps the last good render and says so. Retry after fixing the cause.
Generated and owned renderers
Section titled “Generated and owned renderers”A generated Astro Blog site carries the verifier. It checks the draft with a public key: no render secret, account, session or provider credential is placed in the site, and signature verification does not call the CMS. The installation needs MANAGED_RENDER_HMAC_KEY and FRONTEND_URL; without them the key is not available and Settings → Source & delivery → Advanced — custom renderer says so.
- Open Settings → Source & delivery → Advanced — custom renderer once. Unsaved-entry preview key records the site’s public key for generation.
- Generate the site again and deploy it. The generated site carries that public key, so you do not need to paste it. While no key is recorded the site declares nothing and its endpoint answers
501. - Press Verify and activate in Settings → Source & delivery → Advanced — custom renderer. The site declares
entry_previewwith its Blog model and the key id in/.well-known/dee-wan-renderer.jsononly while a key is present. Preview here then shows unsaved changes.
To override the generated key, press Copy key and paste the JSON into apps/site/src/owned/entry-preview.config.ts in place of null. The file is yours: regeneration never replaces it, and a value there wins over the generated key.
- The signed draft names the site, the renderer’s origin, the entry, the language, the version you loaded, the exact bytes and a 60-second expiry. The renderer refuses any other site, origin, entry, language, version, bytes, editor origin or expiry. The frame must be opened from the editor origin named in the signed draft.
- The endpoint is
/.well-known/dee-wan-entry-preview. It takes the editor’s form post and writes, caches and logs nothing. - Astro refuses cross-origin form posts, and the editor’s frame is on another origin. A generated Blog site therefore turns that check off and repeats it in
src/middleware.tsfor every address except this endpoint. - Rotate key starts a new key and is recorded in the audit ledger as a site settings change. The old key stops working at once: generate and deploy the site again (or update your override) and press Verify and activate again. Until then Preview here shows the saved version and says the site verifies an older key.
- If the site declares no key, or an older one, the pane shows the saved version and says why.
A renderer you own copies apps/site/src/lib/entry-preview-verifier.ts from a generated site. It has no imports and uses only Web Crypto Ed25519 (Node 24, Cloudflare Workers, current browsers). Call verifyEntryPreview({ envelope, body }, config, context) with the two form fields envelope and body, the pasted configuration, and a context of your renderer’s site_id, own origin, release identity (renderer_contract_digest), the models it draws, now in Unix seconds and the editor_origin query parameter of the frame’s address. On success it returns the verified item to draw with your own post view; otherwise a refusal with status (403, 409, 413, 422 or 501) and reason. Declare support in /.well-known/dee-wan-renderer.json with entry_preview: { "version": 1, "models": ["post"], "key_ids": [<kid from the configuration>] }, each model also routed; entryPreviewDeclaration(config, models) builds it, and returns null while there is no configuration. Do not declare it until the endpoint works. A renderer that shares the installation’s render key (the managed renderer) declares no key_ids.
Images on a generated site
Section titled “Images on a generated site”A generated Astro site draws every image through one rule, deliverImage in apps/site/src/lib/media-presentation.ts. The preview route and published pages use the same rule and the same settings, so they produce the same URLs.
- Each image gets
widthandheightfrom its recorded size, or from its crop when the use has one. An image with no recorded size gets neither and nosrcset. - The crop and focal point of that one use are applied. The same image used twice keeps two framings.
- The stored file never changes.
Transformed delivery is off until you declare the media hosts whose Cloudflare zone has Image Transformations enabled. Set imageTransformOrigins in apps/site/src/owned/extensions.ts, for example ["https://media.example.com"]. A wrong entry fails the build by name. An image on a declared host is then served as https://media.example.com/cdn-cgi/image/width=640,quality=85,format=auto,fit=scale-down,trim=<top>;<right>;<bottom>;<left>/<key>, with a srcset of 320, 640, 960, 1280 and 1920 pixels capped at the framed width, and sizes="100vw". trim cuts the crop in pixels before the resize. Every other image keeps its original URL; a crop is then shown by CSS (aspect-ratio, object-fit: cover, object-position), and a focal point by object-position.
Deployed acceptance is a manual gate. Run it once per media host:
- In the Cloudflare dashboard, open the zone that serves the media host, then Images → Transformations, and enable transformations for that zone. Expected: the zone shows as enabled.
- Add the host to
imageTransformOrigins, build and deploy the site. - Open a published page with a framed image. In the browser’s network panel the image request goes to
/cdn-cgi/image/...on the media host and answers200with an image content type. Expected: the picture shows the crop, andwidth/heighton theimgmatch the framed size. - Narrow the window to 390 pixels and reload. Expected: a smaller
srcsetcandidate is fetched. - Request the original URL. Expected: the original bytes, unchanged.
A 404 or 9xxx error on /cdn-cgi/image/ means transformations are not enabled for that zone: remove the host from imageTransformOrigins until they are.
Authorities: backend/src/api/models.ts, backend/src/lib/public-types/generate.ts, backend/src/lib/public-types/contract.ts, frontend/static/dee-wan-client.mjs, backend/src/api/content_lifecycle.ts, backend/src/lib/site-settings.ts, backend/src/lib/graduate/site-media-presentation.ts, backend/src/lib/graduate/site-emit.ts, backend/src/lib/field-link-runtime.ts and backend/src/lib/page-contract/field-links.ts.
Next step
Section titled “Next step”Read the public read API for the full read contract, filters and refusals.
Unreleased / pre-MVP; no stable version
Source revision: fe23db481c422b40e317b92fc98116b4c0c26a87 (modified checkout)
Build date (UTC): 2026-10-18T09:22:34.630Z
Upgrade target: unavailable: modified checkout. Release notes