fix(seo): shorten the footer and center the related-solutions cards
Every landing page was listed in the footer, so adding keyword clusters made that one column taller than the rest of the footer put together. It now lists six — the head terms and the two biggest specialties — and the rest stay reachable through the "other solutions" block each page already renders. That trade only holds if nothing falls out of the link graph, so the old test asserting "the footer links to every page" is replaced by one that walks the related-page graph from the footer links and fails when any page is not reachable. Physiotherapy was the one page that would have been orphaned; the polyclinic page now links to it. The related-solutions heading said "for your trade", which reads oddly for the clinics it is addressing, and its cards were left-aligned in a centered section. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,12 +29,33 @@ final class LandingRegistry
|
||||
/** @var array<string, LandingPage>|null ساخت تنبل؛ هر درخواست حداکثر یک بار میسازدش. */
|
||||
private ?array $pages = null;
|
||||
|
||||
/**
|
||||
* صفحههایی که در فوتر میآیند.
|
||||
*
|
||||
* فوتر فهرست کامل نیست: با هر خوشهٔ کلیدواژهٔ تازه، ستونِ «راهکارها» بلندتر از
|
||||
* بقیهٔ فوتر میشد و صفحه را زشت میکرد. بقیهٔ صفحهها از بخش «راهکارهای دیگر»
|
||||
* همین صفحهها و از sitemap در دسترساند — {@see LandingPageTest} تضمین میکند
|
||||
* هیچکدام از این فهرست جا نمانند.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const FEATURED = [self::CLINIC, self::OFFICE, self::BOOKING, self::DENTAL, self::BEAUTY, self::FREE];
|
||||
|
||||
/** @return array<string, LandingPage> کلید = اسلاگ */
|
||||
public function all(): array
|
||||
{
|
||||
return $this->pages ??= $this->build();
|
||||
}
|
||||
|
||||
/** @return list<LandingPage> صفحههای فوتر، به همان ترتیبِ تعریفشده */
|
||||
public function featured(): array
|
||||
{
|
||||
return array_values(array_filter(array_map(
|
||||
fn (string $slug): ?LandingPage => $this->find($slug),
|
||||
self::FEATURED,
|
||||
)));
|
||||
}
|
||||
|
||||
public function find(string $slug): ?LandingPage
|
||||
{
|
||||
return $this->all()[$slug] ?? null;
|
||||
@@ -134,7 +155,7 @@ final class LandingRegistry
|
||||
['q' => 'تداخل اتاق و دستگاه چطور کنترل میشود؟', 'a' => 'با تعریف منبع. نوبت علاوه بر وقت پزشک، وقت منبع را هم اشغال میکند.'],
|
||||
['q' => 'گزارش مالی تفکیکشده دارد؟', 'a' => 'بله، فهرست پرداختها با وضعیت تسویه و خلاصهٔ مجموع، پرداختشده و تسویهنشده.'],
|
||||
],
|
||||
related: [self::CLINIC, self::ACCOUNTING, self::DENTAL],
|
||||
related: [self::CLINIC, self::ACCOUNTING, self::PHYSIOTHERAPY],
|
||||
),
|
||||
new LandingPage(
|
||||
slug: self::FREE,
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="footer-col">
|
||||
<h4>راهکارها</h4>
|
||||
<div class="foot-links">
|
||||
{% for landing in landing_pages.all() %}
|
||||
{% for landing in landing_pages.featured() %}
|
||||
<a href="/{{ landing.slug }}">{{ landing.h1 }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -130,12 +130,12 @@
|
||||
<div class="wrap">
|
||||
<div class="specs-head reveal">
|
||||
<span class="eyebrow">راهکارهای دیگر</span>
|
||||
<h2>کلینیک پرو برای صنف شما</h2>
|
||||
<h2>کلینیک پرو برای کلینیک شما</h2>
|
||||
<p>هر مجموعه نیاز خودش را دارد؛ نسخهٔ متناسب با کار خود را ببینید.</p>
|
||||
</div>
|
||||
<div class="specs-grid">
|
||||
{% for item in related %}
|
||||
<a class="spec-card reveal" href="/{{ item.slug }}" style="text-decoration:none;color:inherit;display:block">
|
||||
<a class="spec-card reveal" href="/{{ item.slug }}" style="text-decoration:none;color:inherit;display:block;text-align:center">
|
||||
<h3>{{ item.h1 }}</h3>
|
||||
<p style="font-size:13.5px;color:var(--muted);line-height:1.9;margin-top:12px">{{ item.metaDescription }}</p>
|
||||
</a>
|
||||
|
||||
@@ -94,17 +94,43 @@ class LandingPageTest extends WebTestCase
|
||||
}
|
||||
|
||||
/** بدون لینک داخلی، صفحات فقط از sitemap دیده میشوند. */
|
||||
public function testFooterLinksToEveryLandingOnEveryPage(): void
|
||||
public function testFooterLinksToTheFeaturedLandingsOnEveryPage(): void
|
||||
{
|
||||
foreach (['/', '/' . LandingRegistry::CRM] as $path) {
|
||||
$html = $this->html($path);
|
||||
|
||||
foreach (array_keys($this->registry->all()) as $slug) {
|
||||
self::assertStringContainsString('href="/' . $slug . '"', $html, "لینک «{$slug}» در {$path} نیست");
|
||||
foreach ($this->registry->featured() as $page) {
|
||||
self::assertStringContainsString('href="/' . $page->slug . '"', $html, "لینک «{$page->slug}» در {$path} نیست");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* فوتر دیگر همهٔ صفحهها را فهرست نمیکند، پس باید ثابت بماند که از همان چند
|
||||
* لینکِ فوتر، با دنبالکردن «راهکارهای دیگر»، به هر صفحه میرسیم. صفحهای که فقط
|
||||
* در sitemap باشد عملاً بدون لینک داخلی است و رتبه نمیگیرد.
|
||||
*/
|
||||
public function testEveryLandingIsReachableFromTheFooterLinks(): void
|
||||
{
|
||||
$pages = $this->registry->all();
|
||||
$queue = array_map(static fn ($page): string => $page->slug, $this->registry->featured());
|
||||
$reached = array_flip($queue);
|
||||
|
||||
while ($queue !== []) {
|
||||
$slug = array_shift($queue);
|
||||
foreach ($pages[$slug]->related as $next) {
|
||||
if (!isset($reached[$next])) {
|
||||
$reached[$next] = true;
|
||||
$queue[] = $next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($pages) as $slug) {
|
||||
self::assertArrayHasKey($slug, $reached, "«{$slug}» از هیچ صفحهای لینک نمیگیرد");
|
||||
}
|
||||
}
|
||||
|
||||
public function testStructuredDataIsValidJson(): void
|
||||
{
|
||||
foreach (array_keys($this->registry->all()) as $slug) {
|
||||
|
||||
@@ -27,12 +27,17 @@ class LandingRegistryTest extends TestCase
|
||||
// تعداد صفحهها با هر خوشهٔ کلیدواژهٔ تازه بالا میرود، پس عدد ثابت اینجا فقط
|
||||
// یک تستِ شکننده میسازد. آنچه باید ثابت بماند این است که هر ثابتِ کلاس
|
||||
// واقعاً صفحهای دارد — ثابتی که ساخته نشده باشد، لینک داخلیِ ۴۰۴ میسازد.
|
||||
$constants = (new \ReflectionClass(LandingRegistry::class))->getConstants();
|
||||
// فقط ثابتهای عمومی: اسلاگها همانهایند و ثابتهای داخلی (مثل فهرست فوتر)
|
||||
// صفحه نیستند.
|
||||
foreach ((new \ReflectionClass(LandingRegistry::class))->getReflectionConstants() as $constant) {
|
||||
if (!$constant->isPublic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self::assertSame(count($constants), count($slugs));
|
||||
foreach ($constants as $name => $slug) {
|
||||
self::assertContains($slug, $slugs, "ثابت {$name} صفحهای ندارد");
|
||||
self::assertContains($constant->getValue(), $slugs, "ثابت {$constant->getName()} صفحهای ندارد");
|
||||
}
|
||||
|
||||
self::assertCount(count($slugs), array_unique($slugs));
|
||||
}
|
||||
|
||||
public function testUnknownSlugReturnsNull(): void
|
||||
|
||||
Reference in New Issue
Block a user