Skip to content
On this page

Blog API ​

The Blog API covers the modular content system used for posts, categories, tags, comments, reactions, bookmarks, views, translations, and SEO support.

This page is written to help developers understand the moving parts quickly, not just memorize endpoints.

How to read this page ​

Each route is documented like a normal operation page:

  • what it does
  • who can call it
  • the relevant request shape
  • a concrete response example

Base path ​

All routes live under /api/v1/posts.

What the API is for ​

  • Reading and listing posts
  • Creating and updating posts
  • Managing categories and tags
  • Handling comments and moderation
  • Tracking reactions, bookmarks, views, and stats
  • Supporting translations and SEO routes

Languages ​

Blog routes answer only when BLOG_ENABLED=true. Otherwise they return 503 MODULE_DISABLED. BLOG_I18N_ENABLED=true opens translation routes. A post still stores its language code when i18n is off. GLOBAL_LANG is the email and system-message language, not the blog language. See Blog module.

A post or category stores an ISO 639-1 code, such as es. The display name is not stored.

If lang is omitted on create, the post or category uses the blog default language. A code outside this list is rejected.

On list endpoints, lang is one of these codes. An admin or moderator can pass lang=all, or omit lang, to list every language. A public request with no lang, or with lang=all, stays on the default language. An unknown code is rejected on create and falls back to the default language on list.

CodeEnglish name
aaAfar
abAbkhazian
aeAvestan
afAfrikaans
akAkan
amAmharic
anAragonese
arArabic
asAssamese
avAvaric
ayAymara
azAzerbaijani
baBashkir
beBelarusian
bgBulgarian
biBislama
bmBambara
bnBengali
boTibetan
brBreton
bsBosnian
caCatalan
ceChechen
chChamorro
coCorsican
crCree
csCzech
cuChurch Slavic
cvChuvash
cyWelsh
daDanish
deGerman
dvDivehi
dzDzongkha
eeEwe
elGreek
enEnglish
eoEsperanto
esSpanish
etEstonian
euBasque
faPersian
ffFulah
fiFinnish
fjFijian
foFaroese
frFrench
fyWestern Frisian
gaIrish
gdScottish Gaelic
glGalician
gnGuarani
guGujarati
gvManx
haHausa
heHebrew
hiHindi
hoHiri Motu
hrCroatian
htHaitian
huHungarian
hyArmenian
hzHerero
iaInterlingua
idIndonesian
ieInterlingue
igIgbo
iiSichuan Yi
ikInupiaq
ioIdo
isIcelandic
itItalian
iuInuktitut
jaJapanese
jvJavanese
kaGeorgian
kgKongo
kiKikuyu
kjKuanyama
kkKazakh
klKalaallisut
kmKhmer
knKannada
koKorean
krKanuri
ksKashmiri
kuKurdish
kvKomi
kwCornish
kyKyrgyz
laLatin
lbLuxembourgish
lgGanda
liLimburgan
lnLingala
loLao
ltLithuanian
luLuba-Katanga
lvLatvian
mgMalagasy
mhMarshallese
miMaori
mkMacedonian
mlMalayalam
mnMongolian
mrMarathi
msMalay
mtMaltese
myBurmese
naNauru
nbNorwegian Bokmål
ndNorth Ndebele
neNepali
ngNdonga
nlDutch
nnNorwegian Nynorsk
noNorwegian
nrSouth Ndebele
nvNavajo
nyChichewa
ocOccitan
ojOjibwa
omOromo
orOdia
osOssetian
paPunjabi
piPali
plPolish
psPashto
ptPortuguese
quQuechua
rmRomansh
rnRundi
roRomanian
ruRussian
rwKinyarwanda
saSanskrit
scSardinian
sdSindhi
seNorthern Sami
sgSango
siSinhala
skSlovak
slSlovenian
smSamoan
snShona
soSomali
sqAlbanian
srSerbian
ssSwati
stSouthern Sotho
suSundanese
svSwedish
swSwahili
taTamil
teTelugu
tgTajik
thThai
tiTigrinya
tkTurkmen
tlTagalog
tnTswana
toTongan
trTurkish
tsTsonga
ttTatar
twTwi
tyTahitian
ugUyghur
ukUkrainian
urUrdu
uzUzbek
veVenda
viVietnamese
voVolapük
waWalloon
woWolof
xhXhosa
yiYiddish
yoYoruba
zaZhuang
zhChinese
zuZulu

