Laravel 13 应用的搜索引擎优化是提升网站可见性的关键,本文深入探讨高级 SEO 优化技术。

搜索引擎优化架构

SEO 服务提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Providers;

use App\Services\SEO\MetaTagManager;
use App\Services\SEO\SchemaBuilder;
use App\Services\SEO\SEOAnalyzer;
use Illuminate\Support\ServiceProvider;

class SEOServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(MetaTagManager::class);
$this->app->singleton(SchemaBuilder::class);
$this->app->singleton(SEOAnalyzer::class);
}
}

高级 Meta 管理

动态 Meta 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<?php

namespace App\Services\SEO;

class AdvancedMetaManager
{
protected array $meta = [];
protected array $openGraph = [];
protected array $twitter = [];
protected array $jsonLd = [];

public function setTitle(string $title, array $options = []): self
{
$separator = $options['separator'] ?? ' | ';
$siteName = $options['site_name'] ?? config('app.name');

$this->meta['title'] = $title . $separator . $siteName;
$this->openGraph['title'] = $title;
$this->twitter['title'] = $title;

return $this;
}

public function setDescription(string $description): self
{
$this->meta['description'] = $description;
$this->openGraph['description'] = $description;
$this->twitter['description'] = $description;

return $this;
}

public function setImage(string $url, array $options = []): self
{
$this->openGraph['image'] = [
'url' => $url,
'width' => $options['width'] ?? 1200,
'height' => $options['height'] ?? 630,
'type' => $options['type'] ?? 'image/jpeg',
'alt' => $options['alt'] ?? '',
];

$this->twitter['image'] = $url;

return $this;
}

public function setArticle(array $article): self
{
$this->openGraph['type'] = 'article';
$this->openGraph['article'] = [
'published_time' => $article['published_at'] ?? null,
'modified_time' => $article['updated_at'] ?? null,
'author' => $article['author'] ?? null,
'section' => $article['category'] ?? null,
'tag' => $article['tags'] ?? [],
];

return $this;
}

public function setProduct(array $product): self
{
$this->openGraph['type'] = 'product';
$this->openGraph['product'] = [
'price:amount' => $product['price'],
'price:currency' => $product['currency'] ?? 'USD',
'availability' => $product['availability'] ?? 'in stock',
'brand' => $product['brand'] ?? null,
];

return $this;
}

public function addJsonLd(array $data): self
{
$this->jsonLd[] = $data;
return $this;
}

public function render(): string
{
$html = [];

$html[] = $this->renderMeta();
$html[] = $this->renderOpenGraph();
$html[] = $this->renderTwitter();
$html[] = $this->renderJsonLd();

return implode("\n", array_filter($html));
}

protected function renderMeta(): string
{
$html = [];

if (!empty($this->meta['title'])) {
$html[] = '<title>' . e($this->meta['title']) . '</title>';
}

foreach (['description', 'keywords', 'robots', 'author'] as $name) {
if (!empty($this->meta[$name])) {
$html[] = '<meta name="' . $name . '" content="' . e($this->meta[$name]) . '">';
}
}

return implode("\n", $html);
}

protected function renderOpenGraph(): string
{
$html = [];

foreach ($this->openGraph as $property => $content) {
if (is_array($content)) {
foreach ($content as $key => $value) {
if (is_array($value)) {
foreach ($value as $k => $v) {
$html[] = '<meta property="og:' . $property . ':' . $k . '" content="' . e($v) . '">';
}
} else {
$html[] = '<meta property="og:' . $property . ':' . $key . '" content="' . e($value) . '">';
}
}
} else {
$html[] = '<meta property="og:' . $property . '" content="' . e($content) . '">';
}
}

return implode("\n", $html);
}

protected function renderTwitter(): string
{
$html = [];

$html[] = '<meta name="twitter:card" content="summary_large_image">';

foreach ($this->twitter as $name => $content) {
$html[] = '<meta name="twitter:' . $name . '" content="' . e($content) . '">';
}

return implode("\n", $html);
}

protected function renderJsonLd(): string
{
$html = [];

foreach ($this->jsonLd as $data) {
$html[] = '<script type="application/ld+json">' .
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) .
'</script>';
}

return implode("\n", $html);
}
}

