Files
clinicpro/docs/api/blog.md
T
hamed a4b07c2f80 feat(blog): add city_id to blogs for city-specific scoping
- Introduced a new nullable city_id column in the blogs table to allow scoping of blog posts to specific cities.
- Updated Blog entity to include a ManyToOne relationship with the City entity.
- Enhanced BlogController to handle city_id in the request, allowing filtering of posts by city.
- Modified BlogRepository to support querying published posts based on city_id.
- Added tests to ensure correct behavior for city-scoped and nationwide posts, including creation and updating of posts with city associations.
2026-07-19 08:23:57 +03:30

7.4 KiB

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

{
  "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

{
  "success": true,
  "data": {
    "uuid": "...",
    "title": "آشنایی با بیماری دیابت",
    "slug": "ashnayi-ba-bimari-diabat",
    "summary": "خلاصه...",
    "body": "<p>محتوای کامل...</p>",
    "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)

{
  "title": "آشنایی با بیماری دیابت",
  "body": "<p>محتوای کامل مقاله...</p>",
  "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

{
  "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)

{
  "title": "عنوان جدید",
  "body": "<p>محتوای جدید</p>",
  "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

{ "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

{
  "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