راهنمای سئو تکنیکال سایت‌های لاراولی: از URL تا Schema.org

event 06 مرداد 1405
update به‌روزرسانی: 02 آبان 1405
timer 14 دقیقه
راهنمای سئو تکنیکال سایت‌های لاراولی: از URL تا Schema.org
favorite 0

اگه می‌خوای جواب یک‌خطی بخوای: سئو تکنیکال لاراول یعنی سه لایه با هم کار کنن — URL و روتر تمیز و دوزبانه، رندر سریع با Core Web Vitals سبز، و داده‌های ساخت‌یافته Schema.org که گوگل بفهمه محتوای صفحه‌ات چیه. اگه فقط یکی از این سه رو درست کنی، نتیجه نصفه می‌مونه. توی این راهنما، هر سه لایه رو با کد واقعی لاراول ۱۲ نشون می‌دم — نه توصیه‌های کلی، نه کپی از مستندات انگلیسی.

تازه داری برنامه‌نویسی رو شروع می‌کنی و هنوز زبانت رو انتخاب نکردی؟ مقایسه‌ی بهترین زبان‌های برنامه‌نویسی ۲۰۲۶ رو بخون.

چرا سئو تکنیکال لاراول فرق داره؟

لاراول به‌صورت پیش‌فرض یک فریمورک full-stack با روتر هست. این یعنی برخلاف Next.js یا Nuxt که SSR (رندر سمت سرور) رو داخلی دارن، لاراول تصمیم‌گیری درباره رندر رو به عهده خودت می‌ذاره. نتیجه؟ خیلی از سایت‌های لاراولی یا بدون فکر SSR می‌رن سراغ SPA با Inertia، یا برعکس همه‌چیز رو Blade سمت سرور می‌نویسن و با کش اشتباه می‌کنن.

سه تا چالش خاص لاراول که توی فریمورک‌های دیگه کمتر می‌بینی:

  1. دو زبانه بودن با پارامتر locale — خیلی از پروژه‌های ایرانی /{locale}/... می‌نویسن، ولی canonical و hreflang رو درست ست نمی‌کنن. گوگل این رو به‌عنوان محتوای تکراری می‌بینه.
  2. کش — Cache::remember() یا کش کل صفحه، اگه TTL (مدت انقضا) و پاک‌شدنش درست تنظیم نشه، صفحه‌ی کهنه (stale) تحویل خزنده می‌ده.
  3. Livewire و Alpine.js — ترکیب رندر سمت سرور و رفتار سمت client. اگه محتوا یا متادیتای مهم فقط بعد از اجرای JS ظاهر بشه، داری روی این حساب می‌کنی که گوگل درست رندرش کنه.

این مقاله یه نقشه‌ی راه عملیه برای رفع رایج‌ترین اشتباه‌ها توی هر لایه.

پیش‌نیاز: قبل از شروع چه چیزهایی باید بدانی؟

این راهنما برای کسیه که:

  • لاراول ۱۰ به بالا کار کرده و با مفاهیم Router، Middleware، Service Provider و Eloquent آشناست.
  • با مفاهیم پایه SEO (canonical، robots، sitemap) آشنایی اولیه داره.
  • ترجیحاً یک سایت لاراولی در حال اجرا داره که می‌خواد بهینه‌اش کنه.

اگه تازه لاراول رو شروع کردی، اول مستندات رسمی رو بخون. اگه هم سئو مقدماتیت ضعیفه، قبلش راهنمای Google Search Central درباره SEO basics رو مرور کن.

معماری سئو تکنیکال در لاراول — سه لایه اصلی

سئو تکنیکال رو می‌شه به سه لایه تقسیم کرد. هر لایه مستقل نیست — اگه یکی خراب باشه، بقیه هم اثرشون رو از دست می‌دن.

لایه هدف ابزار لاراول اگه اشتباه کنی چی می‌شه؟
Crawl & Index خزنده گوگل بتونه صفحاتت رو پیدا و ذخیره کنه robots.txt, Sitemap, meta robots, canonical صفحاتت indexed نمی‌شن یا duplicate تشخیص داده می‌شن
Render & Performance صفحه سریع و قابل رندر باشه SSR vs CSR، کش، CDN، Core Web Vitals LCP کند و جابه‌جایی صفحه (CLS) تجربه‌ی کاربر و سیگنال‌های page experience رو خراب می‌کنه
Structured Data گوگل بفهمه محتوای صفحه چیه Schema.org via JSON-LD rich snippet (نمایش غنی در نتایج) از دست می‌ره

