WordPress. Плагин «Транслитерация URL»
Исторически сложилось, что латиница предпочтительнее в URL, нежели кириллица. Автоматический транслит WordPress не поддерживает, и чтобы система научилась самостоятельно производить транслитерацию, нужен плагин. Их сущестует великое множество, давайте и мы создадим свой велосипед. Заодно посмотрим, какие подводные камни есть на этом пути.
Итак, создаем директорию tokmakov-translit
и внутри нее файл tokmakov-translit.php
:
<?php /* Plugin Name: Транслитерация URL Plugin URI: https://tokmakov.msk.ru Description: Преобразует символы кириллицы в латиницу при создании новой записи, страницы, рубрики или метки. Version: 1.0 Author: Евгений Токмаков Author URI: https://tokmakov.msk.ru */ register_activation_hook(__FILE__, function() { // проверяем права пользователя на активацию плагинов if (!current_user_can('activate_plugins')) { return; } // изначально транслитерация выключена $settings = [ 'onoff' => 'off', // транслитерация slug включена? 'clean' => 'off', // выполять доп.замены в slug? 'exist' => 'off' // были конвертированы старые? ]; update_option('tokmakov_translit', $settings); }); register_deactivation_hook(__FILE__, function() { // проверяем права пользователя на деактивацию плагинов if (!current_user_can('deactivate_plugins')) { return; } });
Для начала создадим страницу настроек плагина, которая в панели управления будет выглядеть так:
Как видите, у плагина есть довольно подробное описание:
Slug — это несколько слов, которые нужно задать при создании записи, страницы, рубрики или метки. Slug используются для создания постоянных ссылок. Если slug не задан, он будет создан автоматически из заголовка записи, страницы, рубрики или метки. Если загловок содержит кириллицу, то при создании slug будет произведено URL-кодирование. Например, слово «корова» будет преобразовано в «%D0%BA...%D0%B0».
И примечания, которые объясняют, как правильно использовать настройки:
- Используйте настройку «Преобразовать существующие» только один раз. Перед этим крайне желательно сделать резервную копию базы данных.
- Если нужны дополнительные замены при конвертации существующих записей, рубрик и меток — отметьте checkbox «Производить дополнительные замены».
- Если нужны дополнительные замены при создании новых записей, страниц, рубрик и меток — отметьте checkbox «Производить дополнительные замены».
- Настройка «Производить дополнительные замены» не будет работать без «Включить транслитерацию», но будет работать при транслитерации существующих.
Плагин имеет три настройки: включить транслитерацию новых записей, включить дополнительные преобразования и преобразовать существующие. Третий checkbox отвечает за единоразовую конвертацию существующих slug
записей блога, страниц, рубрик и меток. Хотя настроек три, в таблице БД wp_options
сохраняется только одна опция tokmakov_translit
в виде сериализованного массива:
[ 'onoff' => 'off', // транслитерация slug включена? 'clean' => 'off', // выполять доп.замены в slug? 'exist' => 'off' // были конвертированы старые? ]
Если была выполнена конвертация существующих, нам нужно выполнять 301 редиректы со старых URL на новые. В WordPress уже есть функционал редиректа для записей блога, если был изменен slug
поста. Но еще нет функционала редиректа для рубрик, меток и страниц. Так что для редиректа со старых URL постов используем возможности WordPress. А для редиректа со старых URL рубрик, меток и страниц — напишем свой код.
Итак, создаем страницу настроек плагина транслитерации:
/*
**********************************************************
* Код ниже относится к настройкам плагина транслитерации *
**********************************************************
*/
/* * Добавляем страницу настроек плагина, пункт меню этой * страницы будет дочерним для нативного пункта меню WP * верхнего уровня «Настройки» */ add_action('admin_menu', function () { add_options_page( // содержимое тега title этой страницы 'Транслитерация URL', // название пункта меню для этой страницы 'Транслитерация URL', // права доступа, чтобы был показан этот пункт меню 'manage_options', // уникальный идентификатор меню (slug) 'tokmakov_translit_page', // функция выводит содержимое этой страницы function () { ?> <div class="wrap"> <h2>Плагин транслитерации</h2> <p> Slug — это несколько слов, которые нужно задать при создании записи, страницы, рубрики или метки. Slug используются для создания постоянных ссылок. Если slug не задан, он будет создан автоматически из заголовка записи, страницы, рубрики или метки. Если загловок содержит кириллицу, то при создании slug будет произведено URL-кодирование. Например, слово «корова» будет преобразовано в «%D0%BA%D0%BE%D1%80%D0%BE%D0%B2%D0%B0». </p> <p> Плагин автоматически преобразует символы кириллицы в латиницу при создании новой записи, страницы, рубрики или метки. Таким образом в URL слово «корова» будет представлено как «korova». </p> <form method="post" action="options.php"> <?php settings_fields('tokmakov_translit_page'); do_settings_sections('tokmakov_translit_page'); submit_button(); ?> </form> <p>Примечания:</p> <ul> <li> Используйте настройку «Преобразовать существующие» только один раз. Перед этим крайне желательно сделать резервную копию базы данных. </li> <li> Если нужны дополнительные замены при конвертации существующих записей, рубрик и меток — отметьте checkbox «Производить доп.замены». </li> <li> Если нужны дополнительные замены при создании новых записей, страниц, рубрик и меток — отметьте checkbox «Производить доп.замены». </li> <li> Настройка «Производить доп.замены» не будет работать без «Включить транслитерацию», но будет работать при транслитерации существующих. </li> </ul> <?php } ); });
/* * 1. Регистрируем опцию для плагина * 2. Добавляем новую секцию для опции * 3. Добавляем поля формы настроек */ add_action('admin_init', function () { /* * 1. Регистрируем опцию для плагина */ register_setting( // страница меню, куда будет добавлена эта опция 'tokmakov_translit_page', // уникальный идентификатор этой опции (slug) 'tokmakov_translit', /* * Функция нужна, чтобы обработать опцию перед сохранением. Но * мы ее будем использовать еще и для того, чтобы преобразовать * кириллицу в латиницу для уже существующих записей, страниц, * рубрик и меток. */ function ($option) { $exist = get_option('tokmakov_translit')['exist']; $option = [ 'onoff' => 'off', 'clean' => 'off', 'exist' => $exist ]; if (isset($_POST['tokmakov_translit']['onoff'])) { $option['onoff'] = 'on'; // доп.замены можно включить, только если уже включена // транлитерация новых записей, страниц, рубрик, меток if (isset($_POST['tokmakov_translit']['clean'])) { $option['clean'] = 'on'; } } if (isset($_POST['tokmakov_translit_exist']) && 'off' == $exist) { $option['exist'] = 'on'; $clean = isset($_POST['tokmakov_translit']['clean']); tokmakov_convert_existing_slugs($clean); } return $option; } ); /* * 2. Регистрируем секцию для опции */ add_settings_section( // уникальный идентификатор секции 'tokmakov_translit_section', // заголовок секции нам не нужен 'Настройки транслитерации', // функция выводит описание секции function () {}, // страница, куда будет добавлена эта секция 'tokmakov_translit_page' ); /* * 3.1. Добавляем поле формы для редактирования опции */ add_settings_field( 'tokmakov_translit_onoff', // идентификатор поля формы 'Включить транслитерацию', // заголовок поля формы function () { // выводит html-код поля формы $value = get_option('tokmakov_translit')['onoff']; $checked = ''; if ($value == 'on') { $checked = 'checked'; } ?> <input type="checkbox" name="tokmakov_translit[onoff]" id="tokmakov_translit_onoff" value="1" <?= $checked; ?>> <em> Включить транслитерацию для новых записей, страниц, рубрик, меток и загружаемых медиа-файлов. </em> <?php }, 'tokmakov_translit_page', // страница меню 'tokmakov_translit_section' // идентификатор секции ); /* * 3.2. Добавляем поле формы для редактирования опции */ add_settings_field( 'tokmakov_translit_clean', // идентификатор поля формы 'Производить доп.замены', // заголовок поля формы function () { // выводит html-код поля формы $value = get_option('tokmakov_translit')['clean']; $checked = ''; if ($value == 'on') { $checked = 'checked'; } ?> <input type="checkbox" name="tokmakov_translit[clean]" id="tokmakov_translit_clean" value="1" <?= $checked; ?>> <em> Кроме транслитерации, из slug будут удалены все символы, кроме дефиса, подчеркивания, <code>a-z</code> и <code>0-9</code>. </em> <?php }, 'tokmakov_translit_page', // страница меню 'tokmakov_translit_section' // идентификатор секции ); /* * 3.3. Добавляем поле формы для редактирования опции */ add_settings_field( 'tokmakov_translit_exist', // идентификатор поля формы 'Преобразовать существующие', // заголовок поля формы function () { // выводит html-код поля формы $value = get_option('tokmakov_translit')['exist']; $disabled = ''; if ($value == 'on') { $disabled = 'disabled'; } ?> <input type="checkbox" name="tokmakov_translit_exist" id="tokmakov_translit_exist" value="1" <?= $disabled; ?>> <em> Преобразовать кириллицу в латиницу для уже существующих записей, страниц, рубрик и меток. </em> <?php }, 'tokmakov_translit_page', // страница меню 'tokmakov_translit_section', // идентификатор секции ['label_for' => 'tokmakov_translit_old'] ); });
Дальше идет код, который отвечает за преобразование кириллицы в латиницу.
/*
****************************************************************
* Код ниже конвертирует кириллицу в латиницу для новых постов, *
* страниц, рубрик и меток. Или преобразует уже существующие. *
****************************************************************
*/
/* * При вызове функций sanitize_title() и sanitize_file_name() * будем вмешиваться и заменять кириллицу на латиницу */ if ('on' == get_option('tokmakov_translit')['onoff']) { add_filter('sanitize_title', 'tokmakov_convert_slug'); if ('on' == get_option('tokmakov_translit')['clean']) { add_filter('sanitize_title', 'tokmakov_clean_slug'); } add_filter('sanitize_file_name', 'tokmakov_convert_slug'); }
/* * Функция позволяет произвести транслитарацию slug */ function tokmakov_convert_slug($slug) { $replace = array( 'А' => 'A', 'а' => 'a', 'Б' => 'B', 'б' => 'b', 'В' => 'V', 'в' => 'v', 'Г' => 'G', 'г' => 'g', 'Д' => 'D', 'д' => 'd', 'Е' => 'E', 'е' => 'e', 'Ё' => 'Jo', 'ё' => 'jo', 'Ж' => 'Zh', 'ж' => 'zh', 'З' => 'Z', 'з' => 'z', 'И' => 'I', 'и' => 'i', 'Й' => 'J', 'й' => 'j', 'К' => 'K', 'к' => 'k', 'Л' => 'L', 'л' => 'l', 'М' => 'M', 'м' => 'm', 'Н' => 'N', 'н' => 'n', 'О' => 'O', 'о' => 'o', 'П' => 'P', 'п' => 'p', 'Р' => 'R', 'р' => 'r', 'С' => 'S', 'с' => 's', 'Т' => 'T', 'т' => 't', 'У' => 'U', 'у' => 'u', 'Ф' => 'F', 'ф' => 'f', 'Х' => 'H', 'х' => 'h', 'Ц' => 'C', 'ц' => 'c', 'Ч' => 'Ch', 'ч' => 'ch', 'Ш' => 'Sh', 'ш' => 'sh', 'Щ' => 'Shh', 'щ' => 'shh', 'Ъ' => '', 'ъ' => '', 'Ы' => 'Y', 'ы' => 'y', 'Ь' => '', 'ь' => '', 'Э' => 'E', 'э' => 'e', 'Ю' => 'Ju', 'ю' => 'ju', 'Я' => 'Ya', 'я' => 'ya' ); return strtr($slug, $replace); }
/* * Функция убирает из slug все, кроме [-_A-Za-z0-9] */ function tokmakov_clean_slug($slug) { $slug = preg_replace('~[^-_A-Za-z0-9]~', '-', $slug); $slug = preg_replace('~[-]+~', '-', $slug); $slug = preg_replace('~^-+~', '', $slug); $slug = preg_replace('~-+$~', '', $slug); return $slug; }
/* * Функция позволяет произвести транслитарацию slug для * всех уже существующих записей, страниц, рубрик, меток */ function tokmakov_convert_existing_slugs($clean = false) { global $wpdb; /* * Посты */ $query = "SELECT `ID`, `post_name` FROM `".$wpdb->posts."` WHERE `post_name` REGEXP('[^-_A-Za-z0-9]') AND `post_status` IN ('publish', 'future', 'private') AND `post_type` = 'post'"; $posts = $wpdb->get_results($query); foreach ($posts as $post) { $new_slug = tokmakov_convert_slug(rawurldecode($post->post_name)); if ($clean) { $new_slug = tokmakov_clean_slug($new_slug); } if ($post->post_name != $new_slug) { add_post_meta($post->ID, '_wp_old_slug', $post->post_name); $wpdb->update( $wpdb->posts, ['post_name' => $new_slug], ['ID' => $post->ID] ); } } /* * Страницы */ $query = "SELECT `ID`, `post_name` FROM `".$wpdb->posts."` WHERE `post_name` REGEXP('[^-_A-Za-z0-9]') AND `post_status` IN ('publish', 'future', 'private') AND `post_type` = 'page'"; $posts = $wpdb->get_results($query); // сюда будем записывать старые slug, которые будем добавлять // в таблицу БД wp_postmeta, чтобы случайно не добавить дважды $old_slugs = []; // сюда будем записывать новые slug, чтобы перезаписать старые // значения slug в таблице БД wp_posts $new_slugs = []; foreach ($posts as $post) { $new_slug = tokmakov_convert_slug(rawurldecode($post->post_name)); if ($clean) { $new_slug = tokmakov_clean_slug($new_slug); } if ($post->post_name != $new_slug) { /* * Страница может быть родителем для другой страницы. И это * влияет на недоступность по старому URL как этой страницы, * так и ее потомков. Например, сейчас работаем со страницей * «вторая». У нее есть родительская страница «первая» и * дочерняя страница «третья». Нам надо сохранить старые slug: * «первая/вторая» и «первая/вторая/третья». Но, когда будем * работать со страницей «третья», нам уже не нужно сохранять * старый slug «первая/вторая/третья». Мы сохранили его раньше. */ // нам нужно сохранить старый slug не только текущей страницы, // но и всех ее потомков; при этом slug будет иерархический $children = tokmakov_all_children($post->ID); array_unshift($children, $post->ID); foreach ($children as $child) { // получаем родителей, потому что slug иерархический $parents = get_post_ancestors($child); array_unshift($parents, $child); $parents = array_reverse($parents); $items = []; foreach ($parents as $parent) { $items[] = get_post($parent)->post_name; } $parents = implode('/', $items); // этот старый slug уже мог быть добавлен на // одной из предыдущих итераций цикла if (!in_array($parents, $old_slugs)) { $old_slugs[] = $parents; add_post_meta($child, '_tt_old_slug', $parents); } } $new_slugs[$post->ID] = $new_slug; } } // записываем в таблицу БД wp_posts новые значения slug foreach ($new_slugs as $id => $value) { $wpdb->update( $wpdb->posts, ['post_name' => $value], ['ID' => $id] ); } /* * Метки */ $query = "SELECT `".$wpdb->terms."`.`term_id` AS `term_id`, `".$wpdb->terms."`.`slug` AS `slug` FROM `".$wpdb->terms."` INNER JOIN `".$wpdb->term_taxonomy."` USING (`term_id`) WHERE `".$wpdb->term_taxonomy."`.`taxonomy` = 'post_tag' AND `".$wpdb->terms."`.`slug` REGEXP('[^-_A-Za-z0-9]')"; $terms = $wpdb->get_results($query); foreach ($terms as $term) { $new_slug = tokmakov_convert_slug(rawurldecode($term->slug)); if ($clean) { $new_slug = tokmakov_clean_slug($new_slug); } if ($term->slug != $new_slug) { add_term_meta( $term->term_id, '_tt_old_slug', $term->slug ); // записываем в таблицу БД wp_terms новое значение slug $wpdb->update( $wpdb->terms, ['slug' => $new_slug], ['term_id' => $term->term_id] ); } } /* * Рубрики */ $query = "SELECT `".$wpdb->terms."`.`term_id` AS `term_id`, `".$wpdb->terms."`.`slug` AS `slug` FROM `".$wpdb->terms."` INNER JOIN `".$wpdb->term_taxonomy."` USING (`term_id`) WHERE `".$wpdb->term_taxonomy."`.`taxonomy` = 'category' AND `".$wpdb->terms."`.`slug` REGEXP('[^-_A-Za-z0-9]')"; $terms = $wpdb->get_results($query); // сюда будем записывать старые slug, которые будем добавлять // в таблицу БД wp_termmeta, чтобы случайно не добавить дважды $old_slugs = []; // сюда будем записывать новые slug, чтобы перезаписать старые // значения slug в таблице БД wp_terms $new_slugs = []; foreach ($terms as $term) { $new_slug = tokmakov_convert_slug(rawurldecode($term->slug)); if ($clean) { $new_slug = tokmakov_clean_slug($new_slug); } if ($term->slug != $new_slug) { /* * Это иерархическая таксономия, и это влияет на недоступность * по старому URL как этой рубрики, так и ее потомков. */ // нам нужно сохранить старый slug не только текущей рубрики, // но и всех ее потомков; при этом slug будет иерархический $children = get_term_children($term->term_id, 'category'); array_unshift($children, $term->term_id); foreach ($children as $child) { $parents = get_term_parents_list( $child, 'category', [ 'format' => 'slug', 'separator' => '/', 'link' => false, 'inclusive' => true ] ); $parents = trim($parents, '/'); // этот старый slug уже мог быть добавлен на // одной из предыдущих итераций цикла if (!in_array($parents, $old_slugs)) { $old_slugs[] = $parents; add_term_meta( $child, '_tt_old_slug', $parents ); } } $new_slugs[$term->term_id] = $new_slug; } } // записываем в таблицу БД wp_terms новые значения slug foreach ($new_slugs as $id => $value) { $wpdb->update( $wpdb->terms, ['slug' => $value], ['term_id' => $id] ); } }
/* * Вспомогательная функция, возвращает всех потомков страницы */ function tokmakov_all_children($id) { $children = get_children([ 'post_parent' => $id, 'post_type' => 'page', 'post_status' => ['publish', 'future', 'private'] ]); $ids = []; foreach ($children as $child) { $ids[] = $child->ID; $ids = array_merge($ids, tokmakov_all_children($child->ID)); } return $ids; }
Давайте разберем функцию преобразования существующих записей, рубрик и меток подробно. При изменении slug
записи блога через панель управления, WordPress добавляет новую запись в таблицу БД wp_postmeta
. Например, мы изменили some-slug
на other-slug
для поста 1234, при этом будет добавлена запись:
post_id => 1234 meta_key => _wp_old_slug meta_value => some-slug
Когда посетитель зайдет на страницу www.example.com/some-slug/
, он будет перенаправлен на страницу www.example.com/other-slug/
. Если slug
поста будет изменен еще раз на another-slug
, WordPress добавит еще одну запись в таблицу БД:
post_id => 1234 meta_key => _wp_old_slug meta_value => other-slug
Теперь, если посетитель зайдет по адресу www.example.com/some-slug/
или www.example.com/other-slug/
, он будет перенаправлен на страницу www.example.com/another-slug/
.
Когда запущена транслитерация существующих записей типа post
, мы поступаем аналогично — добавляем в таблицу БД wp_postmeta
новую запись со старым slug
-ом. И редиректы за нас будет выполнять WordPress. А вот с записями типа page
уже сложнее — функционал редиректа для страниц еще не реализован. Так что надо это делать самим — при этом учитывать, что страницы поддерживают иерархию, так что записывать надо не «чистый» slug
страницы (как в случае с post
), а с учетом всех предков.
post_id => 2345 meta_key => _tt_old_slug meta_value => parent-slug/current-slug
С таксономиями ситуация аналогична — в WordPress функционал редиректа для рубрик и меток еще не реализован. Так что делаем это сами, опять-таки учитывая, что таксономия меток плоская, а таксономия рубрик — иерархическая. Поэтому для рубрик надо добавлять записи в таблицу БД wp_termmeta
с учетом родителей:
term_id => 3456 meta_key => _tt_old_slug meta_value => parent-slug/current-slug
Дальше идет код, который отвечет за редиректы после конвертации существующих:
/*
***********************************************************
* Код ниже выполняет 301 редирект со старого URL на новый *
***********************************************************
*/
/* * Выполняем редиректы со старых URL страниц, рубрик и меток * на новые, если была конвертация существующих */ if ('on' == get_option('tokmakov_translit')['exist']) { add_filter('init', 'tokmakov_redirect_page'); add_filter('request', 'tokmakov_redirect_term'); }
/* * Функция выполняет редирект со старого URL страницы на новый */ function tokmakov_redirect_page() { $request = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $request = trim($request, '/'); if (!preg_match('~^[^/]+?(/[^/]+)*$~', $request)) { return; } global $wpdb; // пробуем найти старый slug, который был до преобразования $query = "SELECT `".$wpdb->posts."`.`ID` FROM `".$wpdb->posts."` INNER JOIN `".$wpdb->postmeta."` ON `".$wpdb->posts."`.`ID` = `".$wpdb->postmeta."`.`post_id` WHERE `".$wpdb->posts."`.`post_type` = 'page' AND `".$wpdb->posts."`.`post_status` IN ('publish', 'future', 'private') AND `".$wpdb->postmeta."`.`meta_key` = '_tt_old_slug' AND `".$wpdb->postmeta."`.`meta_value` = %s ORDER BY `".$wpdb->postmeta."`.`meta_id` DESC LIMIT 1"; $query = $wpdb->prepare($query, $request); $page_id = (int)$wpdb->get_var($query); // если старый slug найден — выполняем редирект if (!empty($page_id)) { wp_redirect(get_permalink($page_id), 301); die(); } };
/* * Функция выполняет редирект со старого URL рубрики или метки на новый */ function tokmakov_redirect_term($query_vars) { global $wpdb; if (isset($query_vars['tag']) || isset($query_vars['category_name'])) { if (isset($query_vars['category_name'])) { $slug = $query_vars['category_name']; $taxonomy = 'category'; } if (isset($query_vars['tag'])) { $slug = $query_vars['tag']; $taxonomy = 'post_tag'; } // пробуем найти старый slug, который был до преобразования $query = "SELECT `".$wpdb->terms."`.`term_id` FROM `".$wpdb->terms."` INNER JOIN `".$wpdb->termmeta."` USING (`term_id`) INNER JOIN `".$wpdb->term_taxonomy."` USING (`term_id`) WHERE `".$wpdb->term_taxonomy."`.`taxonomy` = '".$taxonomy."' AND `".$wpdb->termmeta."`.`meta_key` = '_tt_old_slug' AND `".$wpdb->termmeta."`.`meta_value` = '".$slug."' ORDER BY `".$wpdb->termmeta."`.`meta_id` DESC LIMIT 1"; $term_id = (int)$wpdb->get_var($query); // если старый slug найден — выполняем редирект if (!empty($term_id)) { wp_redirect(get_term_link($term_id, $taxonomy), 301); die(); } } return $query_vars; }
Давайте рассмотрим этот код подробнее. Как видите, для страниц и таксономий он вешается на разные события. Это сделано неспроста. Шаблон URL страниц иногда может совпадать с шаблоном URL записи блога. Простейший пример:
http://www.example.com/some-page/ http://www.example.com/some-post/
Или вот еще пример — дочерняя страница child-page
и страница вложения поста some-attachment
:
http://www.example.com/some-page/child-page/ http://www.example.com/some-post/some-attachment/
В этом случае WordPress проводит дополнительную проверку на предмет того, что страница с таким slug
существует. И для этого выполняет запрос к базе данных — чтобы правильно установить имена переменных запроса и их значений. Но в нашем случае запрос к БД не поможет, потому что slug
мы уже перезаписали. И переменные запроса скорее всего будут установлены неправильно. Поэтому мы выполянем редирект для страниц на хуке init
, еще до того, как WordPress начнет разбирать URL и выполнять эту проверку.
А редирект для страниц рубрик и меток выполняем на хуке request
, когда URL уже разобран и переменные запроса известны. И редирект выполняем только в том случае, когда запрошена рубрика или метка.
И последее, что нам осталось сделать — создать файл uninstall.php
:
<?php if (!defined('WP_UNINSTALL_PLUGIN')) { exit; } global $wpdb; // если была конвертация существующих — удаляем записи, которые // мы добавили в таблицы wp_postmeta и wp_termmeta $wpdb->delete( $wpdb->postmeta, ['meta_key' => '_tt_old_slug'] ); $wpdb->delete( $wpdb->termmeta, ['meta_key' => '_tt_old_slug'] ); delete_option('tokmakov_translit');
- WordPress. Постоянные ссылки и преобразование URL
- WordPress. Фильтр записей по произвольным полям. Часть 3 из 3
- WordPress. Добавляем мета-теги. Часть 3 из 3
- WordPress. Добавляем мета-теги. Часть 2 из 3
- Битрикс. Система обработки адресов
- WordPress. Фильтр записей по произвольным полям. Часть 2 из 3
- WordPress. Фильтр записей по произвольным полям. Часть 1 из 3
Поиск: CMS • SEO • Web-разработка • Плагин • ЧПУ • WordPress • URL