Rate limits ​

Three buckets. The window is BLOG_RATE_LIMIT_WINDOW (default 3600 seconds). The same window is rate.window on BLOG_MCU. The counter key is the user id when a session is present, otherwise the client IP. Over the limit the route returns 429 with code: RATE_LIMIT_EXCEEDED.

BucketDefaultEnvMCUWho is counted
post10BLOG_POST_RATE_LIMITrate.postEveryone except a session that can publish
comment30BLOG_COMMENT_RATE_LIMITrate.commentEvery caller, including publishers
view200BLOG_VIEW_RATE_LIMITrate.viewEvery caller, including publishers

A session can publish when its role is in BLOG_ALLOWED_POST_ROLES or BLOG_ADMIN_ROLES. The defaults are admin, superadmin, author, and editor. Those sessions skip the post bucket. They still count toward comment and view.

post bucket:

  • GET /api/v1/posts/authors/:id
  • POST /api/v1/posts
  • PUT /api/v1/posts/:id
  • DELETE /api/v1/posts/:id
  • POST /api/v1/posts/:id/publish
  • POST /api/v1/posts/:id/unpublish
  • POST /api/v1/posts/:id/schedule
  • POST /api/v1/posts/:id/restore
  • POST /api/v1/posts/:id/featured-image
  • DELETE /api/v1/posts/:id/featured-image
  • POST /api/v1/posts/categories
  • POST /api/v1/posts/categories/bulk
  • PUT /api/v1/posts/categories/:id
  • DELETE /api/v1/posts/categories/:id
  • POST /api/v1/posts/tags
  • POST /api/v1/posts/tags/bulk
  • PUT /api/v1/posts/tags/:id
  • DELETE /api/v1/posts/tags/:id
  • POST /api/v1/posts/:id/bookmark
  • DELETE /api/v1/posts/:id/bookmark
  • POST /api/v1/posts/id/:id/translations
  • PUT /api/v1/posts/id/:id/translations/:lang
  • DELETE /api/v1/posts/id/:id/translations/:lang

comment bucket:

  • POST /api/v1/posts/:id/comments
  • PUT /api/v1/posts/:id/comments/:commentId
  • DELETE /api/v1/posts/:id/comments/:commentId
  • POST /api/v1/posts/comments/:commentId/moderate
  • POST /api/v1/posts/:id/reactions

view bucket:

  • POST /api/v1/posts/:id/view

List and read routes other than the author profile are not in these buckets.

Route map ​

MethodPathAuthPurpose
GET/api/v1/postsOptionalList posts with filters and pagination
GET/api/v1/posts/authors/:idPublicGet author profile
GET/api/v1/posts/:slugOptionalRead one post by slug
GET/api/v1/posts/id/:idOptionalRead one post by id
POST/api/v1/postsSession + writer roleCreate a post
PUT/api/v1/posts/:idSession + owner/adminUpdate a post
DELETE/api/v1/posts/:idSession + owner/adminDelete a post
POST/api/v1/posts/:id/publishSession + owner/adminPublish a draft
POST/api/v1/posts/:id/unpublishSession + owner/adminReturn a post to draft
POST/api/v1/posts/:id/scheduleSession + owner/adminSchedule a post
POST/api/v1/posts/:id/restoreSession + adminRestore a soft-deleted post
POST/api/v1/posts/:id/featured-imageSession + owner/adminUpload featured image
DELETE/api/v1/posts/:id/featured-imageSession + owner/adminDelete featured image
GET/api/v1/posts/categoriesPublicList categories
POST/api/v1/posts/categoriesSession + adminCreate a category
PUT/api/v1/posts/categories/:idSession + adminUpdate a category
DELETE/api/v1/posts/categories/:idSession + adminDelete a category
GET/api/v1/posts/tagsPublicList tags
POST/api/v1/posts/tagsSession + adminCreate a tag
PUT/api/v1/posts/tags/:idSession + adminUpdate a tag
DELETE/api/v1/posts/tags/:idSession + adminDelete a tag
GET/api/v1/posts/:id/commentsOptionalList comments
POST/api/v1/posts/:id/commentsConditionalCreate a comment
GET/api/v1/posts/:id/reactionsOptionalRead reaction counts
POST/api/v1/posts/:id/reactionsSessionAdd or toggle a reaction
POST/api/v1/posts/:id/bookmarkSessionBookmark a post
GET/api/v1/posts/me/bookmarksSessionList bookmarks
POST/api/v1/posts/:id/viewPublicTrack a view
GET/api/v1/posts/:id/statsOptionalRead aggregated stats
GET/api/v1/posts/seo/sitemap.xmlPublicGenerate the sitemap

