راهنمای سئو تکنیکال سایتهای لاراولی: از URL تا Schema.org
اگه میخوای جواب یکخطی بخوای: سئو تکنیکال لاراول یعنی سه لایه با هم کار کنن — URL و روتر تمیز و دوزبانه، رندر سریع با Core Web Vitals سبز، و دادههای ساختیافته Schema.org که گوگل بفهمه محتوای صفحهات چیه. اگه فقط یکی از این سه رو درست کنی، نتیجه نصفه میمونه. توی این راهنما، هر سه لایه رو با کد واقعی لاراول ۱۲ نشون میدم — نه توصیههای کلی، نه کپی از مستندات انگلیسی.
چرا سئو تکنیکال لاراول فرق داره؟
لاراول بهصورت پیشفرض یک فریمورک full-stack با روتر هست. این یعنی برخلاف Next.js یا Nuxt که SSR (رندر سمت سرور) رو داخلی دارن، لاراول تصمیمگیری درباره رندر رو به عهده خودت میذاره. نتیجه؟ خیلی از سایتهای لاراولی یا بدون فکر SSR میرن سراغ SPA با Inertia، یا برعکس همهچیز رو Blade سمت سرور مینویسن و با کش اشتباه میکنن.
سه تا چالش خاص لاراول که توی فریمورکهای دیگه کمتر میبینی:
- دو زبانه بودن با پارامتر locale — خیلی از پروژههای ایرانی
/{locale}/...مینویسن، ولی canonical و hreflang رو درست ست نمیکنن. گوگل این رو بهعنوان محتوای تکراری میبینه. - کش خودکار Laravel —
Cache::remember()وphp artisan view:cacheاگه بدون TTL (مدت زمان انقضا) درست تنظیم بشن، صفحات stale (منقضی) به خزنده تحویل میدن. - Livewire و Alpine.js — هیبرید SSR + client. اگه متادیتا رو فقط سمت client ست کنی، خزنده گوگل چیزی نمیبینه.
توی ۴ سال کار روی پروژههای لاراولی فارسی و دوزبانه، متداولترین اشتباهات رو دستهبندی کردم. این مقاله نقشه راه کامل رفعشونه.
پیشنیاز: قبل از شروع چه چیزهایی باید بدانی؟
این راهنما برای کسیه که:
- لاراول ۱۰ به بالا کار کرده و با مفاهیم 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 بالا میاد پایین |
| 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. - Keyword signal: گوگل هنوز هم به کلمات داخل URL وزن میده. Slug حاوی کلمه کلیدی، یک سیگنال مثبته.
لاراول و مسیریابی خودکار (Route Model Binding)
توی لاراول ۸ به بالا، وقتی از {post:slug} استفاده میکنی، فریمورک خودش پست رو با slug پیدا میکنه. اگه نبود، اتوماتیک 404 میده. این هم کد رو کوتاهتر میکنه، هم 404 درست رو تضمین میکنه (که خودش یکی از فاکتورهای سئوئه).
زبان در 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/session
// ❌ برای سئو بد — گوگل نمیفهمه کدوم نسخه اصلیه
چرا الگوی ۱ بهترینه؟ گوگل برای هر URL یک canonical مستقل اختصاص میده. اگه locale در URL باشه، canonical هر زبان جداگانه تعریف میشه. در الگوی ۳، گوگل فقط یکی از نسخهها رو index میکنه.
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 --}}
{{ $seoTitle ?? $title }} | {{ config('app.name') }}
@if(!empty($alternates))
@foreach($alternates as $hreflang => $href)
@endforeach
@endif
{{-- OpenGraph --}}
{{-- Twitter --}}
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
Laravel پکیج spatie/laravel-sitemap داره که کارت رو ساده میکنه. ولی قبل از نصب، بدون که چطور خودت بسازی تا وابستگی اضافه نکنی:
// app/Console/Commands/GenerateSitemap.php
public function handle(): int
{
$sitemap = Sitemap::create();
// صفحات ایستا
$sitemap->add(Url::create(route('home'))
->setLastModificationDate(now())
->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY)
->setPriority(1.0));
// بلاگها
Post::published()->each(function (Post $post) use ($sitemap) {
$sitemap->add(
Url::create(route('blog.show', $post->slug))
->setLastModificationDate($post->updated_at)
->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
->setPriority(0.8)
);
});
$sitemap->writeToFile(public_path('sitemap.xml'));
return self::SUCCESS;
}
این کامند رو توی AppServiceProvider یا scheduler صدا بزن:
// app/Console/Kernel.php
$schedule->command('sitemap:generate')->daily();
robots.txt هوشمند
# public/robots.txt
User-agent: *
Disallow: /admin/
Disallow: /api/
Disallow: /livewire/
Allow: /
Sitemap: https://captaincode.tech/sitemap.xml
نکته: Disallow: /admin/ رو حتماً داشته باش. گوگل اگه صفحات ادمین رو crawl کنه و 404 یا خطا ببینه، روی trust سایت اثر منفی میذاره.
لایه ۴: Core Web Vitals — سرعت، سرعت، سرعت
Core Web Vitals سه متریک اصلی گوگل برای تجربه کاربریه:
| متریک | هدف | چطور توی لاراول بهینه کنیم |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | تصاویر WebP، lazy load، 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
use Intervention\Image\Facades\Image;
$image = Image::make($uploadedFile);
$image->encode('webp', 80)->save($path . '.webp');
Lazy loading تصاویر در Blade
نکته: width و height رو همیشه بذار. نبودنشون = CLS بالا.
Vite برای bundle کردن assets
اگه Vite پیکربندی نشده، الان تنظیمش کن:
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['alpinejs', 'livewire'],
},
},
},
},
});
این کار vendor JS رو در یک فایل جدا میریزه تا کش بهتر بخوره.
لایه ۵: Schema.org / JSON-LD — زبان گوگل
Schema.org زبانیه که گوگل از صفحهات میفهمه. مثلاً بدون Schema، گوگل فقط یه تیتر میبینه. با Schema، میفهمه "این یه مقاله با نویسنده X، تاریخ انتشار Y، و امتیاز 4.5 ستارهست".
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)
@endif
BreadcrumbList — ستارهایهای SERP
برای نمایش 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 — برای بخش سوالات متداول
اگه توی مقالهات FAQ داری (که طبق الگوی محتوایی سایت ما داری)، حتماً FAQ Schema بذار:
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);
}
لایه ۶: hreflang برای سایتهای دوزبانه
سایتهای فارسی/انگلیسی یا فارسی/عربی باید به گوگل بگن "این دو URL محتوای یکسان دارن، فقط زبانشون فرق داره":
تولید داینامیک در لاراول:
$alternates = [
'fa' => route('blog.show', ['locale' => 'fa', 'post' => $post->slug]),
'en' => route('blog.show', ['locale' => 'en', 'post' => $post->slug_en ?: $post->slug]),
'x-default' => route('blog.show', ['locale' => 'fa', 'post' => $post->slug]),
];
نکته: حتماً x-default بذار. این به گوگل میگه وقتی زبان کاربر مشخص نیست، کدوم نسخه رو نشون بده (معمولاً زبان اصلی سایت).
Cache و SEO — یک تیغ دولبه
Laravel کش فوقالعادهست ولی اگه درست تنظیم نشه، محتوای stale به گوگل میده. سه قانون:
- هرگز بدون TTL کش نکن:
Cache::remember('key', 3600, ...)— عدد ۳۶۰۰ یعنی یک ساعت. - صفحه ادمین و preview را cache نکن:
Cache::tags(['public'])->flush()وقتی پست جدید منتشر شد. - HTML minify با
gzencodeیا middleware اختصاصی:
// app/Http/Middleware/MinifyHtml.php
public function handle($request, Closure $next)
{
$response = $next($request);
if ($response instanceof Response && str_contains($response->headers->get('Content-Type'), 'text/html')) {
$html = $response->getContent();
// حذف کامنتها و فاصلههای اضافی
$html = preg_replace('/).)*-->/s', '', $html);
$html = preg_replace('/\s+/', ' ', $html);
$response->setContent(trim($html));
}
return $response;
}
Livewire و SSR: چالش خاص لاراول مدرن
Livewire عالیه ولی SEO-دشمنه اگه بیدقتی کنی. مشکل: وقتی صفحه Livewire سمت client رندر میشه، خزنده گوگل ممکنه فقط یه div خالی ببینه.
راهحل: SSR با Livewire
// config/livewire.php
'temporary_script_upload_path' => public_path('livewire-tmp'),
'render_without_redirect' => false,
و در Blade از بهجای @livewire استفاده کن — نسخه جدید سازگارتر با SSR هست.
راهحل بهتر: Inertia.js با SSR
اگه پروژه از اول SPA-style هست، Inertia.js با SSR بهترین گزینهست:
// inertiajs.com/ ssr-setup
// nuxt-like SSR برای Laravel + Vue/React
تست سئو قبل از انتشار
قبل از اینکه مقاله رو منتشر کنی، چک کن:
- Google Rich Results Test: https://search.google.com/test/rich-results — Schema رو validate کن.
- Lighthouse (Chrome DevTools → Lighthouse): Performance، SEO، Accessibility رو بسنج. امتیاز SEO باید ≥ 95 باشه.
- PageSpeed Insights: https://pagespeed.web.dev — Core Web Vitals واقعی رو نشون میده.
- Mobile-Friendly Test: https://search.google.com/test/mobile-friendly — برای mobile-first indexing ضروریه.
اشتباهات رایج در سئو لاراول — تجربه ۴ سال پروژه
| اشتباه | چرا بده | راهحل |
|---|---|---|
meta robots: noindex فراموش شده روی صفحات draft |
گوگل صفحه ناقص رو index میکنه | middleware برای صفحات غیر published |
| canonical اشتباه در صفحات داینامیک | گوگل URL اشتباه رو اصلی میشناسه | canonical رو از route generator بساز نه از url()->current() |
| Sitemap شامل URL های 404 | اعتماد گوگل کم میشه | قبل از Sitemap، URLها رو HTTP check کن |
| Schema با JSON اشتباه | Rich snippet غیرفعال میشه | JSON_UNESCAPED_UNICODE حتماً |
| تصاویر بدون alt | هم SEO هم a11y (دسترسیپذیری) آسیب میبینه | Blade component با alt required |
| Livewire بدون SSR | محتوای client-only | Livewire SSR یا Inertia SSR |
چکلیست نهایی قبل از انتشار سایت لاراول
- همه URL ها slug دارن، ID ندارن
- Canonical در هر صفحه set شده
- hreflang برای زبانهای مختلف
- robots.txt
Disallow: /admin/ - Sitemap.xml تولید و در robots.txt معرفی شده
- تصاویر WebP و lazy load
- Core Web Vitals سبز (LCP < 2.5s, CLS < 0.1)
- Schema.org JSON-LD در همه صفحات محتوایی
- OpenGraph و Twitter Card تست شده در https://www.opengraph.xyz
- Livewire/Inertia SSR فعال
- HTML minify شده
- Cache با TTL مناسب
سوالات متداول
آیا برای سئو لاراول حتماً باید از پکیج استفاده کنم؟
نه. اکثر نیازها با کد بومی لاراول حل میشه. پکیجهایی مثل spatie/laravel-seo یا artesaos/seo برای پروژههای بزرگ با چند توسعهدهنده مفیدن، ولی برای پروژه متوسط، Service اختصاصی + Blade partial کافیه. استفاده از پکیج اضافی یعنی آپدیت، یعنی bug احتمالی، یعنی یک لایه انتزاع دیگه برای دیباگ.
بهترین پکیج sitemap برای لاراول چیه؟
spatie/laravel-sitemap استاندارده. ولی اگه پروژه سادهست، همان کامند سفارشی که بالا نوشتم (با کتابخانه laravelium/sitemap یا خود DOMDocument) کافیه. مهمتر از پکیج، داشتن Sitemap بهروز و بدون URL 404 هست.
چطور Livewire را SEO-friendly کنم؟
سه گزینه داری: (۱) Livewire SSR (از لاراول ۱۰ به بعد پایدارتره)، (۲) مهاجرت به Inertia.js با SSR، (۳) استفاده از @livewire فقط برای بخشهای کوچک مثل فرم نظرات، و محتوای اصلی را با Blade سمت سرور رندر کن. گزینه ۳ کمریسکترین برای پروژههای موجوده.
Schema.org برای سایت فروشگاهی چه نوعی بزنم؟
برای محصول Product با Offer و AggregateRating، برای صفحه اصلی Organization با logo و sameAs (لینک شبکههای اجتماعی)، برای صفحات دستهبندی CollectionPage یا ItemList. ترکیب Product + BreadcrumbList + Organization معمولاً کافیه. حتماً از اسناد رسمی Schema.org پیروی کن.
چه مدت طول میکشه تا نتیجه سئو تکنیکال را ببینم؟
برای سایتهای جدید: ۳ تا ۶ ماه پس از index شدن. برای سایتهای قدیمی که مشکل تکنیکال دارن: ۲ تا ۴ هفته بعد از fix و recrawl. گوگل برای تغییرات ساختاری URL یا Schema معمولاً سریعتر واکنش نشون میده. ولی صبر کلیدیه — سئو تکنیکال درست مثل پایه ساختمونه: اگه درست باشه، بقیه کارها (محتوا، بکلینک) روش ساخته میشه.
آیا CDN برای سایت لاراول ضروریه؟
برای کاربران ایرانی، CDN بینالمللی مثل Cloudflare ضروری نیست (حتی گاهی سرعت رو بدتر میکنه چون route به سرور خارج میخوره). ولی اگه مخاطب بینالمللی داری، Cloudflare یا BunnyCDN بهشدت توصیه میشه. برای تصاویر، imgix یا self-hosted WebP با cache مناسب کافیه.
تفاوت canonical و hreflang چیه؟
canonical میگه "این URL اصلی محتواست، اگه محتوای مشابه در چند URL هست، فقط این رو index کن". hreflang میگه "این محتوا به چند زبان موجوده، هر زبان URL مخصوص خودش رو داره". در سایت دوزبانه: هر صفحه هم canonical داره هم hreflang، و این دو با هم در تضاد نیستن — canonical "تکراری بودن" رو حل میکنه، hreflang "تفاوت زبانی" رو.
اگه الان داری روی یه پروژه لاراولی کار میکنی و میخوای سئوش رو بترکونی، از لایه ۱ شروع کن — URL و canonical. چون اگه این درست نباشه، هر چقدر Schema و Vite و CDN بهینه کنی، گوگل هنوز گیج میمونه. بعد برای هر صفحه محتوایی، Schema.org Article + BreadcrumbList بذار. در نهایت Core Web Vitals رو با Lighthouse چک کن و iterate کن.
لینکهای داخلی پیشنهادی: