From e78b0c4b4c95b9a602b70352644e7394d5e1097d Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 19 Jul 2026 08:09:00 +0330 Subject: [PATCH] chore(prompt): split SEO audit follow-up into backend task prompts The nobat724_front SEO audit produced three backend-side tasks that the frontend work is blocked on or that it only masks: - doctors-list-city-and-limit: implemented in the previous commit - blog-city-field: blog has no city column, so the city-scoped blog work on the public site is written but inert - cleanup-polluted-doctor-clinic-records: records named after a phone number or "test" reach public results; the site currently hides them with a noindex filter, which masks rather than fixes the data Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/prompt/blog-city-field.md | 131 ++++++++++++++++++ .../cleanup-polluted-doctor-clinic-records.md | 86 ++++++++++++ .claude/prompt/doctors-list-city-and-limit.md | 108 +++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 .claude/prompt/blog-city-field.md create mode 100644 .claude/prompt/cleanup-polluted-doctor-clinic-records.md create mode 100644 .claude/prompt/doctors-list-city-and-limit.md diff --git a/.claude/prompt/blog-city-field.md b/.claude/prompt/blog-city-field.md new file mode 100644 index 00000000..ac71d7bc --- /dev/null +++ b/.claude/prompt/blog-city-field.md @@ -0,0 +1,131 @@ +
+ +# افزودن شهر به بلاگ (پیش‌نیاز بلاگ شهر-محور) + +## پروژه + +`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` استفاده کن (قانون پروژه: هرگز `