@lars-plate/delta-client
v0.2.1
Published
This is a simple utility package for the Delta Client
Readme
@platecms/delta-client
TypeScript utilities for building Delta frontends that integrate with Plate CMS. The package provides:
- Window connectors — typed
postMessagecommunication between a Plate preview host and an embedded experience (iframe) - Content parsers — turn GraphQL
ContentItemand experience-component data into plain JavaScript objects keyed by field slug - GraphQL types — generated TypeScript definitions for the Plate GraphQL schema
Installation
npm install @platecms/delta-client
# or
pnpm add @platecms/delta-client
# or
yarn add @platecms/delta-clientPeer/runtime: This package targets browser environments (Window, postMessage). Node.js is only needed for development (build, GraphQL codegen).
Package exports
The library is published as an ES module with optional CommonJS builds. Import from the root or from subpaths:
| Import path | Description |
|-------------|-------------|
| @platecms/delta-client | Everything (connectors, parsers, graphql) |
| @platecms/delta-client/connectors | Window connector client & server |
| @platecms/delta-client/parsers | Content parsing utilities |
| @platecms/delta-client/graphql | Generated GraphQL TypeScript types |
// Full barrel
import { WindowConnectorClient, parseContentItem, type ContentExperience } from '@platecms/delta-client';
// Subpath imports (smaller bundles when tree-shaking)
import { WindowConnectorServer } from '@platecms/delta-client/connectors';
import { parseDataFromExperienceComponent } from '@platecms/delta-client/parsers';
import type { ContentItem, ExperienceComponent } from '@platecms/delta-client/graphql';Window connectors
Plate’s Delta preview runs your experience inside an iframe while the CMS UI lives in the parent window. Connectors wrap window.postMessage with:
- Origin checks for security
- A health-check handshake so the host knows when the iframe is alive
- Typed events for CMS ↔ experience messaging
Architecture
┌─────────────────────────────┐ postMessage ┌─────────────────────────────┐
│ Plate CMS (parent window) │ ◄──────────────────────────► │ Your experience (iframe) │
│ WindowConnectorServer │ HEALTH_CHECK / RESPONSE │ WindowConnectorClient │
└─────────────────────────────┘ CONTENT_EXPERIENCE_SEND └─────────────────────────────┘
GRID_PLACEMENT_CLICKED
ROOT_EXPERIENCE_COMPONENT_SENDConnector status
The server emits status_changed as the link moves through:
| Status | Meaning |
|--------|---------|
| idle | No client configured or after teardown() |
| connecting | Health checks in progress |
| connected | Client responded to a health check |
| disconnected | Max retries exceeded without a response |
Event types
| ConnectorEventType | Direction (typical) | Payload |
|----------------------|---------------------|---------|
| content-experience-send | Host → experience | Full ContentExperience GraphQL object |
| root-experience-component-send | Host → experience | Root ExperienceComponent |
| grid-placement-clicked | Experience → host | { prn, experienceComponentPrn } |
Server (parent / CMS side)
import {
WindowConnectorServer,
ConnectorStatus,
ConnectorEventType,
} from '@platecms/delta-client/connectors';
const iframe = document.querySelector<HTMLIFrameElement>('#delta-preview')!;
const childWindow = iframe.contentWindow!;
const childOrigin = 'https://your-experience.example.com';
const server = new WindowConnectorServer(window, 3); // 3 health-check retries
server
.setClient(childWindow, childOrigin)
.connect();
server.on('status_changed', ({ status }) => {
if (status === ConnectorStatus.CONNECTED) {
console.log('Experience iframe is ready');
}
if (status === ConnectorStatus.DISCONNECTED) {
console.warn('Lost connection to iframe');
}
});
server.on('message', (event) => {
if (event.type === ConnectorEventType.GRID_PLACEMENT_CLICKED) {
console.log('User clicked placement', event.payload.prn);
}
});
// Push content into the iframe when the editor selects an experience
function sendContentExperience(contentExperience: ContentExperience) {
server.send({
type: ConnectorEventType.CONTENT_EXPERIENCE_SEND,
payload: contentExperience,
});
}
// Cleanup when unmounting the preview
function destroy() {
server.teardown();
}Client (iframe / experience side)
import {
WindowConnectorClient,
ConnectorEventType,
} from '@platecms/delta-client/connectors';
import { parseDataFromExperienceComponent } from '@platecms/delta-client/parsers';
const parentOrigin = 'https://cms.plate.example.com';
const parentWindow = window.parent;
const client = new WindowConnectorClient(window, {
origin: parentOrigin,
window: parentWindow,
});
client.on('message', (event) => {
switch (event.type) {
case ConnectorEventType.CONTENT_EXPERIENCE_SEND: {
const experience = event.payload;
const component = experience.experienceComponent;
const data = parseDataFromExperienceComponent(
component.buildingBlock,
component.buildingBlockFieldFulfillments,
);
renderExperience(data);
break;
}
case ConnectorEventType.ROOT_EXPERIENCE_COMPONENT_SEND: {
const component = event.payload;
// Handle root component updates (e.g. layout shell)
break;
}
}
});
// Notify the CMS when the user interacts with a grid placement
function onPlacementClick(prn: string, experienceComponentPrn: string) {
client.send({
type: ConnectorEventType.GRID_PLACEMENT_CLICKED,
payload: { prn, experienceComponentPrn },
});
}
function destroy() {
client.teardown();
}Message format
Internally, messages use this shape over postMessage:
// Application events
{ type: string; payload: string } // payload is JSON.stringify'd
// Health check (built-in)
{ type: 'HEALTH_CHECK' }
{ type: 'HEALTH_CHECK_RESPONSE' }Always pass the correct target origin when constructing connectors so both sides reject messages from unexpected origins.
Content parsers
Parsers convert Plate CMS GraphQL structures into plain objects suitable for templates, React props, or static site generators. Field slugs from the schema become camelCase keys (e.g. hero-title → heroTitle).
parseContentItem
Maps a ContentItem and its contentType.contentFields to { [fieldSlug]: value }.
Supported value types include assets, primitives, nested content items, tags, smart text, grid placements, and more. Nested CONTENT_ITEM values are parsed recursively.
import { parseContentItem } from '@platecms/delta-client/parsers';
import type { ContentItem } from '@platecms/delta-client/graphql';
const contentItem: ContentItem = await fetchContentItemFromApi();
const data = parseContentItem(contentItem);
// Example result:
// {
// heroTitle: 'Welcome',
// heroImage: { url: '...', altText: '...', ... },
// relatedArticles: [{ title: '...' }, { title: '...' }],
// }parseDataFromExperienceComponent
Maps a BuildingBlock plus its BuildingBlockFieldFulfillment[] (from an ExperienceComponent) to the same slug-keyed shape. Use this when rendering from Delta connector events or live preview data.
import { parseDataFromExperienceComponent } from '@platecms/delta-client/parsers';
import type { ExperienceComponent } from '@platecms/delta-client/graphql';
function componentToProps(component: ExperienceComponent) {
return parseDataFromExperienceComponent(
component.buildingBlock,
component.buildingBlockFieldFulfillments,
);
}Placeholder mode (insertPlaceholders)
For local development or Storybook, you can fill required fields with sensible placeholders when values are missing:
import { parseContentItem, type ParseDataConfig } from '@platecms/delta-client/parsers';
const config: ParseDataConfig = { insertPlaceholders: true };
const previewData = parseContentItem(partialContentItem, config);
// Required empty STRING → 'Hello World'
// Required empty ASSET → example asset object
// Required nested CONTENT_ITEM → minimal nested structureWhen insertPlaceholders is false (default), missing fields are null or [] depending on whether the field allows multiple values.
Array vs single values
Whether a field becomes an array is derived from CMS validation rules (COUNT with max > 1). Single-value fields return one value or null; multi-value fields return an array (possibly empty).
GraphQL types
The ./graphql export re-exports TypeScript types generated from the Plate GraphQL schema (e.g. ContentExperience, ExperienceComponent, ContentItem, BuildingBlock, Asset, enums like TagVisibility).
Use these types when:
- Typing connector event payloads
- Typing parser inputs/outputs
- Sharing shapes with your own GraphQL client (
graphql-request, Apollo, etc.)
import type {
ContentExperience,
ExperienceComponent,
ContentItem,
BuildingBlockFieldFulfillment,
} from '@platecms/delta-client/graphql';
async function loadExperience(prn: string): Promise<ContentExperience> {
// Your GraphQL query here — types align with generated definitions
throw new Error('implement with your API client');
}Note: This package does not ship a GraphQL client or queries—only types. Run your own queries against your Plate GraphQL endpoint and pass the results to parsers or connectors.
Regenerating types (maintainers)
If you maintain this repo and need to refresh types from a running Plate API:
pnpm codegenConfigure the schema URL and auth in codegen.ts, then commit the updated src/graphql/generated.ts.
End-to-end example
A minimal experience app inside an iframe that renders CMS content and reports grid clicks:
import { WindowConnectorClient, ConnectorEventType } from '@platecms/delta-client/connectors';
import { parseDataFromExperienceComponent } from '@platecms/delta-client/parsers';
const CMS_ORIGIN = import.meta.env.VITE_PLATE_ORIGIN;
const client = new WindowConnectorClient(window, {
origin: CMS_ORIGIN,
window: window.parent,
});
client.on('message', (event) => {
if (event.type !== ConnectorEventType.CONTENT_EXPERIENCE_SEND) return;
const { experienceComponent } = event.payload;
const props = parseDataFromExperienceComponent(
experienceComponent.buildingBlock,
experienceComponent.buildingBlockFieldFulfillments,
);
document.getElementById('app')!.textContent = JSON.stringify(props, null, 2);
});
document.querySelectorAll('[data-grid-placement]').forEach((el) => {
el.addEventListener('click', () => {
client.send({
type: ConnectorEventType.GRID_PLACEMENT_CLICKED,
payload: {
prn: el.getAttribute('data-prn')!,
experienceComponentPrn: el.getAttribute('data-component-prn')!,
},
});
});
});API reference (summary)
Connectors
| Export | Description |
|--------|-------------|
| WindowConnectorClient | Iframe-side connector; send(), on('message'), teardown() |
| WindowConnectorServer | Parent-side connector; setClient(), connect(), send(), on('message' \| 'status_changed'), teardown() |
| ConnectorEventType | Enum of event type strings |
| ConnectorStatus | Connection lifecycle enum |
| ConnectorEventOption | Union of all typed connector events |
| StatusChangedEvent | { status: ConnectorStatus } |
Parsers
| Export | Description |
|--------|-------------|
| parseContentItem(contentItem, config?) | Content item → slug-keyed object |
| parseDataFromExperienceComponent(buildingBlock, fulfillments, config?) | Experience component data → slug-keyed object |
| ParseDataConfig | { insertPlaceholders?: boolean } |
| defaultParseDataConfig | { insertPlaceholders: false } |
GraphQL
| Export | Description |
|--------|-------------|
| * from @platecms/delta-client/graphql | Generated schema types (see src/graphql/generated.ts) |
Development
# Install dependencies
pnpm install
# Build library (TypeScript + Vite)
pnpm build
# Regenerate GraphQL types (requires Plate API at configured URL)
pnpm codegenBuild output is written to dist/ with separate entry points matching package.json exports.
License
MIT © Lars Baalmans
