136 lines
7.8 KiB
PHP
136 lines
7.8 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', ['name' => $name, 'version' => $version, 'rate' => $rate]),
|
||
'news.index' => __('Новости сервера WoW :version — :name', ['version' => $version, 'name' => $name]),
|
||
'news.show' => $article instanceof News ? "{$article->title} — {$name}" : $name,
|
||
'legal.offer' => __('Публичная оферта — :name', ['name' => $name]),
|
||
'legal.privacy' => __('Политика обработки персональных данных — :name', ['name' => $name]),
|
||
'legal.rules' => __('Правила проекта — :name', ['name' => $name]),
|
||
'login' => __('Вход в личный кабинет — :name', ['name' => $name]),
|
||
'password.request', 'password.reset' => __('Восстановление пароля — :name', ['name' => $name]),
|
||
'verification.notice', 'verification.verify' => __('Подтверждение email — :name', ['name' => $name]),
|
||
'cabinet.index' => __('Личный кабинет — :name', ['name' => $name]),
|
||
'admin.accounts.index' => __('Аккаунты и инвайты — :name', ['name' => $name]),
|
||
'admin.balances.index' => __('Слёзы Элуны — :name', ['name' => $name]),
|
||
'admin.shop.index' => __('Магазин — :name', ['name' => $name]),
|
||
'admin.news.index' => __('Новости — :name', ['name' => $name]),
|
||
'admin.login-screen-news.index' => __('Новости WoW — :name', ['name' => $name]),
|
||
default => $name,
|
||
};
|
||
$description = match ($route) {
|
||
'landing.index' => __(':name — русскоязычный PvP сервер World of Warcraft Vanilla :version с рейтами :rate. Классический Азерот, подземелья и рейды. Создай аккаунт и начни играть.', ['name' => $name, 'version' => $version, 'rate' => $rate]),
|
||
'news.index' => __('Новости PvP сервера WoW Vanilla :version :name: обновления, события, патчноуты и объявления проекта.', ['version' => $version, 'name' => $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.', ['name' => $name]),
|
||
'legal.privacy' => __('Политика обработки и защиты данных пользователей :name.', ['name' => $name]),
|
||
'legal.rules' => __('Запрещённые действия, меры ответственности и порядок обжалования санкций в проекте :name.', ['name' => $name]),
|
||
'login' => __('Вход в личный кабинет игрового сервера :name.', ['name' => $name]),
|
||
'cabinet.index' => __('Персонажи, баланс и настройки игрового аккаунта :name.', ['name' => $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 .= __(' — страница :page', ['page' => Paginator::resolveCurrentPage()]);
|
||
}
|
||
|
||
$image = $this->absoluteUrl($article instanceof News && $article->image_url ? $article->image_url : '/landing.png?v=14e199ab');
|
||
$graph = [];
|
||
|
||
if ($indexable) {
|
||
$graph[] = [
|
||
'@type' => 'WebSite',
|
||
'@id' => $home.'#website',
|
||
'url' => $home,
|
||
'name' => $name,
|
||
'alternateName' => 'Лунный колодец',
|
||
'inLanguage' => app()->getLocale() === 'en' ? 'en-US' : 'ru-RU',
|
||
];
|
||
$graph[] = [
|
||
'@type' => $route === 'news.index' ? 'CollectionPage' : 'WebPage',
|
||
'@id' => $canonical.'#webpage',
|
||
'url' => $canonical,
|
||
'name' => $title,
|
||
'description' => $description,
|
||
'inLanguage' => app()->getLocale() === 'en' ? 'en-US' : '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' => app()->getLocale() === 'en' ? 'en-US' : '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;
|
||
}
|
||
}
|