WordPress и управление rewrite правилами

При переносе одного из сайтов на WordPress, также потребовалось перенести имеющийся каталог продукции. При этом было необходимо не только использовать ЧПУ, но и постараться сохранить прежние адреса элементов каталога.

Каталог двухуровневый и адресация имеет следующий вид:

/тип_продукции/прозвиодитель/имя_продукта

Типов продукции было всего два и в том самописном движке, с которого сайт портировался, каждый тип обслуживался своим скриптом. Если переносить данную модель на WordPress, то логично сделать две страницы, slug имена которых будут соответствовать типам продукции, а страницу производителя и конечного продукта генерировать налету, предварительно не создавая для них отдельных страниц в WP.

У WordPress есть механизм, позволяющий расширять стандартные правила роутинга, основанный на функции add_rewrite_rule(). Более того, в Сети хватает заметок о том, как им пользоваться и что необходимо предпринять, чтобы добиться желаемого результата. Но не одна из них, как оказалось, не описывает проблему полностью. Советы и решения не работают без напильника или описывают неочевидные вещи.

И так, решить задачу можно двумя способами:

  • локально, то есть через используемый шаблон и файл functions.php этого шаблона;
  • разработкой плагина, что несколько сложнее, но идеологически правильно.

Задача решается на WordPress версии 3.3.1

Независимо от того, какой способ будет выбран, принцип работы одинаковый. Описываем в файле плагина или в файле functions.php функцию, например, с именем addRewrite(). В теле функции пишем следующий код:

function addRoutes() {
    add_rewrite_rule('(tyres|discs)/([a-z]+)/?$', 'index.php?pagename=$matches[1]&producer=$matches[2]', 'top');
}

Функция add_rewrite_rule() добавляет правило роутинга или рерайта, если выражаться терминологией, принятой WP. В качестве первого параметра передается регулярное выражение, описывающее допустимое значение uri. Второй параметр задает перенаправление.




В данном конкретном случае, добавляемое правило будет работать для страницы, слаг имя (slug) которой или tyres (каталог шин), или discs (каталог дисков). Второй сегмент, описанный шаблоном ([a-z]+), будет содержать имя производителя. В рамках решения задачи, которую я упоминал вначале, должен быть описан и третий сегмент, где будет передано наименование конкретного товара из каталога. Но я опустил данную секцию, дабы не загромождать регулярное выражение.

Нюансы использования add_rewrite_rule():

  • правило не должно начинаться со слеша. Как вы можете видеть по коду примера, перед (tyres|discs) слеш не стоит;
  • нумерация элементов в массиве $matches начинается с 1 (единицы), а не с нуля;
  • функция может быть вызвана несколько раз, каждый раз добавляя новое правило. Третий параметр задает позицию добавляемого правила в общем стеке правил. В данном примере они добавляются в начало. Значение умолчания – bottom.

Теперь, с помощью add_action() вешаем наш хук на «событие» init:

add_action('init', 'addRoutes');

Это пример чистого использования WordPress функции add_rewrite_rule(). В таком виде работать ничего не будет.

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

Для исправления ситуация необходимо задействовать функцию add_rewrite_tag(), которая регистрирует дополнительные переменные, передаваемые скрипту.

add_rewrite_tag('%producer%', '([^&]+)');

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

Нюансы использования add_rewrite_tag():

  • вызов функции должен произойти до процесса инициализации или во время него. Ситуация, когда функция вызывается внутри хука, повешенного на событие init, наиболее удобна;
  • первый параметр должен принимать имя, которое обернуто знаками % (процент).

Еще одна особенность, о которой почти нигде не говорится, заключается в том, что WordPress кэширует правила переадресации. Кэшируются они в базу данных, в таблицу wp_options. Именем опции является rewrite_rules. В качестве значения хранится сериализованный массив.

Если кэш не сбросить, то ваше новое правило работать не будет. Сбросить кэш можно тремя способами:

  • создать новую страницу или изменить существующую в панели WordPress;
  • в панели WP, на странице «постоянные ссылки» пересохранить правила формирования ЧПУ;
  • вызвать функцию flush_rewrite_rules().

Функция flush_rewrite_rules() должна вызываться после того, как вы добавили свои правила.

