134 lines
6.9 KiB
PHP
134 lines
6.9 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\News;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Pagination\Paginator;
|
||
use Illuminate\Support\Str;
|
||
|
||
class SeoService
|
||
{
|
||
public function routeUrl(string $name, mixed $parameters = []): string
|
||
{
|
||
return $this->absoluteUrl(route($name, $parameters, false));
|
||
}
|
||
|
||
public function absoluteUrl(string $path): string
|
||
{
|
||
if (Str::startsWith($path, ['https://', 'http://'])) {
|
||
return $path;
|
||
}
|
||
|
||
return rtrim(config('app.url'), '/').'/'.ltrim($path, '/');
|
||
}
|
||
|
||
/** @return array<string, mixed> */
|
||
public function forRequest(Request $request): array
|
||
{
|
||
$name = config('moonwell.realm.server_name', 'MoonWell');
|
||
$name = strcasecmp($name, 'MoonWell') === 0 ? 'MoonWell' : $name;
|
||
$version = config('moonwell.realm.client_version', '1.12.1');
|
||
$rate = config('moonwell.realm.rates.experience', 'x1');
|
||
$home = $this->routeUrl('landing.index');
|
||
$route = $request->route()?->getName();
|
||
$article = $request->route('news');
|
||
$indexable = in_array($route, ['landing.index', 'news.index', 'news.show'], true);
|
||
$title = match ($route) {
|
||
'landing.index' => "{$name} — PvP сервер WoW Vanilla {$version} {$rate}",
|
||
'news.index' => "Новости сервера WoW {$version} — {$name}",
|
||
'news.show' => $article instanceof News ? "{$article->title} — {$name}" : $name,
|
||
'legal.offer' => "Публичная оферта — {$name}",
|
||
'legal.privacy' => "Политика обработки персональных данных — {$name}",
|
||
'legal.rules' => "Правила проекта — {$name}",
|
||
'login' => "Вход в личный кабинет — {$name}",
|
||
'cabinet.index' => "Личный кабинет — {$name}",
|
||
'admin.accounts.index' => "Аккаунты и инвайты — {$name}",
|
||
'admin.balances.index' => "Слёзы Элуны — {$name}",
|
||
'admin.shop.index' => "Магазин — {$name}",
|
||
'admin.news.index' => "Новости — {$name}",
|
||
'admin.login-screen-news.index' => "Новости WoW — {$name}",
|
||
default => $name,
|
||
};
|
||
$description = match ($route) {
|
||
'landing.index' => "{$name} — русскоязычный PvP сервер World of Warcraft Vanilla {$version} с рейтами {$rate}. Классический Азерот, подземелья и рейды. Создай аккаунт и начни играть.",
|
||
'news.index' => "Новости PvP сервера WoW Vanilla {$version} {$name}: обновления, события, патчноуты и объявления проекта.",
|
||
'news.show' => $article instanceof News
|
||
? Str::limit(Str::squish(html_entity_decode(strip_tags($article->body), ENT_QUOTES | ENT_HTML5, 'UTF-8')), 160)
|
||
: '',
|
||
'legal.offer' => "Публичная оферта о заключении договора возмездного оказания цифровых услуг {$name}.",
|
||
'legal.privacy' => "Политика обработки и защиты данных пользователей {$name}.",
|
||
'legal.rules' => "Запрещённые действия, меры ответственности и порядок обжалования санкций в проекте {$name}.",
|
||
'login' => "Вход в личный кабинет игрового сервера {$name}.",
|
||
'cabinet.index' => "Персонажи, баланс и настройки игрового аккаунта {$name}.",
|
||
default => '',
|
||
};
|
||
$canonical = match ($route) {
|
||
'landing.index' => $home,
|
||
'news.index' => $this->routeUrl('news.index', Paginator::resolveCurrentPage() > 1 ? ['page' => Paginator::resolveCurrentPage()] : []),
|
||
'news.show' => $article instanceof News ? $this->routeUrl('news.show', $article) : null,
|
||
default => null,
|
||
};
|
||
|
||
if ($route === 'news.index' && Paginator::resolveCurrentPage() > 1) {
|
||
$title .= ' — страница '.Paginator::resolveCurrentPage();
|
||
}
|
||
|
||
$image = $this->absoluteUrl($article instanceof News && $article->image_url ? $article->image_url : '/landing.png');
|
||
$graph = [];
|
||
|
||
if ($indexable) {
|
||
$graph[] = [
|
||
'@type' => 'WebSite',
|
||
'@id' => $home.'#website',
|
||
'url' => $home,
|
||
'name' => $name,
|
||
'alternateName' => 'Лунный колодец',
|
||
'inLanguage' => 'ru-RU',
|
||
];
|
||
$graph[] = [
|
||
'@type' => $route === 'news.index' ? 'CollectionPage' : 'WebPage',
|
||
'@id' => $canonical.'#webpage',
|
||
'url' => $canonical,
|
||
'name' => $title,
|
||
'description' => $description,
|
||
'inLanguage' => 'ru-RU',
|
||
'isPartOf' => ['@id' => $home.'#website'],
|
||
];
|
||
|
||
if ($route === 'news.show' && $article instanceof News) {
|
||
$graph[] = array_filter([
|
||
'@type' => 'NewsArticle',
|
||
'headline' => $article->title,
|
||
'description' => $description,
|
||
'image' => $image,
|
||
'datePublished' => $article->created_at?->toIso8601String(),
|
||
'dateModified' => $article->updated_at?->toIso8601String(),
|
||
'inLanguage' => 'ru-RU',
|
||
'mainEntityOfPage' => ['@id' => $canonical.'#webpage'],
|
||
'author' => ['@type' => 'Organization', 'name' => $name, 'url' => $home],
|
||
'publisher' => ['@type' => 'Organization', 'name' => $name, 'url' => $home],
|
||
], static fn (mixed $value): bool => $value !== null);
|
||
}
|
||
}
|
||
|
||
$seo = [
|
||
'title' => $title,
|
||
'description' => $description,
|
||
'canonical' => $canonical,
|
||
'robots' => $indexable && config('seo.indexable') ? 'index, follow, max-image-preview:large' : 'noindex, follow',
|
||
'type' => $route === 'news.show' ? 'article' : 'website',
|
||
'image' => $image,
|
||
'site_name' => $name,
|
||
'published_at' => $route === 'news.show' && $article instanceof News ? $article->created_at?->toIso8601String() : null,
|
||
'modified_at' => $route === 'news.show' && $article instanceof News ? $article->updated_at?->toIso8601String() : null,
|
||
'structured_data' => $graph ? ['@context' => 'https://schema.org', '@graph' => $graph] : null,
|
||
];
|
||
|
||
// Inertia manages these same server-rendered tags on subsequent visits.
|
||
$seo['head'] = array_map('trim', preg_split('/\R(?=<)/', trim(view('seo.head', ['seo' => $seo])->render())));
|
||
|
||
return $seo;
|
||
}
|
||
}
|