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.
| Code | English name |
|---|---|
aa | Afar |
ab | Abkhazian |
ae | Avestan |
af | Afrikaans |
ak | Akan |
am | Amharic |
an | Aragonese |
ar | Arabic |
as | Assamese |
av | Avaric |
ay | Aymara |
az | Azerbaijani |
ba | Bashkir |
be | Belarusian |
bg | Bulgarian |
bi | Bislama |
bm | Bambara |
bn | Bengali |
bo | Tibetan |
br | Breton |
bs | Bosnian |
ca | Catalan |
ce | Chechen |
ch | Chamorro |
co | Corsican |
cr | Cree |
cs | Czech |
cu | Church Slavic |
cv | Chuvash |
cy | Welsh |
da | Danish |
de | German |
dv | Divehi |
dz | Dzongkha |
ee | Ewe |
el | Greek |
en | English |
eo | Esperanto |
es | Spanish |
et | Estonian |
eu | Basque |
fa | Persian |
ff | Fulah |
fi | Finnish |
fj | Fijian |
fo | Faroese |
fr | French |
fy | Western Frisian |
ga | Irish |
gd | Scottish Gaelic |
gl | Galician |
gn | Guarani |
gu | Gujarati |
gv | Manx |
ha | Hausa |
he | Hebrew |
hi | Hindi |
ho | Hiri Motu |
hr | Croatian |
ht | Haitian |
hu | Hungarian |
hy | Armenian |
hz | Herero |
ia | Interlingua |
id | Indonesian |
ie | Interlingue |
ig | Igbo |
ii | Sichuan Yi |
ik | Inupiaq |
io | Ido |
is | Icelandic |
it | Italian |
iu | Inuktitut |
ja | Japanese |
jv | Javanese |
ka | Georgian |
kg | Kongo |
ki | Kikuyu |
kj | Kuanyama |
kk | Kazakh |
kl | Kalaallisut |
km | Khmer |
kn | Kannada |
ko | Korean |
kr | Kanuri |
ks | Kashmiri |
ku | Kurdish |
kv | Komi |
kw | Cornish |
ky | Kyrgyz |
la | Latin |
lb | Luxembourgish |
lg | Ganda |
li | Limburgan |
ln | Lingala |
lo | Lao |
lt | Lithuanian |
lu | Luba-Katanga |
lv | Latvian |
mg | Malagasy |
mh | Marshallese |
mi | Maori |
mk | Macedonian |
ml | Malayalam |
mn | Mongolian |
mr | Marathi |
ms | Malay |
mt | Maltese |
my | Burmese |
na | Nauru |
nb | Norwegian Bokmål |
nd | North Ndebele |
ne | Nepali |
ng | Ndonga |
nl | Dutch |
nn | Norwegian Nynorsk |
no | Norwegian |
nr | South Ndebele |
nv | Navajo |
ny | Chichewa |
oc | Occitan |
oj | Ojibwa |
om | Oromo |
or | Odia |
os | Ossetian |
pa | Punjabi |
pi | Pali |
pl | Polish |
ps | Pashto |
pt | Portuguese |
qu | Quechua |
rm | Romansh |
rn | Rundi |
ro | Romanian |
ru | Russian |
rw | Kinyarwanda |
sa | Sanskrit |
sc | Sardinian |
sd | Sindhi |
se | Northern Sami |
sg | Sango |
si | Sinhala |
sk | Slovak |
sl | Slovenian |
sm | Samoan |
sn | Shona |
so | Somali |
sq | Albanian |
sr | Serbian |
ss | Swati |
st | Southern Sotho |
su | Sundanese |
sv | Swedish |
sw | Swahili |
ta | Tamil |
te | Telugu |
tg | Tajik |
th | Thai |
ti | Tigrinya |
tk | Turkmen |
tl | Tagalog |
tn | Tswana |
to | Tongan |
tr | Turkish |
ts | Tsonga |
tt | Tatar |
tw | Twi |
ty | Tahitian |
ug | Uyghur |
uk | Ukrainian |
ur | Urdu |
uz | Uzbek |
ve | Venda |
vi | Vietnamese |
vo | Volapük |
wa | Walloon |
wo | Wolof |
xh | Xhosa |
yi | Yiddish |
yo | Yoruba |
za | Zhuang |
zh | Chinese |
zu | Zulu |
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.
| Bucket | Default | Env | MCU | Who is counted |
|---|---|---|---|---|
post | 10 | BLOG_POST_RATE_LIMIT | rate.post | Everyone except a session that can publish |
comment | 30 | BLOG_COMMENT_RATE_LIMIT | rate.comment | Every caller, including publishers |
view | 200 | BLOG_VIEW_RATE_LIMIT | rate.view | Every 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/:idPOST /api/v1/postsPUT /api/v1/posts/:idDELETE /api/v1/posts/:idPOST /api/v1/posts/:id/publishPOST /api/v1/posts/:id/unpublishPOST /api/v1/posts/:id/schedulePOST /api/v1/posts/:id/restorePOST /api/v1/posts/:id/featured-imageDELETE /api/v1/posts/:id/featured-imagePOST /api/v1/posts/categoriesPOST /api/v1/posts/categories/bulkPUT /api/v1/posts/categories/:idDELETE /api/v1/posts/categories/:idPOST /api/v1/posts/tagsPOST /api/v1/posts/tags/bulkPUT /api/v1/posts/tags/:idDELETE /api/v1/posts/tags/:idPOST /api/v1/posts/:id/bookmarkDELETE /api/v1/posts/:id/bookmarkPOST /api/v1/posts/id/:id/translationsPUT /api/v1/posts/id/:id/translations/:langDELETE /api/v1/posts/id/:id/translations/:lang
comment bucket:
POST /api/v1/posts/:id/commentsPUT /api/v1/posts/:id/comments/:commentIdDELETE /api/v1/posts/:id/comments/:commentIdPOST /api/v1/posts/comments/:commentId/moderatePOST /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
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/v1/posts | Optional | List posts with filters and pagination |
GET | /api/v1/posts/authors/:id | Public | Get author profile |
GET | /api/v1/posts/:slug | Optional | Read one post by slug |
GET | /api/v1/posts/id/:id | Optional | Read one post by id |
POST | /api/v1/posts | Session + writer role | Create a post |
PUT | /api/v1/posts/:id | Session + owner/admin | Update a post |
DELETE | /api/v1/posts/:id | Session + owner/admin | Delete a post |
POST | /api/v1/posts/:id/publish | Session + owner/admin | Publish a draft |
POST | /api/v1/posts/:id/unpublish | Session + owner/admin | Return a post to draft |
POST | /api/v1/posts/:id/schedule | Session + owner/admin | Schedule a post |
POST | /api/v1/posts/:id/restore | Session + admin | Restore a soft-deleted post |
POST | /api/v1/posts/:id/featured-image | Session + owner/admin | Upload featured image |
DELETE | /api/v1/posts/:id/featured-image | Session + owner/admin | Delete featured image |
GET | /api/v1/posts/categories | Public | List categories |
POST | /api/v1/posts/categories | Session + admin | Create a category |
PUT | /api/v1/posts/categories/:id | Session + admin | Update a category |
DELETE | /api/v1/posts/categories/:id | Session + admin | Delete a category |
GET | /api/v1/posts/tags | Public | List tags |
POST | /api/v1/posts/tags | Session + admin | Create a tag |
PUT | /api/v1/posts/tags/:id | Session + admin | Update a tag |
DELETE | /api/v1/posts/tags/:id | Session + admin | Delete a tag |
GET | /api/v1/posts/:id/comments | Optional | List comments |
POST | /api/v1/posts/:id/comments | Conditional | Create a comment |
GET | /api/v1/posts/:id/reactions | Optional | Read reaction counts |
POST | /api/v1/posts/:id/reactions | Session | Add or toggle a reaction |
POST | /api/v1/posts/:id/bookmark | Session | Bookmark a post |
GET | /api/v1/posts/me/bookmarks | Session | List bookmarks |
POST | /api/v1/posts/:id/view | Public | Track a view |
GET | /api/v1/posts/:id/stats | Optional | Read aggregated stats |
GET | /api/v1/posts/seo/sitemap.xml | Public | Generate 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:
pagelimitsortorderstatusvisibilitycategorytagauthorqlang— ISO 639-1 code. Admins may passall. See Languages.
Example response:
{
"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:
titleslugbodybody_formatvisibility
Optional body fields:
excerptcomments_enabledreactions_enabledscheduled_atlang— 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 /api/v1/posts/:id/featured-image
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:
{
"data": {},
"user_context": {
"authenticated": true
},
"meta": {}
}Why that matters
dataholds the useful payload.user_contextexplains how the response was interpreted for the current viewer.metacarries pagination and query information.
Example: list posts
GET /api/v1/posts?page=1&limit=10&sort=created_at&order=descExample response:
{
"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
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:
{
"data": {
"id": "post_2",
"slug": "why-modular-docs-matter",
"status": "draft"
},
"user_context": {
"authenticated": true,
"user_type": "writer"
},
"meta": {
"cached": false
}
}Typical flow
- List posts publicly.
- Create or edit a post as an authenticated writer.
- Publish or schedule the post.
- Allow comments and reactions if enabled.
- 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.