174 lines
5.3 KiB
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;
|
|
}
|
|
}
|