agent-cms 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Agent-first headless CMS. Runs as a Cloudflare Worker backed by D1 and R2 in you
6
6
 
7
7
  - **Structured text with typed blocks** — a document tree where rich components (code blocks, media, custom types) are embedded inline. One GraphQL query returns the full tree with discriminated block unions. Map directly to React/Svelte/Vue components in a single server hop. Render with [`react-datocms`](https://github.com/datocms/react-datocms), `vue-datocms`, or `datocms-svelte` — the structured text format is [DAST](https://www.datocms.com/docs/structured-text/dast), an open standard.
8
8
  - **Hybrid search** — FTS5 for keyword matching, Cloudflare Vectorize for semantic similarity, combined with reciprocal rank fusion. All on D1.
9
- - **Draft/publish with scheduling** — records start as drafts. Publishing captures a version snapshot. Schedule publish/unpublish at future datetimes. Full version history — restore to any snapshot, and the restore itself is reversible.
9
+ - **Draft/publish with preview** — records start as drafts. Publishing captures a version snapshot. Models can declare a `canonicalPathTemplate` so agents and editors get preview URLs for drafts. Short-lived preview tokens grant draft access to GraphQL without editor credentials. Schedule publish/unpublish at future datetimes. Full version history — restore to any snapshot, and the restore itself is reversible.
10
10
  - **Geospatial filtering** — `lat_lon` field type with `near(latitude, longitude, radius)` queries in GraphQL.
11
11
  - **Automatic reverse references** — link model A to model B, and B gets a query field for all records in A that reference it, with full filtering, ordering, and pagination.
12
12
  - **Two MCP servers** — admin MCP (`/mcp`) for schema and content, editor MCP (`/mcp/editor`) scoped to content operations only. Create editor tokens with optional expiry.
@@ -107,6 +107,115 @@ FTS5 keyword search with BM25 ranking and snippets, scoped to all models or a si
107
107
  - **`semantic`** — Vectorize cosine similarity.
108
108
  - **`hybrid`** (default) — Reciprocal rank fusion of keyword + semantic results.
109
109
 
110
+ ## Draft preview
111
+
112
+ Records on draft-enabled models start as drafts. The CMS stores both the draft state (real columns) and the published snapshot. GraphQL serves published content by default. Draft preview lets agents and editors see unpublished content on the real site before publishing.
113
+
114
+ ### How it works
115
+
116
+ 1. **Set a URL template on your model** — `canonicalPathTemplate: "/posts/{slug}"`. The CMS resolves `{slug}` from the record and includes `_previewPath` in tool/REST responses.
117
+
118
+ 2. **Configure the site URL** — pass `siteUrl` to `createCMSHandler`. The CMS uses this to generate fully assembled preview links.
119
+
120
+ 3. **Agent or editor requests a preview** — the `get_preview_url` MCP tool returns a ready-to-share link with a short-lived preview token baked in. One tool call, no assembly required.
121
+
122
+ 4. **User clicks the preview link** — the site's enable route validates the token against the CMS, sets a cookie, and redirects to the content page. The site then fetches draft content from GraphQL.
123
+
124
+ 5. **Disable draft mode** — clear the cookie via `/api/draft-mode/disable?redirect=/`.
125
+
126
+ ### MCP workflow
127
+
128
+ The agent never assembles preview URLs manually. One tool call does everything:
129
+
130
+ ```
131
+ Agent: create_record({ modelApiKey: "post", data: { title: "Draft Post", ... } })
132
+ CMS: { id: "abc", _status: "draft", _previewPath: "/posts/draft-post" }
133
+
134
+ Agent: get_preview_url({ recordId: "abc", modelApiKey: "post" })
135
+ CMS: { url: "https://mysite.com/api/draft-mode/enable?token=pvt_...&redirect=/posts/draft-post",
136
+ previewPath: "/posts/draft-post", expiresAt: "2026-03-24T17:00:00Z" }
137
+
138
+ Agent: "Here's your draft preview: https://mysite.com/api/draft-mode/enable?token=pvt_...&redirect=/posts/draft-post"
139
+ ```
140
+
141
+ If the agent has browser access, it can follow the link to visually verify content and iterate before publishing.
142
+
143
+ ### Site integration
144
+
145
+ The package exports `createPreviewHandler` for the common case (Astro, SvelteKit, generic Workers). For Next.js, use `draftMode().enable()` in your route handler alongside the CMS cookie — see the framework-specific notes below.
146
+
147
+ ```ts
148
+ import { createPreviewHandler } from "agent-cms";
149
+
150
+ const preview = createPreviewHandler({
151
+ cmsBaseUrl: "https://my-cms.workers.dev",
152
+ });
153
+
154
+ // Mount at /api/draft-mode/* in your site's router
155
+ ```
156
+
157
+ The handler validates the token against the CMS, sets an `HttpOnly` cookie (`__agentcms_preview`) with the token, and redirects. The cookie's `Max-Age` matches the token's remaining TTL.
158
+
159
+ In your GraphQL client, forward the cookie as a header when present:
160
+
161
+ ```ts
162
+ const previewToken = getCookie("__agentcms_preview");
163
+ const headers: Record<string, string> = {};
164
+ if (previewToken) {
165
+ headers["X-Preview-Token"] = previewToken;
166
+ headers["Cache-Control"] = "no-store"; // bypass CDN for draft content
167
+ }
168
+ ```
169
+
170
+ GraphQL responses in preview mode include `Cache-Control: private, no-store` and a `_preview` field:
171
+
172
+ ```graphql
173
+ { _preview { enabled expiresAt } }
174
+ ```
175
+
176
+ Use this to render a preview banner — or include the `<agent-cms-preview-bar>` web component that shows "Draft Mode" with a disable link.
177
+
178
+ ### Service bindings
179
+
180
+ When your site and CMS are in the same Cloudflare account, skip cookies entirely. Pass `X-Preview-Token` directly on the service binding call:
181
+
182
+ ```ts
183
+ const res = await env.CMS.fetch("https://internal/graphql", {
184
+ headers: {
185
+ "X-Preview-Token": previewToken, // from your site's own session/cookie
186
+ },
187
+ body: JSON.stringify({ query }),
188
+ });
189
+ ```
190
+
191
+ No CORS, no cross-origin cookies, zero latency. This is the recommended path for Cloudflare deployments.
192
+
193
+ ### Framework notes
194
+
195
+ **Astro / SvelteKit / generic Workers** — use `createPreviewHandler` directly. Read the cookie in middleware and forward as `X-Preview-Token`.
196
+
197
+ **Next.js** — your enable route must also call `draftMode().enable()` to integrate with Next.js ISR/caching. Set `SameSite=None; Secure` on the cookie for iframe compatibility. See the DatoCMS Next.js pattern — the same approach applies.
198
+
199
+ ### URL templates
200
+
201
+ Templates use `{field_name}` placeholders resolved from the record:
202
+
203
+ | Template | Record | Result |
204
+ |----------|--------|--------|
205
+ | `/posts/{slug}` | `{ slug: "hello-world" }` | `/posts/hello-world` |
206
+ | `/docs/{category}/{slug}` | `{ category: "guides", slug: "setup" }` | `/docs/guides/setup` |
207
+ | `/{slug}` | `{ slug: "about" }` | `/about` |
208
+
209
+ Templates are paths only — no origin, no locale prefix. Locale URL strategies (prefix, subdomain, root folding) are handled by your site's routing, not the CMS. The enable route accepts an optional `locale` param for your middleware to use.
210
+
211
+ ### Preview tokens
212
+
213
+ - Created internally by `get_preview_url` or via `POST /api/preview-tokens`
214
+ - Default expiry: 24 hours (editorial workflows are async)
215
+ - Stored as SHA-256 hashes in D1 (a database breach doesn't leak usable tokens)
216
+ - Grant read-only access to all draft content via GraphQL
217
+ - `X-Preview-Token` header takes precedence over `__agentcms_preview` cookie if both are present
218
+
110
219
  ## Editor tokens
111
220
 
112
221
  Editor tokens are the credential for non-admin editing flows. Create them via `POST /api/tokens` or the `editor_tokens` MCP tool (with `action: "create"`). The raw token is shown once; the server stores a hash.
@@ -177,6 +286,7 @@ Only `DB` is required. Everything else is optional and degrades gracefully.
177
286
  | `VECTORIZE` | Vectorize | Semantic vector search. Requires `AI`. |
178
287
  | `CMS_WRITE_KEY` | Secret | Auth for writes, MCP, and publish. Without it, writes are open. |
179
288
  | `ASSET_BASE_URL` | Variable | Public URL prefix for assets and Image Resizing. Must be a custom domain for transforms. |
289
+ | `SITE_URL` | Variable | Your site's public URL (e.g. `https://mysite.com`). Required for `get_preview_url` to generate fully assembled preview links. |
180
290
 
181
291
  ```jsonc
182
292
  {
@@ -212,7 +322,8 @@ Focal points, blurhash, and color palette are stored per-asset. Cloudflare Image
212
322
 
213
323
  ## Examples
214
324
 
215
- - [`examples/blog/`](./examples/blog/) — CMS Worker + Astro SSR site with typed GraphQL (gql.tada), structured text rendering, responsive images, service bindings
325
+ - [`examples/blog/`](./examples/blog/) — CMS Worker + Astro SSR site with typed GraphQL (gql.tada), structured text rendering, responsive images, service bindings, **draft preview mode**
326
+ - [`examples/nextjs/`](./examples/nextjs/) — Next.js App Router with `draftMode()` integration, multi-root GraphQL queries, preview bar component
216
327
  - [`examples/editor-mcp/`](./examples/editor-mcp/) — editor onboarding: app-land OAuth gateway, scoped editor tokens, separate MCP URLs for developers and editors
217
328
 
218
329
  ## License