。 */ 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); } }