И так, рабочий пример кода выглядит следующим образом:

function addRoutes() {
    add_rewrite_tag('%producer%', '([^&]+)');
    add_rewrite_rule('(tyres|discs)/([a-z]+)/?$', 'index.php?pagename=$matches[1]&producer=$matches[2]', 'top');

    flush_rewrite_rules();
}

add_action('init', 'addRoutes');

Если этот код засунуть в файл functions.php, то функция flush_rewrite_rules() будет вызываться при каждом обновлении страницы. Это совершенно неправильно, конечно. В рабочих проектах так делать недопустимо.

Для получения доступа к значению зарегистрированной переменной необходимо использовать объект $wp_query и его свойство query_vars.

Не пытайтесь общаться с данным объектом из файла functions.php вашей темы, так как массив $wp_query->query_vars будет пустым и свою переменную вы там не найдете. Но в теле шаблона все будет так, как и ожидается:

echo $wp_query->query_vars['producer'];

Выведет значение переданной переменной &producer.

Дальше, например, можно подставить это значение в SQL запрос и получить список товаров для требуемого производителя. Так делать некрасиво. Работать с моделью из представления – совсем дурной тон, но что уж поделаешь, коль в WordPress такой способ хорошо прижился и многими практикуется. В общем, это на вашей совести.

WordPress rewrite в плагине с использованием Shortcode API

Идеологически верным является написание небольшого плагина, который сделает все тоже самое, но не будет вынуждать нас писать логику в шаблоны. Самый простой вариант – реализация через WordPress Shortcode API.

В тело страницы или записи блога вставляем код shortcode, например такой [my_catalog]. В файле плагина пишем тот же самый код, что и в случае с шаблонами и добавляем к нему регистрацию нового шорткода:

function addRoutes() {
    add_rewrite_tag('%producer%', '([^&]+)');
    add_rewrite_rule('(tyres|discs)/([a-z]+)/?$', 'index.php?pagename=$matches[1]&producer=$matches[2]', 'top');

    flush_rewrite_rules();
}

add_action('init', 'addRoutes');

function catalogShortCode() {
    echo get_query_var('producer');
}
add_shortcode('my_catalog', 'catalogShortCode');

Внутри функции catalogShortCode() пишем остальную логику, где уже работаем с БД и прочее.

В целом, точно также, через shortcode, можно работать и в файле functions.php, но вариант с плагином более изящен и практичен. Если вы примете решение сменить тему оформления, вам не придется копировать код из одного шаблона в другой. А если у вас мультидоменная установка WP, то без плагина не обойтись в принципе.

Использование flush_rewrite_rules() по уму.

В документации WordPress дается правильный пример сброса кэша правил роутинга. Предлагается вот такой вариант:

function my_flush_rules(){
    // Получаем текущее состояне кэша
	$rules = get_option( 'rewrite_rules' );

	// Если наше правило в кэше отсутствует, сбрасываем кэш
	if ( ! isset( $rules['(project)/(\d*)$'] ) ) {
		global $wp_rewrite;
	   	$wp_rewrite->flush_rules();
	}
}

Как вы могли заметить, массив $rules является ассоциативным, где ключами служат регулярные выражения, передаваемые в качестве значения первого параметра функции add_rewrite_rule().