高级结构化数据

产品 Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php

namespace App\Services\SEO\Schema;

class ProductSchema
{
public static function generate(array $product): array
{
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $product['name'],
'description' => $product['description'],
'image' => $product['images'] ?? [],
'sku' => $product['sku'] ?? null,
'mpn' => $product['mpn'] ?? null,
'brand' => [
'@type' => 'Brand',
'name' => $product['brand'] ?? null,
],
'offers' => [
'@type' => 'Offer',
'url' => $product['url'],
'priceCurrency' => $product['currency'] ?? 'USD',
'price' => $product['price'],
'priceValidUntil' => $product['price_valid_until'] ?? null,
'availability' => $product['in_stock']
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
'seller' => [
'@type' => 'Organization',
'name' => config('app.name'),
],
],
];

if (!empty($product['rating'])) {
$schema['aggregateRating'] = [
'@type' => 'AggregateRating',
'ratingValue' => $product['rating']['value'],
'reviewCount' => $product['rating']['count'],
'bestRating' => 5,
'worstRating' => 1,
];
}

if (!empty($product['reviews'])) {
$schema['review'] = array_map(function ($review) {
return [
'@type' => 'Review',
'reviewRating' => [
'@type' => 'Rating',
'ratingValue' => $review['rating'],
'bestRating' => 5,
],
'author' => [
'@type' => 'Person',
'name' => $review['author'],
],
'reviewBody' => $review['content'],
];
}, $product['reviews']);
}

return $schema;
}
}

文章 Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

namespace App\Services\SEO\Schema;

class ArticleSchema
{
public static function generate(array $article): array
{
return [
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => $article['title'],
'description' => $article['description'] ?? '',
'image' => $article['image'] ?? '',
'datePublished' => $article['published_at'],
'dateModified' => $article['updated_at'] ?? $article['published_at'],
'author' => [
'@type' => 'Person',
'name' => $article['author']['name'] ?? '',
'url' => $article['author']['url'] ?? null,
],
'publisher' => [
'@type' => 'Organization',
'name' => config('app.name'),
'logo' => [
'@type' => 'ImageObject',
'url' => asset('images/logo.png'),
],
],
'mainEntityOfPage' => [
'@type' => 'WebPage',
'@id' => $article['url'],
],
];
}
}

面包屑 Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

namespace App\Services\SEO\Schema;

class BreadcrumbSchema
{
public static function generate(array $items): array
{
return [
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => array_map(function ($item, $index) {
return [
'@type' => 'ListItem',
'position' => $index + 1,
'name' => $item['name'],
'item' => $item['url'],
];
}, $items, array_keys($items)),
];
}

public static function generateFromPath(string $path): array
{
$segments = array_filter(explode('/', $path));

$items = [
[
'name' => 'Home',
'url' => url('/'),
],
];

$currentPath = '';
foreach ($segments as $segment) {
$currentPath .= '/' . $segment;
$items[] = [
'name' => ucwords(str_replace('-', ' ', $segment)),
'url' => url($currentPath),
];
}

return self::generate($items);
}
}

SEO 中间件

自动 SEO 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

namespace App\Http\Middleware;

use App\Services\SEO\AdvancedMetaManager;
use Closure;
use Illuminate\Http\Request;

class AutoSEOMiddleware
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);

if ($this->shouldOptimize($request, $response)) {
$this->addSEOHeaders($response);
}

return $response;
}

protected function shouldOptimize(Request $request, $response): bool
{
return $request->isMethod('GET') &&
!$request->ajax() &&
!$request->wantsJson() &&
$response->getStatusCode() === 200;
}

protected function addSEOHeaders($response): void
{
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-XSS-Protection', '1; mode=block');

if (app()->environment('production')) {
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
}
}

SEO 分析工具

