Documentation

Content JSON

Blobify stores one mutable draft and, after the first publish, one published snapshot for each content item. The published snapshot is a public artifact at the bucket root. The draft and the edit history are authoring artifacts in the private scope, under {privateStoragePrefix}/ (the unguessable pv_... folder at the organization root):

{privateStoragePrefix}/spaces/{spaceId}/content/{model}/{contentId}/draft.jsonspaces/{spaceId}/content/{model}/{contentId}/published.json

The {contentId}/ directory is part of the path. History files live in the private scope under {contentId}/history/, beside the draft document.

Draft document

The dashboard edits draft.json. Audit values use { "at", "by" }, where by identifies a user, API key, or system action. updated records the latest locale-specific edits. updatedShared records the latest edit to a non-translatable field.

json
code
{
  "id": "art_123",
  "model": "article",
  "created": {
    "at": "2026-03-01T10:00:00.000Z",
    "by": { "type": "user", "userId": "usr_1" }
  },
  "updated": {
    "en": {
      "at": "2026-03-02T08:55:00.000Z",
      "by": { "type": "user", "userId": "usr_1" }
    }
  },
  "updatedShared": {
    "at": "2026-03-02T08:55:00.000Z",
    "by": { "type": "user", "userId": "usr_1" }
  },
  "published": {
    "en": {
      "at": "2026-03-02T09:00:00.000Z",
      "by": { "type": "user", "userId": "usr_1" }
    }
  },
  "publishedLocales": ["en"],
  "firstPublished": {
    "at": "2026-03-02T09:00:00.000Z",
    "by": { "type": "user", "userId": "usr_1" }
  },
  "lastPublished": {
    "at": "2026-03-02T09:00:00.000Z",
    "by": { "type": "user", "userId": "usr_1" }
  },
  "fields": {
    "title": { "en": "Hello world" },
    "slug": "hello-world",
    "coverImage": { "type": "asset", "assetId": "img_456" },
    "author": { "model": "author", "id": "author_1" }
  }
}

The dashboard compares per-locale updated values and updatedShared with publish timestamps to identify drafts that need republishing.

Published document

published.json contains only the locales that are live. Public audit values are ISO timestamp strings, so actor identity is not exposed in the public document.

json
code
{
  "id": "art_123",
  "model": "article",
  "created": "2026-03-01T10:00:00.000Z",
  "publishedLocales": ["en"],
  "published": {
    "en": "2026-03-02T09:00:00.000Z"
  },
  "firstPublished": "2026-03-02T09:00:00.000Z",
  "lastPublished": "2026-03-02T09:00:00.000Z",
  "fields": {
    "title": { "en": "Hello world" },
    "slug": "hello-world",
    "coverImage": { "type": "asset", "assetId": "img_456" },
    "author": { "model": "author", "id": "author_1" }
  }
}

Publishing one locale does not leak unpublished edits from another locale. Blobify merges the requested locales with the previous published snapshot and preserves the other locales that were already live.

Localized fields

A field with translatable: true is a locale map:

json
code
{
  "title": {
    "en": "Welcome",
    "is": "Velkomin"
  }
}

A non-translatable field is stored once:

json
code
{
  "slug": "welcome"
}

This rule applies to every field type. A translatable rich-text field is a locale map of root AST values. A translatable blocks field is a locale map of block objects or arrays.

Content and asset references

Content references store identity, not an expanded document:

json
code
{
  "author": {
    "model": "author",
    "id": "author_1"
  }
}

Add spaceId when the target is in another allowed space. Generated clients can resolve references from summaries or full content according to client options.

Asset fields use a tagged reference:

json
code
{
  "coverImage": {
    "type": "asset",
    "assetId": "img_456"
  }
}

Localized alt text, intrinsic dimensions, and the media URL belong to the asset authoring metadata document in the private scope at spaces/{spaceId}/assets/{assetId}.json. They are not duplicated on every asset-field reference.

Focal points and crops

An image reference can carry optional per-use framing data. Values are fractions from 0 to 1 in the original image coordinate space.

json
code
{
  "coverImage": {
    "type": "asset",
    "assetId": "img_456",
    "focalPoint": { "x": 0.28, "y": 0.52 },
    "crop": { "x": 0.1, "y": 0, "width": 0.5, "height": 0.5 }
  }
}

focalPoint identifies the point to keep visible during a fixed-size cover crop. crop identifies the rectangular region to retain. The generated client applies these values when producing Cloudflare Images parameters. The original media file is unchanged.

Links

A link field stores one tagged value:

json
code
{
  "internalLink": {
    "type": "internal",
    "model": "page",
    "id": "page_1"
  },
  "assetLink": {
    "type": "asset",
    "assetId": "file_1"
  },
  "externalLink": {
    "type": "external",
    "url": "https://example.com",
    "newTab": true
  }
}

Internal content and asset links can also carry spaceId for a cross-space target.

Blocks

A multiple blocks field stores an ordered array. Block fields sit beside the required type and id properties.

json
code
{
  "sections": [
    {
      "type": "hero-block",
      "id": "blk_1",
      "heading": { "en": "Welcome" },
      "image": { "type": "asset", "assetId": "img_456" }
    }
  ]
}

With multiple: false, the field stores one block object. With translatable: true, the outer value is a locale map whose values follow the configured single or multiple shape.

Rich text

Rich text is a root AST. Inline text values are plain strings because localization happens at the field level.

json
code
{
  "body": {
    "en": {
      "type": "root",
      "children": [
        {
          "type": "paragraph",
          "children": [
            { "type": "text", "value": "Hello world", "marks": ["bold"] }
          ]
        },
        {
          "type": "paragraph",
          "children": [
            {
              "type": "contentRef",
              "ref": { "model": "article", "id": "art_999" },
              "children": [{ "type": "text", "value": "Read next" }]
            }
          ]
        },
        {
          "type": "block",
          "block": {
            "type": "callout-block",
            "id": "blk_2",
            "heading": { "en": "Important" }
          }
        }
      ]
    }
  }
}

The full validated AST also supports headings, lists, blockquotes, code, horizontal rules, and asset nodes. The model schema can restrict nodes, marks, heading levels, content-reference models, block types, and assets.

Physical keys with a content prefix

If the space sets contentPrefix, the logical content path is rewritten physically:

spaces/main/cp_site_a1/content/article/art_123/published.json

The prefix appears as a typed cp_... segment in the public tree only. The generated client and dashboard handle this mapping. Use their path helpers instead of inserting the segment by hand.

Archived documents

Deleting a live entry archives its current draft.json and published.json into the archive, which lives in the private scope. The archived draft receives an archived audit event. History stays under the content item history path in the private scope until the already archived entry is permanently deleted.