Route details ​

Posts ​

GET /api/v1/posts/authors/:id ​

Get one public author profile by id.

Auth: public.

Rate limit: post bucket. See Rate limits.

GET /api/v1/posts ​

List posts with pagination and filters.

Auth: optional.

Returns: a paginated list of posts.

Query params:

  • page
  • limit
  • sort
  • order
  • status
  • visibility
  • category
  • tag
  • author
  • q
  • lang — ISO 639-1 code. Admins may pass all. See Languages.

Example response:

json
{
  "data": [],
  "user_context": {
    "authenticated": false
  },
  "meta": {
    "page": 1,
    "limit": 10,
    "total": 0,
    "hasMore": false
  }
}

GET /api/v1/posts/:slug ​

Read one post by slug.

GET /api/v1/posts/id/:id ​

Read one post by id.

POST /api/v1/posts ​

Create a new post.

Auth: session + writer role.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

Required body fields:

  • title
  • slug
  • body
  • body_format
  • visibility

Optional body fields:

  • excerpt
  • comments_enabled
  • reactions_enabled
  • scheduled_at
  • lang — ISO 639-1 code, for example "lang": "es". Omitted means the default language. The same field can be sent on update.

PUT /api/v1/posts/:id ​

Update an existing post.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

DELETE /api/v1/posts/:id ​

Delete a post.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

POST /api/v1/posts/:id/publish ​

Publish a draft post.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

POST /api/v1/posts/:id/unpublish ​

Return a published post to draft.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

POST /api/v1/posts/:id/schedule ​

Schedule a post for later publishing.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

POST /api/v1/posts/:id/restore ​

Restore a previously soft-deleted post.

Auth: session + admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

POST /api/v1/posts/:id/featured-image ​

Upload featured image using multipart form-data.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

Delete the current featured image.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

Categories and tags ​

GET /api/v1/posts/categories ​

List categories.

Auth: public.

POST /api/v1/posts/categories ​

Create a category.

Auth: session + admin.

Rate limit: post bucket, including POST /api/v1/posts/categories/bulk. A session that can publish is not counted. See Rate limits.

PUT /api/v1/posts/categories/:id ​

Update a category.

Auth: session + admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

DELETE /api/v1/posts/categories/:id ​

Delete a category.

Auth: session + admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

GET /api/v1/posts/tags ​

List tags. Optional lang is an ISO 639-1 code and returns only tags stored with that code. Omit lang, or pass all, to return every tag. Tags with no lang appear only in that full list.

Auth: public.

POST /api/v1/posts/tags ​

Create a tag. Optional lang is an ISO 639-1 code. When it is omitted, the tag is stored as BLOG_DEFAULT_LANG. The same field applies to POST /api/v1/posts/tags/bulk.

Auth: session + admin.

Rate limit: post bucket, including POST /api/v1/posts/tags/bulk. A session that can publish is not counted. See Rate limits.

PUT /api/v1/posts/tags/:id ​

Update a tag.

Auth: session + admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

DELETE /api/v1/posts/tags/:id ​

Delete a tag.

Auth: session + admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