页面分析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
<?php

namespace App\Services\SEO;

class PageAnalyzer
{
public function analyze(string $url): array
{
$content = @file_get_contents($url);

if (!$content) {
return ['error' => 'Unable to fetch page'];
}

return [
'score' => $this->calculateScore($content),
'title' => $this->analyzeTitle($content),
'meta' => $this->analyzeMeta($content),
'headings' => $this->analyzeHeadings($content),
'images' => $this->analyzeImages($content),
'links' => $this->analyzeLinks($content),
'performance' => $this->analyzePerformance($content),
'social' => $this->analyzeSocial($content),
'structured_data' => $this->analyzeStructuredData($content),
];
}

protected function calculateScore(string $content): int
{
$score = 100;

if (!$this->hasTitle($content)) $score -= 15;
if (!$this->hasMetaDescription($content)) $score -= 15;
if (!$this->hasH1($content)) $score -= 10;
if ($this->hasMultipleH1($content)) $score -= 5;
if ($this->hasImagesWithoutAlt($content)) $score -= 5;
if (!$this->hasStructuredData($content)) $score -= 5;
if (!$this->hasOpenGraph($content)) $score -= 5;

return max(0, $score);
}

protected function hasTitle(string $content): bool
{
return preg_match('/<title[^>]*>[^<]+<\/title>/i', $content) === 1;
}

protected function hasMetaDescription(string $content): bool
{
return preg_match('/<meta[^>]*name="description"[^>]*>/i', $content) === 1;
}

protected function hasH1(string $content): bool
{
return preg_match('/<h1[^>]*>/i', $content) === 1;
}

protected function hasMultipleH1(string $content): bool
{
return preg_match_all('/<h1[^>]*>/i', $content) > 1;
}

protected function hasImagesWithoutAlt(string $content): bool
{
preg_match_all('/<img[^>]*>/i', $content, $matches);

foreach ($matches[0] as $img) {
if (!preg_match('/alt="[^"]*"/i', $img)) {
return true;
}
}

return false;
}

protected function hasStructuredData(string $content): bool
{
return preg_match('/<script[^>]*type="application\/ld\+json"[^>]*>/i', $content) === 1;
}

protected function hasOpenGraph(string $content): bool
{
return preg_match('/<meta[^>]*property="og:/i', $content) === 1;
}

protected function analyzeTitle(string $content): array
{
preg_match('/<title>([^<]+)<\/title>/i', $content, $matches);

$title = $matches[1] ?? '';

return [
'value' => $title,
'length' => strlen($title),
'optimal' => strlen($title) >= 30 && strlen($title) <= 60,
];
}

protected function analyzeMeta(string $content): array
{
$meta = [];

preg_match('/<meta[^>]*name="description"[^>]*content="([^"]+)"[^>]*>/i', $content, $desc);
$meta['description'] = [
'value' => $desc[1] ?? '',
'length' => strlen($desc[1] ?? ''),
'optimal' => strlen($desc[1] ?? '') >= 120 && strlen($desc[1] ?? '') <= 160,
];

preg_match('/<meta[^>]*name="keywords"[^>]*content="([^"]+)"[^>]*>/i', $content, $keywords);
$meta['keywords'] = $keywords[1] ?? null;

preg_match('/<meta[^>]*name="robots"[^>]*content="([^"]+)"[^>]*>/i', $content, $robots);
$meta['robots'] = $robots[1] ?? 'index, follow';

return $meta;
}

protected function analyzeHeadings(string $content): array
{
$headings = [];

for ($i = 1; $i <= 6; $i++) {
preg_match_all('/<h' . $i . '[^>]*>([^<]+)<\/h' . $i . '>/i', $content, $matches);
$headings['h' . $i] = [
'count' => count($matches[0]),
'values' => $matches[1] ?? [],
];
}

return $headings;
}

protected function analyzeImages(string $content): array
{
preg_match_all('/<img[^>]*>/i', $content, $matches);

$images = [];
$missingAlt = 0;
$missingTitle = 0;

foreach ($matches[0] as $img) {
preg_match('/src="([^"]+)"/i', $img, $src);
preg_match('/alt="([^"]*)"/i', $img, $alt);
preg_match('/title="([^"]*)"/i', $img, $title);

if (empty($alt[1])) $missingAlt++;
if (empty($title[1])) $missingTitle++;

$images[] = [
'src' => $src[1] ?? '',
'alt' => $alt[1] ?? '',
'title' => $title[1] ?? '',
];
}

return [
'total' => count($images),
'missing_alt' => $missingAlt,
'missing_title' => $missingTitle,
];
}

protected function analyzeLinks(string $content): array
{
preg_match_all('/<a[^>]*href="([^"]+)"[^>]*>/i', $content, $matches);

return [
'total' => count($matches[1]),
'internal' => count(array_filter($matches[1], fn($url) => !str_starts_with($url, 'http'))),
'external' => count(array_filter($matches[1], fn($url) => str_starts_with($url, 'http'))),
];
}

protected function analyzePerformance(string $content): array
{
return [
'size' => strlen($content),
'size_formatted' => $this->formatBytes(strlen($content)),
];
}

protected function analyzeSocial(string $content): array
{
return [
'open_graph' => preg_match_all('/<meta[^>]*property="og:/i', $content),
'twitter' => preg_match_all('/<meta[^>]*name="twitter:/i', $content),
];
}

protected function analyzeStructuredData(string $content): array
{
preg_match_all('/<script[^>]*type="application\/ld\+json"[^>]*>([^<]+)<\/script>/i', $content, $matches);

$schemas = [];

foreach ($matches[1] as $json) {
$data = json_decode($json, true);
if ($data) {
$schemas[] = $data['@type'] ?? 'Unknown';
}
}

return [
'count' => count($schemas),
'types' => $schemas,
];
}

protected function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB'];
$power = $bytes > 0 ? floor(log($bytes, 1024)) : 0;
return number_format($bytes / pow(1024, $power), 2) . ' ' . $units[$power];
}
}

