Files
nobat724_front/.claude/prompt/blog-city-scoping-activate.md
hamedandClaude Opus 4.8 21f5f07bc6 chore(prompt): record SEO audit and split remaining work
P1-P11 of the live SEO audit are implemented and verified against a local
production build, so the audit file becomes a reference rather than a
task list: it now records what shipped, the root causes that differed
from the original hypotheses, and the deliberate trade-offs.

Remaining work is split into smaller prompts, ordered by dependency:

- seo-post-deploy-verification: the acceptance criteria were "curl on
  production" but were only run against a local build
- blog-city-scoping-activate: blocked on the backend blog city column
- sitemap-simplify-with-city: drops the 35-sweep workaround once the
  doctors list exposes city

Each names its blocking dependency and carries reference numbers so a
regression is visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 08:09:21 +03:30

7.1 KiB
Raw Permalink Blame History

فعال‌سازی بلاگ شهر-محور (بعد از افزودن شهر در backend)

پروژه

nobat724_front

پیش‌نیاز قطعی: clinicpro/.claude/prompt/blog-city-field.md اجرا و deploy شده باشد. تا وقتی GET /api/v1/blogs فیلد شهر برنگرداند، این پرامپت کاری برای انجام ندارد.

زمینه

منطق شهر-محورِ بلاگ سمت فرانت از قبل نوشته شده ولی چون داده‌اش وجود ندارد غیرفعال است. این پرامپت آن را فعال، تکمیل و تست می‌کند.

وضعیت داده هنگام نگارش: GET /api/v1/blogs صفر رکورد دارد (totalRecords: 0) و هیچ فیلد شهری ندارد.

وضعیت فعلی (کدی که آمادهٔ فعال‌شدن است)

app/blog/[slug]/page.js — canonical و شهر از همان هلپر مشترک:

const blogCityId = extractEntityCityId(blog);
const blogCity = findCityById(blogCityId);
const origin = await getEntityOrigin(blogCityId);

return {
  title,
  description,
  alternates: { canonical: `${origin}/blog/${slug}` },
  // ...
};

JSON-LD مقاله فقط با وجود شهر spatialCoverage می‌گیرد:

...(blogCity && {
  spatialCoverage: { "@type": "Place", name: blogCity.name },
}),

components/blog/head/index.js — برچسب بصری، فقط با cityName:

{cityName && (
  <div className="flex items-center justify-start gap-1">
    <p className="text-[#9B9B9B] text-[11px] md:text-[13px] lg:text-[14px] font-normal">
      مخصوص شهر:
    </p>
    <p className="text-[#9B9B9B] text-[11px] md:text-[14px] lg:text-[16px] font-medium">
      {cityName}
    </p>
  </div>
)}

lib/domainHelpers.js — استخراج شهر، هر سه شکل پاسخ را می‌پذیرد:

export function extractEntityCityId(entity) {
  if (!entity) return null;
  const candidates = [
    entity.city_id,
    ...(Array.isArray(entity.city) ? entity.city.map((c) => c?.id) : [entity.city?.id]),
    ...(Array.isArray(entity.address) ? entity.address.map((a) => a?.city?.id) : []),
  ].filter((id) => id != null);
  const withDomain = candidates.find((id) => findDomainByCityId(id));
  return withDomain ?? candidates[0] ?? null;
}

app/sitemap.js — مسیریابی پست به sitemap دامنهٔ canonical خودش:

async function getBlogUrls(baseUrl, scope, currentDomain) {
    const blogs = await fetchAllPages('/api/v1/blogs');
    return blogs
        .filter((b) => b?.slug || b?.uuid)
        .filter((b) => {
            const cityDomain = findDomainByCityId(extractEntityCityId(b));
            return cityDomain ? cityDomain === currentDomain : scope.isRoot;
        })
        // ...
}

وظایف

۱. تأیید سازگاری شکل پاسخ

اول پاسخ واقعی را ببین:

curl -s "https://clinic-pro.ir/api/v1/blogs?page=1&limit=5" | jq '.data.data[0]'

بررسی کن extractEntityCityId روی شکل واقعی جواب می‌دهد. اگر backend شکل دیگری داد (مثلاً city رشته به‌جای آبجکت)، extractEntityCityId را گسترش بده — نه یک استخراج جدا برای بلاگ بنویس.

