Обратная совместимость ссылок средствами Apache

Что это такое?

Обратная совместимость - это адекватная реакция программулины на устаревшие данные. К примеру, не секрет, что майкрософт офис 2007 прекрасно работает с документами от более ранних версий. То есть, офис 2007 имеет хорошую обратную совместимость.

Что касается сайтов, то под обратной совместимостью ссылок я понимаю примерно следующее: если какой-либо раздел был перемещён, то он должен работать и по старому адресу, и по новому.

Зачем оно нужно?

Простое перемещение разделов сайта плохо сказывается на поисковиках и прямых ссылках на разделы.

Во-первых, поисковики. Они животные подневольные, телепатическими способностями не обладают, поэтому будут пытаться индексировать перемещённый раздел по старому адресу ещё долгое время после переезда раздела.

Во-вторых, пользователи. Было бы просто замечательно, если бы все перемещались по сайту, используя только родную навигацию. Но зачастую ссылки на полезную информацию распространяются в абсолютном виде сразу на целевой рездел. Поэтому, если такой раздел переедет и ссылка будет вести в никуда, то это плохо скажется и на поисковиках, и на репутации сайта.

Чтобы избежать подобных проблем, достаточно по старому адресу просто сообщить адрес переезда. Благо, HTTP имеет методы делать это культурно, используя редиректы.

Как оно бывает

Когда мой сайт только зарождался, то разделов было чуть больше, чем шиш да ещё чуть-чуть. Сайт состоял в основном из статей, которые располагались примерно так:

http://сайтец/articles/vm-server/
http://сайтец/articles/ImageCounter/
http://сайтец/articles/ImageList/
http://сайтец/articles/mysql-charsets/

Через некоторое время сайт подрос, статьи расплодились, и возникла необходимость расположить их уже по новым адресам:

http://сайтец/articles/os/vm/vm-server/
http://сайтец/articles/php/image-counter/
http://сайтец/articles/js/image-list/
http://сайтец/articles/mysql/mysql-charsets/

Редиректы в апаче

Поизучав документацию на нетленном http://apache.org, я понял, что всё, как в сказке. В смысле, есть три пути, и на всех грабли.

Путь первый, mod_alias, Redirect

Первый путь решения проблемы с редиректами привёл к mod_alias, директиве Redirect.

Функционал сей конструкции сводится к следующему:

Redirect [type|code] addr1 add2

То есть, если начало запроса соответствует addr1, то оно будет заменено на addr2, а полученный адрес отправится клиенту с кодом возврата code.

Всего доступно четыре типа редиректа:

permanent, код 301
ресурс был перемещён на ПМЖ (indicating that the resource has moved permanently)
temp, код 302
ресурс временно перемещён (temporary redirect status). Этот тип редиректа используется по умолчанию
seeother, код 303
ресурс был замещён (indicating that the resource has been replaced)
gone, код 410
ресурс тут больше не живёт (indicating that the resource has been permanently removed)

В моём случае, Redirect можно было бы использовать так

Файл - .htaccess
1
2
3
Redirect permanent /articles/vm-server/  /articles/os/vm/vm-server/
Redirect permanent /articles/ImageCounter/  /articles/php/image-counter/
...

Весьма просто и функционально. Однако, сделать какую-то дополнительную обработку запроса в данном случае не представляется возможным, а запрашиваемый путь должен точно соответствовать addr1.

Впрочем, все недостатки Redirect были устранены в RedirectMatch.

Путь второй, mod_alias, RedirectMatch

В общем и целом, RedirectMatch повторяет Redirect, за тем исключением, что вместо addr1 можно использовать шаблон, а в addr2 значения, найденные в шаблоне.

Моя реализация выглядела таким образом:

Файл - .htaccess
RedirectMatch permanent ^/articles/vm-server/?(.*)$  /articles/os/vm/vm-server/$1
RedirectMatch permanent ^/articles/ImageCounter/?(.*)$  /articles/php/image-counter/$1
RedirectMatch permanent ^/articles/ImageList/?(.*)$  /articles/js/image-list/$1
RedirectMatch permanent ^/articles/mysql-charsets/?(.*)$  /articles/mysql/mysql-charsets/$1

На домашнем сервере такой код работал на ура, но при заливке на хостинг произошла неприятность. Как только код оказался на рабочем сервере, где был настроен движок на mod_rewrite, все редиректы перестали работать, как надо. Причиной стал тот самый mod_rewrite, который каждый URL редиректа дополнительно модифицировал без зазрения совести.

Поизучав оную проблему в интернете, выяснилось, что mod_rewrite не будет дружить с редиректами mod_alias и лучше использовать что-то одно. Собственно выбора, как такового, и не пришлось делать, движок был важнее. От редиректов средствами mod_alias пришлось отказаться.

Путь третий, mod_rewrite, RewriteRule

Когда mod_alias показал свою несостоятельность, пришлось делать редиректы средствами mod_rewrite.

Чем плох сей метод?.. Всё ничего, да только сам модуль реврайта кривой и страшный как чудо-юдо болотное. Но об этом несколько позже.

Модуль mod_rewrite позволяет менять URL-запроса в зависимости от ситуации и осуществлять внутренние или внешние переходы. Обработка ведётся при помощи правил RewriteRule. Логика работы правил такая же, как и у RedirectMatch, то есть, если найден шаблон, то выполняется правило. Но в отличие от RedirectMatch один запрос может обрабатываться несколькими правилами RewriteRule, включая подзапросы.

Моя реализация редиректов на mod_rewrite на данный момент выглядит следующим образом:

Файл - .htaccess
RewriteEngine On
Options +FollowSymlinks

## Текущее расположение относительно корня сайта
RewriteBase /

## Совместимость со старыми разделами
## Для работы требуется mod_rewrite
# ---------------------------------------------
# Статьи
RewriteRule ^articles/vm-server/?(.*)$  /articles/os/vm/vm-server/$1 [R=301,L]
RewriteRule ^articles/ImageCounter/?(.*)$  /articles/php/image-counter/$1 [R=301,L]
RewriteRule ^articles/ImageList/?(.*)$  /articles/js/image-list/$1 [R=301,L]
RewriteRule ^articles/mysql-charsets/?(.*)$  /articles/mysql/mysql-charsets/$1 [R=301,L]

Флаги [R=301,L] означают, что правило должно инициировать редирект с кодом 301, и правило является "последним" (L - last). То есть, если шаблон будет найден и правило сработает, то все остальные правила должны исключаться.

Так где же грабли?

Для проверки логики работы приведённого выше кода, я включил логи реврайта. Каково же было моё удивление, когда оказалось, что проверке подвергались шаблоны всех правил без исключений. То есть, ключ L по-сути ни на что не влиял.

Дальнейшие эксперименты привели к следующему коду:

Файл - .htaccess
RewriteEngine On
RewriteRule index\.php$   index1.html [L]
RewriteRule index1\.html$ index2.html [L]
RewriteRule index2\.html$ index3.html [L]

Моему удивлению не было предела! Мало того, что тестировались все три шаблона, дак ещё и срабатывали все три правила, если запрашивался index.php.

Проверив таким способом апачи 1.3 и 2.2, я получил одинаковый результат, всегда возвращался index3.html. Из чего пришлось сделать вывод, что mod_rewrite, мягко говоря, не комильфо. Однако, за неимением лучшего, пришлось остаться на реврайте.