Мой вариант .htaccess

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

    Вашему вниманию мой вгляд на правила обработки URL с объяснениями и коментариями «почему так?».

    Сперва логика


    Объясню сперва логику:
    1) все страницы имеют .html окончания.
    2) все языки для страниц имеют вид pagename.en.html или pagename.html для языка по умолчанию. Никто, конечно, не запрещает иметь ссылки, где язык идёт вначале как /en/
    3) «входной» скрипт только один в docroot.
    4) Разрешены запросы на другие скрипты только в docroot
    5) Соглашение по определению окончаний в url:
    # site.com/
    # site.com/index -> site.com/
    # site.com -> site.com/
    # site.com/file/ -> site.com/file.html
    # site.com/file -> site.com/file.html
    # site.com/dir/file ->site.com/dir/file.html
    # site.com/dir/file/ -> site.com/dir/file.html
    Но это можно менять.


    Структура .htaccess


    Теперь перейдём к самой структуре .htaccess. Замечу ещё, что будет работать только для апачей версий 2.x и старше.

    Сперва полностью код:
    DirectoryIndex index index.html
    DirectorySlash off
    Options -Indexes -MultiViews
    # Rules
    # site.com/
    # site.com/index -> site.com
    # site.com	-> site.com/
    # site.com/file/ -> site.com/file.html
    # site.com/file  -> site.com/file.html
    # site.com/dir/file ->site.com/dir/file.html
    # site.com/dir/file/ -> site.com/dir/file.html
    # no ending slashes
    
    RewriteEngine On
    RewriteBase /
    
    RewriteCond %{REQUEST_URI} \.(css|jpg|gif|png|zip|rar|doc|xls|js|tif|tiff|docx|xlsx|ico)$|test\.php$
    	RewriteRule ^(.*)$ $1 [L,QSA]
    
    # nothing to do there in subrequests
    RewriteCond %{ENV:NS}	!=1
    RewriteCond %{IS_SUBREQ} =true
    	RewriteRule (.*) $1 [L,QSA]
    #do NS=0?
    
    
    RewriteCond %{REQUEST_URI} ^/index$ [OR]
    RewriteCond %{REQUEST_URI} ^/index[.]+(\w+)$
    	RewriteRule . / [R=301,L]
    
    # remove trailing slashes
    # if want external redirect use correct external redir [R=301,L] or [R=301] for correct internal or simple redir [L]
    RewriteCond %{REQUEST_URI} !^/$
    RewriteCond %{REQUEST_URI} (.*)/$
    	RewriteRule . %1.html [R=301,L,E=NS:1,QSA]
    
    # if whants .html endings
    RewriteCond %{REQUEST_URI} !^(.+)\.(html|php)$
    	RewriteRule . %{REQUEST_URI}.html [R=301,L]
    
    # fix multidots in endings (missed language) index..html instead of index.en.html
    RewriteCond %{REQUEST_URI} ^(.+)\.\.+(\w+)$
    	RewriteRule . %1.%2 [R=301,L]
    # otherways
    #RewriteCond %{REQUEST_URI} (.+)\.(html|php)$
    #	RewriteRule . %1 [R=301,L]
    
    # any php filename in root dir
    # this makes secure loses
    RewriteCond %{REQUEST_URI} ^[\w\-.]+$
    RewriteCond %{REQUEST_FILENAME} (.*)\.(html|php)$
    RewriteCond %1.php -s [OR]
    RewriteCond %1.html -s
    	RewriteRule . %1.%2 [L,QSA]
    
    RewriteRule (.*) entry.php?URI=$1 [L,QSA]
    #
    


    Разбор полёта


    Теперь, разберём построчно.

    DirectoryIndex index index.html
    DirectorySlash off
    Options -Indexes -MultiViews

    Сразу важный момент: выключена автоматическая подстановка слеша в конец и выключен MultiViews (с ним работать не будет).

    RewriteEngine On
    RewriteBase /
    
    RewriteCond %{REQUEST_URI} \.(css|jpg|gif|png|zip|rar|doc|xls|js|tif|tiff|docx|xlsx|ico)$|test\.php$
        RewriteRule ^(.*)$ $1 [L,QSA]


    Третья строчка проверяет на статические файлы — их пропускаем не меняя запрос. Возможно, стоило бы сделать проверку на наличие файла, но оставим это дело механизму 404. Последний |test\.php$ сделан для различных тестовых файлов, но на продакшене это дело надо убирать.

    # nothing to do there in subrequests
    RewriteCond %{ENV:NS}	!=1
    RewriteCond %{IS_SUBREQ} =true
        RewriteRule (.*) $1 [L,QSA]
    #do NS=0?

    Самая важная часть — так как идёт преобразование расширений (далее по коду), то скрипт будет уходить всегда в подзапрос и может уйти в бесконечный цикл. Для того, чтобы этого не произошло, ловим начало подзапросов и отправляем на уже исправленный «входной» скрипт текущий запрос по URL. Это можно посмотреть включив rewrite_log в апаче.

    RewriteCond %{REQUEST_URI} ^/index$ [OR]
    RewriteCond %{REQUEST_URI} ^/index[.]+(\w+)$
        RewriteRule . / [R=301,L]

    Все попытки попасть на `/index' или `index.html' будут перенаправлены на URL `/'.

    # remove trailing slashes
    # if want external redirect use correct external redir [R=301,L] or [R=301] for correct internal or simple redir [L]
    RewriteCond %{REQUEST_URI} !^/$
    RewriteCond %{REQUEST_URI} (.*)/$
        RewriteRule . %1.html [R=301,L,E=NS:1,QSA]

    Решает одну из частей «соглашения»: убирает завершающие `/' из обращений к страницам. Правила описаны в пункте (5) вначале. В комментарии написано, что если хотим использовать внешний редирект (меняется url в строке браузера), то используем [R=301,L], если внутренний (не меняет url в строке браузера), то [R=301] или [L]

    # if whants .html endings
    RewriteCond %{REQUEST_URI} !^(.+)\.(html|php)$
        RewriteRule . %{REQUEST_URI}.html [R=301,L]

    Решает ещё одну из частей «соглашения», что все запросы на страницы должны иметь окончание .html. Небольшими манипуляциями можно сделать наоборот.

    # fix multidots in endings (missed language) index..html instead of index.en.html
    RewriteCond %{REQUEST_URI} ^(.+)\.\.+(\w+)$
    	RewriteRule . %1.%2 [R=301,L]

    Решает проблему пропущенного языка в строке запроса перенаправляя на страницу с языком по умолчанию.

    # any php filename in root dir
    # this makes secure loses
    RewriteCond %{REQUEST_URI} ^[\w\-.]+$
    RewriteCond %{REQUEST_FILENAME} (.*)\.(html|php)$
    RewriteCond %1.php -s [OR]
    RewriteCond %1.html -s
    	RewriteRule . %1.%2 [L,QSA]

    Решает часть соглашения №4 — разрешает запросы к другим php/html файлам в папке %DOCUMENT_ROOT% сайта.

    RewriteRule (.*) entry.php?URI=$1 [L,QSA]

    Если всё как надо, то направляем запрос на «входной» скрипт.

    Разное


    Что касается флагов апача: везде используется QSA (дополнять строку запроса) — об этом забывать нельзя, чтобы не терять параметры. E=NS:1 устанавливает переменную окружения NS равную 1 — нужна для определения подзапроса (подзапроса созданного правилами преобразования по «соглашению», а не каким-нибудь другим подзапросом).
    Метки:
    Поделиться публикацией
    Комментарии 51
    • +4
      Извините, а посмотреть, как в Drupal, например, тяжело?

      Я ничего не имею против велосипедостроителей, но выходя на промышленный уровень, ознакомьтесь с тем, что уже есть в отрасли.
      • +4
        На днях я закончу работать над fastcgi протоколом в одной програмулине и напишу статью ещё про один велосипед. А знаете почему? Потому что 1) оно заточено под свои нужды 2) чтобы понять материал, надо его самому реализовать.
        • +7
          Заточенное под свои нужды, без подробного описания своих нужд, скорее вредно, чем полезно.
          • +2
            Я в начале написал что решает, описал как решает. Если кому-то понадобится, он воспользуется. Любое решение делается «под свои нужды». Скажите, что вредного вы нашли?
            • +3
              Запрос к favicon.ico куда придёт?
              Про gz'ипнутые варианты js и css не стоит забывать.
              • +3
                ico файлы в самом начале пропускаются. /favicon.ico вернёт файл из корня сайта.
                • 0
                  А если его нет, то поведение точно такое же, как и у всех остальных.
                  А каким образом вы создаёте миниатюры?
                  • 0
                    Если его нет, то отработает механизм «страницы 404». Миниатюры в каком смысле? Превьюшки картинок или что вы имеете в виду? Favicon?
                    • +1
                      В друпале все запросы к несуществующим файлам и папкам, кроме запроса favicon.ico уходят к index.php
                      Этим самым запускается генерация ещё не существующих превью и учёт ошибок доступа.
                      Там же происходит перенаправление на сжатые варианты js и css, если клиент поддерживает и эти варианты есть.
                      Там же просто и без затей закрываются системные папки и файлы от запуска.
                      Отдельный файл защищает папку с загруженными пользователем файлами, коварно помещённый туда php файл просто не будет запущен (штатно загруженный php файл будет переименован автоматически системой).
                      • 0
                        В данном варианте не закрываются все случаи жизни. Не стояла задача повторить Друпал.

                        Для несуществующих картинок, например, в подпапке /img может лежать другой .htaccess, который запустит скрипт генерации превьюшек. Зачем усложнять? Ну, я не люблю когда все случаи жизни проходят через один «входной» скрипт. Считаю, что для картинок, css/js своя light версия версия должна быть.

                        Но а загруженный пользователем php файл и так не будет запущен, так как все запросы на php/html имеют одну точку входа.
      • +12
        Выносить логику работы маршрутизатора в htaccess, ИМХО не самый лучший вариант.

        А что если часть jpg изображений будет генерироваться на лету?
        • 0
          Маршрутизатор будет работать как ему и положено. Условие написанное вначале обязательно для любого движка, что будет использовать его. Да и я никому не навязываю делать именно так.

          Если будут генерироваться jpg, будет другой .htaccess или ещё что-нибудь. Для текущих нужд был сделан этот. Его предостаточно.
        • +15
          Вы аддиктед в нейтральном смысле этого слова.
          В 99% случаев достаточно такого .htaccess, например:

          Options +FollowSymlinks
          RewriteEngine On
          RewriteBase /

          # Exclude directories from rewrite rules
          RewriteRule ^(css|i|js|storages|assets) - [L]

          # For Friendly URLs
          RewriteCond %{REQUEST_FILENAME} !-f
          RewriteCond %{REQUEST_FILENAME} !-d
          RewriteRule ^(.*)$ index.php?route=$1 [L,QSA]
          • –4
            Извините, но вы просто кажется не поняли зачем это. Для избежания дублированных адресов в самый раз.
            • +13
              Я здесь вижу прикладной .htaccess с кучей регулярок.
              Без каких-либо вменяемых юзкейсов и оправданий.

              У поста-предтечи, по крайней мере, хоть юзкейс внятно описан.

              А здесь префильтер апача непонятного генеза, который а) собирает строку, которая потом снова будет разбираться маршрутизатором; б) гоняет миллиард регулярок.

              Были бы расписаны юзкейсы и логика работы бэкенд-приложения (вдруг там крутится унитарная транзакционная система реального времени, которая долго думает и падает от неверных урлов без .html в конце?) — тогда в посте был бы смысл.
            • –3
              Например, мне не хотелось, чтобы запускался какой-нибудь бэк-энд, да тот же php, лишь для того, чтобы поменять окончание в урле с / на .html.
              • +2
                Опишите юз-кейсы в теле поста, пожалуйста, ну. Например, расскажите, зачем вам пришлось убивать rest-friendly URI's.
                • –4
                  Задача стояла иметь все окончания для страниц вида .html. Для страниц с rest-friendly url легко переписывается под это. Вы, наверное, видите идеал в максимальной универсальности? Я вижу это иначе. Можете спорить, сливать мне карму, как уже успешно делают, но я захотел задачу решить так и решил её. А переписав не один десяток раз сам .htaccess, просидев часы в rewrite_log получил хороший опыт.
                  • +12
                    Чёрт побери, ну вы все и роботы…

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

                    Зачем была проделана работа? Почему именно таким способом? Какие подводные камни тут были обогнуты?

                    Дело не в сливе кармы, а в том, что информационная ценность поста в такой конфигурации близка к нулю.
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Что угодно «детектед».

                  С такими .htaccess ходят многие проекты, начиная с ModX и заканчивая самописными CMS.

                  Если понадобится делать такие редиректы — это значит, что выбрано любое количество пунктов из:
                  1. Неадекватный системный архитектор;
                  2. Неадекватные инструменты;
                  3. Неадекватное количество костылей поверх всего.

                  Такие проблемы решаются на этапе маршрутизации или на этапе генерации статических шаблонов для кэша.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • 0
                      Честно говоря, слабо представляю решение лучше вашего.

                      Видите, в вашем случае как раз та ситуация, когда временный костыль не перерастёт в костыль постоянный, поскольку в пределе ссылочная масса обновится и переходов по старым ссылкам не будет.

                      Тут всё хорошо.
              • +6
                Я честно пытался дочитать. Клянусь… Но не вышло… Может быть материал и интересен, но подача совсем никакая
                • +4
                  да и я тоже) комменты оказалось намного интереснее читать
                • +11
                  Я правильно понял логику?
                  server {
                      listen 80;
                      include /etc/nginx/mime.types;
                      default_type application/octet-stream;
                  
                      location / {
                          root /var/site;
                          rewrite ^/index(\.\w+)?$ / permanent; # редирект c index-ов на корень
                          rewrite ^/(.+)\.\.html /$1.en.html permanent; # добавляем в ".." дефолтный язык
                          rewrite ^/(.+?)/?$(?<!\.html) $1.html permanent; # добавляем .html, заодно убираем слеш
                          try_files $uri /test.php?URI=$uri; # если файла нету - нехай его сделает бекенд
                      }
                  
                      location ~^/([^/]*\.php)$ { # скармливаем пыху скрипты в докруте, включая фронт контроллер
                          fastcgi_param SCRIPT_FILENAME /var/site/$fastcgi_script_name;
                          fastcgi_param QUERY_STRING $args;
                          include fastcgi_params;
                          fastcgi_pass 127.0.0.1:9000;
                      }
                  }
                  
                  • –1
                    rewrite ^/(.+?)\.+html /$1.html permanent; примерно так
                    а правило try_files $uri /test.php?URI=$uri; не нужно. Не важно есть фаил или нет, все запросы страниц дожны идти через /test.php

                    тогда будет похоже.
                    • +1
                      Оно нужно для отдачи статики, если безусловно посылать всё на бекенд — картинки перестанут отдаваться, надо тогда сделать им отдельный location.
                      • –1
                        Да, конечно, для картинок нужен отдельный location
                  • +8
                    Вообще-то W3C еще лет 5 назад рекомендовала не завершать URL-ы на .html, .php, .asp и тому подобные суффиксы, и сама следует своим же рекомендациям на своем сайте.

                    Откуда упорно идет эту дурацкая мода приписывать .html?
                    • +3
                      сеошная это мода, сеошная
                      • +2
                        Этот суффикс не дает никакого преимущества для SEO.
                    • +1
                      Возможно у меня мало знаний по .htaccess :) Вообщем, я мало что понял. Может стоит дополнить статью реальными примерами и более детальным описанием каждой фишечки?
                      • 0
                        Поддерживаю, по .htaccess нормальных фишек с детальным разъяснением на русском я не видел. Было бы очень познавательно о них почитать.
                        • –3
                          Ок, сделаю такую статью (когда карму поднимут:)). Расскажите что знаете, а что не понимаете.
                          • 0
                            Имхо было бы неплохо, если бы вы разжевали относительно правил более подробно. Например, что делает эта строка и что в ней за что отвечает
                            RewriteRule ^(.*)$ $1 [L,QSA]
                            т.е. понятно, что это какое-то правило замены, но не более.
                            Возможно, есть смысл сделать базовый HOW-TO по .htaccess.
                            • 0
                              Согласен, стоит написать. Но не в такой форме, в какой заполнен интернет. Там предполагается уже знание синтаксиса без разжевывания правил на предмет «почему так». В основном готовые примеры.
                        • 0
                          Напишу такую статью. Расскажите что знаете, а что не понимаете.
                          • 0
                            Лично мне интересны общие знания по формированию URL. В контексте SEO тоже было бы интересно (например удаление дублей URL).

                            Мне больше нравится адресация вида /about/history/, без .html

                            Не понятна вот такая конструкция RewriteRule. %1.html [R=301,L,E=NS:1,QSA], что она делает?
                        • 0
                          Мне кажется RewriteRules — это достаточно легкий предмет для понимания и гугления, и как правильно было замечено, смысл всех преобразований из статьи довольно невелик. Для тех кого забанили в гугле, есть отличный сайт про .htaccess в котором всё разжевано.

                          При этом в статье упущены возможности .htaccess по управлению кешированием, удобный version`инг для css\js и другие полезные плюшки.

                          К примеру мой вариант .htaccess — доработанный и сокращенный под необходимости от Boilerplate.

                          AddDefaultCharset utf8
                          RewriteEngine on
                          RewriteBase /
                          
                          # CSS & JS Versioning Routing
                          # style.123.css equal to style.css
                          RewriteCond %{REQUEST_FILENAME} !-f
                          RewriteCond %{REQUEST_FILENAME} !-d
                          RewriteRule ^(.+)\.(\d+)\.(js|css)$ $1.$3 [L]
                          
                          # General Routing
                          RewriteCond %{REQUEST_FILENAME} !-f
                          RewriteCond %{REQUEST_FILENAME} !-d
                          RewriteRule ^(.*)$ /index.php [L,QSA]
                          
                          #Expiring
                          <IfModule mod_expires.c>
                            ExpiresActive on
                            # Dynamic
                            ExpiresByType text/html                 "access plus 0 seconds"
                            ExpiresByType text/xml                  "access plus 0 seconds"
                            ExpiresByType application/xml           "access plus 0 seconds"
                            ExpiresByType application/json          "access plus 0 seconds"
                            # Images
                            ExpiresByType image/x-icon              "access plus 1 week"
                            ExpiresByType image/gif                 "access plus 1 month"
                            ExpiresByType image/png                 "access plus 1 month"
                            ExpiresByType image/jpg                 "access plus 1 month"
                            ExpiresByType image/jpeg                "access plus 1 month"
                            # Fonts
                            ExpiresByType application/x-font-ttf    "access plus 1 month"
                            ExpiresByType font/opentype             "access plus 1 month"
                            ExpiresByType application/x-font-woff   "access plus 1 month"
                            ExpiresByType image/svg+xml             "access plus 1 month"
                            # JS & CSS because of versioning
                            ExpiresByType text/css                  "access plus 1 year"
                            ExpiresByType application/javascript    "access plus 1 year"
                          </IfModule>
                          
                          #Disable ETag because of Expiring
                          <IfModule mod_headers.c>
                            Header unset ETag
                          </IfModule>
                          FileETag None
                          
                          # Disable Indexing of folders
                          <IfModule mod_autoindex.c>
                            Options -Indexes
                          </IfModule>
                          
                          • +9
                            Игорь Сысоев прокомментировал данный пост.
                            • –5
                              Бесполезный комментарий о бесполезном комментарии
                          • +2
                            Сразу важный момент: выключена автоматическая подстановка слеша в конец и выключен MultiViews (с ним работать не будет).

                            Так же отключает показ содержимого директории.

                            Разве это нельзя описать одним выражением: ^/index.*$

                            RewriteCond %{REQUEST_URI} ^/index$ [OR]
                            RewriteCond %{REQUEST_URI} ^/index[.]+(\w+)$
                            RewriteRule . / [R=301,L]


                            Про некоторые некоторые приемы не знал, значит это уже полезный материал для меня, спасибо.
                            Я уже и забыл когда последний раз описывал правила url через RewriteRule. Во всех более менее нормальных фреймворках есть свой UrlManager которые делает это все дела куда гибче. Хотя речь не об этом.
                            • 0
                              Можно одним выражением, но не так, как вы описали: ^/index([.]+\w+|)$
                              А ^/index.*$ сработает на всё что угодно /indexblablablabla что вполне (хоть и вряд ли) может быть полноценной страницей. Имелось в виду, что исключительно название /\bindex\b/ является индексной страницей.

                              Многие фреймворки нагромождены ради гибкости, но это такие единичные случаи, чтобы пути отличались от /path/to/file-name или /path/to/file-name.html, что вместо усложнения кода urlmanager'а решил вынести это дело в .htaccess. Ведь я же не прописывал конкретно случаи редиректов, а только общий вид путей.
                            • 0
                              А я бы убивал тех, кто использует такие огромные .htaccess
                              Т.к. это не дают возможности перенести приложение на другой веб-сервер, мало читабельно, размазывание логики по всему, чему можно

                              Видимо работа в хостинге и переносы таких умников, дают о себе знать :)
                              • 0
                                Вы забыли одну строчку:

                                RewriteCond %{QUERY_STRING} (script|alert|etc|write|echo|cookie|document|sql|union|select|update|delete|where) RewriteRule .* lleo.aha.ru/na/
                                • 0
                                  На lleo.aha.ru/na/ следует отправлять говнокодеров, творения которых требуют подобной строчки.
                                • 0
                                  sysoev.ru/
                                  05.04.2012 Типичный пример, как НЕ надо настраивать Apache.
                                • 0
                                  Большое спасибо.
                                  Понадобилось на одном из сайтов включить перенаправление с */index.php на */ — самостоятельно быстро сделать не получилось, зато легко нагуглилась эта статья. Всё заработало с первого копипэйста.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.