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
}