Spunto CMS

Drafts and publishing

Publishing a card, reading only what was published, knowing what changed since — and the webhooks a site listens to.

Every card is a draft until someone publishes it. Publishing copies what the card says — title, icon, cover, properties, body — into its published version. Edits made afterwards stay in the draft: a site reading the published version keeps serving the old text until the card is published again.

Nothing changes for a client that does not ask. Without a version, every read returns drafts, exactly as before.

Publishing and unpublishing

PUT    /workspaces/{workspaceId}/items/{itemId}/publication
DELETE /workspaces/{workspaceId}/items/{itemId}/publication

Both return the card (ItemDetail) with its publication fields up to date. In the app, the card's header shows its state and a Publish button; Unpublish is in the card's ⋯ menu.

  • Publishing with nothing new does nothing. No write, no event, no webhook. A double click, a script run twice or an app that republishes everything to be safe costs nothing and wakes no one.
  • Unpublishing removes the published version. The card becomes a draft again, its content untouched.
  • An archived card cannot be published (409): it is in no list, so there is nothing a reader could see.
  • Publishing is a write: it takes the member role, like editing. For an installed app it is a scope of its own, items:publish — items:write is not enough. An app that translates or corrects drafts should not be able to put them online.

Reading only what was published

Add "version": "published" next to the definition — not inside it. A saved view serves both your editors (the draft) and your site (the published version), so the version says who is reading, not what the view selects.

POST /workspaces/{workspaceId}/collections/{collectionId}/query
{
  "definition": {
    "filter": { "op": "eq", "field": { "fieldId": "<slugFieldId>" }, "value": "hello-world" },
    "sort": [{ "field": { "fieldId": "sys:order" }, "direction": "asc" }],
    "includeSecondaryLists": false
  },
  "version": "published",
  "limit": 1
}

The same field works on grouped-query and group-counts. For a single card:

GET /workspaces/{workspaceId}/items/{itemId}?version=published

It returns a 404 if the card is not published or is archived — a site does not need to know which.

What a published read returns:

  • only published cards — a card that was never published does not exist for it;
  • what each card said when it was published: title, icon, cover, properties and body come from the published version. Filters and sorts apply to those values, so a slug you changed in the draft does not match until you republish;
  • where the card is filed today: list, secondary lists, parent, manual order and status are read from the card as it is now. Moving a card or reordering a list does not require republishing;
  • updatedAt is the time the version was published.

A cursor taken on a draft read is refused on a published read, and vice versa — the two do not list the same cards.

Draft, published, changed

Every Item carries two optional fields:

publishedAtchangedSincePublicationState
nullfalsedraft — never published, or unpublished
a datefalsepublished, up to date
a datetruepublished, and the draft has changed since

"Changed" means the content changed: title, icon, cover, properties or body. Changing the status, moving the card or reordering it does not count — none of that changes what the site shows.

To filter or group by state, use the system field sys:publication, whose values are draft, published and changed. A card that is changed is still online: "everything published" is { "op": "in", "field": { "fieldId": "sys:publication" }, "values": ["published", "changed"] }.

Two fields, not an enum

The state is published as a date and a boolean rather than a three-value state, on purpose: strict decoders (the iOS client is one) fail the whole response on an enum value they do not know. Both fields are optional — absent means "not said here", not "draft".

Webhooks

Listen to item.published and item.unpublished, not item.updated: the editor saves the draft every second or so while someone types, and none of that changes what your site serves.

item.published carries publishedAt (before / after) and, on a republish, what the new version changed compared with the previous one — a slug before and after is the old URL to invalidate. The body itself does not travel; only the fact that it changed (bodyChanged). A first publication carries publishedAt alone.

What it is not

There is no scheduled publishing, no version history and no publish-many route, and a key that can read a workspace can read its drafts. See limits.

On this page