feat(public): add About Us page

Adds a public "درباره ما" page at /درباره-ما reusing the landing theme
(same webpack entry, header, footer, registration modal). Section ids
match the header anchors so the nav works on this page too.

The explicit route takes precedence over LandingController's catch-all
/{slug}, which is registered with priority -10.

Also links it from the header nav and footer quick links, and lists it
in sitemap.xml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-19 14:54:06 +03:30
co-authored by Claude Opus 5
parent 40b1da64be
commit 1ca20b6f53
7 changed files with 300 additions and 4 deletions
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Shared\Controller;
use App\Shared\Captcha\AltchaService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* صفحهٔ «دربارهٔ ما».
*
* اسلاگ فارسی است تا با بقیهٔ صفحات عمومی یک‌دست باشد. روت صریح تعریف شده و
* بنابراین بر `/{slug}` صفحات فرود — که با priority منفی ثبت شده — مقدم است.
*/
final class AboutController extends AbstractController
{
public const SLUG = 'درباره-ما';
public function __construct(private readonly AltchaService $altcha) {}
#[Route('/' . self::SLUG, name: 'about', methods: ['GET'])]
public function index(): Response
{
return $this->render('public/about.html.twig', [
'altcha_enabled' => $this->altcha->enabled(),
]);
}
}
+2 -1
View File
@@ -36,7 +36,8 @@ class SeoController extends AbstractController
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
. '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"
. $this->urlNode("{$base}/", 'weekly', '1.0');
. $this->urlNode("{$base}/", 'weekly', '1.0')
. $this->urlNode($base . '/' . rawurlencode(AboutController::SLUG), 'yearly', '0.5');
// اسلاگ‌ها فارسی‌اند و باید percent-encode شوند، وگرنه XML نامعتبر است.
foreach ($this->landings->all() as $page) {
+1
View File
@@ -29,6 +29,7 @@
<a href="#features">امکانات</a>
<a href="#why">چرا کلینیک پرو</a>
<a href="#download" style="display:none">دانلود نرم‌افزار</a>
<a href="/درباره-ما">دربارهٔ ما</a>
<a href="#contact">تماس با ما</a>
</div>
</div>
+1
View File
@@ -12,6 +12,7 @@
<a href="#features">امکانات</a>
<a href="#why">چرا کلینیک پرو</a>
<a href="#download" style="display:none">دانلود</a>
<a href="/درباره-ما">دربارهٔ ما</a>
<a href="#contact">تماس با ما</a>
<a href="#" id="openRegModalNav" class="nav-reg-mobile">ثبت نام</a>
</nav>
+216
View File
@@ -0,0 +1,216 @@
{# صفحهٔ «دربارهٔ ما». همان تم صفحهٔ اصلی: همان entry وبپک، همان هدر و فوتر و مودال.
idهای بخش‌ها (hero/features/why/contact) عمداً با لینک‌های هدر یکی است تا منو
در این صفحه هم کار کند. #}
{% set base = app.request.schemeAndHttpHost %}
{% set url = base ~ '/درباره-ما' %}
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>دربارهٔ ما | کلینیک پرو</title>
<link rel="icon" type="image/svg+xml" href="/logo.svg"/>
<link rel="apple-touch-icon" href="/logo.svg"/>
<meta name="description" content="کلینیک پرو را تیم توسعه فناوری اطلاعات حامی ساخته است؛ نرم‌افزار مدیریت مطب و کلینیک، متناسب با روال واقعی کار درمان در ایران."/>
<meta name="keywords" content="درباره کلینیک پرو, تیم کلینیک پرو, نرم افزار مدیریت مطب, شرکت توسعه فناوری اطلاعات حامی"/>
<link rel="canonical" href="{{ url }}"/>
<meta name="robots" content="index, follow, max-image-preview:large"/>
<meta property="og:type" content="website"/>
<meta property="og:site_name" content="کلینیک پرو"/>
<meta property="og:title" content="دربارهٔ ما | کلینیک پرو"/>
<meta property="og:description" content="داستان، تیم و مسیر کلینیک پرو؛ نرم‌افزاری که برای مطب‌ها و کلینیک‌های ایران ساخته شده است."/>
<meta property="og:url" content="{{ url }}"/>
<meta property="og:image" content="{{ base }}/logo.svg"/>
<meta property="og:locale" content="fa_IR"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:title" content="دربارهٔ ما | کلینیک پرو"/>
<meta name="twitter:description" content="داستان، تیم و مسیر کلینیک پرو؛ نرم‌افزاری که برای مطب‌ها و کلینیک‌های ایران ساخته شده است."/>
<meta name="twitter:image" content="{{ base }}/logo.svg"/>
{# JSON-LD با json_encode ساخته می‌شود نه رشتهٔ دستی: متن فارسی نقل‌قول و
کاراکتر کنترلی دارد و JSONِ دست‌ساز در Twig خیلی راحت نامعتبر می‌شود. #}
<script type="application/ld+json">
{{ {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'AboutPage',
'@id': url ~ '#about',
'url': url,
'name': 'دربارهٔ ما | کلینیک پرو',
'inLanguage': 'fa-IR',
'mainEntity': { '@id': base ~ '/#organization' }
},
{
'@type': 'Organization',
'@id': base ~ '/#organization',
'name': 'کلینیک پرو',
'legalName': 'شرکت توسعه فناوری اطلاعات حامی',
'url': base ~ '/',
'logo': base ~ '/logo.svg',
'email': 'info@clinic-pro.ir',
'telephone': '+982191550875',
'address': {
'@type': 'PostalAddress',
'addressCountry': 'IR',
'addressRegion': 'کهگیلویه و بویراحمد',
'addressLocality': 'یاسوج',
'streetAddress': 'مهریان، خیابان شهید ستاره شیرازی، خیابان امام‌زاده احمد، پلاک ۸'
}
},
{
'@type': 'BreadcrumbList',
'@id': url ~ '#breadcrumb',
'itemListElement': [
{ '@type': 'ListItem', 'position': 1, 'name': 'کلینیک پرو', 'item': base ~ '/' },
{ '@type': 'ListItem', 'position': 2, 'name': 'دربارهٔ ما', 'item': url }
]
}
]
}|json_encode(constant('JSON_UNESCAPED_UNICODE') b-or constant('JSON_UNESCAPED_SLASHES'))|raw }}
</script>
{{ encore_entry_link_tags('home') }}
</head>
<body>
{{ include('public/_header.html.twig') }}
<main>
<section class="hero" id="hero">
<div class="wrap hero-grid">
<div class="hero-copy">
<h1 class="reveal">دربارهٔ کلینیک پرو</h1>
<p class="reveal d1">ما تیمی هستیم که سال‌ها کنار پزشک‌ها و منشی‌ها نشسته و کار روزمرهٔ مطب را از نزدیک دیده‌ایم. کلینیک پرو حاصل همان دیدن است: نرم‌افزاری که به‌جای اضافه‌کردن کار، کار را کم می‌کند.</p>
<div class="hero-actions reveal d2">
{# همان idهایی که اسکریپت مودال به آن‌ها bind می‌شود. #}
<button class="btn btn-coral" id="openRegModal2">رایگان شروع کنید</button>
<a href="#contact" class="btn btn-blue">تماس با ما</a>
</div>
</div>
{{ include('public/_hero_art.html.twig') }}
</div>
</section>
<section class="specs" id="story">
<div class="wrap">
<div class="specs-head reveal">
<span class="eyebrow">داستان ما</span>
<h2>از یک دفتر کاغذی شروع شد</h2>
<p>ایدهٔ کلینیک پرو از یک مشکل ساده بیرون آمد: نوبت‌ها روی کاغذ، حساب‌ها در ذهن و پروندهٔ بیمار در کشو.</p>
</div>
<div class="specs-grid">
<div class="spec-card reveal">
<h3>مشاهده</h3>
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">چند فصل در مطب‌ها نشستیم و دیدیم بیشترِ وقت منشی صرف پیدا کردن یک پرونده و هماهنگی دوبارهٔ نوبت می‌شود.</p>
</div>
<div class="spec-card reveal d1">
<h3>ساخت</h3>
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">اولین نسخه را برای یک مطب نوشتیم و هر هفته کنار خودشان اصلاحش کردیم؛ نه از روی حدس، از روی کار واقعی.</p>
</div>
<div class="spec-card reveal d2">
<h3>گسترش</h3>
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">امروز کلینیک پرو از مطب تک‌پزشکه تا درمانگاه چندتخصصی را پوشش می‌دهد؛ با نوبت‌دهی آنلاین، پرونده، بیمه و حسابداری.</p>
</div>
<div class="spec-card reveal d3">
<h3>ادامه</h3>
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">هر قابلیت تازه از درخواست کاربرها می‌آید. مسیر محصول را کسانی می‌سازند که هر روز با آن کار می‌کنند.</p>
</div>
</div>
</div>
</section>
<section class="fcols" id="features">
<div class="wrap fcols-grid">
<div class="fcol reveal">
<h3>کارِ مطب، سرِ جای خودش</h3>
<p>نوبت، پرونده، نسخه، بیمه و صندوق در یک جا جمع می‌شود؛ دیگر لازم نیست بین چند دفتر و چند فایل بگردید.</p>
</div>
<div class="fcol reveal d1">
<h3>ساده بماند</h3>
<p>هر صفحه را تا جایی ساده می‌کنیم که منشی تازه‌وارد بدون آموزش طولانی بتواند کار کند.</p>
</div>
<div class="fcol reveal d2">
<h3>اطلاعات مالِ شماست</h3>
<p>داده‌های بیماران شما فقط برای شماست. پشتیبان‌گیری منظم و دسترسی نقش‌محور از روز اول در محصول بوده است.</p>
</div>
<div class="fcol reveal d3">
<h3>کنار شما می‌مانیم</h3>
<p>راه‌اندازی، انتقال اطلاعات و آموزش تیم را خودمان انجام می‌دهیم؛ فروش پایان کار نیست، شروع آن است.</p>
</div>
</div>
</section>
<section class="band">
<div class="icons" id="why">
<div class="wrap icon-grid">
<div class="icon-cell reveal">
<div class="ic-circ">
<svg viewbox="0 0 24 24" fill="none"><path d="M7 11v9H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3Z" stroke="currentColor" stroke-linejoin="round"/><path d="M7 11l4.5-7a2 2 0 0 1 3.5 1.3V9h4.2a2 2 0 0 1 2 2.4l-1.3 7A2 2 0 0 1 17.9 20H7" stroke="currentColor" stroke-linejoin="round"/></svg>
</div>
<h3>ساخت ایران، برای ایران</h3>
<p>تقویم شمسی، بیمه‌های داخلی، پیامک فارسی و روال واقعی مطب‌های ایرانی؛ نه ترجمهٔ یک محصول خارجی.</p>
</div>
<div class="icon-cell reveal d1">
<div class="ic-circ">
<svg viewbox="0 0 24 24" fill="none"><path d="M12 3 4 6v5c0 5 3.5 8.5 8 10 4.5-1.5 8-5 8-10V6l-8-3Z" stroke="currentColor" stroke-linejoin="round"/><path d="m9 12 2 2 4-4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<h3>امانت‌داری در داده</h3>
<p>پروندهٔ بیمار امانت است. دسترسی‌ها دقیق تعریف می‌شود و هر تغییر مهم ثبت و قابل پیگیری است.</p>
</div>
<div class="icon-cell reveal d2">
<div class="ic-circ">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 5h16a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H9l-4 4v-4H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z" stroke="currentColor" stroke-linejoin="round"/><path d="M8.5 10.5h7M8.5 13h4" stroke="currentColor" stroke-linecap="round"/></svg>
</div>
<h3>حرف کاربر، خطِ اول</h3>
<p>هر بازخورد خوانده می‌شود. بخش بزرگی از قابلیت‌های امروز، درخواست پزشک‌ها و منشی‌ها بوده است.</p>
</div>
<div class="icon-cell reveal d3">
<div class="ic-circ">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 17V9m5 8V5m5 12v-6m5 6V8" stroke="currentColor" stroke-linecap="round"/></svg>
</div>
<h3>رشد پیوسته</h3>
<p>محصول هر ماه به‌روز می‌شود و به‌روزرسانی‌ها برای مشترکان فعال رایگان است.</p>
</div>
</div>
</div>
</section>
<section class="specs" id="company">
<div class="wrap">
<div class="specs-head reveal">
<span class="eyebrow">پشت محصول</span>
<h2>شرکت توسعه فناوری اطلاعات حامی</h2>
<p>کلینیک پرو محصول این شرکت است؛ تیمی از توسعه‌دهنده، طراح و کارشناس پشتیبانی که همگی روی یک محصول کار می‌کنند.</p>
</div>
<div class="specs-grid">
<div class="spec-card reveal">
<h3>تیم محصول</h3>
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">طراحی و توسعهٔ وب، دسکتاپ و موبایل؛ همه با یک هستهٔ مشترک تا رفتار محصول همه‌جا یکسان بماند.</p>
</div>
<div class="spec-card reveal d1">
<h3>پشتیبانی</h3>
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">تماس، پیام و راه دور؛ مشکل شما تا حل‌شدن پیگیری می‌شود، نه تا بسته‌شدن تیکت.</p>
</div>
<div class="spec-card reveal d2">
<h3>راه‌اندازی</h3>
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">انتقال اطلاعات از دفتر یا نرم‌افزار قبلی و آموزش تیم مطب را کنار خودتان انجام می‌دهیم.</p>
</div>
<div class="spec-card reveal d3">
<h3>همکاری</h3>
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">برای نمایندگی، همکاری فنی یا پیشنهاد قابلیت تازه، از همین صفحه با ما تماس بگیرید.</p>
</div>
</div>
</div>
</section>
{{ include('public/_reg_modal.html.twig') }}
</main>
{{ include('public/_footer.html.twig') }}
{{ include('public/_page_scripts.html.twig') }}
{{ encore_entry_script_tags('home') }}
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Tests\Shared;
use App\Shared\Controller\AboutController;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* صفحهٔ «دربارهٔ ما» رندر می‌شود و در سایت‌مپ و منو دیده می‌شود.
*
* روت `/{slug}` صفحات فرود هر مسیر تک‌بخشی را می‌گیرد؛ این تست تضمین می‌کند
* اسلاگ فارسیِ این صفحه به آن نیفتد و ۴۰۴ نشود.
*/
class AboutPageTest extends WebTestCase
{
private KernelBrowser $client;
protected function setUp(): void
{
$this->client = static::createClient();
}
public function testAboutPageRenders(): void
{
$this->client->request('GET', '/' . AboutController::SLUG);
$html = $this->client->getResponse()->getContent();
self::assertSame(200, $this->client->getResponse()->getStatusCode());
self::assertStringContainsString('<h1 class="reveal">دربارهٔ کلینیک پرو</h1>', $html);
self::assertStringContainsString('<title>دربارهٔ ما | کلینیک پرو</title>', $html);
self::assertStringContainsString('شرکت توسعه فناوری اطلاعات حامی', $html);
}
public function testHeaderLinksToAbout(): void
{
$this->client->request('GET', '/');
self::assertStringContainsString('href="/درباره-ما"', $this->client->getResponse()->getContent());
}
public function testSitemapListsAbout(): void
{
$this->client->request('GET', '/sitemap.xml');
self::assertStringContainsString(rawurlencode(AboutController::SLUG), $this->client->getResponse()->getContent());
}
}
+3 -3
View File
@@ -84,9 +84,9 @@ class LandingPageTest extends WebTestCase
$xml = $this->html('/sitemap.xml');
$doc = new \SimpleXMLElement($xml);
// خانه + هر صفحهٔ فرود. عدد از خودِ رجیستری می‌آید تا افزودن خوشهٔ کلیدواژهٔ
// تازه این تست را نشکند، ولی جاافتادن یک صفحه از sitemap هنوز قرمز شود.
self::assertCount(1 + count($this->registry->all()), $doc->url);
// خانه + دربارهٔ ما + هر صفحهٔ فرود. عدد از خودِ رجیستری می‌آید تا افزودن خوشهٔ
// کلیدواژهٔ تازه این تست را نشکند، ولی جاافتادن یک صفحه از sitemap هنوز قرمز شود.
self::assertCount(2 + count($this->registry->all()), $doc->url);
foreach (array_keys($this->registry->all()) as $slug) {
self::assertStringContainsString(rawurlencode($slug), $xml, "«{$slug}» در sitemap نیست");