Posts

Blog posts with Markdown body, SEO metadata, visibility/prominence flags, inline author and tag junctions, and per-language translations.

Service: blog.posts

Blog post. The post body lives in content (Markdown / plain text); SEO metadata in meta_title / meta_description / meta_keywords; visibility and prominence via is_public / is_featured. Authors and tags are attached through inline junction collections edited on the Post form.

Fields

Field Type Required Description
id string Primary key. Read-only, auto-generated
slug string URL slug and lookup key, unique (format: slug)
hub_id string? Hub this post belongs to. null if uncategorized
meta_title string SEO title
meta_description string Overwrites the auto-generated description based on content. Default ""
meta_keywords string Overwrites the auto-generated keywords based on the post’s tags. Default ""
content string Post body (Markdown / plain text)
is_public boolean Whether the post is publicly visible. Default false
is_featured boolean Whether this post is highlighted at the top of the index. Default false
icon string Bootstrap Icons class for the post cover. Default "bi-file-text"
color string? Brand color (hex); gradient end derived in CSS. null if unset
created_at string ISO 8601 creation timestamp. Read-only
updated_at string ISO 8601 last update timestamp. Read-only
reading_minutes integer Estimated reading time of content in whole minutes (min 1, ~200 wpm). Read-only, computed
post_translation PostTranslation[] Inline translations
post_author PostAuthor[] Inline author assignments (post ↔ user)
post_tag PostTag[] Inline tag assignments (post ↔ tag)

Filterable: hub_id · is_public · is_featured

PostTranslation

Per-language overlay for a post. Translatable fields are nullable so a partial translation overlays onto the base post without clobbering untranslated fields (null = inherit base).

Field Type Required Description
id string Primary key. Read-only. Include when updating existing rows
source_id string Post being translated. Read-only, auto-set from parent
language_id string Language of this translation
meta_title string? Translated SEO title (null = inherit base)
meta_description string? Translated SEO description (null = inherit base)
meta_keywords string? Translated SEO keywords (null = inherit base)
content string? Translated post body (null = inherit base)
is_public boolean Whether this translation is publicly visible. Default false
created_at string ISO 8601 creation timestamp. Read-only
updated_at string ISO 8601 last update timestamp. Read-only

Unique constraint: { source_id, language_id } — one translation per language per post.

PostAuthor

Junction row linking a post to an author (user). Edited inline on the Post form.

Field Type Required Description
id string Junction row id. Read-only. Include when updating existing assignments
post_id string Post id. Read-only, auto-set from parent
user_id string Author user id

Unique constraint: { post_id, user_id } — a user is listed as an author of a given post at most once.

PostTag

Junction row linking a post to a tag. Edited inline on the Post form.

Field Type Required Description
id string Junction row id. Read-only. Include when updating existing assignments
post_id string Post id. Read-only, auto-set from parent
tag_id string Tag id

Unique constraint: { post_id, tag_id } — a tag is applied to a given post at most once.

find

List posts with filtering, sorting, and pagination.

HTTP

Request

GET /v1/blog/posts?filter=%5B%5B%22is_public%22%2C%22%24eq%22%2Ctrue%5D%2C%5B%22hub_id%22%2C%22%24eq%22%2C%221920438291700%22%5D%5D&sort=-updated_at&fields=id,slug,hub_id,meta_title,is_public,is_featured,icon,color,reading_minutes,created_at,updated_at,post_author.id,post_author.user_id,post_tag.id,post_tag.tag_id&limit=100
Authorization: Bearer <token>

Response

{
  "items": [
    {
      "id": "1920438291750",
      "slug": "end-to-end-encryption",
      "hub_id": "1920438291700",
      "meta_title": "End-to-end encryption in Enbox",
      "is_public": true,
      "is_featured": true,
      "icon": "bi-shield-lock",
      "color": "#6f42c1",
      "reading_minutes": 5,
      "created_at": "2026-05-01T10:30:00Z",
      "updated_at": "2026-05-13T18:30:00Z",
      "post_author": [
        { "id": "1920438291760", "user_id": "42" }
      ],
      "post_tag": [
        { "id": "1920438291765", "tag_id": "1920438291800" }
      ]
    }
  ],
  "total": 1
}

WebSocket

Request

