# سینک کامل صفحهٔ بلاگ با بک‌اند + تصویر پیش‌فرض برند + باطل‌سازی کش ## پروژه `nobat724_front` (سایت عمومی) پرامپت همتا (cross-repo، **اول اجرا شود**): `clinicpro/.claude/prompt/blog-admin-edit-and-dark-mode.md` — اندپوینت ادمین و فراخوانندهٔ webhook آنجا ساخته می‌شود؛ **مصرف‌کنندهٔ** webhook اینجاست. ## زمینه کاربر گزارش داده صفحهٔ بلاگ سایت (`https://nobat724.com/blog/`) همهٔ اطلاعاتی را که در پنل ادمین مدیریت می‌شود نشان نمی‌دهد، و مقالهٔ بدون تصویر شاخص با تصویر پیش‌فرض نامناسب نمایش داده می‌شود. بررسی کد نشان می‌دهد بک‌اند در `Blog::toArray()` این فیلدها را برمی‌گرداند: ``` uuid, title, slug, summary, body, image_url, tags, sources, status, review_status, reviewer, reviewed_at, review_note, topic_slug, meta_title, meta_description, primary_keyword, secondary_keywords, faq, internal_links, external_links, reading_time, canonical_url, og_image, representation, author, city, created_at, updated_at ``` اما صفحهٔ بلاگ فقط `title`، `author`، `created`، `tag[0]` (در بردکرامب)، `city` و `body` را رندر می‌کند. ## مشکل / هدف ### مشکل ۱ — فیلدهایی که رندر نمی‌شوند | فیلد بک‌اند | وضعیت فعلی در سایت | |---|---| | `summary` (خلاصه) | ❌ فقط در متادیتا؛ در HTML صفحه هیچ‌جا نیست | | `tags` | ⚠️ فقط تگ اول در بردکرامب (`components/blog/head/index.js:5`)؛ فهرست تگ‌ها نیست | | `reading_time` | ❌ رندر نمی‌شود | | `sources` (منابع، سیگنال E-E-A-T) | ❌ رندر نمی‌شود | | `faq` | ⚠️ فقط داخل JSON-LD (`app/blog/[slug]/page.js:173`)؛ در HTML قابل‌مشاهده نیست — گوگل برای rich result نیازمند حضور مرئی همان پرسش/پاسخ‌هاست، پس FAQPage فعلی در معرض بی‌اعتبار شدن است | | `internal_links` / `external_links` | ❌ رندر نمی‌شود | | `status` | ✅ درست: پستِ غیرمنتشر از بک‌اند ۴۰۴ می‌گیرد | | `slug` / `meta_*` / `canonical_url` / `og_image` | ✅ در `generateMetadata` مصرف می‌شود | **نکتهٔ خلاف درخواست کاربر:** بلاگ در بک‌اند **دسته‌بندی (category) ندارد**؛ فقط ستون json `tags`. پس «دسته‌بندی‌ها» با نمایش تگ‌ها پوشش داده می‌شود. ساخت Entity دسته‌بندی تسک جدا و migration جدا می‌خواهد و در محدودهٔ این پرامپت نیست. ### مشکل ۲ — تصویر پیش‌فرض سه fallback ناهماهنگ و نامناسب در کد وجود دارد و در صفحهٔ جزئیات اصلاً fallback ای نیست: `components/blog/detail/Caption.js:7,10` ```jsx const cover = data?.images?.[0]?.url ? imageUrl(data.images[0].url) : ""; ... {cover && cover.trim() !== "" && ( ``` → مقالهٔ بدون تصویر **هیچ تصویری** ندارد. `components/blogs/latestArticles/Article.js:8-10` ```jsx const imageSrc = data.images?.[0]?.url ? imageUrl(data.images[0].url, "/assets/images/cover-blog-1.png") : "/assets/images/cover-blog-1.png"; ``` `components/blog/relatedContent/Item.js:8-10` ```jsx const cover = data?.images?.[0]?.url ? imageUrl(data.images[0].url) : "/assets/images/cover-blog-1.png"; ``` `app/blog/[slug]/page.js:16` ```js const FALLBACK_IMG = "/assets/images/og-image.png"; ``` و پیش‌فرضِ خودِ هلپر یک آواتار انسان است (`helper/index.js`): ```js export const imageUrl = (url, fallback = "/assets/images/user.png") => { ``` یعنی هر جای دیگری که `imageUrl(blog.image_url)` بدون آرگومان دوم صدا زده شود، عکسِ «کاربر» را برای مقاله نشان می‌دهد. ### مشکل ۳ — تأخیر تا یک ساعت در نمایش تغییرات ادمین `app/blog/[slug]/page.js:21-32` ```js const getBlog = cache(async (slug, cityId) => { const query = cityId != null ? `?city_id=${cityId}` : ""; const res = await fetch(`${API_URL}/api/v1/blog/${slug}${query}`, { next: { revalidate: 3600, tags: [`blog-${slug}`] }, }); ``` tag تعریف شده ولی **هیچ‌کس آن را باطل نمی‌کند** (`app/api/` فقط `auth` دارد). پس تغییر ادمین تا ۶۰ دقیقه دیده نمی‌شود. ### مشکل ۴ — استایل محتوای CKEditor حذف می‌شود `lib/sanitize.js` تگ‌های `figure`/`figcaption` را در `ALLOWED_TAGS` ندارد و `class`/`style` را هم در `ALLOWED_ATTR` ندارد. خروجی جدول/تصویر CKEditor به شکل `
` است؛ DOMPurify با `KEEP_CONTENT` پیش‌فرض، محتوای داخلی را نگه می‌دارد ولی wrapper و کلاس‌ها را حذف می‌کند → جدول و تصویرِ داخل متن بدون هیچ استایلی و چسبیده رندر می‌شوند. (این حذف داده نیست، حذف ساختار/استایل است — واقعیت را همین‌طور گزارش کن.) ## معیار پذیرش - ✅ **موفق:** - در `/blog/` یک مقالهٔ کامل: خلاصه، فهرست تگ‌ها، زمان مطالعه، تصویر شاخص، متن، بخش «سؤالات متداول» مرئی، بخش «منابع» با لینک‌های `sources` — همه رندر می‌شوند و مقادیرشان دقیقاً با پاسخ `GET /api/v1/blog/{slug}` یکی است. - مقالهٔ بدون `image_url` در صفحهٔ جزئیات، کارت‌های لیست، مقالات مرتبط و OG/Twitter همگی **همان یک** تصویر پیش‌فرض برنددار را نشان می‌دهند. - `POST /api/revalidate` با هدر صحیح → `200 {"revalidated":true}` و بلافاصله پس از آن، صفحهٔ بلاگ محتوای جدید را نشان می‌دهد (بدون انتظار یک‌ساعته). - `npm run build` و `npm run lint` سبز. - ❌ **خطا:** - `POST /api/revalidate` بدون هدر یا با سکرت غلط → `401` و هیچ باطل‌سازی‌ای انجام نشود. - `POST /api/revalidate` با بدنهٔ نامعتبر (بدون `tags` یا `tags` غیرآرایه) → `400`. - اسلاگ ناموجود → همان `notFound()` فعلی (۴۰۴)، بدون خطای رندر. - ⚠️ **مرزی:** - مقاله‌ای با `faq: []` و `sources: []` و `tags: []` → هیچ سکشن خالی یا هدینگ بی‌محتوا رندر نشود (نه «سؤالات متداول» خالی، نه `