Файл .htaccess — обычный текстовый файл в папке сайта. Его читает Apache: программа, которая принимает запросы из браузера и отдаёт страницы. Смотрит он туда до того, как отдаст сайт, и до того, как WordPress что-то нарисует. Кладу его в корень — рядом с wp-config.php и папками wp-admin, wp-content, wp-includes. Имя начинается с точки, поэтому в файловом менеджере его иногда не видно, пока не включишь показ скрытых. Зачем он нужен, простыми словами: всех с http перекинуть на https, не показывать содержимое папок, сжать HTML/CSS/JS, запомнить картинки и стили, сделать адреса вроде /statya/, а не /?p=123, закрыть служебные файлы и левые запросы.
Это не «магический плагин» и не настройка в админке — это файл сервера. Нет Apache — нет эффекта. На чистом nginx он просто лежит, его никто не читает. На популярных хостингах Apache встречается часто, поэтому файл обычно работает. Если хостер пишет «чистый nginx» — можно не класть, те же идеи идут в конфиг сервера.
Ниже — универсальный шаблон для WordPress на Apache: один и тот же файл копирую между проектами — на свой сайт, на клиентский, на тестовый. В нём нет редиректов на конкретные страницы, нет URL и путей под один домен. Строку выкидываю, только если понимаю, зачем она была. «На всякий случай» резать файл не стоит. На новом сайте скачиваю текущий .htaccess по FTP — сразу видно, пустой он или WordPress уже нагенерил свой кусок. Свой Security и Cache вставляю выше маркеров WordPress: всё между # BEGIN WordPress и # END WordPress при сохранении постоянных ссылок может перезаписаться. Сохраняю, открываю сайт. Если вдруг 500 или 403 на всех страницах — возвращаю копию и ищу опечатку: часто незакрытый >, кавычка или ошибка в правиле dotfiles. Перед правкой всегда оставляю копию — на всякий случай.
Готовый файл
Универсальный шаблон для WordPress на Apache — без редиректов и путей под конкретный домен. Можно копировать целиком на любой проект. Дальше разберём его сверху вниз.
Options -Indexes
AddDefaultCharset UTF-8
<FilesMatch "^(wp-config.php|wp-config-sample.php|readme.html|license.txt|llms.txt|error_log|debug.log|.user.ini|php.ini)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order Allow,Deny
Deny from all
</IfModule>
</FilesMatch>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/.well-known/ [NC]
RewriteRule (^|/). - [F,L]
</IfModule>
<Files xmlrpc.php>
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order Allow,Deny
Deny from all
</IfModule>
</Files>
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
RewriteRule ^sitemap-.+.xml$ - [F,L]
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
AddOutputFilterByType DEFLATE application/javascript application/json application/xml
AddOutputFilterByType DEFLATE application/rss+xml application/atom+xml image/svg+xml
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 month"
ExpiresByType text/html "access plus 0 seconds"
ExpiresByType text/xml "access plus 0 seconds"
ExpiresByType application/json "access plus 0 seconds"
ExpiresByType application/xml "access plus 0 seconds"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch ".(css|js|jpg|jpeg|png|gif|webp|svg|ico|woff2|woff|ttf)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
FileETag None
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
Разбираем файл сверху вниз
Дальше — не «умная документация Apache», а перевод на обычный язык. Если встретится термин, сразу скажу, что он значит одной фразой.
| Кусок файла | Что это значит |
|---|---|
| Security | |
|
Если в папке нет стартовой страницы (index.php / index.html), Apache по умолчанию может показать список файлов. Как открытая папка на компьютере, только в интернете. Минус перед Indexes это выключает: вместо списка посетитель получит «нет доступа» или 404. Листать wp-content/uploads чужим людям не нужно. |
|
Говорит серверу: если у файла не указана кодировка, считай её UTF-8. Это та кодировка, в которой нормально живут русские буквы. Без этого иногда всплывает кракозябра в текстовых ответах. |
|
FilesMatch — «если имя файла совпадает со списком». Эти файлы из браузера открывать нельзя. wp-config.php — главный секрет WordPress: пароль от базы и ключи. wp-config-sample.php — образец того же файла. readme.html и license.txt — типовые файлы из поставки. По ним боты понимают, что это WordPress. llms.txt — служебный текст. error_log и debug.log — логи: пути сервера и ошибки PHP, для взломщика подсказка. .user.ini и php.ini — настройки PHP, их тоже закрываем. IfModule — страховка: «если на сервере есть этот модуль». mod_authz_core — современный модуль прав доступа. Тогда Require all denied значит отказ всем. Восклицательный знак в !mod_authz_core — «если модуля нет». Старый синтаксис Order Allow,Deny + Deny from all делает то же самое. На разных хостингах Apache разный: файл не должен упасть с 500. Нет модуля — блок просто пропускается. |
|
mod_rewrite — модуль Apache, который умеет менять адреса: редиректы, запреты, «красивые ссылки». RewriteEngine On включает этот механизм. Без него правила ниже могут не сработать. RewriteCond — «проверь условие, прежде чем применять правило». Здесь: адрес не должен начинаться с /.well-known/. В эту папку кладут проверочный файл для сертификата SSL (Let’s Encrypt). Если запретить все «файлы с точкой», выпуск сертификата может сломаться. [NC] — не учитывать регистр букв. RewriteRule с [F,L] : если в пути есть кусок, который начинается с точки, отдать 403 — Forbidden, «вход запрещён». [L] — last, на этом останавливаемся. Так закрываются .git, .env, .htpasswd и прочее, что не должно торчать в интернет. В шаблоне (^|/)\. после слэша стоит экранированная точка — \. в regex означает буквально символ «точка», а не «любой символ». Частая опечатка при копировании — (^|/). без обратного слэша: тогда точка в regex = любой символ, правило срабатывает на каждый URL, и сайт отдаёт 403 на все страницы. |
|
xmlrpc.php — старый способ удалённо дёргать WordPress: публикации из приложений, пинги, часть интеграций. Сейчас почти всё умеет REST API (современный программный доступ) и обычная админка. Боты этот файл очень любят: подбирают пароли, делают «усиление» запросов. Поэтому закрываю целиком — всем отказ. Админка /wp-admin/ при этом работает как работала. Зайти и править сайт можно. Исключение: если тебе реально нужен Jetpack или старое приложение WordPress, этот кусок лучше не копировать. У меня на обычных сайтах он не нужен. |
|
mod_headers добавляет к ответу сервера служебные заголовки. Посетитель их не видит, браузер слушается. X-Content-Type-Options: nosniff — «не угадывай тип файла». Иначе браузер может принять текст за скрипт. Меньше шансов подсунуть опасный файл под видом картинки. X-Frame-Options: SAMEORIGIN — сайт можно вставить в рамку iframe только со своего домена. Чужой сайт не спрячет твою страницу и не повесит поверх прозрачную кнопку. Referrer-Policy: strict-origin-when-cross-origin — когда человек уходит на другой сайт, мы не светим полный адрес текущей страницы, только свой домен. Permissions-Policy с пустыми camera / microphone / geolocation — страница не просит камеру, микрофон и геолокацию. Для визитки это лишнее. Слово always — ставить заголовок даже на ошибочных ответах, не только на «успешных» страницах. |
|
HTTPS — замочек в браузере: связь шифруется. Если зашли по обычному http://, я перекидываю на https://. RewriteEngine On снова включает переписывание адресов. Повторять можно, вреда нет. RewriteCond %{HTTPS} !=on — «если соединение ещё не защищённое». X-Forwarded-Proto — вторая проверка. На многих хостингах перед Apache стоит прокси (промежуточный сервер) или nginx. Тогда Apache думает, что пришли по http, хотя снаружи уже https. Если это не учесть, получится бесконечный редирект: сайт будет крутиться и не откроется. [L,R=301] — отправь браузер на тот же адрес, но с https, и запомни это насовсем. 301 — постоянный редирект, поисковики запоминают правильный адрес. L — дальше это правило не продолжаем. Комментарий в файле честный: если весь трафик и так приходит по https через прокси, блок часто не нужен. У меня не мешает — срабатывает только когда зашли по http. |
|
Эта строка живёт в том же блоке rewrite, сразу после HTTPS. Запрещает адреса вроде /sitemap-1.xml и /sitemap-news.xml — такие файлы оставляют SEO-плагины. Мне нужна одна карта: /sitemap.xml. Остальное — шум и дубли. [F,L] снова «запретить и остановиться». На сам /sitemap.xml правило не действует: там нет хвоста sitemap-…. # END Security — снова только пометка, где кончается защита. |
| Cache — чтобы страницы открывались быстрее | |
|
Вторая большая часть файла не про защиту, а про скорость. Текстовые ответы лучше отдавать сжатыми — меньше трафик, быстрее загрузка. mod_deflate сжимает ответ на лету, примерно как zip, только браузер сам распаковывает. AddOutputFilterByType DEFLATE — «сжимай такие типы содержимого»: HTML, текст, XML, CSS, JavaScript, JSON, RSS, SVG. Картинки JPEG/PNG/WebP уже сжаты своим форматом — их тут нет, повторно жать почти бесполезно. Если модуля нет, блок пропускается. Сайт будет работать, просто без этого сжатия. |
|
mod_expires ставит дату, до которой браузер может не переспрашивать файл. ExpiresActive On — включили. ExpiresDefault — если тип не перечислен ниже, помни месяц. Для HTML, XML и JSON стоит 0 секунд — не кэшировать. Иначе поправишь текст на сайте, а у человека в браузере будет старая страница. CSS, JavaScript, картинки и шрифт woff2 — на год. Это статика: она редко меняется, а если меняется, у файла обычно другой адрес (в WordPress к стилям часто приписывают номер версии). |
|
Браузеры смотрят не только Expires, но и заголовок Cache-Control. Для css, js, картинок и шрифтов я ставлю public, max-age=31536000, immutable. public — можно кэшировать не только дома, но и на промежуточных серверах. max-age=31536000 — 31 536 000 секунд, это год. immutable — «этот файл сам не изменится». Браузер меньше дёргает сервер с проверкой «а вдруг обновилось?». Если я меняю стиль, WordPress обычно отдаёт его как style.css?ver=1.2.3 — для браузера это уже другой адрес. |
|
ETag — метка файла вроде «версии». Браузер потом спрашивает: «у меня вот такая метка, файл ещё тот?» На своей машине это нормально. На CDN (сеть кэширующих серверов по миру) разные машины могут выдать разные ETag на один и тот же файл — и кэш начинает сбоить. Header unset ETag убирает этот заголовок, FileETag None говорит Apache его не считать. Дальше браузер опирается на Cache-Control и Expires — этого достаточно. |
| Блок WordPress — красивые ссылки | |
|
Это стандартный кусок, который WordPress сам пишет между маркерами BEGIN/END WordPress. Я его не выдумываю и руками внутри не правлю. Если ссылки разъехались — «Настройки → Постоянные ссылки → Сохранить». Верхние секции останутся на месте. Без mod_rewrite красивые ссылки на Apache не заработают. RewriteRule .* — [E=HTTP_AUTHORIZATION:…] — техническая строка: прокинуть заголовок авторизации в PHP. Нужно некоторым приложениям и REST-запросам с логином. На внешний вид сайта не влияет. RewriteBase / — сайт живёт в корне домена. Если WordPress в подпапке /blog/, тут было бы /blog/. RewriteRule ^index.php$ — [L] — если уже запрошен index.php, ничего не переписываем. !-f и !-d — «если по этому адресу нет реального файла и это не реальная папка». Тогда отдай запрос в index.php. WordPress сам поймёт, какую запись показать. Человек видит /statya/, сервер внутри кормит это движку. Админку /wp-admin/ этот блок не закрывает: это реальная папка, условие «это не папка» не срабатывает. Плагины, картинки, CSS — тоже реальные файлы, их отдают напрямую. |
На хостинге должны быть включены mod_rewrite и mod_headers — на обычном shared-хостинге с Apache это почти всегда так. Если красивые ссылки или заголовки не работают, спроси поддержку. В правиле dotfiles критична точка: (^|/)\. — с обратным слэшем (\) перед точкой. Опечатка (^|/). ломает весь сайт: regex воспринимает точку как «любой символ», и каждый запрос получает 403.
Обычно две причины. Первая: на сервере выключен mod_rewrite. Вторая: в конфиге Apache для папки сайта стоит AllowOverride None — тогда сервер вообще игнорирует .htaccess. Нужно AllowOverride All (или хотя бы FileInfo). Это уже настройка сервера, не WordPress. На обычном хостинге с панелью это почти всегда уже разрешено — иначе и сам WordPress не смог бы писать постоянные ссылки.
Если хостинг на чистом nginx, этот файл не читается. Те же идеи переносятся в конфиг сервера: запрет файлов, заголовки, gzip, expires, try_files вместо rewrite WordPress. Копировать .htaccess в корень nginx-сайта бессмысленно — он там просто полежит.
Комментарии
Пока нет комментариев — можете написать первый.