Files
clinicpro/.claude/prompt/seo-landing-pages.md
T
hamed 68b8b05630 feat: add landing page templates and registration modal
- Implemented header and hero section in _header.html.twig and _hero_art.html.twig.
- Created a pre-registration modal in _reg_modal.html.twig with form fields and validation.
- Added page scripts for dynamic behavior and interaction in _page_scripts.html.twig.
- Developed landing page structure in landing.html.twig, integrating header, footer, and modal.
- Introduced tests for landing page rendering and registry validation in LandingPageTest.php and LandingRegistryTest.php.
2026-08-10 14:38:27 +03:30

326 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# هفت لندینگ‌پیج سئویی با تم صفحهٔ اصلی
## زمینه
سایت عمومی کلینیک‌پرو الان فقط یک صفحه دارد: `/` که با
`src/Shared/Controller/HomeController.php` رندر می‌شود و کل محتوایش در یک فایل
۹۹۳ خطی `templates/public/home.html.twig` هاردکد است — شامل `<head>` کامل، متای سئو،
JSON-LD، هدر، شش بخش محتوا، مودال ثبت‌نام و فوتر.
`sitemap.xml` هم فقط همان یک URL را دارد
(`src/Shared/Controller/SeoController.php`).
برای هفت کلیدواژهٔ تجاری، هفت صفحهٔ فرود جدا لازم است. هرکدام باید عنوان، توضیحات،
canonical، `h1` و متن مخصوص خودش را داشته باشد، ولی ظاهرش دقیقاً همان تم صفحهٔ اصلی
باشد.
## مشکل / هدف
هفت لندینگ با این کلیدواژه‌ها:
| کلیدواژه | آدرس |
|---|---|
| نرم‌افزار مدیریت کلینیک زیبایی | `/نرم-افزار-مدیریت-کلینیک-زیبایی` |
| نرم‌افزار مدیریت کلینیک دندان‌پزشکی | `/نرم-افزار-مدیریت-کلینیک-دندانپزشکی` |
| نرم‌افزار مدیریت کلینیک فیزیوتراپی | `/نرم-افزار-مدیریت-کلینیک-فیزیوتراپی` |
| سیستم‌های جامع درمانگاهی | `/سیستم-جامع-درمانگاهی` |
| نرم‌افزار مجانی مدیریت کلینیک | `/نرم-افزار-رایگان-مدیریت-کلینیک` |
| نرم‌افزار مدیریت مطب | `/نرم-افزار-مدیریت-مطب` |
| CRM کلینیک‌ها | `/crm-کلینیک` |
**تصمیم معماری (تأییدشده):** یک قالب مشترک + رجیستری PHP. کپی‌کردن
`home.html.twig` هفت بار یعنی هر تغییر تم باید هشت بار تکرار شود و بعد از دو ماه هشت
نسخهٔ واگرا داریم. متن هر صفحه داده است، نه کد.
## معیار پذیرش
- ✅ موفق: `GET /نرم-افزار-مدیریت-کلینیک-زیبایی` → ۲۰۰ با `<title>` و
`<meta name="description">` و `<link rel="canonical">` و `<h1>` مخصوص همان صفحه؛
ظاهرش همان تم صفحهٔ اصلی است (همان CSS، همان هدر و فوتر)؛ مودال «ثبت نام» باز
می‌شود و فرمش به `/api/v1/pre-registration` ارسال می‌کند.
- ❌ خطا: `GET /یک-اسلاگ-ناموجود` → ۴۰۴ استاندارد Symfony، نه صفحهٔ خالی و نه ۲۰۰
با محتوای پیش‌فرض.
- ⚠️ مرزی: `GET /sitemap.xml` هر هشت URL را دارد (خانه + هفت لندینگ) و XML معتبر
است؛ هیچ دو صفحه‌ای `title` یا `h1` یا `canonical` یکسان ندارند؛ روی دامنهٔ
`.ddev.site` مقدار `robots.txt` همچنان `Disallow: /` می‌ماند.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/Shared/Controller/HomeController.php` | کنترلر فعلی صفحهٔ اصلی |
| `src/Shared/Controller/SeoController.php` | robots.txt و sitemap.xml — باید لندینگ‌ها را بشناسد |
| `templates/public/home.html.twig` | تم مرجع؛ هدر، فوتر، مودال و بخش‌ها از اینجا می‌آیند |
| `assets/home/styles.css` | تمام کلاس‌های تم (`hero`, `fcols`, `band`, `devices`, `specs`, `btn-*`, `wrap`) |
| `webpack.config.js` | entry به‌نام `home` که لندینگ‌ها هم از آن استفاده می‌کنند |
| `src/Shared/Landing/` | **جدید** — رجیستری و مدل لندینگ |
| `templates/public/landing.html.twig` | **جدید** — قالب مشترک |
| `templates/public/_reg_modal.html.twig` | **جدید** — مودال ثبت‌نام، مشترک بین خانه و لندینگ‌ها |
## وضعیت فعلی
کنترلر فعلی:
```php
class HomeController extends AbstractController
{
public function __construct(private readonly AltchaService $altcha) {}
#[Route('/', name: 'home', methods: ['GET'])]
public function index(): Response
{
return $this->render('public/home.html.twig', [
'altcha_enabled' => $this->altcha->enabled(),
]);
}
}
```
`sitemap.xml` فقط یک URL دارد:
```php
#[Route('/sitemap.xml', name: 'seo_sitemap', methods: ['GET'])]
public function sitemap(Request $request): Response
{
$base = $request->getSchemeAndHttpHost();
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
. '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"
. ' <url>' . "\n"
. " <loc>{$base}/</loc>\n"
. ' <changefreq>weekly</changefreq>' . "\n"
. ' <priority>1.0</priority>' . "\n"
. ' </url>' . "\n"
. '</urlset>' . "\n";
```
اسکلت صفحهٔ اصلی — خطوطی که مرزهای قابل استخراج‌اند:
```
66: {{ encore_entry_link_tags('home') }}
74: <header class="site-header" id="header"> … تا 99
101: <main> … تا 744
600: <script> ← منطق مودال ثبت‌نام (submitReg)
747: <footer class="site-footer" id="contact"> … تا 822
991: {{ encore_entry_script_tags('home') }}
```
مودال ثبت‌نام به `altcha_enabled` وابسته است و به این اندپوینت می‌فرستد:
```js
fetch('/api/v1/pre-registration', {
```
## وظایف
### ۱. مدل و رجیستری لندینگ
`src/Shared/Landing/LandingPage.php` — یک value object فقط‌خواندنی:
```php
final readonly class LandingPage
{
/**
* @param list<array{title: string, body: string}> $features
* @param list<array{q: string, a: string}> $faq
* @param list<string> $related اسلاگ لندینگ‌های مرتبط
*/
public function __construct(
public string $slug,
public string $metaTitle,
public string $metaDescription,
public string $keywords,
public string $h1,
public string $lead,
public array $features,
public array $faq,
public array $related,
) {}
}
```
`src/Shared/Landing/LandingRegistry.php` — تنها منبع تعریف هفت صفحه:
```php
final class LandingRegistry
{
/** @return array<string, LandingPage> کلید = اسلاگ */
public function all(): array { }
public function find(string $slug): ?LandingPage { }
}
```
**چرا رجیستری و نه دیتابیس:** متن این صفحات محتوای بازاریابی است و با کد دیپلوی
می‌شود؛ جدول و CRUD برای هفت رکوردِ کم‌تغییر، پیچیدگی بی‌مصرف است (guidelines §۵).
اگر بعداً لازم شد از پنل ویرایش شود، همین interface جای تعویض دارد.
**نحوه تست:** یک تست PHPUnit در `tests/Shared/LandingRegistryTest.php`:
هر هفت اسلاگ وجود دارند؛ هیچ `metaTitle` یا `h1` تکراری نیست؛ طول
`metaDescription` بین ۱۲۰ و ۱۶۰ نویسه است؛ هر `related` به اسلاگی اشاره می‌کند که
واقعاً در رجیستری هست.
### ۲. استخراج هدر، فوتر و مودال از صفحهٔ اصلی
سه partial بساز و `home.html.twig` را طوری تغییر بده که همان‌ها را `include` کند —
یعنی خروجی رندرشدهٔ `/` **هیچ تغییری نکند**:
- `templates/public/_header.html.twig` (خطوط ۷۴ تا ۹۹)
- `templates/public/_footer.html.twig` (خطوط ۷۴۷ تا ۸۲۲)
- `templates/public/_reg_modal.html.twig` (مودال + اسکریپت `submitReg`)
**نحوه تست:** خروجی `/` را قبل و بعد ذخیره کن و diff بگیر؛ باید یکسان باشد:
```bash
ddev exec curl -s http://localhost/ > /tmp/home-before.html
# … بعد از تغییر
ddev exec curl -s http://localhost/ > /tmp/home-after.html
diff /tmp/home-before.html /tmp/home-after.html # باید خالی باشد
```
### ۳. قالب مشترک لندینگ
`templates/public/landing.html.twig` — همان اسکلت `home.html.twig` ولی داده‌محور:
- `<head>` با `page.metaTitle`، `page.metaDescription`، `page.keywords` و
`<link rel="canonical" href="{{ base }}/{{ page.slug }}">`
- `{{ encore_entry_link_tags('home') }}` و `{{ encore_entry_script_tags('home') }}`
همان entry، پس تم دقیقاً یکی است و CSS دومی ساخته نمی‌شود
- هدر و فوتر و مودال با `include` از وظیفهٔ ۲
- بخش hero با `page.h1` و `page.lead` و همان کلاس‌های `hero`, `hero-grid`,
`hero-copy`, `btn btn-coral`, `btn btn-blue`
- بخش امکانات با کلاس‌های `fcols`, `fcols-grid`, `fcol` روی `page.features`
- بخش پرسش‌های متداول روی `page.faq`
- بخش «راهنماهای مرتبط» با لینک داخلی به `page.related` — لینک داخلی بین لندینگ‌ها
برای سئو لازم است و صفحات را یتیم نمی‌گذارد
**قید مهم:** هیچ کلاس CSS جدیدی تعریف نکن. اگر بخشی از تم لازم است که کلاسش وجود
ندارد، از همان بخش‌های موجود (`band`, `specs`, `devices`) استفاده کن. تم باید یکی
بماند، نه شبیه.
**نحوه تست:** بعد از وظیفهٔ ۴، هر هفت آدرس را باز کن و با اسکرین‌شات با `/` مقایسه
کن.
### ۳.۱ JSON-LD هر صفحه
در قالب، سه schema بگذار:
- `SoftwareApplication` با `applicationCategory: "BusinessApplication"` و
`offers` — برای صفحهٔ رایگان `price: "0"`
- `FAQPage` از `page.faq` — این برای کلیدواژه‌های تجاری در نتایج گوگل شانس
rich result دارد
- `BreadcrumbList` با دو سطح: خانه ← همین صفحه
**نحوه تست:** خروجی هر صفحه را در
[validator.schema.org](https://validator.schema.org) بگذار، یا حداقل با
`python3 -c "import json,sys; json.load(sys.stdin)"` روی محتوای هر بلاک
`application/ld+json` صحت JSON را بسنج — Twig با نقل‌قول فارسی راحت JSON را خراب
می‌کند.
### ۴. کنترلر لندینگ
`src/Shared/Controller/LandingController.php`:
```php
final class LandingController extends AbstractController
{
public function __construct(
private readonly LandingRegistry $registry,
private readonly AltchaService $altcha,
) {}
#[Route('/{slug}', name: 'landing_show', methods: ['GET'], priority: -10)]
public function show(string $slug): Response
{
$page = $this->registry->find($slug);
if ($page === null) {
throw $this->createNotFoundException();
}
return $this->render('public/landing.html.twig', [
'page' => $page,
'altcha_enabled' => $this->altcha->enabled(),
]);
}
}
```
**چرا `priority: -10`:** روت `/{slug}` هر مسیر تک‌بخشی را می‌گیرد. بدون اولویت
منفی، ممکن است جلوی `/admin`، `/robots.txt` یا مسیرهای دیگر را بگیرد. با اولویت
منفی، آخرین گزینه است.
**نحوه تست:**
```bash
ddev exec php bin/console debug:router | grep landing
for s in نرم-افزار-مدیریت-کلینیک-زیبایی crm-کلینیک; do
curl -sk -o /dev/null -w "%{http_code} $s\n" "https://clinic-pro.ddev.site/$s"
done
curl -sk -o /dev/null -w "%{http_code} اسلاگ-ناموجود\n" https://clinic-pro.ddev.site/اسلاگ-ناموجود # باید 404
curl -sk -o /dev/null -w "%{http_code} /admin\n" https://clinic-pro.ddev.site/admin # باید 200
curl -sk -o /dev/null -w "%{http_code} /robots.txt\n" https://clinic-pro.ddev.site/robots.txt # باید 200
```
سه خط آخر مهم‌ترین‌اند: اگر روت لندینگ حریص باشد، پنل ادمین را می‌خورد.
### ۵. sitemap و لینک داخلی
`SeoController::sitemap()` را طوری تغییر بده که `LandingRegistry` را تزریق بگیرد و
هر هفت لندینگ را با `priority: 0.8` اضافه کند. اسلاگ فارسی باید در XML
**percent-encode** شود، وگرنه XML نامعتبر است:
```php
$loc = $base . '/' . rawurlencode($page->slug);
```
در فوتر (`_footer.html.twig`) یک ستون «راهکارها» با لینک به هر هفت صفحه اضافه کن.
بدون این، لندینگ‌ها فقط از sitemap دیده می‌شوند و از داخل سایت لینک نمی‌گیرند.
**نحوه تست:**
```bash
curl -sk https://clinic-pro.ddev.site/sitemap.xml | python3 -c "
import sys, xml.etree.ElementTree as ET
root = ET.fromstring(sys.stdin.read())
ns = {'s': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
locs = [u.find('s:loc', ns).text for u in root.findall('s:url', ns)]
print(len(locs), 'url'); [print(' ', l) for l in locs]
assert len(locs) == 8, 'باید هشت آدرس باشد'
"
```
### ۶. تست خودکار محتوای صفحات
`tests/Shared/LandingPageTest.php` با `ApiTestCase` یا `WebTestCase`:
- هر هفت اسلاگ → ۲۰۰
- `title` و `h1` و `canonical` هر صفحه یکتا و متعلق به خودش است
- اسلاگ ناموجود → ۴۰۴
- `/` هنوز ۲۰۰ می‌دهد و `h1` قدیمی‌اش را دارد
- `sitemap.xml` هشت `<loc>` دارد
**نحوه تست:** `ddev exec php bin/phpunit tests/Shared/LandingPageTest.php`
## نکات مهم
- **متن هر صفحه باید واقعاً متفاوت باشد.** هفت صفحه با یک متن و فقط عوض‌شدن کلمهٔ
«زیبایی/دندان‌پزشکی/فیزیوتراپی» از نگاه گوگل محتوای تکراری است و هر هفت‌تا را
پایین می‌کشد. برای هر صنف، دردِ خودش را بنویس: کلینیک زیبایی → دوره‌های چندجلسه‌ای
و مصرف کالا؛ دندان‌پزشکی → پروندهٔ دندان و بیمهٔ تکمیلی؛ فیزیوتراپی → جلسات
درمانی و نوبت تکراری؛ درمانگاه → چند پزشک و چند بخش و منابع مشترک؛ مطب → سادگی و
یک‌نفره بودن؛ CRM → پیگیری بیمار و یادآوری و بازگشت مراجع.
- **صفحهٔ «رایگان» باید صادق باشد.** پلن `free` واقعاً در محصول هست
(`SubscriptionPlan` با نام `free`)، پس ادعای رایگان درست است — ولی در همان صفحه
صریح بنویس چه چیزی در پلن رایگان هست و چه چیزی نیست. وعدهٔ نادرست، هم نرخ تبدیل
را خراب می‌کند هم اعتماد را.
- **تم را کپی نکن، مشترک کن.** اگر بعد از این کار، تغییر رنگ دکمه در صفحهٔ اصلی
به‌طور خودکار در هر هفت لندینگ دیده نشد، یعنی وظیفهٔ ۲ و ۳ درست انجام نشده‌اند.
- **الگو: Registry + Value Object.** دلیل انتخاب: هفت نمونهٔ هم‌شکل که فقط داده‌شان
فرق دارد. این «abstraction برای آینده» نیست؛ همین حالا هفت مصرف‌کننده دارد.
- روت `/{slug}` با اسلاگ فارسی کار می‌کند ولی در لاگ و ابزارها percent-encoded دیده
می‌شود. این عادی است و نباید «درست» شود.
- این تغییر backend عمومی است و API ندارد، پس `docs/api/` دست نمی‌خورد. در عوض یک
یادداشت کوتاه در `README.MD` یا `docs/` بنویس که لندینگ‌ها کجا تعریف می‌شوند —
وگرنه نفر بعدی دنبال فایل Twig هر صفحه می‌گردد.
- `nobat724_front` از این تغییر متأثر نیست؛ این صفحات روی دامنهٔ خود کلینیک‌پرو
هستند.