Настройка сайта на web-сервере с помощью .htaccess, показанная на реальном примере

Новиков М.Г.
28.04.2015

Часть 1 — Часть 2 — Часть 3

Часть 2. Практическая реализация

Содержание:

Постановка задачи

Давайте сформулируем нашу цель, а именно, что мы хотим получить в итоге:

  1. Построить сайт на php, но использовать на нём «Человеку Понятные Урлы» (ЧПУ), исключив из адресов страниц всяческие параметры, стоящие после знака вопроса, и трансформировав их в реальный адрес страницы. При этом иерархия директорий в адресах страниц будет совпадать с фактической иерархией директорий сайта на диске сервера, что повысит прозрачность архитектуры и облегчит сопровождение сайта.
  2. Склеить разные адреса, ведущие на наш сайт, чтобы избежать дублирования контента с точки зрения поисковика и, как следствие, снижения их ранжирования в поисковой выдаче:
    1. www.mysite.ru (убрать www)
    2. mysite.ru. (убрать точку корневого домена нулевого уровня)
    3. mysite.ru/index.html (убрать имя индексной страницы из запроса)
  3. Добавить слеш в конце адресов директорий для повышения эргономичности и, опять же, ухода от дублирования. Последние тенденции сайтостроения говорят нам о том, что слеш в конце любых адресов, напротив, убирают. Но нам важна эргономичность, а не следование тенденциям или даже рекомендациям, основанным на каких-то других соображениях, которые могут со временем и меняться. Пользователь должен даже по адресной строке понимать, где именно он находится.
  4. Сменить расширение страниц с php на более привычное html. Незачем морочить голову пользователям различными техническими расширениями. Раз уж мы применяем ЧПУ, то будем последовательны. Последнее время существуют рекомендации вообще убирать расширение страницы из адресной строки. Но это, опять же, снижает эргономику адреса — может создаться ложное впечатление, что пользователь находится не на конечной странице, а в корне раздела.
  5. Убрать index.html из всех путей. Отображение этой страницы означает нахождение пользователя в корне раздела, что мы уже обозначаем слешем. Поэтому имя этой страницы в адресе будет только путать пользователя.
  6. Заменить стандартные страницы сервера об ошибках на свои.

[Вернуться в начало]

Реализация

Создадим текстовый файл со следующим содержимым и разместим его в корне сайта. Это реально действующий скрипт, работу которого вы можете оценить у меня на сайте.

=============================================================================================

### ФАЙЛ ПОЛЬЗОВАТЕЛЬСКОЙ НАСТРОЙКИ СЕРВЕРА

# По умолчанию отдавать страницы в Юникоде
AddDefaultCharset UTF-8

### НАСТРОЙКА ОПЦИЙ
# Опция -ExecCGI запрещает запуск CGI скриптов, не будем их использовать.
# Лучше разрешить потом только для конкретных папок. Повысит безопасность.
# Опция -Indexes запрещает показывать содержимое каталогов, если в них нет индексного файла.
# Опция -Includes запрещает SSI. Не бкдем использовать.
# Опция +FollowSymLinks позволяет использовать в скрипте символические ссылки на файлы или каталоги,
# не находящиеся в пределах корня сайта.

Options All -ExecCGI -Indexes -Includes +FollowSymLinks

### СКРИПТ МОДИФИКАЦИИ ЗАПРОСА, ПРИХОДЯЩЕГО НА СЕРВЕР ОТ ПОЛЬЗОВАТЕЛЯ
# Удостоверимся, есть ли у Апача нужный нам модуль,
# иначе при его отсутствии сервер станет выдавать 500-ую ошибку.

<IfModule mod_rewrite.c>

