Общая схема
Механизм состоит из четырёх частей, и каждая решает свою задачу.
- Правило сервера направляет все запросы в одну точку входа.
- Маршрутизатор разбирает путь и определяет, что запрошено.
- Хранилище сопоставляет текстовый адрес с записью в базе.
- Обработчик ошибок отвечает за ненайденные адреса и переезды.
Точка входа
Все запросы, для которых нет реального файла, отправляются в один скрипт.
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
Два условия важны: без них сервер начнёт направлять в скрипт запросы к картинкам, стилям и загруженным файлам.
Разбор пути
Маршрутизатор получает путь и раскладывает его на сегменты.
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');
$segments = $path === '' ? [] : explode('/', $path);
Дальше логика зависит от структуры сайта. Простой вариант — сопоставление первого сегмента с типом раздела, а остального с конкретной записью.
| Адрес | Сегменты | Что показать |
|---|---|---|
/ | — | Главную |
/catalog/ | catalog | Список разделов |
/catalog/divany/ | catalog, divany | Раздел по slug |
/blog/kak-vybrat/ | blog, kak-vybrat | Статью по slug |
Хранение адреса
Ключевое архитектурное решение: адрес хранится в базе как отдельное поле, а не вычисляется из заголовка при каждом обращении.
CREATE TABLE pages (
id INT PRIMARY KEY AUTO_INCREMENT,
slug VARCHAR(190) NOT NULL UNIQUE,
title VARCHAR(255) NOT NULL,
...
);
Причина в предсказуемости: если адрес пересчитывается динамически, любая правка заголовка молча изменит URL проиндексированной страницы. Уникальный индекс на поле защищает от совпадений на уровне базы.
Сам slug формируется один раз при сохранении — функции транслитерации для трёх языков разобраны в отдельной статье.
Длина поля 190, а не 255. При кодировке utf8mb4 и стандартном размере индекса 767 байт уникальный индекс на 255 символов не создастся. 190 символов — безопасный предел, и для адреса его более чем достаточно.
История адресов
Отдельная таблица со старыми адресами избавляет от ручной настройки редиректов при каждом переименовании.
CREATE TABLE url_history (
old_slug VARCHAR(190) PRIMARY KEY,
page_id INT NOT NULL,
created_at DATETIME NOT NULL
);
Логика работы: при смене адреса старое значение записывается в историю. Маршрутизатор, не найдя страницу по текущему slug, проверяет историю и отдаёт 301 на актуальный адрес. Общие правила перенаправлений — в инструкции по 301.
Обработка ненайденных адресов
Порядок проверок при неизвестном пути:
- Есть ли адрес в истории переездов — отдать 301.
- Отличается ли путь только регистром — отдать 301 на нижний регистр.
- Отличается ли только слэшем на конце — отдать 301 на выбранный формат.
- Ничего не подошло — отдать 404 с полезной страницей.
Важно: страница ошибки должна отдавать именно код 404, а не 200 с текстом «страница не найдена». Иначе поисковая система будет считать её обычной страницей и индексировать сотнями копий.
Что учесть на старте
- Один канонический формат адреса: регистр, слэш, наличие расширения.
- Slug пишется в базу при сохранении, а не вычисляется при выводе.
- Уникальность обеспечивается индексом базы, а не только кодом.
- История адресов ведётся с первого дня — добавить её потом сложнее.
- Внутренние ссылки строятся из хранимого slug, а не собираются вручную в шаблонах.
Производительность маршрутизации
Поиск страницы по текстовому адресу выполняется при каждом запросе, поэтому мелочи здесь имеют значение.
- Индекс на поле slug обязателен — без него база перебирает всю таблицу.
- Один запрос вместо цепочки. Для вложенных путей храните полный путь отдельным полем, а не собирайте его рекурсивными запросами к родителям.
- Кеш маршрутов для статичных разделов: карта «путь → обработчик» редко меняется и хорошо кешируется.
- Ранний выход для запросов к файлам: правило сервера уже отсекает их, но проверка в коде не помешает.
Безопасность разбора пути
Путь приходит от пользователя, а значит, это недоверенные данные. Три обязательных правила:
- Никогда не подставляйте сегменты пути в имена файлов напрямую — последовательности с точками позволяют выйти за пределы каталога.
- Экранируйте значения в запросах к базе или используйте подготовленные выражения.
- Ограничьте длину и набор символов ещё до поиска: адрес не может содержать ничего, кроме латиницы, цифр, дефиса и слэша.
Последнее правило заодно избавляет от лишних запросов к базе при сканировании сайта ботами.
Частые вопросы
Хранить slug в базе или вычислять из заголовка?
Только хранить. При динамическом вычислении любая правка заголовка меняет адрес уже проиндексированной страницы, и появляются ошибки 404.
Как обеспечить уникальность адресов?
Уникальным индексом на поле slug в базе. Код при сохранении проверяет занятость и при необходимости добавляет числовой суффикс.
Зачем нужна таблица истории адресов?
Чтобы редиректы со старых адресов настраивались автоматически при переименовании страницы, а не вручную в конфигурации сервера.
Готовый адрес можно собрать прямо сейчас: откройте генератор ЧПУ — он транслитерирует фразу по таблице Яндекса, уберёт служебные слова и покажет длину адреса.