SEO 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php

namespace App\Console\Commands;

use App\Services\SEO\PageAnalyzer;
use Illuminate\Console\Command;

class SEOAnalyzeCommand extends Command
{
protected $signature = 'seo:analyze {url}';
protected $description = 'Analyze page SEO';

public function handle(PageAnalyzer $analyzer): int
{
$url = $this->argument('url');

$this->info("Analyzing: {$url}");
$this->newLine();

$result = $analyzer->analyze($url);

if (isset($result['error'])) {
$this->error($result['error']);
return self::FAILURE;
}

$this->info("SEO Score: {$result['score']}/100");
$this->newLine();

$this->info('=== Title ===');
$this->line("Value: {$result['title']['value']}");
$this->line("Length: {$result['title']['length']}");
$this->line("Optimal: " . ($result['title']['optimal'] ? 'Yes' : 'No'));

$this->newLine();

$this->info('=== Meta Description ===');
$this->line("Value: {$result['meta']['description']['value']}");
$this->line("Length: {$result['meta']['description']['length']}");
$this->line("Optimal: " . ($result['meta']['description']['optimal'] ? 'Yes' : 'No'));

$this->newLine();

$this->info('=== Headings ===');
foreach ($result['headings'] as $tag => $data) {
$this->line("{$tag}: {$data['count']}");
}

$this->newLine();

$this->info('=== Images ===');
$this->line("Total: {$result['images']['total']}");
$this->line("Missing Alt: {$result['images']['missing_alt']}");

$this->newLine();

$this->info('=== Structured Data ===');
$this->line("Count: {$result['structured_data']['count']}");
$this->line("Types: " . implode(', ', $result['structured_data']['types']));

return self::SUCCESS;
}
}

总结

Laravel 13 的搜索引擎优化需要从 Meta 标签、结构化数据、页面分析和性能优化等多个方面入手。通过系统化的 SEO 策略和技术实现,可以显著提升网站在搜索引擎中的可见性和排名。