# Используемые флаги:
# L (Last) - Прервать выполнение текущего прохода скрипта
# (если указан вместе с R, то тут же происходит редирект).
# R (Redirect) - Выполнить постоянный (301) внешний редирект на полученный URL.
# NC (NoCase) - В регулярном выражении не учитывать регистр букв.
# OR (or) - Выполнять правило, если соблюдено либо текущее, либо следующее условие.
# F (Forbidden) - Блокировать переход, отправить пользователю ответ со статусом 403 (Forbidden).
# S (skip) - Если правило нашло соответствие, пропустить указанное количество следующих правил.
# C (chain) - Если правило не нашло соответствие, пропустить весь оставшийся блок правил,
# связанный этим же флагом.
# E (Env) - Создать переменную среды окружения и занести в неё значения: E=переменная:значение.

# Используемые переменные:
# %{HTTP_HOST} - домен сайта из запроса (например: www.site.ru).
# %{REQUEST_URI} - ресурс, запрошенный в первой строке запроса (например: /articles/article1.html).
# %{QUERY_STRING} - строка параметров, которая располагается в переданной ссылке после знака вопроса
# (например: page=articles/article1.html).
# %{REQUEST_FILENAME} - полный путь в файловой системе сервера к файлу, соответствующий запрошенному
# ресурсу (например: Z:/home/site.ru/public_html/articles/article1.html).
# %{ENV:REDIRECT_LINKFORMED} - пользовательская переменная среды окружения, содержащая "1",
# если фактическая ссылка для получения страницы сформирована.

# На вход RewriteRule попадает путь от .htaccess до файла или директории включительно,
# без предваряющего слеша (например: articles/article1.html).
# Правила выполняются "домиком", т.е. сначала проверяется соответствие пути первому аргументу
# RewriteRule, затем соответствие в RewriteCond, а затем выполняется модификация согласно
# второму аргументу RewriteRule.

### Включить обработку скрипта модулем mod_rewrite.c
RewriteEngine On

### ЗАДАТЬ БАЗОВЫЙ ПУТЬ (путь от корня сайта до .htaccess.)
# Подсоединяется слева к результату после выполнения всех операций RewriteRule,
# если результат после них является относительным путём и отличается от исходного.

RewriteBase /

### ЗАВЕРШИТЬ ПРОХОЖДЕНИЕ СКРИПТА, ЕСЛИ PHP-ССЫЛКА УЖЕ В НАЛИЧИИ
# Значение переменной LINKFORMED после редиректа уходит в автоматически создаваемую переменную
# REDIRECT_LINKFORMED, а переменная LINKFORMED обнуляется.
# Редирект может выполняться принудительно (R) или автоматически в конце скрипта, быть внешним или
# внутренним. Значение "1" в переменную заносится в конце скрипта, когда php-ссылка сформирована.

    # Если в переменной REDIRECT_LINKFORMED есть хотя бы 1 символ, значит скрипт на предыдущем
    # проходе дошёл до конца и сформировал php-ссылку, модифицировать которую больше не нужно,

    RewriteCond %{ENV:REDIRECT_LINKFORMED} . [OR]
    # или это прямое обращение к директории форума, скрипту счётчика закачек или другим страницам,
    # и модифицировать его тоже не нужно.

    RewriteCond %{REQUEST_URI} /forum/.*$ [OR,NC]
    RewriteCond %{REQUEST_URI} /counter_fd.php$ [OR,NC]
    RewriteCond %{REQUEST_URI} /unicode_template.html$ [NC]

# Путь не менять и прекратить выполнение текущего прохода скрипта. В текущем проходе изменений
# не было, поэтому выполнение скрипта прекращается совсем.

RewriteRule ^ - [L]

### НЕ ОБРАБАТЫВАТЬ НИЧЕГО КРОМЕ ФАЙЛОВ СТРАНИЦ И ДИРЕКТОРИЙ
    # Если это не страница, в том числе без расширения после точки (а файлов других типов -
    # изображений, стилей и т.п.),

    RewriteCond %{REQUEST_URI} !\.(php|html|htm|)$ [NC]
    # и это не адрес вообще без расширения,
    RewriteCond %{REQUEST_URI} !.*/[^/\.]+$
    # и это не директория,
    RewriteCond %{REQUEST_FILENAME} !-d
# то путь не менять и прекратить выполнение текущего прохода скрипта. В текущем проходе изменений
не было, поэтому выполнение скрипта прекращается совсем.

