Files
moonwell-web/app/Services/SeoService.php
T
2026-10-20 19:17:49 +04:00

134 lines
6.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}