{
  "jsonrpc": "2.0",
  "method": "blog.posts.find",
  "params": {
    "filter": {
      "is_public": { "$eq": true },
      "hub_id": { "$eq": "1920438291700" }
    },
    "sort": [{ "field": "updated_at", "dir": "desc" }],
    "fields": [
      "id", "slug", "hub_id", "meta_title", "is_public", "is_featured",
      "icon", "color", "reading_minutes", "created_at", "updated_at",
      "post_author.id", "post_author.user_id",
      "post_tag.id", "post_tag.tag_id"
    ],
    "limit": 100
  },
  "id": 20
}

Response

{
  "jsonrpc": "2.0",
  "result": {
    "items": [
      {
        "id": "1920438291750",
        "slug": "end-to-end-encryption",
        "hub_id": "1920438291700",
        "meta_title": "End-to-end encryption in Enbox",
        "is_public": true,
        "is_featured": true,
        "icon": "bi-shield-lock",
        "color": "#6f42c1",
        "reading_minutes": 5,
        "created_at": "2026-05-01T10:30:00Z",
        "updated_at": "2026-05-13T18:30:00Z",
        "post_author": [
          { "id": "1920438291760", "user_id": "42" }
        ],
        "post_tag": [
          { "id": "1920438291765", "tag_id": "1920438291800" }
        ]
      }
    ],
    "total": 1
  },
  "id": 20
}

findOneById

Fetch a single post by id with full detail including the body, authors, tags, and translations.

HTTP

Request

GET /v1/blog/posts/1920438291750?fields=id,slug,hub_id,meta_title,meta_description,meta_keywords,content,is_public,is_featured,icon,color,reading_minutes,created_at,updated_at,post_translation.id,post_translation.language_id,post_translation.meta_title,post_translation.content,post_translation.is_public,post_author.id,post_author.user_id,post_tag.id,post_tag.tag_id
Authorization: Bearer <token>

Response

{
  "id": "1920438291750",
  "slug": "end-to-end-encryption",
  "hub_id": "1920438291700",
  "meta_title": "End-to-end encryption in Enbox",
  "meta_description": "",
  "meta_keywords": "",
  "content": "## How E2E encryption works\n\nEvery note, contact, and file is encrypted on your device before it ever reaches our servers...\n",
  "is_public": true,
  "is_featured": true,
  "icon": "bi-shield-lock",
  "color": "#6f42c1",
  "reading_minutes": 5,
  "created_at": "2026-05-01T10:30:00Z",
  "updated_at": "2026-05-13T18:30:00Z",
  "post_translation": [
    {
      "id": "1920438291755",
      "language_id": "2",
      "meta_title": "Chiffrement de bout en bout dans Enbox",
      "content": "## Comment fonctionne le chiffrement E2E\n\n...",
      "is_public": false
    }
  ],
  "post_author": [
    { "id": "1920438291760", "user_id": "42" }
  ],
  "post_tag": [
    { "id": "1920438291765", "tag_id": "1920438291800" }
  ]
}

WebSocket

Request

{
  "jsonrpc": "2.0",
  "method": "blog.posts.findOneById",
  "params": {
    "id": "1920438291750",
    "fields": [
      "id", "slug", "hub_id", "meta_title", "meta_description", "meta_keywords",
      "content", "is_public", "is_featured", "icon", "color",
      "reading_minutes", "created_at", "updated_at",
      "post_translation.id", "post_translation.language_id",
      "post_translation.meta_title", "post_translation.content", "post_translation.is_public",
      "post_author.id", "post_author.user_id",
      "post_tag.id", "post_tag.tag_id"
    ]
  },
  "id": 21
}

Response

{
  "jsonrpc": "2.0",
  "result": {
    "id": "1920438291750",
    "slug": "end-to-end-encryption",
    "hub_id": "1920438291700",
    "meta_title": "End-to-end encryption in Enbox",
    "meta_description": "",
    "meta_keywords": "",
    "content": "## How E2E encryption works\n\nEvery note, contact, and file is encrypted on your device before it ever reaches our servers...\n",
    "is_public": true,
    "is_featured": true,
    "icon": "bi-shield-lock",
    "color": "#6f42c1",
    "reading_minutes": 5,
    "created_at": "2026-05-01T10:30:00Z",
    "updated_at": "2026-05-13T18:30:00Z",
    "post_translation": [
      {
        "id": "1920438291755",
        "language_id": "2",
        "meta_title": "Chiffrement de bout en bout dans Enbox",
        "content": "## Comment fonctionne le chiffrement E2E\n\n...",
        "is_public": false
      }
    ],
    "post_author": [
      { "id": "1920438291760", "user_id": "42" }
    ],
    "post_tag": [
      { "id": "1920438291765", "tag_id": "1920438291800" }
    ]
  },
  "id": 21
}