در ادامه هر لایه رو با کد واقعی باز می‌کنیم.

لایه ۱: URL و روتر — پایه سئو لاراول

URL اولین چیزیه که هم کاربر و هم گوگل می‌بینه. URL بد، بقیه کارها رو بی‌اثر می‌کنه.

URL باید slug داشته باشه، نه ID

// ❌ بد — ID در URL
Route::get('/blog/{id}', [BlogController::class, 'show']);

// ✅ خوب — slug در URL
Route::get('/blog/{post:slug}', [BlogController::class, 'show']);

دو دلیل اصلی:

  • UX: کاربر captaincode.com/blog/laravel-technical-seo-guide رو راحت‌تر به خاطر می‌سپاره تا .../blog/847.
  • وضوح: راهنمای SEO Starter گوگل هم می‌گه از کلمه‌های توصیفی توی URL استفاده کن، چون هم به آدم‌ها و هم به گوگل کمک می‌کنه بفهمن صفحه درباره‌ی چیه.

لاراول و مسیریابی خودکار (Route Model Binding)

از لاراول ۷ به بعد، وقتی از {post:slug} استفاده می‌کنی، فریمورک خودش پست رو با slug پیدا می‌کنه و اگه نبود، خودکار 404 می‌ده. هم کد کوتاه‌تر می‌شه، هم مطمئنی صفحه‌ی ناموجود واقعاً status 404 برمی‌گردونه، نه یه صفحه‌ی «پیدا نشد» با status 200.

زبان در URL: prefix یا middleware؟

برای سایت‌های دوزبانه مثل پروژه‌های ایرانی، سه الگو مرسومه:

// الگوی ۱: prefix در URL (پیشنهادی برای سئو)
// /fa/blog/...   /en/blog/...
Route::prefix('{locale}')->middleware('locale')->group(function () {
    Route::get('/blog', [BlogController::class, 'index']);
    Route::get('/blog/{post:slug}', [BlogController::class, 'show']);
});

// الگوی ۲: subdomain (fa.example.com)
// نیاز به wildcard DNS — برای پروژه‌های بزرگ‌تر

// الگوی ۳: بدون prefix، locale از session/cookie
// ❌ برای سئو بد — همه‌ی زبان‌ها یه URL دارن

چرا الگوی ۱ بهتره؟ هر زبان URL خودش رو داره، پس canonical و جای خودش رو توی نتایج جستجو داره. توی الگوی ۳ فقط یه URL هست، پس گوگل فقط یه نسخه‌ی زبانی ازش رو می‌تونه index کنه. راهنمای گوگل برای نسخه‌های محلی‌شده هم دقیقاً همین رو فرض می‌کنه: برای هر زبان یه URL جدا.

یه حالت رایج از الگوی ۱ اینه که زبان پیش‌فرض بدون prefix بمونه و بقیه prefix بگیرن. همین سایت هم همین‌طوره: فارسی روی /blog/... و انگلیسی روی /en/blog/...:

// زبان پیش‌فرض (fa): بدون prefix
Route::get('/blog/{post:slug}', [BlogController::class, 'show'])->name('blog.show');

// انگلیسی: با prefix /en
Route::prefix('en')->name('en.')->group(function () {
    Route::get('/blog/{post:slug}', [BlogController::class, 'show'])->name('blog.show');
});

Middleware locale: validation و redirect

// app/Http/Middleware/SetLocale.php
public function handle(Request $request, Closure $next)
{
    $locale = $request->route('locale');

    if (!in_array($locale, ['fa', 'en'])) {
        abort(404);
    }

    App::setLocale($locale);
    Carbon::setLocale($locale); // تاریخ‌ها هم فارسی/انگلیسی

    return $next($request);
}

نکته: abort(404) بهتر از redirect() هست چون URL نامعتبر رو به خزنده گوگل به‌عنوان صفحه واقعی نشون نمی‌ده.