Комментарии (17)

  1. Большое спасибо, ваша статья помогла.
    не забывайте перед echo $wp_query->query_vars[‘producer’];
    global $wp_query;

  2. add_rewrite_tag(‘%pagetype%’, ‘([^&]+)’);
    add_rewrite_rule(‘^(catalog)/([^/]*)/?’, ‘index.php?pagename=$matches[1]&pagetype=$matches[2]’, ‘bottom’);

    Если я иду по адресу:
    http://мой сайт/catalog/?pagename=derevo

    В результате если страничка с url = http://мой сайт/catalog/derevo/ существует, то переход происходит на этот адрес, а если такой страницы не существует, то мы видим «…Запрошенную информацию найти не удалось….».

    Разве так и должно быть. Почему мы не останемся на странице http://мой сайт/catalog/ при этом чтоб был url http://мой сайт/catalog/derevo/

    (в конкретном случае derevo это элемент каталога, в каталоге много элементов и если под каждый делать свою страницу, то страниц будет нереально много, а надо красивый url )

    • Да, так и должно быть. Параметр pagename используется самим WordPress и если он передан с каким-либо значением, WP пытается найти соответствующую страницу.

      Используйте другое имя параметра, например, catalog_page.

      • Попробовал как вы сказали, заменил параметр:
        add_rewrite_tag(‘%pagetype%’, ‘([^&]+)’);
        add_rewrite_rule(‘^(catalog)/([^/]*)/?’, ‘index.php?el_name=$matches[1]&pagetype=$matches[2]’, ‘bottom’);

        Ввожу в адресную строку
        http://мой сайт/catalog/?el_name=norka

        Переход не происходит, вообще ничего не происходит.

        Пробовал:
        add_rewrite_tag(‘%el_name%’, ‘([^&]+)’);
        add_rewrite_tag(‘%pagetype%’, ‘([^&]+)’);
        add_rewrite_rule(‘^(catalog)/([^/]*)/?’, ‘index.php?el_name=$matches[1]&pagetype=$matches[2]’, ‘bottom’);

        Тоже ничего не происходит.

        • А «catalog» это у вас что? Если slugname страницы, то его нужно передавать тоже. Тогда ваш первый вариант был верным и «index.php?pagename=$matches[1]» — это правильно. Я неверно понял, пожалуй.

          Только /catalog/?pagename=derevo должно выглядеть иначе, например, так

          /?pagename=catalog&pagetype=derevo

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

  3. http://мой сайт/catalog/

    catalog — это существующая страница на которой у меня плагином выводится список товаров

    дальше я хочу чтобы по нажатию на единицу товара мне показывалась подробная информация только этого единственного товара (как в нормальном каталоге), но чтобы url был красивый, типа: /catalog/derevo1/, а не /catalog/?el_name=derevo1

    • В своем первом варианте вы были на верном пути. Только там есть еще одна ошибка. Правило необходимо добавлять не в конец, а в начало, чтобы оно шло прежде, чем правила самого WP. Значением третьего параметра функции add_rewrite_rule должно быть «top»

  4. Вобщем так у меня ничего и не вышло =(
    Спасибо за помощь

    • Последовательно проверьте, все ли сделано верно. Например, сохраняется ли ваше правило в массиве правил. Выведите на печать get_option( ‘rewrite_rules’ ). Ваше правило должно быть одним из первых элементов массива (может быть чуть ниже, если другие плагины добавляли свои после вашего).

      Не забываете ли вы сбрасывать кэш? Передается ли ваш параметр приложению? Сделайте var_dump() для объекта $wp_query. Регулярное выражение проверьте.

      Как минимум, пример из заметки должен работать, так как он выдернут из живого проекта.

  5. Елена

    Здравствуйте, Олег!
    Никак не могу справиться с задачей, может Вы поймете, что я делаю не так.
    Есть кастомная таксономия town и обычная категория
    Цель-иметь такой вид ссылок:
    Домен/город/категория/подкатегория/пост
    Домен/город/категория/подкатегория (архив постов относящихся к городу, категории и подкатегории)
    Домен/город/категория (архив постов относящихся к городу и категории)
    Например
    Домен/helsinki/activitety/muzei/post (должен показать пост относящийся к подкатегории музеи категории активитеты в городе Хельсинки.
    В фаил functions.php добавляю код
    function addRoutes() {
    add_rewrite_tag(‘%town%’, ‘(.+?)’);
    add_rewrite_rule(‘(.+?)/(.+?)/([^/]+)(/[0-9]+)?/?$’, ‘index.php?town=$matches[1]&category_name=$matches[2]&name=$matches[3]&page=$matches[4]’, ‘top’);
    add_rewrite_rule(‘(.?.+?)(/[0-9]+)?/?$’, ‘index.php?pagename=$matches[1]&page=$matches[2]’, ‘top’);
    flush_rewrite_rules();
    }
    add_action(‘init’, ‘addRoutes’);
    В настройках постоянных ссылок задаю структуру/ %town%/%category%/%postname%/
    , но это не работает
    Домен/город/категория/подкатегория/пост –выдает ошибку ,похоже не понимает, что такое town
    домен/town/porvoo/ пишет статей нет-хотя они есть
    Что я делаю не так? Подскажите,пожалуйста!

    • Мне кажется, у вас что-то с таксономиями. Без ЧПУ все работает так, как задумано или тоже возникают ошибки? На первый взгляд, правила вы добавляете верно, хотя утверждать не могу, так как не проверял.

  6. Елена

    а в таком виде
    http://domen/helsinki/aktivitety/muzei/ ошибка
    http://domen/helsinki/aktivitety/muzei/post тоже ошибка
    назначаю ЧПУ, как / %town%/%category%/%postname%/

    function add_custom_taxonomies_town() {
    register_taxonomy(‘town’, ‘post’, array(
    ‘hierarchical’ => true,
    ‘labels’ => array(
    ‘name’ => _x( ‘Города’, ‘taxonomy general name’ ),
    ‘singular_name’ => _x( ‘Город’, ‘taxonomy singular name’ ),
    ‘search_items’ => __( ‘Найти города’ ),
    ‘all_items’ => __( ‘Все города’ ),
    ‘parent_item’ => __( ‘Родительский город’ ),
    ‘parent_item_colon’ => __( ‘Родительский город:’ ),
    ‘edit_item’ => __( ‘Редактировать город’ ),
    ‘update_item’ => __( ‘Обновить город’ ),
    ‘add_new_item’ => __( ‘Добавить новый город’ ),
    ‘new_item_name’ => __( ‘Название нового города’ ),
    ‘menu_name’ => __( ‘Города’ ),
    ),
    ‘rewrite’ => array(
    ‘slug’ => ‘town’,
    ‘with_front’ => false,
    ‘hierarchical’ => true
    ),
    ));
    }

  7. Елена

    Пыталась решить проблему таким способом.
    Добавила в function.php код
    function town_permalink($permalink, $post_id, $leavename) {
    if (strpos($permalink, ‘%town%’) === FALSE) return $permalink;

    // Get post
    $post = get_post($post_id);
    if (!$post) return $permalink;

    // Get taxonomy terms
    $terms = wp_get_object_terms($post->ID, ‘town’);
    if (!is_wp_error($terms) && !empty($terms) && is_object($terms[0])) $taxonomy_slug = $terms[0]->slug;
    else $taxonomy_slug = ‘not-town’;

    return str_replace(‘%town%’, $taxonomy_slug, $permalink);}

    В админке /%town%/%category%/%postname%/

    тогда ссылки вида
    domen/helsinki/aktivitety/postname работают
    domen/town/helsinki тоже работает
    domen/category/activitety работают
    domen/helsinki/aktivitety работает

    но мне нужны ссылки с подкатегориями, а они не работают
    domen/helsinki/aktivitety/muzei -не работает
    domen/helsinki/aktivitety/muzei/postname не работает

  8. Доброго дня!

    может подскажите.. как сделать перенаправление:

    domain1.ltd/../../.jpg на domain2.ltd/../../../.jpg ?

  9. Андрей

    Здравствуйте, помогите пожалуйста с проблемой.

    Нужно чтобы метка открывалась как поддомен, например site.ru?tag=dom было доступно по адресу dom.site.ru

    сервер для работы с поддоменами настроен.

    function sd_category_rewrite_rules( $rules ) {

    $url = getenv( ‘HTTP_HOST’ );// для получения слага метки из урл dom.site.ru
    $domain = explode( «.», $url );
    $categorystr = $domain[0]; // тут остается только dom

    $rules = array();
    $rules[‘$’] = ‘index.php?tag=’.categorystr;
    return $rules;
    }
    add_filter( ‘rewrite_rules_array’, ‘sd_category_rewrite_rules’ );

    Вот почему то если брать слаг метки из урл и передавать его в переменную $categorystr, то ничего не работает.
    $categorystr = $domain[0]; //в таком виде ошибка 404

    хотя если я сам укажу к переменной слаг метки вот так:
    $categorystr = ‘dom’; то все работает.

    Подскажите, в чем может быть проблема?

  10. Станочник

    $after – не обязательный параметр, который говорит, куда добавить новое правило, перед остальными правилами или после них.

Добавить комментарий для Мурашов Олег Отменить ответ

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