# Blog API > **Prefix:** `/api/v1/blog`, `/api/v1/blogs` --- ## GET `/api/v1/blogs` List published blog posts. **Permission:** `PUBLIC` ### Query Parameters | Param | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `page` | integer | ❌ | 1 | Page number | | `limit` | integer | ❌ | 20 | Items per page | | `tag` | string | ❌ | — | Filter by exact tag **name** (e.g. `?tag=دیابت`). Blog tags are stored as a JSON array of names; only blogs whose `tags` array contains this exact name are returned. | | `city_id` | integer | ❌ | — | Scope to one city. Returns that city's posts **plus every nationwide post** (`city_id IS NULL`). Omit it to return all published posts regardless of city. | ### Response `200` ```json { "success": true, "data": [ { "uuid": "...", "title": "آشنایی با بیماری دیابت", "slug": "ashnayi-ba-bimari-diabat", "summary": "خلاصه مطلب...", "image": "https://...", "author": { "uuid": "...", "real_name": "دکتر احمدی" }, "tags": [{ "id": 1, "name": "دیابت" }], "status": "published", "city": { "id": "123", "name": "یاسوج" }, "created_at": 1717000000 } ], "meta": { "totalRecords": 25, "totalPages": 2, "currentPage": 1, "limit": 20 } } ``` ### فیلد `city` — شهر پست `city` در پاسخ لیست و جزئیات وجود دارد و دو حالت دارد: | مقدار | معنا | |-------|------| | `{ "id": "123", "name": "یاسوج" }` | پست **شهری** — به آن شهر تعلق دارد | | `null` | پست **سراسری** — حالت دائمی و معتبر، نه «تنظیم‌نشده» | - `city_id` روی `blogs` **nullable** است و `NULL` معنای دائمی «سراسری» دارد. رکوردهای قبل از این تغییر همگی سراسری شدند. - حذف شهر پست را حذف نمی‌کند (`ON DELETE SET NULL`) — پست سراسری می‌شود. - پست سراسری روی **همهٔ** دامنه‌های شهری در لیست دیده می‌شود؛ فقط canonical آن روی دامنهٔ اصلی می‌نشیند. به همین دلیل `city_id=X` هم پست‌های شهر X و هم پست‌های سراسری را برمی‌گرداند — نه فقط شهر X. > 🔗 مصرف‌کننده: سایت عمومی چند-دامنه‌ای (`nobat724_front`) با همین فیلد تصمیم می‌گیرد پست را روی دامنهٔ شهر canonical کند یا روی دامنهٔ اصلی، و در کدام sitemap بگذارد. تغییر معنای `null` قرارداد آن را می‌شکند. --- ## GET `/api/v1/blog/{slug}` Get a single blog post by slug. **Permission:** `PUBLIC` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `slug` | string | URL slug (e.g. `ashnayi-ba-bimari-diabat`) | ### Response `200` ```json { "success": true, "data": { "uuid": "...", "title": "آشنایی با بیماری دیابت", "slug": "ashnayi-ba-bimari-diabat", "summary": "خلاصه...", "body": "

محتوای کامل...

", "image": "https://...", "author": { "uuid": "...", "real_name": "دکتر احمدی" }, "tags": [{ "id": 1, "name": "دیابت" }], "status": "published", "created_at": 1717000000, "updated_at": 1717100000 } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_NOT_FOUND_001` | 404 | Blog not found or not published | --- ## POST `/api/v1/blog` Create a new blog post. **Permission:** `ROLE_ADMIN` ### Request Body (`application/json`) ```json { "title": "آشنایی با بیماری دیابت", "body": "

محتوای کامل مقاله...

", "summary": "خلاصه کوتاه از مقاله", "tags": [1, 2], "status": "draft", "image_url": "/uploads/blogs/blog_abc.jpg" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `title` | string | ✅ | Post title (slug auto-generated) | | `body` | string | ✅ | Full HTML body (from the admin CKEditor) | | `summary` | string | ❌ | Short excerpt | | `tags` | integer[] | ❌ | Array of tag IDs | | `status` | string | ❌ | `"draft"` (default) or `"published"` | | `image_url` | string | ❌ | Cover image path returned by the upload endpoint | | `city_id` | integer\|null | ❌ | City this post belongs to. **Omitting it, or sending `null`/`0`, creates a nationwide post.** An unknown city id is rejected with `422`. | ### Response `201` ```json { "success": true, "data": { "uuid": "blog-uuid-...", "title": "آشنایی با بیماری دیابت", "slug": "ashnayi-ba-bimari-diabat", "summary": "...", "body": "...", "image": null, "tags": [], "status": "draft", "created_at": 1717000000 } } ``` **Blog Status Values:** | Value | Description | |-------|-------------| | `draft` | Not visible to public | | `published` | Visible in public listing | | `archived` | Hidden from listing | ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_VALIDATION_002` | 422 | Missing required fields | --- ## PATCH `/api/v1/blog/{uuid}` Update a blog post. **Permission:** `ROLE_ADMIN` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | Blog UUID | ### Request Body (`application/json`) ```json { "title": "عنوان جدید", "body": "

محتوای جدید

", "summary": "خلاصه جدید", "tags": [1, 3], "status": "published", "image_url": "/uploads/blogs/blog_abc.jpg" } ``` All fields optional. Send `image_url: ""` to clear the cover image. `city_id` follows PATCH semantics: **omit it and the post's city is left untouched**; send `null` (or `0`) to turn the post into a nationwide one; send a city id to move it to that city. ### Response `200` Updated blog object. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Blog not found | | `ERR_VALIDATION_002` | 422 | Unknown `city_id` | --- ## DELETE `/api/v1/blog/{uuid}` Delete a blog post. **Permission:** `ROLE_ADMIN` ### Response `200` ```json { "success": true, "data": { "message": "مقاله حذف شد" } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Blog not found | --- ## POST `/file/upload/clinic_pro/blog/field_image` Upload blog post header image. **Permission:** `ROLE_ADMIN` ### Request `Content-Type: multipart/form-data` | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | binary | ✅ | Image (max 5MB) | | `blog_uuid` | string (UUID) | ❌ | Auto-associate with blog post | ### Response `200` ```json { "success": true, "data": { "url": "https://clinic-pro.ddev.site/uploads/blog/post_abc.jpg", "uuid": "...", "filename": "post_abc.jpg", "filemime": "image/jpeg", "filesize": 204800 } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_FILE_001` | 422 | Invalid file type |