لایه ۲: Meta Tags و OpenGraph — هویت هر صفحه

هر صفحه لاراول باید ۴ گروه meta tag داشته باشه: basic SEO، OpenGraph (فیسبوک/تلگرام/لینکدین)، Twitter Card، و canonical/hreflang.

الگوی Blade برای meta tags

بهتره یک partial اختصاصی بسازی تا هر صفحه فقط title و description خودش رو پاس بده:

{{-- resources/views/partials/meta.blade.php --}}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title>{{ $seoTitle ?? $title }} | {{ config('app.name') }}</title>
<meta name="description" content="{{ $seoDescription ?? Str::limit(strip_tags($excerpt ?? ''), 160) }}">
<meta name="robots" content="{{ $robots ?? 'index, follow' }}">
<link rel="canonical" href="{{ $canonical ?? url()->current() }}">

@if(!empty($alternates))
    @foreach($alternates as $hreflang => $href)
        <link rel="alternate" hreflang="{{ $hreflang }}" href="{{ $href }}">
    @endforeach
@endif

{{-- OpenGraph --}}
<meta property="og:type" content="{{ $ogType ?? 'website' }}">
<meta property="og:title" content="{{ $seoTitle ?? $title }}">
<meta property="og:description" content="{{ $seoDescription ?? '' }}">
<meta property="og:url" content="{{ $canonical ?? url()->current() }}">
<meta property="og:image" content="{{ $ogImage ?? asset('images/og-default.jpg') }}">
<meta property="og:site_name" content="{{ config('app.name') }}">
<meta property="og:locale" content="{{ app()->getLocale() === 'fa' ? 'fa_IR' : 'en_US' }}">

{{-- Twitter --}}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $seoTitle ?? $title }}">
<meta name="twitter:description" content="{{ $seoDescription ?? '' }}">
<meta name="twitter:image" content="{{ $ogImage ?? asset('images/og-default.jpg') }}">

نکته: og:locale فرمت language_TERRITORY با آندرلاین داره (fa_IR، en_US)، نه کد زبان با خط تیره.

Service اختصاصی برای SEO — نه helper

// app/Services/Seo/SeoService.php
class SeoService
{
    public function __construct(
        private string $title,
        private string $description,
        private ?string $image = null,
        private string $type = 'website',
        private array $alternates = [],
    ) {}

    public function toArray(): array
    {
        return [
            'seoTitle' => $this->title,
            'seoDescription' => Str::limit($this->description, 160),
            'ogImage' => $this->image ?: asset('images/og-default.jpg'),
            'ogType' => $this->type,
            'alternates' => $this->alternates,
        ];
    }
}

چرا Service و نه helper ساده؟ چون با رشد پروژه، منطق canonical/hreflang پیچیده می‌شه و نگهداریش در Blade یا Controller کثیف‌کاری می‌شه.

لایه ۳: Sitemap و Robots.txt

Sitemap داینامیک با Laravel

پکیج استاندارد اینجا spatie/laravel-sitemap ـه. یه کامند زمان‌بندی‌شده که از پست‌های منتشرشده sitemap می‌سازه این شکلیه:

// app/Console/Commands/GenerateSitemap.php
use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\Tags\Url;

public function handle(): int
{
    $sitemap = Sitemap::create();

    // صفحات ایستا
    $sitemap->add(Url::create(route('home'))
        ->setLastModificationDate(now()));

    // بلاگ‌ها (برای هر زبان یه URL)
    Post::published()->each(function (Post $post) use ($sitemap) {
        $sitemap->add(Url::create(route('blog.show', $post->slug))
            ->setLastModificationDate($post->updated_at));
        $sitemap->add(Url::create(route('en.blog.show', $post->slug))
            ->setLastModificationDate($post->updated_at));
    });

    $sitemap->writeToFile(public_path('sitemap.xml'));
    return self::SUCCESS;
}

توی لاراول ۱۱ به بعد، توی routes/console.php زمان‌بندیش کن:

use Illuminate\Support\Facades\Schedule;

Schedule::command('sitemap:generate')->daily();

