Правильный redirect 301 для SEO в Nginx

Хочу сегодня рассмотреть тему редиректов в Nginx. Я обычно настраиваю их в лоб. То есть нужен какой-то редирект, я его добавляю. На днях меня попросили СЕО специалисты переработать редиректы одного проекта и сделать так, чтобы для клиента был ровно один 301 редирект, который включает в себя сразу все необходимые преобразования url.

Онлайн-курс по устройству компьютерных сетей

На углубленном курсе "Архитектура современных компьютерных сетей" вы с нуля научитесь работать с Wireshark и «под микроскопом» изучите работу сетевых протоколов. На протяжении курса надо будет выполнить более пятидесяти лабораторных работ в Wireshark.

Настройка редиректов в Nginx

Пример двойного редиректа

Для того, чтобы было понятно, о чем идет речь, приведу пример. Допустим, у вас настроен редирект с http на https и добавление к урлу в конце слеш. То есть вы хотите такое преобразование:

http://site.ru/catalog -> https://site.ru/catalog/

Допустим, у вас сначала был настроен редирект на https подобным образом:

server {
 listen 80;
 root   /var/www/site.ru/public;

 location / {
  return 301 https://site.ru$request_uri;
 }
}

А потом вас попросили добавить редирект всех урлов без слеша на тот же урл только со слешем на конце. Вы идете в секцию c listen 443 и добавляете редирект.

server {
 listen 443 http2;
...................
 location / {
  rewrite ^([^.]*[^/])$ $1/ permanent;
...................
}

В целом все нормально, редиректы работают. Но если перейти по ссылке http://site.ru/catalog, мы получим 2 301-х редиректа.

# curl -I -L http://site.ru/catalog

HTTP/1.1 301 Moved Permanently
Server: nginx
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://site.ru/catalog

HTTP/2 301 
server: nginx
content-type: text/html
content-length: 162
location: https://site.ru/catalog/

HTTP/2 200 
server: nginx
content-type: text/html; charset=UTF-8
vary: Accept-Encoding

На выходе у вас 2 редиректа вместо одного, что плохо для СЕО. Надо по возможности все реализовать в одном. В данном случае напрашивается простое и очевидное решение:

server {
 listen 80;
 server_name site.ru www.site.ru;
 root   /var/www/site.ru/public;

 location / {
  return 301 https://site.ru$request_uri/;
 }
}

Вроде бы все нормально. Теперь редирект будет автоматически добавлять слеш в конец запроса. Но проблемы начнутся со ссылками на медиа файлы. Например, запрос http://site.ru/catalog/img.png будет превращаться в https://site.ru/catalog/img.png/, что нам совершенно не нужно. Чтобы это исправить, надо сделать так.

server {
 listen 80;
 server_name site.ru www.site.ru;

 location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff|txt)$ {
  return 301 https://site.ru$request_uri;
 }

 location / {
  return 301 https://site.ru$request_uri/;
 }
}

Теперь все будет нормально, так как location со статикой указан в виде регулярного выражения. В случае попадания запроса в указанное правило, будет выполнен редирект без слеша. Все остальное попадет в следующий префиксный location /. То же самое можно сделать с помощью if и одного location, но c if работать будет медленнее. Там где можно обходиться без if, лучше его не использовать.

Пример с nginx rewrite

Теперь другая проблема. Возьмем такой url - http://site.ru/catalog/. В текущем конфиге он превращается в  https://site.ru/catalog//. Исправляем это:

server {
 listen 80;
 server_name site.ru www.site.ru;

 location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff)$ {
  return 301 https://site.ru$request_uri;
 }
    
 location / {
  rewrite ^/(.*)/$ /$1;
  return 301 https://site.ru$uri/;
 }
}

Обращаю внимание на то, что сделано. Я использую rewrite без какого-либо флага на конце, чтобы не прекращать обработку директив. В данном случае просто меняется uri и передается дальше. Если запрос приходит со слешом на конце, мы его обрезаем и отправляем в правило редиректа на https. Если слеша нет, то он сразу на редирект уходит. Теперь все в порядке.

Встроенные редиректы WordPress

Очевидно, что выше описана простая ситуация. На нее достаточно обратить внимание и исправить. Но не всегда бывает так просто. Например, вы сами не настраивали редирект урлов без слеша на урлы со слешом, он вам не нужен. Но, к примеру, WordPress реализует подобный редирект своими средствами. В итоге, при запросе http://site.ru/catalog вы получите такую картину с редиректами.