create

Create a new post. The server assigns id, created_at, and updated_at; reading_minutes is derived from content. Inline post_translation, post_author, and post_tag entries without id are created alongside the post.

HTTP

Request

POST /v1/blog/posts
Authorization: Bearer <token>
Content-Type: application/json

{
  "slug": "end-to-end-encryption",
  "hub_id": "1920438291700",
  "meta_title": "End-to-end encryption in Enbox",
  "content": "## How E2E encryption works\n\nEvery note, contact, and file is encrypted on your device before it ever reaches our servers...\n",
  "is_public": true,
  "is_featured": true,
  "icon": "bi-shield-lock",
  "color": "#6f42c1",
  "post_author": [
    { "user_id": "42" }
  ],
  "post_tag": [
    { "tag_id": "1920438291800" }
  ]
}

Response

{
  "id": "1920438291750",
  "slug": "end-to-end-encryption",
  "meta_title": "End-to-end encryption in Enbox",
  "reading_minutes": 5,
  "created_at": "2026-05-13T18:30:00Z"
}

WebSocket

Request

{
  "jsonrpc": "2.0",
  "method": "blog.posts.create",
  "params": {
    "document": {
      "slug": "end-to-end-encryption",
      "hub_id": "1920438291700",
      "meta_title": "End-to-end encryption in Enbox",
      "content": "## How E2E encryption works\n\nEvery note, contact, and file is encrypted on your device before it ever reaches our servers...\n",
      "is_public": true,
      "is_featured": true,
      "icon": "bi-shield-lock",
      "color": "#6f42c1",
      "post_author": [
        { "user_id": "42" }
      ],
      "post_tag": [
        { "tag_id": "1920438291800" }
      ]
    }
  },
  "id": 22
}

Response

{
  "jsonrpc": "2.0",
  "result": {
    "id": "1920438291750",
    "slug": "end-to-end-encryption",
    "meta_title": "End-to-end encryption in Enbox",
    "reading_minutes": 5,
    "created_at": "2026-05-13T18:30:00Z"
  },
  "id": 22
}

updateOneById

Partial update — only specified fields change; omitted fields are untouched. Nested collections are updated automatically: items with id are updated, items without id are created, items missing from the array are deleted.

HTTP

Request

PATCH /v1/blog/posts/1920438291750
Authorization: Bearer <token>
Content-Type: application/json

{
  "meta_description": "A walkthrough of how Enbox encrypts your data on the client before upload.",
  "is_featured": false,
  "post_tag": [
    { "id": "1920438291765", "tag_id": "1920438291800" },
    { "tag_id": "1920438291801" }
  ]
}

Response

{
  "id": "1920438291750",
  "meta_description": "A walkthrough of how Enbox encrypts your data on the client before upload.",
  "is_featured": false,
  "updated_at": "2026-05-13T18:45:00Z"
}

WebSocket

Request

{
  "jsonrpc": "2.0",
  "method": "blog.posts.updateOneById",
  "params": {
    "id": "1920438291750",
    "document": {
      "meta_description": "A walkthrough of how Enbox encrypts your data on the client before upload.",
      "is_featured": false,
      "post_tag": [
        { "id": "1920438291765", "tag_id": "1920438291800" },
        { "tag_id": "1920438291801" }
      ]
    }
  },
  "id": 23
}

Response

{
  "jsonrpc": "2.0",
  "result": {
    "id": "1920438291750",
    "meta_description": "A walkthrough of how Enbox encrypts your data on the client before upload.",
    "is_featured": false,
    "updated_at": "2026-05-13T18:45:00Z"
  },
  "id": 23
}

replaceOneById

Full replacement — all writable fields must be provided. Unlike updateOneById, omitted fields are reset to defaults. Inline collections (post_translation, post_author, post_tag) are not accepted — use updateOneById to modify them. reading_minutes is read-only and recomputed from content.

HTTP

Request

PUT /v1/blog/posts/1920438291750
Authorization: Bearer <token>
Content-Type: application/json