نکته: طبق مستندات sitemap گوگل، گوگل <priority> و <changefreq> رو نادیده می‌گیره، ولی اگه <lastmod> همیشه دقیق باشه ازش استفاده می‌کنه. پس وقتت رو بذار روی lastmod درست، نه priority. URL هر زبان رو هم فقط وقتی به sitemap اضافه کن که اون نسخه واقعاً منتشر شده باشه؛ URLی که 404 می‌ده فقط crawl رو هدر می‌ده.

robots.txt هوشمند

# public/robots.txt
User-agent: *
Disallow: /admin/
Disallow: /api/
Allow: /

Sitemap: https://captaincode.tech/sitemap.xml

دو تا نکته:

  • مسیرهایی که CSS و JS سایت رو سرو می‌کنن رو بلاک نکن (از جمله route اسکریپت Livewire زیر /livewire/). گوگل برای اینکه صفحه رو همون‌طور که کاربر می‌بینه رندر کنه، بهشون نیاز داره.
  • robots.txt ابزار امنیتی نیست. عمومیه، و URLی که بلاکش کردی اگه از جای دیگه لینک بگیره هنوز ممکنه index بشه. پنل ادمین رو پشت لاگین نگه دار و به هر صفحه‌ای که نباید توی نتایج بیاد noindex بده (مقدمه‌ی robots.txt گوگل این فرق رو توضیح داده).

لایه ۴: Core Web Vitals — سرعت، سرعت، سرعت

Core Web Vitals سه متریک اصلی گوگل برای تجربه‌ی کاربریه. آستانه‌های «خوب» پایین از web.dev ـه:

متریک خوب چطور توی لاراول بهینه کنیم
LCP (Largest Contentful Paint) ≤ 2.5s تصاویر WebP، اولویت بالا برای تصویر اصلی، کش، CDN
INP (Interaction to Next Paint) ≤ 200ms JS کمتر، defer کردن اسکریپت‌های غیرضروری
CLS (Cumulative Layout Shift) ≤ 0.1 width/height روی تصاویر، font-display: swap

تصاویر WebP با Intervention

// composer require intervention/image   (نسخه‌ی ۴ به PHP 8.3+ نیاز داره)
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Format;