RewriteRule ^ - [L]

### ВНЕШНИЕ РЕДИРЕКТЫ ------------------------------------------------------------------------------
# Заставляют браузер обращаться по новой, передаваемой ему ссылке.


### ОТРЕЗАТЬ ВСЕ ПАРАМЕТРЫ ОТ ЗАПРОСА
### (ОНИ МОГУТ ПОЯВИТЬСЯ ЗДЕСЬ ТОЛЬКО ПРИ НАБОРЕ ИХ ПОЛЬЗОВАТЕЛЕМ)
# Отсекаются все php-запросы, которые могли бы выполниться в обход ЧПУ, чтобы не дублировать ссылки.

    # Если в строке параметров существует хоть один символ,
    RewriteCond %{QUERY_STRING} .
# то к пути добавить "?" без параметров, тем самым уничтожая параметры.
RewriteRule (.*) $1? [R=301,L]

### УБРАТЬ ПОДДОМЕН WWW ИЗ ДОМЕННОГО ИМЕНИ
# Мера против дублирования ссылок.

    # Если имя сайта начинается с "www",
    RewriteCond %{HTTP_HOST} ^www\.(.*) [NC]
# то построить то же имя, но без www, и заставить браузер пользователя перейти (R)
# по постоянному (301) новому адресу сразу же (L).

RewriteRule (.*) http://%1/$1 [R=301,L]
# Примечание: В "DENWER 3" (Apache 2.2.22) в лог отладки первичный процесс замены почему-то
# не попадает, но попадает повторная проверка, где %{HTTP_HOST} уже без www.

### УБРАТЬ ЗАВЕРШАЮЩУЮ ТОЧКУ (КОРНЕВОЙ ДОМЕН, Т.Е. ДОМЕН НУЛЕВОГО УРОВНЯ)
### ИЗ ДОМЕННОГО ИМЕНИ
# Мера против дублирования ссылок.

    # Если имя сайта заканчивается точкой,
    RewriteCond %{HTTP_HOST} (.*)\.$
# то построить то же имя, но без завершающей точки в конце доменного имени, и заставить браузер
# пользователя перейти (R) по постоянному (301) новому адресу сразу же (L).

RewriteRule (.*) http://%1/$1 [L,R=301]
# Примечание: В "DENWER 3" (Apache 2.2.22) в лог отладки первичный процесс замены почему-то
# не попадает, но попадает повторная проверка, где %{HTTP_HOST} уже без точки.

### ДОБАВИТЬ "/" НА КОНЦЕ, ЕСЛИ ЭТО ЗАПРОС ДИРЕКТОРИИ
# Мера против дублирования ссылок и повышения эргономичности адресной строки.

    # Если запрашиваемый URL - директория,
    RewriteCond %{REQUEST_FILENAME} -d
# то, если нет слеша в конце, дописать его и заставить браузер пользователя перейти (R)
# по постоянному (301) новому адресу сразу же (L).

RewriteRule (.*[^/])$ $1/ [R=301,L]
# Примечание: Непосредственно после доменного имени слеш присутствует в запросе всегда
# (согласно стандартам формата запроса), вне зависимости от желания пользователя.

### ПУТЬ С РАСШИРЕНИЕМ ".PHP" или ".HTM" или "." ПОМЕНЯТЬ НА ПУТЬ С РАСШИРЕНИЕМ ".HTML"
# Если в конце пути по ошибке стоит расширение ".php" или ".htm", то поменять расширение на ".html"
# и заставить браузер пользователя перейти (R) по постоянному (301) новому адресу сразу же (L).

RewriteRule (.*)\.(php|htm|)$ $1.html [NC,R=301,L]

### ДОБАВИТЬ ".HTML" НА КОНЦЕ, ЕСЛИ ЭТО ЗАПРОС НЕ ДИРЕКТОРИИ, А РАСШИРЕНИЯ НЕТ

    # Если это не директория,
    RewriteCond %{REQUEST_FILENAME} !-d
