feat(blog): add admin endpoint for blog details and cache invalidation
- Implemented `adminDetail()` method in `BlogController` to retrieve blog posts of any status for admin editing. - Introduced `BlogCacheInvalidator` service to handle cache invalidation after blog create/update/delete actions. - Updated existing methods in `BlogController` and `RepresentationBlogController` to call cache invalidation on blog modifications. - Enhanced `BlogFormPage` and `RepresentationBlogFormPage` to utilize the new admin endpoint for fetching blog data. - Added tests for `BlogCacheInvalidator` to ensure proper functionality and error handling. - Updated documentation to reflect new API endpoint and cache invalidation behavior.
This commit is contained in:
+151
-12
@@ -100,21 +100,113 @@ Get a single blog post by slug.
|
||||
> بدون این پارامتر، پستی که از لیستِ یک دامنه فیلتر شده بود همچنان با URL مستقیم روی همان دامنه ۲۰۰ میگرفت و یک محتوا روی چند دامنه تکرار میشد. سایت عمومی (`nobat724_front`) روی دامنههای شهری همیشه `city_id` را میفرستد.
|
||||
|
||||
### Response `200`
|
||||
|
||||
> ⚠️ پاسخ **double-nested** است (`data.data`) — کنترلر `$this->success(['data' => $blog->toArray()])` برمیگرداند. کلاینت باید `data?.data` را بخواند.
|
||||
|
||||
شکل کامل، عیناً خروجی `Blog::toArray()`:
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
"data": {
|
||||
"uuid": "...",
|
||||
"title": "آشنایی با بیماری دیابت",
|
||||
"slug": "ashnayi-ba-bimari-diabat",
|
||||
"summary": "خلاصه...",
|
||||
"body": "<p>محتوای کامل...</p>",
|
||||
"image_url": "/uploads/blogs/blog_xxx_cover.png",
|
||||
"tags": ["دیابت", "تغذیه"],
|
||||
"sources": [{ "url": "https://...", "title": "..." }],
|
||||
"status": "published",
|
||||
"review_status": null,
|
||||
"reviewer": null,
|
||||
"reviewed_at": null,
|
||||
"review_note": null,
|
||||
"topic_slug": null,
|
||||
"meta_title": null,
|
||||
"meta_description": null,
|
||||
"primary_keyword": null,
|
||||
"secondary_keywords": [],
|
||||
"faq": [{ "q": "...", "a": "..." }],
|
||||
"internal_links": [],
|
||||
"external_links": [],
|
||||
"reading_time": null,
|
||||
"canonical_url": null,
|
||||
"og_image": null,
|
||||
"scheduled_at": null,
|
||||
"representation": null,
|
||||
"author": { "uuid": "...", "name": "احمدی" },
|
||||
"city": null,
|
||||
"created_at": 1717000000,
|
||||
"updated_at": 1717100000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> نکته: `tags` آرایهٔ **رشته** است (نه آبجکت `{id,name}`)، تصویر در `image_url` (نه `image`) و نام نویسنده در `author.name` (نه `real_name`). نسخهٔ قبلی این سند شکل دیگری نشان میداد که با کد همخوان نبود.
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_NOT_FOUND_001` | 404 | Blog not found, not published, or owned by another city (when `city_id` is sent) |
|
||||
|
||||
---
|
||||
|
||||
## GET `/api/v1/admin/blog/{uuid}`
|
||||
|
||||
Get **one** blog post of any status for the admin edit form.
|
||||
|
||||
**Permission:** `ROLE_ADMIN`
|
||||
|
||||
اندپوینت عمومی `GET /api/v1/blog/{slug}` هر پستِ غیر `published` را ۴۰۴ میکند، پس فرم ویرایش پنل ادمین نمیتوانست پیشنویس و آرشیو را بارگذاری کند (فرم خالی بالا میآمد). این اندپوینت هیچ فیلتر `status`/`city` ندارد. اندپوینت عمومی عمداً دستنخورده ماند تا پاسخِ cache-شدهٔ آن وابسته به نقشِ فراخوان نشود.
|
||||
|
||||
### Path Parameters
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `uuid` | string | UUID مقاله. الگوی اجباری `[0-9a-fA-F-]{36}` — بدون آن این route مسیر `/api/v1/admin/blog/review-queue` را میدزدید. **فقط UUID؛ slug پذیرفته نمیشود.** |
|
||||
|
||||
### Response `200` (خروجی واقعی curl روی پیشنویس)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": {
|
||||
"uuid": "56fd9a20-9594-4aa1-a651-346fa86720bd",
|
||||
"title": "rtertert",
|
||||
"slug": "نوبتدهی-آنلاین-کلینیک-از-صف-انتظار-تا-پروندهٔ-دیجیتال-56fd9a20",
|
||||
"summary": "ertet",
|
||||
"body": "<p>etetetet</p>",
|
||||
"image_url": null,
|
||||
"tags": [],
|
||||
"sources": [],
|
||||
"status": "draft",
|
||||
"review_status": null,
|
||||
"reviewer": null,
|
||||
"reviewed_at": null,
|
||||
"review_note": null,
|
||||
"topic_slug": null,
|
||||
"meta_title": null,
|
||||
"meta_description": null,
|
||||
"primary_keyword": null,
|
||||
"secondary_keywords": [],
|
||||
"faq": [
|
||||
{ "q": "etrtert", "a": "retet" },
|
||||
{ "q": "eertret", "a": "ertet" }
|
||||
],
|
||||
"internal_links": [],
|
||||
"external_links": [],
|
||||
"reading_time": null,
|
||||
"canonical_url": null,
|
||||
"og_image": null,
|
||||
"scheduled_at": null,
|
||||
"representation": null,
|
||||
"author": { "uuid": "7464eba2-6754-4cea-bcde-83e47050e829", "name": "1402/12/17 12" },
|
||||
"city": null,
|
||||
"created_at": 1784827054,
|
||||
"updated_at": 1785157798
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -122,7 +214,9 @@ Get a single blog post by slug.
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_NOT_FOUND_001` | 404 | Blog not found, not published, or owned by another city (when `city_id` is sent) |
|
||||
| `ERR_AUTH_001` | 401 | بدون توکن |
|
||||
| `ERR_FORBIDDEN_001` | 403 | توکن معتبر ولی بدون `ROLE_ADMIN` |
|
||||
| `ERR_NOT_FOUND_001` | 404 | مقاله با این UUID وجود ندارد |
|
||||
|
||||
---
|
||||
|
||||
@@ -433,3 +527,48 @@ Upload blog post header image.
|
||||
| `ERR_AUTH_001` | 401 | Missing token |
|
||||
| `ERR_AUTH_006` | 403 | Not admin |
|
||||
| `ERR_FILE_001` | 422 | Invalid file type |
|
||||
|
||||
---
|
||||
|
||||
## باطلسازی کش سایت عمومی (webhook خروجی)
|
||||
|
||||
سایت عمومی (`nobat724_front`) پاسخ `GET /api/v1/blog/{slug}` را با `next: { revalidate: 3600, tags: [...] }` کش میکند. بدون باطلسازی، هر تغییر در پنل ادمین تا یک ساعت روی سایت دیده نمیشد.
|
||||
|
||||
`App\Blog\Service\BlogCacheInvalidator` بعد از **هر نوشتن** یک درخواست خروجی میفرستد:
|
||||
|
||||
| نقطهٔ فراخوانی | فایل |
|
||||
|---|---|
|
||||
| `POST /api/v1/blog` (create) | `BlogController::create` |
|
||||
| `PATCH /api/v1/blog/{uuid}` | `BlogController::update` |
|
||||
| `POST /api/v1/admin/blog/{uuid}/review` | `BlogController::review` |
|
||||
| `DELETE /api/v1/blog/{uuid}` | `BlogController::delete` (**قبل** از `remove`) |
|
||||
| `POST/PATCH/DELETE /api/v1/representation/blog…` | `RepresentationBlogController` |
|
||||
|
||||
### قرارداد درخواست (خروجی — مصرفکننده `nobat724_front`)
|
||||
|
||||
```
|
||||
POST {REVALIDATE_WEBHOOK_URL}
|
||||
X-Revalidate-Secret: {REVALIDATE_WEBHOOK_SECRET}
|
||||
Content-Type: application/json
|
||||
|
||||
{"tags":["blog-<slug>","blog-<uuid>","blog-list"]}
|
||||
```
|
||||
|
||||
بدنهٔ واقعی یک `PATCH` (خروجی شنوندهٔ تست):
|
||||
|
||||
```json
|
||||
{"tags":["blog-نوبتدهی-آنلاین-کلینیک-از-صف-انتظار-تا-پروندهٔ-دیجیتال-56fd9a20","blog-56fd9a20-9594-4aa1-a651-346fa86720bd","blog-list"]}
|
||||
```
|
||||
|
||||
پاسخ مورد انتظار سایت: `200 {"revalidated": true}`؛ سکرت غلط `401`؛ بدنهٔ بدون `tags` → `400`.
|
||||
|
||||
### متغیرهای محیطی
|
||||
|
||||
| متغیر | پیشفرض | معنا |
|
||||
|---|---|---|
|
||||
| `REVALIDATE_WEBHOOK_URL` | خالی | آدرس webhook سایت. **خالی = هیچ درخواستی ارسال نمیشود** |
|
||||
| `REVALIDATE_WEBHOOK_SECRET` | خالی | باید با `REVALIDATE_SECRET` سمت سایت یکی باشد. خالی = ارسال نمیشود |
|
||||
|
||||
### رفتار خطا — fail-open
|
||||
|
||||
timeout سه ثانیه است و هر خطای شبکه فقط با سطح `warning` لاگ میشود؛ **ذخیره/حذف مقاله هرگز به خاطر شکست webhook شکست نمیخورد.** تستشده: با URL غیرقابلدسترس، `PATCH` همچنان `200` برمیگرداند.
|
||||
|
||||
Reference in New Issue
Block a user