Comments ​

GET /api/v1/posts/:id/comments ​

List comments for a post.

Auth: optional.

POST /api/v1/posts/:id/comments ​

Create a comment.

Auth: conditional.

Rate limit: comment bucket. Every caller is counted. See Rate limits.

PUT /api/v1/posts/:id/comments/:commentId ​

Update a comment.

Auth: session.

Rate limit: comment bucket. Every caller is counted. See Rate limits.

DELETE /api/v1/posts/:id/comments/:commentId ​

Delete a comment.

Auth: session.

Rate limit: comment bucket. Every caller is counted. See Rate limits.

POST /api/v1/posts/comments/:commentId/moderate ​

Moderate a comment.

Auth: session + moderator/admin.

Rate limit: comment bucket. Every caller is counted. See Rate limits.

Engagement ​

GET /api/v1/posts/:id/reactions ​

Read reaction counters for one post.

Auth: optional.

POST /api/v1/posts/:id/reactions ​

Add or toggle a reaction.

Auth: session.

Rate limit: comment bucket. Every caller is counted. See Rate limits.

POST /api/v1/posts/:id/bookmark ​

Bookmark a post.

Auth: session.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

DELETE /api/v1/posts/:id/bookmark ​

Remove a bookmark.

Auth: session.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

GET /api/v1/posts/me/bookmarks ​

List current user bookmarks.

Auth: session.

POST /api/v1/posts/:id/view ​

Track a view.

Auth: public.

Rate limit: view bucket. Every caller is counted. See Rate limits.

GET /api/v1/posts/:id/stats ​

Read aggregated stats.

Auth: optional.

Translations and SEO ​

GET /api/v1/posts/id/:id/translations ​

List translations for a post.

Auth: optional.

POST /api/v1/posts/id/:id/translations ​

Create a translation.

Auth: session + writer role.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

PUT /api/v1/posts/id/:id/translations/:lang ​

Update a translation.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

DELETE /api/v1/posts/id/:id/translations/:lang ​

Delete a translation.

Auth: session + owner/admin.

Rate limit: post bucket. A session that can publish is not counted. See Rate limits.

GET /api/v1/posts/seo/sitemap.xml ​

Generate the SEO sitemap.

Auth: public.

Response shape ​

Most blog routes use a consistent envelope:

json
{
  "data": {},
  "user_context": {
    "authenticated": true
  },
  "meta": {}
}

Why that matters ​

  • data holds the useful payload.
  • user_context explains how the response was interpreted for the current viewer.
  • meta carries pagination and query information.

Example: list posts ​

http
GET /api/v1/posts?page=1&limit=10&sort=created_at&order=desc

Example response:

json
{
  "data": [
    {
      "id": "post_1",
      "slug": "hello-world",
      "title": "Hello World",
      "body": "# Hello",
      "status": "published",
      "visibility": "public"
    }
  ],
  "user_context": {
    "authenticated": false
  },
  "meta": {
    "page": 1,
    "limit": 10,
    "total": 1,
    "hasMore": false
  }
}

Example: create a post ​

http
POST /api/v1/posts
Content-Type: application/json

{
  "title": "Why modular docs matter",
  "slug": "why-modular-docs-matter",
  "body": "# Why modular docs matter",
  "body_format": "markdown",
  "visibility": "public",
  "comments_enabled": true
}

Example response:

json
{
  "data": {
    "id": "post_2",
    "slug": "why-modular-docs-matter",
    "status": "draft"
  },
  "user_context": {
    "authenticated": true,
    "user_type": "writer"
  },
  "meta": {
    "cached": false
  }
}

Typical flow ​

  1. List posts publicly.
  2. Create or edit a post as an authenticated writer.
  3. Publish or schedule the post.
  4. Allow comments and reactions if enabled.
  5. Use stats and bookmarks for engagement.

Practical notes ​

  • The API is easiest to understand when you think in layers: content, moderation, engagement, and metadata.
  • Use the response envelope to keep frontend code predictable.
  • Treat publish and schedule as workflow actions, not just CRUD operations.

Next ​