# Если после последнего слеша нет точки
RewriteRule (.*/[^/\.]+)$ $1.html [R=301,L]

### УБРАТЬ INDEX.HTML ИЗ ПУТИ
# Мера против дублирования ссылок.
# Если "index.html" присутствует в конце запроса, то убрать его и заставить браузер пользователя
# перейти (R) по постоянному (301) новому адресу сразу же (L).

RewriteRule (.*)index\.html$ $1 [NC,R=301,L]

### БОЛЬШЕ ВНЕШНИХ РЕДИРЕКТОВ НЕ БУДЕТ --------------------------------------------------------
### Код ниже выполняется единожды, скрипт повторно не выполняется благодаря установке
### пользовательской флаговой переменной LINKFORMED.

### ПОМЕНЯТЬ РАСШИРЕНИЕ ФАЙЛА .HTML НА ФАКТИЧЕСКОЕ (.PHP)
# Если правило сработало, то это точно файл, а не директория, значит следующее правило
# выполнять не имеет смысла, пропустим его (S=1).

RewriteRule (.*)\.html$ $1.php [NC,S=1]

### ЕСЛИ УКАЗАНА ТОЛЬКО ДИРЕКТОРИЯ, ДОБАВИТЬ К НЕЙ ИНДЕКСНЫЙ ФАЙЛ
# Избегаем серверных подзапросов, указывая этот файл самостоятельно.
# Не выполняется, если сработало предыдущее правило.

RewriteRule (.*/)$ $1index.php

### ЕСЛИ УКАЗАННОГО РЕСУРСА НЕТ, НЕ ФОРМИРОВАТЬ PHP-ССЫЛКУ,
### А ГЕНЕРИТЬ ОШИБКУ 404 ПО ПРЯМОЙ ССЫЛКЕ
# Иначе при вставке (include) несуществующей страницы внутри php ошибка 404 не генерится,
# и испорченная страница может быть проиндексирована поисковиком.

    # Если путь не является файлом,
    RewriteCond %{REQUEST_FILENAME} !-f
    # и не является каталогом,
    RewriteCond %{REQUEST_FILENAME} !-d
# то путь не менять, установить флаг завершения формирования ссылки и прекратить выполнение скрипта.
RewriteRule ^ - [E=LINKFORMED:1,L]

### ПРЕВРАЩЕНИЕ ЧПУ В PHP-ССЫЛКУ
# Вставить содержимое пути в параметр "page" корневой страницы index.php
# и установить флаг завершения формирования ссылки.

RewriteRule (.*) index.php?page=$1 [E=LINKFORMED:1]
# Далее осуществляется внутренний редирект с попыткой повторного прохождения скрипта,
# которая завершается в самом его начале блпгодаря переменной LINKFORMED.

</IfModule>

# ПЕРЕНАПРАВЛЕНИЕ ОШИБОК НА СОБСТВЕННУЮ СТРАНИЦУ ОБРАБОТКИ ОШИБОК
# Bad Rquest

ErrorDocument 400 /error_pages/err.html
# Authorization Required
ErrorDocument 401 /error_pages/err.html
# Forbidden
ErrorDocument 403 /error_pages/err.html
# Not found
ErrorDocument 404 /error_pages/err.html
# Method Not Allowed
ErrorDocument 405 /error_pages/err.html
# Request Timed Out
ErrorDocument 408 /error_pages/err.html
# Gone
ErrorDocument 410 /error_pages/err.html
# Request URI Too Long
ErrorDocument 414 /error_pages/err.html
# Не все следующие ошибки могут быть выведены через PHP.
# Internal Server Error

ErrorDocument 500 /error_pages/err.html
# Not Implemented
ErrorDocument 501 /error_pages/err.html
# Bad Gateway
ErrorDocument 502 /error_pages/err.html
# Service Unavailable
ErrorDocument 503 /error_pages/err.html
# Gateway Timeout
ErrorDocument 504 /error_pages/err.html

=============================================================================================

В следующей части я опишу работу этого скрипта.

[Вернуться в начало]