{
  "slug": "end-to-end-encryption",
  "hub_id": "1920438291700",
  "meta_title": "End-to-end encryption in Enbox",
  "meta_description": "",
  "meta_keywords": "",
  "content": "## How E2E encryption works\n\nEvery note, contact, and file is encrypted on your device before it ever reaches our servers...\n",
  "is_public": true,
  "is_featured": true,
  "icon": "bi-shield-lock",
  "color": "#6f42c1"
}

Response

{
  "id": "1920438291750",
  "slug": "end-to-end-encryption",
  "meta_title": "End-to-end encryption in Enbox",
  "is_public": true,
  "is_featured": true,
  "updated_at": "2026-05-13T18:50:00Z"
}

WebSocket

Request

{
  "jsonrpc": "2.0",
  "method": "blog.posts.replaceOneById",
  "params": {
    "id": "1920438291750",
    "document": {
      "slug": "end-to-end-encryption",
      "hub_id": "1920438291700",
      "meta_title": "End-to-end encryption in Enbox",
      "meta_description": "",
      "meta_keywords": "",
      "content": "## How E2E encryption works\n\nEvery note, contact, and file is encrypted on your device before it ever reaches our servers...\n",
      "is_public": true,
      "is_featured": true,
      "icon": "bi-shield-lock",
      "color": "#6f42c1"
    }
  },
  "id": 24
}

Response

{
  "jsonrpc": "2.0",
  "result": {
    "id": "1920438291750",
    "slug": "end-to-end-encryption",
    "meta_title": "End-to-end encryption in Enbox",
    "is_public": true,
    "is_featured": true,
    "updated_at": "2026-05-13T18:50:00Z"
  },
  "id": 24
}

update

Batch update multiple posts in a single transaction (all-or-nothing). See Batch operations.

HTTP

Request

PATCH /v1/blog/posts
Authorization: Bearer <token>
Content-Type: application/json

{
  "items": [
    { "id": "1920438291750", "document": { "is_featured": true } },
    { "id": "1920438291751", "document": { "is_featured": false } }
  ]
}

Response

{
  "affected": 2,
  "items": [
    { "id": "1920438291750", "is_featured": true },
    { "id": "1920438291751", "is_featured": false }
  ]
}

WebSocket

Request

{
  "jsonrpc": "2.0",
  "method": "blog.posts.update",
  "params": {
    "items": [
      { "id": "1920438291750", "document": { "is_featured": true } },
      { "id": "1920438291751", "document": { "is_featured": false } }
    ]
  },
  "id": 25
}

Response

{
  "jsonrpc": "2.0",
  "result": {
    "affected": 2,
    "items": [
      { "id": "1920438291750", "is_featured": true },
      { "id": "1920438291751", "is_featured": false }
    ]
  },
  "id": 25
}

deleteOneById

Delete a post. All comments, translations, and author / tag assignments for this post are deleted with it (cascade).

HTTP

Request

DELETE /v1/blog/posts/1920438291750
Authorization: Bearer <token>

Response

{ "id": "1920438291750", "meta_title": "End-to-end encryption in Enbox" }

WebSocket

Request

{
  "jsonrpc": "2.0",
  "method": "blog.posts.deleteOneById",
  "params": { "id": "1920438291750" },
  "id": 26
}

Response

{
  "jsonrpc": "2.0",
  "result": { "id": "1920438291750", "meta_title": "End-to-end encryption in Enbox" },
  "id": 26
}

delete

Batch delete posts in a single transaction (all-or-nothing). See Batch operations.

HTTP

Request

DELETE /v1/blog/posts
Authorization: Bearer <token>
Content-Type: application/json

{
  "items": [
    { "id": "1920438291750" },
    { "id": "1920438291751" }
  ]
}

Response

{
  "affected": 2,
  "items": [
    { "id": "1920438291750" },
    { "id": "1920438291751" }
  ]
}

WebSocket

Request

{
  "jsonrpc": "2.0",
  "method": "blog.posts.delete",
  "params": {
    "items": [
      { "id": "1920438291750" },
      { "id": "1920438291751" }
    ]
  },
  "id": 27
}

Response

{
  "jsonrpc": "2.0",
  "result": {
    "affected": 2,
    "items": [
      { "id": "1920438291750" },
      { "id": "1920438291751" }
    ]
  },
  "id": 27
}