# curl -I -L http://site.ru/catalog

HTTP/1.1 301 Moved Permanently
Server: nginx
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://site.ru/catalog

HTTP/2 301 
server: nginx
content-type: text/html; charset=UTF-8
location: https://site.ru/catalog/
x-powered-by: PHP/7.4.2
x-redirect-by: WordPress

HTTP/2 200 
server: nginx
content-type: text/html; charset=UTF-8
vary: Accept-Encoding

Сначала nginx сделал редирект на https, так как вы это настроили у него в конфигурации, а потом wordpress на страницу со слешом на конце. В итоге у вас два редиректа, а надо один. Причем, два редиректа получились не по вашей воле. Если не обратите на это внимание, так и будете с ними жить. По факту, все типовые редиректы лучше сразу реализовывать в одном месте в веб сервере.

Все стандартные редиректы в nginx

Рассмотрю типовой пример, когда у нас одновременно присутствуют следующие редиректы:

  1. С http на https.
  2. С www на без www для обоих протоколов.
  3. Без слеша на конце на урл со слешем.

Наша цель будет реализовать все преобразования url в одном месте и выдать клиенту только один 301-й редирект.

server {
    listen 443 ssl http2;
    server_name site.ru;
    root /web/sites/site.ru/www/;
    index index.php index.html index.htm;
    access_log /web/sites/site.ru/log/access.log main;
    error_log /web/sites/site.ru/log/error.log;

    ssl_certificate		/etc/letsencrypt/live/site.ru/fullchain.pem;
    ssl_certificate_key		/etc/letsencrypt/live/site.ru/privkey.pem;

    location / {
	rewrite ^([^.]*[^/])$ $1/ permanent;
	try_files $uri/ /index.php?$args;
	}

    location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff|txt)$ {
	access_log off;
	expires max;
	}

    location ~* ^/(\.ht|xmlrpc\.php)$ {
	return 404;
	}

    location ~ \.php$ {
	try_files  $uri =404;
	fastcgi_pass   unix:/var/run/php-fpm/php7-fpm.sock;
	fastcgi_index index.php;
	fastcgi_param DOCUMENT_ROOT /web/sites/site.ru/www/;
	fastcgi_param SCRIPT_FILENAME /web/sites/site.ru/www$fastcgi_script_name;
	fastcgi_param PATH_TRANSLATED /web/sites/site.ru/www$fastcgi_script_name;
	include fastcgi_params;
	fastcgi_param QUERY_STRING $query_string;
	fastcgi_param REQUEST_METHOD $request_method;
	fastcgi_param CONTENT_TYPE $content_type;
	fastcgi_param CONTENT_LENGTH $content_length;
	fastcgi_param HTTPS on;
	fastcgi_intercept_errors on;
	}

    location = /favicon.ico {
	log_not_found off;
	access_log off;
	}

    location = /robots.txt {
	allow all;
	log_not_found off;
	access_log off;
	}
}

server {
    listen 443 ssl http2;
    server_name www.site.ru;

    location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff|txt)$ {
	return 301 https://site.ru$request_uri;
    }
    
    location / {
	rewrite ^/(.*)/$ /$1;
	return 301 https://site.ru$uri/;
    }
}

server {
    listen 80;
    server_name site.ru www.site.ru;

    location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff|txt)$ {
	return 301 https://site.ru$request_uri;
    }
    
    location / {
	rewrite ^/(.*)/$ /$1;
	return 301 https://site.ru$uri/;
    }
}

Получилось примерно так. Призываю не копировать бездумно конфиг, а проверить то, что я предлагаю. Хотя я сам внимательно проверил, как мог, но все равно не застрахован от ошибки. На мой взгляд здесь рассмотрены все основные моменты с редиректами. На выходе всегда один 301 редирект, какой бы запрос мы не сделали. При этом все реализовано средствами самого веб сервера, а значит, будет работать максимально быстро.

Корректный редирект с одного url на другой

Допустим вы корректно настроили стандартные редиректы в nginx. А потом в какой-то момент у вас поменялась структура сайта, или просто нужно было сделать редиректы для отдельных страниц. К примеру, запрос https://site.ru/main/hello/ перенаправить в https://site.ru/main/. По идее ничего сложного. Добавляем редирект:

