Keyword research put the head demand on four phrases — clinic management software, doctor's office software, online booking, and the price/comparison questions that precede a purchase — and we only had a page for one of them. The new pages cover the head terms (clinic management, online booking), the buying intent (price, how to choose, download), the positioning that separates a hosted product from the installed ones that dominate these results (web based, no install), and two long-tails where competition is thin (clinic accounting, psychology practice). The download page deliberately answers the intent rather than the wording: someone searching for an installer wants to try something without paying, and the page gives them that while explaining why there is nothing to install. Copy is written per page rather than templated — eight pages saying the same thing in different words is duplicate content, and every claim on them maps to something the product actually does. Internal links from the older pages were repointed at the new head-term pages, and the registry tests no longer hardcode the page count: they now assert that every declared slug has a page, and that titles, headings and descriptions stay unique. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
135 lines
5.2 KiB
PHP
135 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Shared;
|
|
|
|
use App\Shared\Landing\LandingRegistry;
|
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
|
|
|
/**
|
|
* صفحات فرود واقعاً رندر میشوند و متادیتای هرکدام مالِ خودش است.
|
|
*
|
|
* روت `/{slug}` هر مسیر تکبخشی را میگیرد، پس اینجا هم بررسی میشود که صفحهٔ اصلی و
|
|
* مسیرهای موجود را نخورده باشد.
|
|
*/
|
|
class LandingPageTest extends WebTestCase
|
|
{
|
|
private KernelBrowser $client;
|
|
private LandingRegistry $registry;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->client = static::createClient();
|
|
$this->registry = new LandingRegistry();
|
|
}
|
|
|
|
private function html(string $path): string
|
|
{
|
|
$this->client->request('GET', $path);
|
|
|
|
return $this->client->getResponse()->getContent();
|
|
}
|
|
|
|
public function testEveryLandingRenders(): void
|
|
{
|
|
foreach ($this->registry->all() as $slug => $page) {
|
|
$html = $this->html('/' . $slug);
|
|
|
|
self::assertSame(200, $this->client->getResponse()->getStatusCode(), "صفحهٔ «{$slug}» باز نشد");
|
|
self::assertStringContainsString('<h1 class="reveal">' . $page->h1 . '</h1>', $html);
|
|
self::assertStringContainsString('<title>' . $page->metaTitle . '</title>', $html);
|
|
self::assertStringContainsString('content="' . $page->metaDescription . '"', $html);
|
|
}
|
|
}
|
|
|
|
public function testCanonicalPointsAtTheSamePage(): void
|
|
{
|
|
foreach (array_keys($this->registry->all()) as $slug) {
|
|
$html = $this->html('/' . $slug);
|
|
|
|
self::assertMatchesRegularExpression(
|
|
'~<link rel="canonical" href="[^"]+/' . preg_quote($slug, '~') . '"/>~u',
|
|
$html,
|
|
"canonical صفحهٔ «{$slug}» به خودش اشاره نمیکند",
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testUnknownSlugIsNotFound(): void
|
|
{
|
|
$this->client->request('GET', '/یک-اسلاگ-که-وجود-ندارد');
|
|
|
|
self::assertSame(404, $this->client->getResponse()->getStatusCode());
|
|
}
|
|
|
|
/** روت catch-all نباید صفحهٔ اصلی را بخورد. */
|
|
public function testHomePageStillWorks(): void
|
|
{
|
|
$html = $this->html('/');
|
|
|
|
self::assertSame(200, $this->client->getResponse()->getStatusCode());
|
|
self::assertStringContainsString('هوشمند، سریع و', $html);
|
|
}
|
|
|
|
public function testExistingRoutesAreNotShadowed(): void
|
|
{
|
|
foreach (['/sitemap.xml', '/health'] as $path) {
|
|
$this->client->request('GET', $path);
|
|
self::assertSame(200, $this->client->getResponse()->getStatusCode(), "{$path} گرفته شد");
|
|
}
|
|
}
|
|
|
|
public function testSitemapListsHomeAndEveryLanding(): void
|
|
{
|
|
$xml = $this->html('/sitemap.xml');
|
|
$doc = new \SimpleXMLElement($xml);
|
|
|
|
// خانه + هر صفحهٔ فرود. عدد از خودِ رجیستری میآید تا افزودن خوشهٔ کلیدواژهٔ
|
|
// تازه این تست را نشکند، ولی جاافتادن یک صفحه از sitemap هنوز قرمز شود.
|
|
self::assertCount(1 + count($this->registry->all()), $doc->url);
|
|
|
|
foreach (array_keys($this->registry->all()) as $slug) {
|
|
self::assertStringContainsString(rawurlencode($slug), $xml, "«{$slug}» در sitemap نیست");
|
|
}
|
|
}
|
|
|
|
/** بدون لینک داخلی، صفحات فقط از sitemap دیده میشوند. */
|
|
public function testFooterLinksToEveryLandingOnEveryPage(): 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} نیست");
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testStructuredDataIsValidJson(): void
|
|
{
|
|
foreach (array_keys($this->registry->all()) as $slug) {
|
|
$html = $this->html('/' . $slug);
|
|
|
|
preg_match('~<script type="application/ld\+json">(.*?)</script>~s', $html, $m);
|
|
self::assertNotEmpty($m, "صفحهٔ «{$slug}» بلاک JSON-LD ندارد");
|
|
|
|
$data = json_decode(html_entity_decode($m[1]), true, flags: JSON_THROW_ON_ERROR);
|
|
$types = array_column($data['@graph'], '@type');
|
|
|
|
self::assertContains('SoftwareApplication', $types);
|
|
self::assertContains('BreadcrumbList', $types);
|
|
self::assertContains('FAQPage', $types);
|
|
}
|
|
}
|
|
|
|
/** تم مشترک است، پس همان CSS صفحهٔ اصلی باید بارگذاری شود. */
|
|
public function testLandingUsesTheHomepageTheme(): void
|
|
{
|
|
preg_match_all('~href="(/build/[^"]+\.css)"~', $this->html('/'), $home);
|
|
preg_match_all('~href="(/build/[^"]+\.css)"~', $this->html('/' . LandingRegistry::BEAUTY), $landing);
|
|
|
|
self::assertNotEmpty($home[1]);
|
|
self::assertSame($home[1], $landing[1], 'لندینگ باید همان CSS صفحهٔ اصلی را بارگذاری کند');
|
|
}
|
|
}
|