# افزودن شهر به بلاگ (پیش‌نیاز بلاگ شهر-محور) ## پروژه `clinicpro` (backend) **cross-repo:** این قرارداد را سایت عمومی مصرف می‌کند. پرامپت همتا (بعد از این اجرا شود): `nobat724_front/.claude/prompt/blog-city-scoping-activate.md` ## زمینه سایت عمومی ۳۵ دامنهٔ شهری دارد و هر شهر نمایندهٔ محتوایی خودش را دارد، اما بلاگ هیچ ارتباطی با شهر ندارد: نه در URL، نه در متادیتا، نه در مدل داده. نتیجه اینکه محتوای بلاگ روی همهٔ ۳۵ دامنه یکسان است و هیچ اعتبار محلی نمی‌سازد. سمت فرانت، منطق شهر-محور **از قبل نوشته و مستقر شده** ولی غیرفعال است چون داده‌اش وجود ندارد: - `nobat724_front/app/blog/[slug]/page.js` — canonical پست را با `extractEntityCityId(blog)` حساب می‌کند؛ چون `city_id` نیست، همیشه fallback به self می‌خورد. - برچسب بصری «مخصوص شهر: X» در `components/blog/head/index.js` فقط وقتی `cityName` بیاید رندر می‌شود. - `spatialCoverage` در JSON-LD مقاله فقط با وجود شهر اضافه می‌شود. - `app/sitemap.js` هر پست را فقط در sitemap دامنهٔ canonical خودش می‌گذارد. یعنی به‌محض اینکه API فیلد شهر بدهد، همهٔ این‌ها خودکار فعال می‌شوند. **وضعیت فعلی داده:** `GET /api/v1/blogs` صفر رکورد برمی‌گرداند (`totalRecords: 0`). پس این تغییر روی داده‌ای اعمال می‌شود که هنوز تولید نشده — فرصت خوبی برای اضافه‌کردن فیلد قبل از پرشدن جدول. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `clinicpro/src/Blog/Entity/Blog.php` | Entity — فیلد جدید اینجا | | `clinicpro/src/Blog/Controller/BlogController.php` | endpointهای عمومی و ادمین | | `clinicpro/src/Blog/Repository/BlogRepository.php` | فیلتر لیست | | `clinicpro/docs/api/blog.md` | مستندات | | `clinicpro/assets/admin/pages/` (صفحهٔ بلاگ) | انتخاب شهر در فرم ادمین | ## وضعیت فعلی `src/Blog/Entity/Blog.php` — هیچ ارجاعی به شهر ندارد: ```php #[ORM\Entity(repositoryClass: BlogRepository::class)] #[ORM\Table(name: 'blogs')] #[ORM\Index(columns: ['status', 'created_at'], name: 'idx_blogs_status')] class Blog { #[ORM\Column(type: 'string', length: 255)] private string $title; #[ORM\Column(type: 'string', length: 255, unique: true)] private string $slug; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(name: 'author_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')] private User $author; #[ORM\Column(type: 'json')] private array $tags = []; #[ORM\Column(type: 'string', length: 20)] private string $status = self::STATUS_DRAFT; // ... } ``` ## وظایف ### ۱. فیلد شهر روی Entity رابطهٔ `ManyToOne` اختیاری به Entity شهر (همان Entity که `Address`/`Clinic` استفاده می‌کنند — از آن‌ها الگو بگیر): ```php #[ORM\ManyToOne(targetEntity: City::class)] #[ORM\JoinColumn(name: 'city_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] private ?City $city = null; ``` - **`nullable: true` الزامی است** و معنای صریح دارد: پست بدون شهر = «پست سراسری» که روی دامنهٔ اصلی canonical می‌شود. این حالت باید برای همیشه پشتیبانی شود، نه یک وضعیت موقت. - `onDelete: 'SET NULL'` تا حذف شهر پست را نکشد. - ایندکس روی `city_id` اضافه شود (فیلتر لیست per-domain روی همین ستون است). ### ۲. Migration ```bash ddev exec php bin/console doctrine:migrations:diff --no-interaction ddev exec php bin/console doctrine:migrations:migrate --no-interaction ``` رکوردهای موجود `city_id = NULL` می‌گیرند ⇒ همه «سراسری» می‌شوند. رفتار فعلی سایت را نمی‌شکند. ### ۳. شهر در پاسخ API در سریال‌سازی پست (هم لیست هم جزئیات) شهر با **همان شکلی که سایت عمومی از کلینیک انتظار دارد** برگردد: ```php 'city' => $this->city ? [ 'id' => (string) $this->city->getId(), 'name' => $this->city->getName(), ] : null, ``` `nobat724_front/lib/domainHelpers.js` → `extractEntityCityId()` هر سه شکل `city_id` مسطح، `city: {id}` و `city: [{id}]` را می‌پذیرد؛ پس هر کدام از این‌ها کار می‌کند — ولی `{ id, name }` را انتخاب کن چون `name` برای برچسب بصری «مخصوص شهر: X» لازم است. ### ۴. فیلتر شهر در لیست عمومی `GET /api/v1/blogs` پارامتر `city_id` بپذیرد با این معنا: > پست‌های **آن شهر** + پست‌های **سراسری** (`city_id IS NULL`) این دقیقاً همان چیزی است که لیست بلاگ per-domain لازم دارد (`app/blogs/page.js` روی هر دامنهٔ شهری). بدون این پارامتر، رفتار فعلی (همهٔ پست‌ها) حفظ شود. ```sql WHERE b.status = 'published' AND (b.city_id = :cityId OR b.city_id IS NULL) ``` ### ۵. انتخاب شهر در پنل ادمین در فرم ایجاد/ویرایش بلاگ، انتخابگر شهر با گزینهٔ خالی «سراسری». از `SearchableSelect` استفاده کن (قانون پروژه: هرگز `