normalizeBlog در helper/index.js نباید فیلد شهر را حذف کند؛ اگر فیلدها را صریح map می‌کند، city را اضافه کن.

۲. لیست بلاگ per-domain

app/blogs/page.js روی هر دامنهٔ شهری باید پست‌های آن شهر + سراسری را بگیرد. با پارامتر city_id که backend اضافه می‌کند:

  • روی دامنهٔ شهری: city_id شهر همان دامنه (از getStateInfo)
  • روی دامنهٔ ریشه (isRoot): بدون پارامتر — همهٔ پست‌ها

canonical لیست بلاگ self روی همان دامنه بماند (C1-a). این از قبل درست است — کانونیکال hardcode شده به دامنهٔ اصلی قبلاً حذف شده:

// C1-a — لیست بلاگ per-domain است (پست‌های همان شهر + سراسری)؛
// canonical لایهٔ layout (self روی همان دامنه) درست است.

۳. عنوان و متادیتای شهرمحور

generateMetadata صفحهٔ پست: اگر پست شهر دارد، نام شهر در title/description بیاید. اگر سراسری است، هیچ نام شهری اضافه نشود (نه نام شهرِ دامنهٔ سرو‌کننده).

۴. تست

با دادهٔ واقعی (حداقل یک پست شهری + یک پست سراسری) تأیید کن:

S=<slug-پست-یاسوجی>
G=<slug-پست-سراسری>

# پست شهری: canonical به دامنهٔ شهر، روی هر دامنه‌ای
curl -s "https://yasuj-nobat.ir/blog/$S"  | grep -o 'rel="canonical" href="[^"]*"'  # → yasuj-nobat.ir
curl -s "https://nobat724.com/blog/$S"    | grep -o 'rel="canonical" href="[^"]*"'  # → yasuj-nobat.ir
curl -s "https://yasuj-nobat.ir/blog/$S"  | grep -o "یاسوج"                          # نام شهر در HTML قابل‌مشاهده
curl -s "https://yasuj-nobat.ir/blog/$S"  | grep -o 'spatialCoverage'

# پست سراسری: self-canonical، بدون نام شهر
curl -s "https://nobat724.com/blog/$G"    | grep -o 'rel="canonical" href="[^"]*"'  # → nobat724.com
curl -s "https://yasuj-nobat.ir/blog/$G"  | grep -c 'spatialCoverage'                # → 0

# sitemap: هر پست فقط در دامنهٔ canonical خودش
curl -s https://yasuj-nobat.ir/sitemap.xml | grep -c "$S"   # → 1
curl -s https://nobat724.com/sitemap.xml   | grep -c "$S"   # → 0
curl -s https://nobat724.com/sitemap.xml   | grep -c "$G"   # → 1
curl -s https://nobat724.com/sitemap.xml   | grep -c "/blog/"  # → بیشتر از ۰

تست واحد برای extractEntityCityId روی شکل واقعی پاسخ بلاگ به lib/domainHelpers.test.js اضافه کن.

نکات مهم

  • پست سراسری حالت دائمی است، نه استثنا. پستی بدون شهر باید روی دامنهٔ اصلی self-canonical شود و در همهٔ لیست‌ها دیده شود. هرگز شهرِ دامنهٔ سرو‌کننده را به پست بدون شهر نسبت نده.
  • findCityById رکورد ریشه (id: 600 = nobat724.com) را شهر حساب نمی‌کند و null برمی‌گرداند — یعنی پستی که به‌اشتباه به رکورد ریشه وصل شده، سراسری تلقی می‌شود. رفتار درست است.
  • برچسب بصری شهر با تایپوگرافی «نویسنده/تاریخ» هم‌خانواده است؛ عنصر بصری ناهماهنگ اضافه نکن.
  • الگوی Soft-404 (app/blog/[slug]/page.js) بعد از تغییرات همچنان باید کار کند: curl -o /dev/null -w "%{http_code}" https://nobat724.com/blog/invalid-xxx404. توجه: app/blog/[slug]/loading.js عمداً حذف شده — دوباره اضافه‌اش نکن، وگرنه Soft-404 برمی‌گردد.