Files
evus.acvisa.com/app/service/SeoMetaService.php

174 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace app\service;
final class SeoMetaService
{
private array $config;
public function __construct(?array $config = null)
{
$this->config = $config ?? (array) config('seo');
}
public function forRoute(
string $app,
string $controller,
string $action,
array $query = [],
array $overrides = []
): array {
$app = strtolower(trim($app));
$controller = strtolower(trim($controller));
$action = strtolower(trim($action));
$routeKey = implode('/', [$app, $controller, $action]);
$page = (array) (($this->config['pages'] ?? [])[$routeKey] ?? []);
$data = array_merge($page, $overrides);
$title = trim((string) ($data['title'] ?? $this->config['default_title'] ?? ''));
$siteName = trim((string) ($this->config['site_name'] ?? ''));
if ($siteName !== '' && $title !== '' && stripos($title, $siteName) === false) {
$title .= ' - ' . $siteName;
}
$robots = (string) ($data['robots'] ?? $this->config['default_robots'] ?? 'noindex,follow');
$noindexControllers = array_map(
'strtolower',
(array) ($this->config['noindex_controllers'] ?? [])
);
if (in_array($controller, $noindexControllers, true)) {
$robots = 'noindex,follow';
}
foreach ((array) ($this->config['sensitive_query_keys'] ?? []) as $key) {
if (isset($query[$key]) && $query[$key] !== '') {
$robots = 'noindex,nofollow';
break;
}
}
$canonical = trim((string) ($data['canonical'] ?? ''));
if ($canonical === '') {
$canonical = $this->canonical(
(string) ($data['canonical_path'] ?? ''),
$query,
(array) ($data['canonical_query_keys'] ?? [])
);
}
return [
'title' => $title,
'description' => trim((string) (
$data['description']
?? $this->config['default_description']
?? ''
)),
'keywords' => trim((string) (
$data['keywords']
?? $this->config['default_keywords']
?? ''
)),
'robots' => $robots,
'canonical' => $canonical,
'lang' => (string) ($data['lang'] ?? $this->config['lang'] ?? 'zh-CN'),
'applicable_device' => (string) (
$data['applicable_device']
?? $this->config['applicable_device']
?? 'pc,mobile'
),
'baidu_site_verification' => trim((string) (
$this->config['baidu_site_verification']
?? ''
)),
];
}
public function canonical(
string $path,
array $query = [],
array $allowedQueryKeys = []
): string {
$baseUrl = $this->normalizedBaseUrl();
if ($baseUrl === '' || $path === '') {
return '';
}
if (str_starts_with($path, '//') || parse_url($path, PHP_URL_HOST) !== null) {
return '';
}
$path = '/' . ltrim($path, '/');
$canonicalQuery = [];
foreach ($allowedQueryKeys as $key) {
if (!array_key_exists($key, $query) || is_array($query[$key])) {
continue;
}
if ($query[$key] === '' || $query[$key] === null) {
continue;
}
$value = (string) $query[$key];
if (in_array($key, ['id', 'page'], true)) {
if (preg_match('/^[1-9]\d*$/', $value) !== 1) {
continue;
}
if ($key === 'page' && $value === '1') {
continue;
}
}
$canonicalQuery[$key] = $value;
}
$url = $baseUrl . ($path === '/' ? '/' : rtrim($path, '/'));
if ($canonicalQuery !== []) {
$url .= '?' . http_build_query($canonicalQuery, '', '&', PHP_QUERY_RFC3986);
}
return $url;
}
public function descriptionFromHtml(string $html, int $maxLength = 100): string
{
$html = (string) preg_replace(
'#<(script|style)\b[^>]*>.*?</\1>#isu',
' ',
$html
);
$text = (string) preg_replace('/<[^>]+>/u', ' ', $html);
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = trim((string) preg_replace('/\s+/u', ' ', $text));
if ($maxLength < 1) {
return '';
}
if (function_exists('mb_substr')) {
return mb_substr($text, 0, $maxLength, 'UTF-8');
}
return substr($text, 0, $maxLength);
}
public function baseUrl(): string
{
return $this->normalizedBaseUrl();
}
private function normalizedBaseUrl(): string
{
$baseUrl = rtrim(trim((string) ($this->config['base_url'] ?? '')), '/');
if ($baseUrl === '' || filter_var($baseUrl, FILTER_VALIDATE_URL) === false) {
return '';
}
$scheme = strtolower((string) parse_url($baseUrl, PHP_URL_SCHEME));
if (!in_array($scheme, ['http', 'https'], true)) {
return '';
}
return $baseUrl;
}
}