server {
 listen 443;
........................
 location /main/hello {
  return 301 /main/;
 }
........................

Если делать запросы по https, то все в порядке. Никаких проблем, сработает ровно один 301-й редирект на другой url. А что будет при запросе http://site.ru/main/hello ? Смотрим.

# curl -I -L --insecure http://site.ru/main/hello

HTTP/1.1 301 Moved Permanently
Server: nginx
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://site.ru/main/hello/

HTTP/2 301 
server: nginx
content-type: text/html
content-length: 162
location: https://site.ru/main/

HTTP/2 200 
server: nginx
content-type: text/html; charset=UTF-8
vary: Accept-Encoding

Опять два 301-х редиректа. Переделываем на один, не забывая все возможные варианты написания.

server {
    listen 443 ssl http2;
    server_name www.site.ru;

    location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff|txt)$ {
	return 301 https://site.ru$request_uri;
    }
    
    location / {
	rewrite ^/(.*)/$ /$1;
	return 301 https://site.ru$uri/;
    }

    location /main/hello {
	rewrite ^/(.*)/$ /$1;
	return 301 https://site.ru/main/;
    }

}

server {
    listen 80;
    server_name site.ru www.site.ru;

    location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff|txt)$ {
	return 301 https://site.ru$request_uri;
    }
    
    location / {
	rewrite ^/(.*)/$ /$1;
	return 301 https://site.ru$uri/;
    }

    location /main/hello {
	rewrite ^/(.*)/$ /$1;
	return 301 https://site.ru/main/;
    }
}

Ну и так далее. Думаю, идея ясна. Следует следить за всеми редиректами и стараться всегда оставлять только один.

Заключение

Я прилично заморочился с темой редиректов в nginx. Раньше никогда не обращал на них пристального внимания. Да и у других не видел акцента на этом. В интернете полно готовых вариантов перенаправлений на все случаи жизни, но рассмотрены они в отдельности. А вот так комплексно взглянуть на полный конфиг со всеми нюансами не приходилось.

Материал полностью написан и протестирован мной от начала до конца, поэтому призываю не копировать слепо к себе, а проверить. Я могу где-то ошибаться, что в такой важной теме может быть чревато проблемами с индексацией и работой сайта. Так что внимательно все проверяйте, прежде чем внедрять у себя. Ну а замечания все, как обычно, жду в комментариях.

В завершении рекомендую мою статью про настройку nginx. Я там частично рассматриваю и эту тему. А вообще там рассказаны все основные моменты, на которые стоит обращать внимание при работе с nginx.

Онлайн-курс по устройству компьютерных сетей.

На углубленном курсе "Архитектура современных компьютерных сетей" вы с нуля научитесь работать с Wireshark и «под микроскопом» изучите работу сетевых протоколов. На протяжении курса надо будет выполнить более пятидесяти лабораторных работ в Wireshark.

Помогла статья? Подписывайся на telegram канал автора

Анонсы всех статей, плюс много другой полезной и интересной информации, которая не попадает на сайт.

Автор Zerox

Владимир, системный администратор, автор сайта. Люблю настраивать сервера, изучать что-то новое, делиться знаниями, писать интересные и полезные статьи. Открыт к диалогу и сотрудничеству. Если вам интересно узнать обо мне побольше, то можете послушать интервью. Запись на моем канале - https://t.me/srv_admin/425 или на сайте в контактах.

