。
*/
class SeoMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
if (! $this->shouldEnhance($response)) {
return $response;
}
$content = $response->getContent();
$data = $this->structuredData($request);
if (empty($data)) {
return $response;
}
$jsonLd = '';
$og = $this->ogTags($request, $data);
$content = preg_replace('/<\/head>/', $jsonLd."\n".$og."\n", $content, 1);
$response->setContent($content);
return $response;
}
private function shouldEnhance(Response $response): bool
{
$contentType = $response->headers->get('Content-Type', '');
// 跳过后台与静态资源
if (str_starts_with(request()->path(), 'admin')) {
return false;
}
return $response->getStatusCode() === 200
&& str_contains($contentType, 'text/html');
}
private function structuredData(Request $request): array
{
$siteName = blog_setting('site_name', config('blog.name'));
$sameAs = $this->socialLinks();
$route = $request->route();
if ($route && $route->getName() === 'posts.show') {
$id = $route->parameter('id') ?? $route->parameter('slug');
$post = Cache::remember('seo.post.'.$id, now()->addHour(), function () use ($id) {
return Post::query()->where('id', (int) $id)->orWhere('slug', (string) $id)->first();
});
if ($post && $post->status === 'published') {
$data = [
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'headline' => $post->title,
'url' => route('posts.show', $post->id),
'datePublished' => $post->published_at?->toIso8601String(),
'dateModified' => $post->updated_at?->toIso8601String(),
'author' => ['@type' => 'Person', 'name' => $post->author?->name ?? $siteName] + ($sameAs ? ['sameAs' => $sameAs] : []),
'publisher' => ['@type' => 'Organization', 'name' => $siteName, 'url' => url('/')] + ($sameAs ? ['sameAs' => $sameAs] : []),
'description' => Str::limit(strip_tags($post->excerpt_or_fallback), 200),
'mainEntityOfPage' => route('posts.show', $post->id),
];
if ($cover = $post->coverImageUrl()) {
$data['image'] = [$cover];
}
return $data;
}
}
// 分类页:面包屑结构化数据(GEO 友好)
if ($route && $route->getName() === 'category.show') {
$cid = $route->parameter('cid') ?? $route->parameter('slug');
$category = Cache::remember('seo.category.'.$cid, now()->addHour(), function () use ($cid) {
return \App\Models\Category::query()->where('id', (int) $cid)->orWhere('slug', (string) $cid)->first();
});
if ($category) {
return [
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => [
['@type' => 'ListItem', 'position' => 1, 'name' => $siteName, 'item' => url('/')],
['@type' => 'ListItem', 'position' => 2, 'name' => $category->name, 'item' => route('category.show', $category->id)],
],
];
}
}
return [
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'name' => $siteName,
'url' => url('/'),
] + ($sameAs ? ['sameAs' => $sameAs] : []);
}
/**
* 后台配置的社交账号 -> schema sameAs(微博/公众号/抖音/小红书/GitHub 等全平台)。
*/
private function socialLinks(): array
{
return \App\Blog\Support\SocialLinks::sameAs();
}
private function ogTags(Request $request, array $data): string
{
$siteName = blog_setting('site_name', config('blog.name'));
$isArticle = ($data['@type'] ?? '') === 'BlogPosting';
$image = $data['image'][0] ?? null;
$tags = [
'',
'',
'',
'',
'',
'',
'',
'',
'',
];
if ($image) {
$tags[] = '';
$tags[] = '';
$tags[] = '';
}
if ($isArticle) {
$tags[] = '';
$tags[] = '';
}
return implode("\n", $tags);
}
}