$manager = ImageManager::usingDriver(GdDriver::class);
$manager->decodePath($uploadedFile->getRealPath())
    ->encodeUsingFormat(Format::WE`P, quality: 80)
    ->save($path . '.webp');

نکته: آموزش‌های قدیمی از Image::make() استفاده می‌کنن که API نسخه‌ی ۲ ـه و توی نسخه‌های فعلی وجود نداره. مستندات Intervention Image رو برای نسخه‌ای که نصب می‌کنی چک کن.

لود درست تصاویر در Blade

تصویر اصلی (LCP) باید هرچه زودتر لود بشه؛ تصویرهای پایین‌تر صفحه می‌تونن صبر کنن:

{{-- تصویر اصلی / LCP: بدون lazy، با اولویت بالا --}}
<img src="{{ $post->hero_image }}"
     alt="{{ $post->title }}"
     width="1200" height="630"
     fetchpriority="high"
     decoding="async">

{{-- تصویرهای پایین صفحه --}}
<img src="{{ $image->url }}"
     alt="{{ $image->alt }}"
     width="800" height="450"
     loading="lazy"
     decoding="async">

نکته: width و height رو همیشه بذار. بدونشون مرورگر نمی‌تونه جا رزرو کنه و وقتی تصویر می‌رسه صفحه جابه‌جا می‌شه (CLS). روی تصویر LCP هم هیچ‌وقت loading="lazy" نذار، چون دقیقاً همون چیزی رو عقب می‌ندازه که LCP اندازه می‌گیره.

Vite برای bundle کردن assets

لاراول خودش با Vite میاد. توی production، دستور @vite فایل‌های بیلدشده رو با hash توی اسمشون لود می‌کنه (مثلاً app-[hash].js)، پس می‌تونی خیلی طولانی کششون کنی: محتوا که عوض بشه، اسم فایل هم عوض می‌شه.

# کش طولانی برای فایل‌های hashدار Vite
location /build/assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

اگه Livewire 3 یا بالاتر داری، Alpine.js خودش توی JS لایوایر هست، پس یه نسخه‌ی دیگه از Alpine رو جدا لود نکن.

لایه ۵: Schema.org / JSON-LD — زبان گوگل

Schema.org واژگانیه که گوگل باهاش صفحه‌ات رو می‌فهمه. بدون Schema، گوگل یه تیتر و یه مقدار متن می‌بینه. با Schema، می‌فهمه "این یه مقاله‌ی فنیه با نویسنده X، تاریخ انتشار Y و آخرین آپدیت Z".

Article Schema در لاراول

// app/Services/Seo/SchemaService.php
public static function blogPost(Post $post): string
{
    return json_encode([
        '@context' => 'https://schema.org',
        '@type' => 'TechArticle',
        'headline' => $post->seo_title ?: $post->title,
        'description' => $post->seo_description,
        'image' => [
            '@type' => 'ImageObject',
            'url' => $post->og_image_url,
            'width' => 1200,
            'height' => 630,
        ],
        'datePublished' => $post->published_at?->toIso8601String(),
        'dateModified' => $post->updated_at?->toIso8601String(),
        'author' => [
            '@type' => 'Person',
            'name' => $post->author?->name ?? 'Captain Code',
            'url' => route('about'),
        ],
        'publisher' => [
            '@type' => 'Organization',
            'name' => config('app.name'),
            'logo' => [
                '@type' => 'ImageObject',
                'url' => asset('images/logo.png'),
            ],
        ],
        'mainEntityOfPage' => [
            '@type' => 'WebPage',
            '@id' => route('blog.show', $post->slug),
        ],
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

تزریق در Blade

@if($schema ?? null)
    <script type="application/ld+json">
        {!! $schema !!}
    </script>
@endif

برای هر صفحه یه type اصلی انتخاب کن (مثلاً TechArticle یا BlogPosting، نه هر دو برای یه مقاله) تا سیگنال‌ها ضد هم نباشن.

BreadcrumbList — مسیر ناوبری توی نتایج گوگل

برای اینکه گوگل بتونه زیر نتیجه‌ات به‌جای URL خام، مسیر breadcrumb نشون بده:

public static function breadcrumbs(array $items): string
{
    $list = [];
    foreach ($items as $i => $item) {
        $list[] = [
            '@type' => 'ListItem',
            'position' => $i + 1,
            'name' => $item['name'],
            'item' => $item['url'],
        ];
    }
    return json_encode([
        '@context' => 'https://schema.org',
        '@type' => 'BreadcrumbList',
        'itemListElement' => $list,
    ], JSON_UNESCAPED_UNICODE);
}

FAQ Schema — برای بخش سوالات متداول

اگه مقاله‌ات بخش سوالات متداول داره، می‌تونی با FAQPage توصیفش کنی:

public static function faq(array $faqs): string
{
    return json_encode([
        '@context' => 'https://schema.org',
        '@type' => 'FAQPage',
        'mainEntity' => array_map(fn($q, $a) => [
            '@type' => 'Question',
            'name' => $q,
            'acceptedAnswer' => [
                '@type' => 'Answer',
                'text' => $a,
            ],
        ], array_keys($faqs), $faqs),
    ], JSON_UNESCAPED_UNICODE);
}

ولی انتظارت رو تنظیم کن: از آگوست ۲۰۲۳ گوگل rich result سوالات متداول رو فقط برای سایت‌های معتبر و شناخته‌شده‌ی دولتی و سلامت نشون می‌ده. برای بقیه‌ی سایت‌ها این markup ضرری نداره، ولی معمولاً rich result قابل‌دیدنی نمی‌سازه.

لایه ۶: hreflang برای سایت‌های دوزبانه

سایت‌های فارسی/انگلیسی یا فارسی/عربی باید به گوگل بگن "این دو URL محتوای یکسان دارن، فقط زبانشون فرق داره":

<link rel="alternate" hreflang="fa" href="https://captaincode.tech/blog/laravel-technical-seo-guide">
<link rel="alternate" hreflang="en" href="https://captaincode.tech/en/blog/laravel-technical-seo-guide">
<link rel="alternate" hreflang="x-default" href="https://captaincode.tech/blog/laravel-technical-seo-guide">

تولید داینامیک در لاراول (اسم routeها از مثال لایه ۱):

$alternates = [
    'fa' => route('blog.show', $post->slug),
    'en' => route('en.blog.show', $post->slug_en ?: $post->slug),
    'x-default' => route('blog.show', $post->slug),
];

نکته‌ها:

  • x-default رو بذار. به گوگل می‌گه وقتی هیچ زبانی با کاربر جور نیست، کدوم نسخه رو نشون بده (معمولاً زبان اصلی سایت).
  • هر نسخه‌ی زبانی باید همه‌ی نسخه‌ها، از جمله خودش، رو لیست کنه و تگ‌ها دوطرفه باشن؛ وگرنه ممکنه گوگل نادیده‌شون بگیره (راهنمای localized versions گوگل).
  • تگ en فقط وقتی بذار که نسخه‌ی انگلیسی واقعاً منتشر شده. hreflangی که به 404 اشاره کنه به درد گوگل نمی‌خوره و نشونه‌ی خراب بودن تنظیماته.

Cache و SEO — یک تیغ دولبه

Laravel کش فوق‌العاده‌ست ولی اگه درست تنظیم نشه، محتوای stale به گوگل می‌ده. سه قانون:

  1. هرگز بدون TTL کش نکن: Cache::remember('key', 3600, ...) — عدد ۳۶۰۰ یعنی یک ساعت.
  2. وقتی محتوا عوض شد کش رو پاک کن، و ادمین و preview رو هیچ‌وقت کش نکن: موقع انتشار پست، کلیدهای مربوطش رو پاک کن. Cache::tags(['public'])->flush() فقط روی درایورهایی کار می‌کنه که tag پشتیبانی می‌کنن (Redis، Memcached)؛ با درایور file یا database، کلیدهای مشخص رو با forget() پاک کن. کش کل صفحه هم ممکنه با سرو کردن توکن CSRF قدیمی فرم‌ها رو خراب کنه؛ راهنمای خطای 419 Page Expired لاراول این حالت رو توضیح داده.
  3. به‌جای minify کردن HTML با regex، فشرده‌سازی رو بده دست وب‌سرور. middlewareی که همه‌ی فاصله‌ها رو جمع می‌کنه بلوک‌های <pre> کد و اسکریپت‌های inline رو هم خراب می‌کنه، ولی gzip یا Brotli بیشترِ همون کاهش حجم رو امن بهت می‌ده:
# nginx: وقتی gzip روشنه، text/html همیشه فشرده می‌شه
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;

Livewire و SSR: چالش خاص لاراول مدرن

اول خبر خوب: کامپوننت‌های Livewire توی درخواست اول سمت سرور رندر می‌شن، پس HTMLشون مثل Blade معمولی توی سورس صفحه هست. گوگل بدون اجرای هیچ JSی اون محتوا رو می‌بینه.

ریسک از محتواییه که توی همون HTML اول نیست:

  • کامپوننت‌های lazy یا defer: طبق مستندات Livewire، وقتی وارد viewport می‌شن یا بلافاصله بعد از لود صفحه لود می‌شن. تا اون موقع فقط یه placeholder توی HTML هست. برای یه ویجت فرعی اوکیه، برای متن اصلی مقاله‌ات نه.
  • محتوایی که بعد از یه تعامل یا با wire:init لود می‌شه: اگه فقط بعد از کلیک یا یه درخواست بعدی ظاهر بشه، روی index شدنش حساب نکن.

راه‌حل: محتوای اصلی (تیتر، متن، headingها، لینک‌ها) رو توی HTML اولیه رندر کن و lazy رو فقط برای بخش‌های فرعی مثل نظرات یا ویجت‌های مرتبط بذار. <livewire:component /> و @livewire('component') دقیقاً یه‌جور رندر می‌شن، پس عوض کردنشون هیچ فرقی برای سئو نداره.

اگه از Inertia.js استفاده می‌کنی: بدون SSR، جواب HTML اول تقریباً فقط یه المنت root خالیه و محتوا بعد از اجرای JS میاد. برای صفحه‌هایی که باید رتبه بگیرن، SSR خود Inertia رو روشن کن.

تست سئو قبل از انتشار

قبل از اینکه مقاله رو منتشر کنی، چک کن:

  1. Google Rich Results Test: https://search.google.com/test/rich-results — Schema رو validate کن.
  2. Lighthouse (Chrome DevTools → Lighthouse): Performance، SEO، Accessibility و نمای موبایل رو بسنج. به‌جای دنبال کردن یه عدد خاص، همه‌ی auditهای SEO که fail شدن رو درست کن.
  3. PageSpeed Insights: https://pagespeed.web.dev — اگه سایتت ترافیک کافی داشته باشه، داده‌ی واقعی Core Web Vitals رو نشون می‌ده.
  4. URL Inspection توی Search Console: HTMLی که گوگل واقعاً رندر کرده و قابل index بودن صفحه رو ببین. (ابزار جداگانه‌ی Mobile-Friendly Test رو گوگل دسامبر ۲۰۲۳ بازنشسته کرد و الان Lighthouse همین کار رو می‌کنه.)

اشتباهات رایج در سئو لاراول

اشتباه چرا بده راه‌حل
meta robots: noindex فراموش شده روی صفحات draft گوگل صفحه ناقص رو index می‌کنه middleware یا منطق layout که به صفحه‌های منتشرنشده noindex بده
canonical اشتباه در صفحات داینامیک گوگل URL اشتباه رو اصلی می‌شناسه canonical رو از اسم routeها بساز نه از url()->current() (که هر host، scheme یا مسیری که درخواست شده رو عیناً برمی‌گردونه)
Sitemap شامل URL های 404 crawl هدر می‌ره و اعتماد گوگل به sitemapت کم می‌شه قبل از نوشتن sitemap، URLها رو HTTP check کن (یا فقط منتشرشده‌ها رو اضافه کن)
Schema با JSON اشتباه گوگل نمی‌تونه parseش کنه و نادیده‌اش می‌گیره با json_encode() روی آرایه بسازش، نه چسبوندن رشته، و validateش کن
تصاویر بدون alt هم SEO هم a11y (دسترسی‌پذیری) آسیب می‌بینه Blade component با alt required
محتوای اصلی lazy توی Livewire یا فقط سمت client توی Inertia محتوا توی HTML اولیه نیست محتوای اصلی رو توی درخواست اول رندر کن؛ برای Inertia، SSR

چک‌لیست نهایی قبل از انتشار سایت لاراول

  • همه URL ها slug دارن، ID ندارن
  • Canonical در هر صفحه set شده
  • hreflang برای هر نسخه‌ی زبانیِ منتشرشده، دوطرفه، با x-default
  • ادمین پشت لاگین + noindex؛ robots.txt جلوی CSS/JS رو نمی‌گیره
  • sitemap.xml ساخته شده، lastmod دقیق داره و توی robots.txt معرفی شده
  • تصاویر WebP؛ lazy فقط برای پایین صفحه
  • Core Web Vitals توی محدوده‌ی «خوب» (LCP ≤ 2.5s، INP ≤ 200ms، CLS ≤ 0.1)
  • Schema.org JSON-LD توی همه‌ی صفحه‌های محتوایی، یه type اصلی برای هر صفحه
  • OpenGraph و Twitter Card تست شده در https://www.opengraph.xyz
  • محتوای اصلی توی HTML اولیه‌ی سمت سرور
  • فشرده‌سازی gzip یا Brotli روشن
  • کش‌ها TTL منطقی دارن و موقع انتشار پاک می‌شن

سوالات متداول

آیا برای سئو لاراول حتماً باید از پکیج استفاده کنم؟

نه. اکثر نیازها با کد بومی لاراول حل می‌شه. پکیج‌هایی مثل artesaos/seotools یا ralphjsmit/laravel-seo برای پروژه‌های بزرگ با چند توسعه‌دهنده مفیدن، ولی برای پروژه متوسط، Service اختصاصی + Blade partial کافیه. استفاده از پکیج اضافی یعنی آپدیت، یعنی bug احتمالی، یعنی یک لایه انتزاع دیگه برای دیباگ.

بهترین پکیج sitemap برای لاراول چیه؟

spatie/laravel-sitemap عملاً استاندارده و کامند بالا هم از همین استفاده می‌کنه. برای یه سایت خیلی ساده می‌تونی XML رو خودت هم بنویسی. مهم‌تر از پکیج اینه که sitemap به‌روز باشه، lastmod دقیق داشته باشه و URL 404 توش نباشه.

چطور Livewire را SEO-friendly کنم؟

Livewire خودش توی درخواست اول سمت سرور رندر می‌کنه. فقط محتوای اصلیت رو توی کامپوننت‌های lazy/defer یا چیزی که بعد از تعامل لود می‌شه نذار، و lazy رو برای بخش‌های فرعی مثل نظرات نگه دار. نتیجه رو با View Source یا URL Inspection سرچ کنسول چک کن.

Schema.org برای سایت فروشگاهی چه نوعی بزنم؟

برای محصول Product با Offer (و AggregateRating فقط اگه نظر واقعی داری)، برای صفحه اصلی Organization با logo و sameAs (لینک شبکه‌های اجتماعی)، برای صفحات دسته‌بندی CollectionPage یا ItemList. ترکیب Product + BreadcrumbList + Organization معمولاً کافیه. از مستندات Product توی Schema.org و راهنمای structured data خود گوگل پیروی کن.

چه مدت طول می‌کشه تا نتیجه سئو تکنیکال را ببینم؟

زمان ثابتی نداره. گوگل باید صفحه‌های تغییرکرده رو دوباره crawl و پردازش کنه؛ برای صفحه‌هایی که زیاد بهشون سر می‌زنه سریع‌تره و برای بقیه‌ی سایت خیلی کندتر. با ثبت sitemap و درخواست index برای URLهای کلیدی توی Search Console می‌تونی پیدا شدنشون رو سریع‌تر کنی. سئو تکنیکال مثل پایه‌ی ساختمونه: خودش رتبه نمی‌سازه، ولی تا خراب باشه محتوا و بک‌لینک هم کارشون رو نمی‌کنن.

آیا CDN برای سایت لاراول ضروریه؟

بستگی داره بازدیدکننده‌هات کجان. اگه بیشترشون نزدیک سرورت هستن (مثلاً کاربر ایرانی و سرور ایران)، CDN شاید فایده‌ی زیادی نداشته باشه، و CDNی که نقطه‌ی نزدیک نداره حتی ممکنه کندترش کنه. اگه مخاطبت توی چند کشوره، Cloudflare یا BunnyCDN ارزشش رو داره. برای تصاویر، WebP روی سرور خودت با هدرهای کش درست معمولاً کافیه.

تفاوت canonical و hreflang چیه؟

canonical می‌گه "این URL اصلی محتواست، اگه محتوای مشابه در چند URL هست، فقط این رو index کن". hreflang می‌گه "این محتوا به چند زبان موجوده، هر زبان URL مخصوص خودش رو داره". توی سایت دوزبانه هر صفحه هر دو رو داره و با هم تضادی ندارن: هر نسخه‌ی زبانی باید canonical به خودش داشته باشه (canonical صفحه‌ی انگلیسی به URL انگلیسی)، و hreflang نسخه‌ها رو به هم وصل می‌کنه.


اگه الان داری روی یه پروژه لاراولی کار می‌کنی و می‌خوای سئوش رو بترکونی، از لایه ۱ شروع کن — URL و canonical. چون اگه این درست نباشه، هر چقدر Schema و Vite و CDN بهینه کنی، گوگل هنوز گیج می‌مونه. بعد برای هر صفحه‌ی محتوایی TechArticle (یا Article) و BreadcrumbList بذار. در نهایت Core Web Vitals رو با Lighthouse چک کن و iterate کن.

بیشتر بخوانید:

Captain Code
verified
نویسنده

Captain Code

مشاهده پروفایل arrow_back

عاشق دنیای کدنویسی و به اشتراک‌گذاری دانش. ما در کاپیتان کد تلاش می‌کنیم بهترین محتوا را برای شما تولید کنیم.

کاپیتان کد

این را برای پروژه‌ات می‌خواهی؟

پروژه را به کاپیتان کد بسپار، یا با نیکس همین مسئله را دیباگ کن.

forum

نظرات

lock_person

برای ثبت نظر وارد شوید

برای شرکت در گفتگو، لطفاً ابتدا وارد حساب کاربری‌تان شوید.

login ورود
chat_bubble_outline

هنوز نظری ثبت نشده

اولین نفری باشید که نظر خود را درباره این مطلب به اشتراک می‌گذارد!