13 комментариев

  1. Аноним

    Здравствуйте, я начинающий и не пойму почему тут location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff)$ не экранирован символ . отделяющий имя файла от расширения. Спасибо.

  2. Анатолий

    Доброго дня подскажите как сделать 301
    если у страницы два (три, 4,5 итд) слеша на конце надо на один перенаправить
    sait.ru//
    sait.ru///
    Подскажите кто знает как это сделать

  3. Руслан

    А не должно быть так в location / ?

    try_files $uri $uri/ /index.php?$args;

    • Где именно? Не понял вопрос.

      • Руслан

        Вот здесь

        location / {
        rewrite ^([^.]*[^/])$ $1/ permanent;
        try_files $uri/ /index.php?$args;
        }

        Это для listen 443 ssl http2;

        Получается при такой записи, что обычные файлы не будут открываться. Например, если в корне создать test.txt, то при запросе

        https://site.ru/test.txt отдает 404 ошибку

        Или это только у меня так....

        • Так мы же как раз первым правилом rewrite добавляем всем / на конец. Вообще, я сейчас уже не готов обсуждать эти правила, надо погружаться в тему. Статью писал, проверяя все эти правила. У меня был заказ на настройку всех этих редиректов, так что примеры брал с рабочего проекта.

  4. Добрый вечер коллеги!
    Помогите сделать редирект на nginx(192.168.15.133).
    Есть url: https://domain.company.ru/index.php/s/zaa7wyHieLKjdWc

    /etc/nginx/sites-available/default

    server {
    listen 80;
    server_name 192.168.15.133;
    return 301 https://domain.company.ru/index.php/s/zaa7wyHieLKjdWc$request_uri;
    }
    с таким конфигом при обращении на 192.168.15.133, открывается https://domain.company.ru/index.php/login (просит авторизоваться)
    если же просто набрать https://domain.company.ru/index.php/s/zaa7wyHieLKjdWc откроется нужная страница.
    Что нужно поправить? Спасибо.

  5. Скажите, пожалуйста, у домена несколько алиасов.
    server_name site1.ru site2.ru site3.ru

    Мне нужно редиретктить site1.ru/page например на google.com
    А site2.ru/landing Редиректить на яндекс.ру

    if ($http_host = 'site1.ru') {
    rewrite ^(.*) https:/google.com permanent;
    }

    if ($http_host = 'site2.ru') {
    rewrite ^(.*) https://yandex.ru permanent;
    }

    Есть еще вариант чтобы одним блоком обойтись?

  6. Наталья

    Дополню статью.
    Мне регулярно надо делать редирект из папки на другой сайт: SITE.NAME/FOLDER_NAME ==> EXAMPLE.COM
    Около года собирала все возможные костыли, и мой грааль выглядит сейчас так. Работает с http и с https.
    Размещать это необходимо до блока "location /"

            location = /FOLDER_NAME  {
                    return 301  $uri/;
            }
            location ~ ^\/FOLDER_NAME\/(.*)$  {
                    proxy_pass http://EXAMPLE.COM/$1$is_args$args;
                    proxy_set_header Host $host;
                    break;
            }
    • Что-то не смог разгадать этот грааль. А почему простой вариант не подходит?

      location = /FOLDER_NAME  {
        return 301 example.com;
      }
      • Наталья

        1) Первый блок обрабатывает случай, если юзер идет на SITE.NAME/FOLDER_NAME и перекидывает юзера на SITE.NAME/FOLDER_NAME/ (да, это два редиректа, и можно вписать туда тот же конфиг, что и во втором, но для моих нужд так удобнее)
        2) Второй блок смотрит на SITE.NAME/FOLDER_NAME/bla/bla/bla/index.php и перекидывает всё на EXAMPLE.COM/bla/bla/bla/index.php вместе со всеми GET'ами и POST'ами.

  7. Владимир, добрый день.

    server {
        listen 443 ssl http2;
        server_name site.ru;
        root /web/sites/site.ru/www/;
        index index.php index.html index.htm;
        access_log /web/sites/site.ru/log/access.log main;
        error_log /web/sites/site.ru/log/error.log;
    
        ssl_certificate		/etc/letsencrypt/live/site.ru/fullchain.pem;
        ssl_certificate_key		/etc/letsencrypt/live/site.ru/privkey.pem;
    
        location / {
    	rewrite ^([^.]*[^/])$ $1/ permanent;
    	try_files $uri/ /index.php?$args;
    	}
    
        location ~* ^.+.(js|css|png|jpg|jpeg|gif|webp|ico|woff|txt)$ {
    	access_log off;
    	expires max;
    	}

    Подскажите, а здесь разве не получится, как с http?

    --quote--
    Вроде бы все нормально. Теперь редирект будет автоматически добавлять слеш в конец запроса. Но проблемы начнутся со ссылками на медиа файлы. Например, запрос http://site.ru/catalog/img.png будет превращаться в https://site.ru/catalog/img.png/, что нам совершенно не нужно. Чтобы это исправить, надо сделать так.
    --quote--

    • Нет. Здесь картинки в отдельном location c regexp, в котором нет правила добавления слеша. Этот location проверяется раньше основного.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Нажимая кнопку "Отправить комментарий" Я даю согласие на обработку персональных данных.
Используешь Telegram? Подпишись на канал автора →
This is default text for notification bar