До тази седмица всяка игра в сайта ни с игри живееше на адрес като games.pnscripts.com/title/wheatwhirl. Думата title не казваше нищо нито на посетителите, нито на търсачките, удължаваше всяка връзка и беше единствената част от сайта, направена така. На 9 ноември преместихме всички 158 публични страници на игри в корена на домейна и същата страница вече е games.pnscripts.com/wheatwhirl, а българската ѝ версия е /bg/wheatwhirl. Тази статия показва как го направихме в Laravel, без да загубим нито една стара връзка, защо всеки стар адрес отговаря с точно едно пренасочване и какво измерихме след това на живия сайт.
#Защо краткият адрес си струва преместването
Самият Google е скромен за думите в адреса: те помагат повече на хората, отколкото на класирането. Ползата е другаде. Кратък и четим адрес се споделя, чете на глас и внушава доверие по-лесно в чат или в резултат от търсене, а една и съща форма за всички страници означава, че всяка връзка, каноничен етикет, запис hreflang и ред в картата на сайта сочат към едно и също. Рискът при преместване също е известен: всяка стара връзка, отметка и индексирана страница трябва да продължи да работи, иначе сигналите, които носи, се губят.
Google препоръчва постоянно пренасочване от сървъра (301 или 308) от всеки стар адрес към новия, пренасочванията да се пазят поне година, а вътрешните връзки, каноничните етикети и картите на сайта да се обновят, така че роботите изобщо да не минават през пренасочването. Съветва също да се избягват вериги: всяка допълнителна стъпка струва време на посетителя и усилие на търсачката. Затова правилото ни беше една стъпка, никога две.
#Новият маршрут и старият
Домейнът с игри вече има два маршрута за страниците на игри. Старата форма получава собствен малък маршрут, който само пренасочва. Новата е записана последна в групата, така че всяка истинска страница на домейна печели първа:
// Old game page addresses: one permanent 301 to the page at the host root.
Route::get('/title/{slug}', [GamesController::class, 'legacyTitle'])->name('title.legacy'.$suffix);
// Game pages at the host root: /{slug} and /bg/{slug} (owner 09.11.2026, no /title/).
// Registered last in the group and never matching a reserved word or a
// language prefix (GameUrlSlugs); a games-site CMS page with this slug
// still opens through GamesController::show.
Route::get('/{slug}', [GamesController::class, 'show'])
->where('slug', GameUrlSlugs::routePattern())
->name('title.show'.$suffix);
Самото пренасочване е няколко реда. То пита един помощен клас за крайната страница на езика на посетителя и пази параметрите след въпросителния знак, така че етикетите на кампании във връзките от стари публикации пак пристигат:
public function legacyTitle(Request $request, string $slug): RedirectResponse
{
$target = GamePageTarget::forSlug($slug);
abort_if($target === null, 404);
$query = $request->getQueryString();
return redirect()->to($query !== null && $query !== '' ? $target.'?'.$query : $target, 301);
}
#Една стъпка дори за игри, преместени два пъти
Интересният случай не е игра на живо, а игра, която вече няма собствена страница. Някои от по-старите ни игри бяха слети с по-нова, а други бяха свалени от сайта. Наивното пренасочване би пратило /title/tunefurrow към /tunefurrow, което после пак пренасочва към играта, в която е слята. Това е верига. Вместо това един помощен клас решава крайната страница за всеки адрес на игра и го ползват и старият, и новият маршрут:
public static function forSlug(string $slug, ?string $locale = null): ?string
{
return self::inLocale($locale, static function () use ($slug): ?string {
$game = Game::query()->published()->where('slug', $slug)->first();
if ($game instanceof Game) {
return LobbyTitle::href($game);
}
return GameSlugRedirect::targetUrl($slug) ?? self::platformHubFor($slug);
});
}
Игра на живо отива на собствената си страница. Слята игра отива направо при играта, която я замени. Игра, свалена от сайта, отива на страницата на своята платформа (браузър, компютър или телефон), най-близката страница, която още помага на посетителя. Заглавие, което е обявено, но още не е излязло, запазва своя отговор 404, защото никога не е имало страница. Междинният слой, който оправя главни букви, www. пред домейна и /index.php, ползва същия помощен клас, така че и www.games.pnscripts.com/title/tunefurrow, и /Title/Tunefurrow стигат до крайната страница с една стъпка.
#Адрес в корена не бива да скрие истинска страница
Преместването в корена има скрита цена: адресът на игра вече дели първия сегмент на пътя с всички останали страници на домейна. Игра с име legal или play би заела правните страници или папката, от която се зареждат игрите. Пазим подреден списък от 61 запазени първи сегмента (маршрути, публични папки, системни файлове) и от него, заедно с езиковите представки, строим шаблона на маршрута:
public static function routePattern(): string
{
$words = array_merge(self::RESERVED, Localization::prefixSegmentLocales());
$alternatives = implode('|', array_map(static fn (string $word): string => preg_quote($word, '#'), $words));
return '(?!(?:'.$alternatives.')$)'.self::SHAPE;
}
Такъв списък остарява в деня, в който някой добави маршрут, затова един тест обхожда всеки записан маршрут на домейна с игри и всеки файл и папка в публичната директория и се проваля, ако нещо липсва в списъка. Моделът Game и съдържателните страници на сайта с игри също отказват да запишат адрес, който съвпада с другия, така че и редактор не може да счупи адрес от административния панел.
#Връзките, които сме записали сами
Пренасочването пази връзките, които не контролираме. Тези, които контролираме, не бива да имат нужда от него. Статии в блога, описания на продукти, съдържателни страници, секции, елементи от менюта и описания на игри съдържаха стари адреси, някои като обикновен текст, други в JSON с екранирани наклонени черти. Една миграция ги пренаписа в тези шест таблици:
return (string) preg_replace(
'#((?:/|\\\\/)games(?:(?:/|\\\\/)bg)?|games\.pnscripts\.(?:com|local)(?:(?:/|\\\\/)bg)?)(/|\\\\/)title(?:/|\\\\/)([a-z0-9]+(?:-[a-z0-9]+)*)#',
'$1$2$3',
$text,
);
Нейният down() връща /title/ само пред адреси, които са игри, така че връщането назад възстановява точно това, което е променено. После тест проверява, че никое записано съдържание в блога не сочи към старата форма, нито обикновена, нито екранирана.
#Карти на сайта само с крайни страници
Картата на сайта трябва да изброява страниците, които искате да бъдат индексирани, а не адреси, които пренасочват. Нашите карти се създават от същите маршрути, затова новите адреси се появиха сами. Използвахме преместването, за да оправим още нещо: дотогава българските страници бяха само алтернативи на английските. Сега всяка езикова версия е отделен запис със същия взаимен набор от връзки hreflang, точно във формата, която Google описва за многоезични карти на сайта. Нашето ръководство за многоезични сайтове обяснява защо тази симетрия е важна.
#Как се тества една стъпка
Тестовете не спират при кода на отговора. Всяка проверка следва заглавката Location веднъж и иска отговор 200, така че веригата проваля теста:
private function assertOneHop(string $from, string $to): void
{
$response = $this->fresh()->get($this->games($from));
$response->assertStatus(301);
$location = (string) $response->headers->get('Location');
$this->assertSame($this->games($to), $location, $from);
// The target answers 200 itself: no second hop.
$this->fresh()->get($location)->assertOk();
}
Същата проверка покрива българската форма, етикет на кампания в адреса и слята игра, при която /title/tunefurrow трябва да отиде направо на /gleamtide.
#Какво отговаря живият сайт
Тестовете вървят срещу тестова база данни. За да знаем какво получават посетителите, на 9 ноември попитахме живия сайт, по една заявка с пауза между тях и без да следваме пренасочванията, за стария адрес на всяка игра, която някога сме публикували:
| Проверка | Резултат |
|---|---|
Проверени стари адреси с /title/ | 252 |
| Отговорили с едно пренасочване 301 | 250 |
| към собствената страница на играта | 158 |
| към играта, в която е слята | 5 |
| към страницата на платформата (свалена игра) | 87 |
| 404 (обявени игри, които още нямат страница) | 2 |
| Крайни страници, които сами отговарят с 200 | 161/161 |
| Стари български адреси, случайна извадка | 25/25, всеки с една стъпка |
| Медианно време за отговор с пренасочване (цялата заявка, нова връзка) | 216 ms |
Връзки с /title/ в четирите карти на сайта | 0 |
Преди преместването обхождането на четирите карти на сайта намери 315 адреса на игри с /title/, като се броят и двата езика. Сега няма нито един, а картата на сайта с игри изброява 330 адреса, сред тях всички 316 страници на игри: 158 на английски и 158 на български, всяка като отделен запис.
#Какво остава след преместването
Три неща се случват извън кода. Картите на сайта се подават отново в Search Console, за да бъдат новите адреси обходени по-скоро, а няколко седмици старите ще се виждат там като страници с пренасочване, което е очаквано, а не грешка. Връзките в собствените ни публикации в социалните мрежи и в описанията в магазините преминават към новата форма при следващата им редакция, а дотогава ги носи пренасочването. Самите пренасочвания остават в кода завинаги, защото старите връзки в други сайтове никога не изчезват напълно.
Всички 158 игри са безплатни и отворени в каталога с игри на PN Scripts. Ако искате същата грижа и за собствения си сайт, добро продължение е статията ни за раздели с въпроси, които търсачките разбират.
#Често задавани въпроси
301 или 308 да ползвам при преместване на страници?
И двата кода са постоянни пренасочвания и Google ги третира еднакво. Ние ползваме 301, защото всеки клиент го разбира, а страниците на игрите ни се четат само със заявка GET.
Защо има значение една стъпка, щом браузърите и без това следват вериги?
Всяка стъпка е още едно отиване и връщане за посетителя и още една заявка за робота, а дългите вериги може и да не бъдат проследени докрай. Когато всеки стар адрес сочи направо крайната страница, тази цена изчезва и няма какво да се счупи, когато страница се премести отново.
Трябва ли да пренаписвам вътрешните връзки, щом пренасочването работи?
Да. Пренасочванията са за връзките, които не можете да промените. Вътрешните връзки, каноничните етикети, записите hreflang и картите на сайта трябва да назовават крайния адрес, за да не им трябва пренасочването на читателите и роботите.
Колко дълго да остават старите пренасочвания?
Google съветва поне година. Ние пазим нашите завинаги, защото струват един малък маршрут, а стари връзки от публикации, форуми и отметки идват с години.
#Източници
- Google Search Central: пренасочвания и Google Търсене (на английски)
- Google Search Central: преместване на сайт с промяна на адресите (на английски)
- Google Search Central: добри практики за структурата на адресите (на английски)
- Google Search Central: езикови версии на страниците (на английски)
- RFC 9110: 301 Moved Permanently (на английски)
Коментари
Коментари
Все още няма коментари. Задайте въпрос или споделете какво проработи при вас.
Оставете коментар