Удаление префикса таксономии из URL иерархической пользовательской таксономии
Я создал таксономию 'forum', используя следующие правила:
register_taxonomy(
'forum',
array('topic'),
array(
'public' => true,
'name' => _a('Форумы'),
'singular_name' => _a('Форум'),
'show_ui' => true,
'show_in_nav_menus' => true,
'hierarchical' => true,
'labels' => array(
'name' => _a('Форумы'),
'singular_name' => _a('Форум'),
'search_items' => _a('Поиск форумов'),
'popular_items' => _a('Популярные форумы'),
'all_items' => _a('Все форумы'),
'parent_item' => _a('Родительский форум'),
'parent_item_colon' => _a('Родительский форум:'),
'edit_item' => _a('Редактировать форум'),
'update_item' => _a('Обновить форум'),
'add_new_item' => _a('Добавить новый форум'),
'new_item_name' => _a('Название нового форума'),
),
'query_var' => true,
'rewrite' => array('slug' => 'forums', 'with_front' => false, 'hierarchical' => true),
)
);
На фронтенде URL выглядят так:
forums/general-discussion/sub-forum
Как мне убрать префикс ("forums")? То есть, изменить URL на:
general-discussion/sub-forum
Если я передаю пустой аргумент slug в register_taxonomy(), это работает, но это вызывает проблемы с постоянными ссылками типа записей, связанных с этой таксономией

ОБНОВЛЕНИЕ
После написания этой статьи в ядро WordPress был добавлен хук 'do_parse_request'
, который позволяет элегантно обрабатывать маршрутизацию URL без необходимости расширять класс WP
. Я подробно рассмотрел эту тему в своем докладе на WordCamp Atlanta 2014 под названием "Жесткая маршрутизация URL"; слайды доступны по ссылке.
ОРИГИНАЛЬНЫЙ ОТВЕТ
Дизайн URL важен для меня уже более десяти лет; я даже написал об этом блог несколько лет назад. И хотя WordPress в целом является блестящим программным обеспечением, к сожалению, его система перезаписи URL оставляет желать лучшего (ИМХО, конечно. :) В любом случае, приятно видеть людей, заботящихся о дизайне URL!
Ответ, который я предоставлю, это плагин под названием WP_Extended
, который является доказательством концепции для этого предложения на Trac (Обратите внимание, что предложение начиналось как одно, а превратилось в другое, поэтому нужно прочитать полностью, чтобы понять его направление.)
Основная идея заключается в создании подкласса класса WP
, переопределении метода parse_request()
и затем присвоении глобальной переменной $wp
экземпляра этого подкласса. Внутри parse_request()
вы фактически анализируете путь по сегментам, вместо использования списка регулярных выражений, которые должны полностью соответствовать URL.
Чтобы было ясно, эта техника вставляет логику перед parse_request()
, которая проверяет соответствие URL регулярным выражениям, но сначала ищет совпадения с терминами таксономии, и ТОЛЬКО заменяет parse_request()
, оставляя всю остальную систему маршрутизации WordPress нетронутой, включая и особенно использование переменной $query_vars
.
Для вашего случая использования эта реализация только сравнивает сегменты пути URL с терминами таксономии, так как это все, что вам нужно. Эта реализация проверяет термины таксономии с учетом родительско-дочерних отношений и при нахождении совпадения присваивает путь URL (без начальных и конечных слэшей) переменным $wp->query_vars['category_name']
, $wp->query_vars['tag']
или $wp->query_vars['taxonomy']
& $wp->query_vars['term']
и обходит метод parse_request()
класса WP
.
С другой стороны, если путь URL не соответствует термину из указанной вами таксономии, он делегирует логику маршрутизации URL системе перезаписи WordPress, вызывая метод parse_request()
класса WP
.
Чтобы использовать WP_Extended
для вашего случая, вам нужно вызвать функцию register_url_route()
из файла functions.php
вашей темы следующим образом:
add_action('init','init_forum_url_route');
function init_forum_url_route() {
register_url_route(array('taxonomy'=>'forum'));
}
Вот исходный код плагина:
<?php
/*
Имя файла: wp-extended.php
Название плагина: WP Extended for Taxonomy URL Routes
Автор: Mike Schinkel
*/
function register_url_route($args=array()) {
if (isset($args['taxonomy']))
WP_Extended::register_taxonomy_url($args['taxonomy']);
}
class WP_Extended extends WP {
static $taxonomies = array();
static function on_load() {
add_action('setup_theme',array(__CLASS__,'setup_theme'));
}
static function register_taxonomy_url($taxonomy) {
self::$taxonomies[$taxonomy] = get_taxonomy($taxonomy);
}
static function setup_theme() { // Настройка темы - это первый код, выполняемый после создания WP.
global $wp;
$wp = new WP_Extended(); // Заменяем глобальную $wp
}
function parse_request($extra_query_vars = '') {
$path = $_SERVER['REQUEST_URI'];
$domain = str_replace('.','\.',$_SERVER['SERVER_NAME']);
//$root_path = preg_replace("#^https?://{$domain}(/.*)$#",'$1',WP_SITEURL);
$root_path = $_SERVER['HTTP_HOST'];
if (substr($path,0,strlen($root_path))==$root_path)
$path = substr($path,strlen($root_path));
list($path) = explode('?',$path);
$path_segments = explode('/',trim($path,'/'));
$taxonomy_term = array();
$parent_id = 0;
foreach(self::$taxonomies as $taxonomy_slug => $taxonomy) {
$terms = get_terms($taxonomy_slug);
foreach($path_segments as $segment_index => $path_segment) {
foreach($terms as $term_index => $term) {
if ($term->slug==$path_segments[$segment_index]) {
if ($term->parent!=$parent_id) { // Убеждаемся, что проверяем родителей
$taxonomy_term = array();
} else {
$parent_id = $term->term_id; // Захватываем ID родителя для проверки
$taxonomy_term[] = $term->slug; // Собираем slug как сегмент пути
unset($terms[$term_index]); // Нет необходимости сканировать его снова
}
break;
}
}
}
if (count($taxonomy_term))
break;
}
if (count($taxonomy_term)) {
$path = implode('/',$taxonomy_term);
switch ($taxonomy_slug) {
case 'category':
$this->query_vars['category_name'] = $path;
break;
case 'post_tag':
$this->query_vars['tag'] = $path;
break;
default:
$this->query_vars['taxonomy'] = $taxonomy_slug;
$this->query_vars['term'] = $path;
break;
}
} else {
parent::parse_request($extra_query_vars); // Делегируем классу WP
}
}
}
WP_Extended::on_load();
P.S. ПРЕДУПРЕЖДЕНИЕ №1
Хотя для конкретного сайта эта техника работает блестяще, эта техника НИКОГДА не должна использоваться в плагине для распространения на WordPress.org. Если она является основой программного пакета на основе WordPress, то это может быть допустимо. В противном случае эта техника должна быть ограничена улучшением маршрутизации URL для конкретного сайта.
Почему? Потому что только один плагин может использовать эту технику. Если два плагина попытаются использовать ее, они будут конфликтовать друг с другом.
Кроме того, эту стратегию можно расширить для универсальной обработки практически любого шаблона использования, и это то, что я намерен реализовать, как только найду свободное время или клиента, который сможет спонсировать время, необходимое для создания полностью универсальных реализаций.
ПРЕДУПРЕЖДЕНИЕ №2
Я написал это для переопределения parse_request()
, который является очень большой функцией, и вполне возможно, что я пропустил одно или два свойства глобального объекта $wp
, которые следовало установить. Поэтому если что-то работает некорректно, дайте мне знать, и я с радостью исследую это и при необходимости пересмотрю ответ.
В любом случае...

После написания этого я осознал, что тестировал для категорий, а не для таксономических терминов в целом, поэтому вышеуказанное не будет работать для таксономии 'forum'
, однако я пересмотрю это, чтобы работало позже сегодня...

Итак, я обновил код, чтобы решить проблему, о которой упомянул в предыдущем комментарии.

не получается заставить это работать... нужно ли изменять правила перезаписи?

@One Trick Pony - Немного больше диагностической информации помогло бы. :) Что вы пробовали? Что происходит, когда вы вводите URL в браузере? Вы случайно не назвали свою таксономию 'forums'
вместо 'forum'
? Вы ожидаете, что URL-адреса, которые ссылаются на эти страницы, изменятся (если да, то неудивительно, мой код не касается формирования URL-адресов, только их маршрутизации.)

нет, я могу изменить URL-адреса (думаю, для этого мне нужно перехватить функцию term_link). site/rootforum/
работает, а site/rootforum/subforum/
нет (ошибка 404) ...

@One Trick Pony - Является ли subforum
дочерним термином таксономии для rootforum
? Пожалуйста, предоставьте больше информации о вашей текущей настройке; действительно сложно предложить что-то рабочее, когда мы не уверены, как именно у вас всё настроено.

нет, это одна и та же таксономия. subforum
является дочерним термином для rootforum

@One Trick Pony - Что происходит, когда "это не работает?" Какие (полные) URL-адреса не работают? Какой фактический домен, где это не работает? URL для /forums/foo/bar
работает, а /foo/bar
нет? Дайте мне больше информации, и я помогу вам это исправить, но сейчас мне кажется, что у меня недостаточно данных для отладки, так как у меня всё работает отлично.

смотрите - http://dev.digitalnature.ro/wp99420/forum1/ - затем попробуйте перейти по http://dev.digitalnature.ro/wp99420/forum1/general-discussion/ (general-discussion
является подфорумом для forum1
)

если это актуально, вот полный код системы форума: http://pastebin.com/N509zCh3

@One Trick Pony - Что такое wp99420? Это корень сайта? Мой код в настоящее время не обрабатывает определение корня сайта в подкаталоге; это то, что вам нужно?

@One Trick Pony - Я обновил код для обработки сайтов в поддомене.

не работает, я всё равно получаю 404, но всё равно спасибо. Я только что узнал о плагине bbpress, так что больше не буду использовать это

@One Trick Pony - Очень хотелось бы разобраться, почему у вас это не работает. Я хочу сделать из этого плагин, который любой сможет использовать для любых URL, поэтому выяснение причины вашей проблемы было бы полезным.

при тестировании на localhost/wp
я получаю такой $path_segments: Array ( [0] => wp [1] => rootforum [2] => subforum )
. $taxonomy_term и $taxonomy_slug тоже в порядке, так что parse_request(), кажется, выполняет свою работу. Возможно, проблема в parent::parse_request(), который сбрасывает массив query_vars (см. строку 123 в class-wp)?

@One Trick Pony - Какое значение у вас установлено для WP_SITEURL
? Возвращает ли $_SERVER['SERVER_NAME']
значение 'localhost'
? Я обычно настраиваю "локальные" домены для тестирования (например, http://mytest.dev
вместо http://localhost/wp
), поэтому часто не замечаю ошибки, возникающие в нестандартных настройках.

@One Trick Pony - Я обновил код, чтобы он не зависел от WP_SITEURL; попробуйте снова?

то же самое... В любом случае, я нашел проблему, по крайней мере, в моей системе - это строка $path
, которую вы передаете в query_vars['term']. Она выглядит как rootforum/subform
(должна быть только последняя часть, subforum
)

@One Trick Pony - Разве rootforum
не является родительским термином для 'subforum'
в таксономии 'forum'
?

rootforum - это термин, а subforum - дочерний термин для rootforum. Оба являются частью таксономии forum

@MikeSchinkel: мне очень хочется, чтобы этот тикет в треке попал в ядро. Мне кажется, это отличная идея (насколько я могу судить). Кстати, есть ли у тебя этот плагин в репозитории WP?

Эй @MikeSchinkel, это вообще сдвинулось с мёртвой точки? Твои идеи просто великолепны и решают мои основные боли с WP. Этот плагин вообще появился?

@PeterB - Спасибо за добрые слова. Я всё ещё работаю над универсальным плагином, но подготовил подробный доклад на WordCamp про URL-маршрутизацию, слайды здесь: http://hardcorewp.com/2014/hardcore-url-routing-for-wordpress/

Всё просто.
Шаг 1: Полностью откажитесь от параметра rewrite. Мы будем создавать свои собственные правила перезаписи.
'rewrite'=>false;
Шаг 2: Установите подробные правила для страниц. Это заставляет обычные Страницы иметь свои собственные правила вместо того, чтобы быть универсальным решением в самом низу.
Шаг 3: Создайте несколько правил перезаписи для обработки ваших случаев использования.
Шаг 4: Вручную принудительно обновите правила перезаписи. Самый простой способ: перейдите в Настройки → Постоянные ссылки и нажмите кнопку "Сохранить изменения". Я предпочитаю этот способ активации плагина для собственного использования, так как могу принудительно обновлять правила, когда вношу изменения.
Итак, время кода:
function test_init() {
// создаем новую таксономию
register_taxonomy(
'forum',
'post',
array(
'query_var' => true,
'public'=>true,
'label'=>'Forum',
'rewrite' => false,
)
);
// принудительно включаем подробные правила.. это заставляет каждую Страницу иметь собственное правило,
// вместо универсального, которое мы будем использовать для таксономии форума
global $wp_rewrite;
$wp_rewrite->use_verbose_page_rules = true;
// два правила для обработки лент
add_rewrite_rule('(.+)/feed/(feed|rdf|rss|rss2|atom)/?$','index.php?forum=$matches[1]&feed=$matches[2]');
add_rewrite_rule('(.+)/(feed|rdf|rss|rss2|atom)/?$','index.php?forum=$matches[1]&feed=$matches[2]');
// одно правило для обработки пагинации записей в таксономии
add_rewrite_rule('(.+)/page/?([0-9]{1,})/?$','index.php?forum=$matches[1]&paged=$matches[2]');
// одно правило для отображения таксономии форума в обычном режиме
add_rewrite_rule('(.+)/?$', 'index.php?forum=$matches[1]');
}
add_action( 'init', 'test_init' );
Помните, что после добавления этого кода, он должен быть активным, когда вы обновляете правила постоянных ссылок (сохраняя страницу в Настройки → Постоянные ссылки)!
После обновления правил и сохранения в базе данных, путь /whatever должен вести на страницу вашей таксономии forum=whatever.
Правила перезаписи на самом деле не так сложны, если вы понимаете регулярные выражения. Я использую этот код для отладки:
function test_foot() {
global $wp_rewrite;
echo '<pre>';
var_dump($wp_rewrite->rules);
echo '</pre>';
}
add_action('wp_footer','test_foot');
Таким образом, я могу видеть текущие правила с первого взгляда на странице. Просто помните, что для любого URL система начинает проверять правила сверху вниз, пока не найдет совпадение. Совпадение затем используется для перезаписи запроса в более привычный вид ?ключ=значение. Эти ключи затем парсятся в объект WP_Query. Просто.
Примечание: Этот метод, вероятно, будет работать только если ваша обычная структура пользовательских записей начинается не с универсального параметра, например %category% или что-то подобное. Вам нужно начать её со статичной строки или числа, например %year%. Это нужно, чтобы предотвратить перехват вашего URL до того, как он дойдет до ваших правил.

Если вы хотите упростить отладку правил перезаписи, я (ещё раз) рекомендую свой плагин для анализа rewrite правил, который позволяет тестировать правила и видеть переменные запроса на лету.

К сожалению, текущая система перезаписи URL вынуждена сводить все потенциальные шаблоны URL в один большой список вместо следования присущей URL-путям древовидной структуре. Текущая настройка не может удобно сопоставлять массив литералов таких как категории или названия форумов; как вы знаете, она вынуждает все URL "Страниц" обрабатываться первыми. Сопоставление по сегментам пути и множественные способы сопоставления (массив литералов, категории, метки, термины таксономий, имена пользователей, типы записей, названия записей, колбэки, хуки фильтров и наконец регулярные выражения) лучше масштабировались бы для сложных случаев и были бы проще для понимания.

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

Спасибо, но кажется, я уже пробовал это раньше (http://wordpress.stackexchange.com/questions/9455/custom-post-type-permalinks-giving-404s)

К счастью, WordPress Answers теперь позволяет людям, которые действительно хотят контролировать свои URL наконец-то высказаться, и таких, похоже, много (100+). Но я понимаю, что вы, возможно, не сможете последовать моему примеру до полной реализации. Я предсказываю, что как только подход, который я предлагаю, будет полностью реализован в плагине, и пройдет около 6-12 месяцев, он станет предпочтительным способом маршрутизации URL для CMS-сайтов на WordPress. Так что давайте возобновим эту дискуссию примерно через 9 месяцев.

Вы не сможете добиться этого только с помощью WP_Rewrite, так как он не различает слаги терминов и слаги записей.
Вам также необходимо подключиться к хуку 'request' и предотвратить 404 ошибку, установив переменную запроса для записи вместо таксономии.
Что-то вроде этого:
function fix_post_request( $request ) {
$tax_qv = 'forum'; // Переменная запроса таксономии
$cpt_name = 'post'; // Название типа записи
if ( !empty( $request[ $tax_qv ] ) ) {
$slug = basename( $request[ $tax_qv ] ); // Получаем slug из URL
// Если это приведет к 404 ошибке
if ( !get_term_by( 'slug', $slug, $tax_qv ) ) {
// Устанавливаем правильные переменные запроса
$request[ 'name' ] = $slug;
$request[ 'post_type' ] = $cpt_name;
unset( $request[$tax_qv] ); // Удаляем переменную таксономии
}
}
return $request;
}
add_filter( 'request', 'fix_post_request' );
Обратите внимание, что таксономия должна быть определена до типа записи.
Это хороший момент, чтобы отметить, что использование одинаковой переменной запроса для таксономии и типа записи — Плохая Идея.
Кроме того, вы не сможете получить доступ к записям, чьи слаги совпадают с терминами.

Согласен, что использование одного и того же query var для таксономии и типа записи — плохая идея, но это может навести людей на мысль, что использование одного и того же названия для таксономии и типа записи — тоже плохая идея, что не так. Если используются одинаковые названия, то только у одного из них должен быть query var.

Я бы посмотрел код плагина Top Level Cats:
http://fortes.com/projects/wordpress/top-level-cats/
Вы могли бы легко адаптировать его для работы с вашей пользовательской таксономией, изменив
$category_base = get_option('category_base');
на строке 74 на что-то вроде:
$category_base = 'forums';

Я бы порекомендовал взглянуть на плагин Custom Post Permalinks. Сейчас у меня нет времени протестировать его, но, возможно, он поможет в вашей ситуации.

Поскольку я знаком с вашим другим вопросом, отвечу с учетом этого.
Я не тестировал этот код, но он может сработать, если выполнить его сразу после регистрации всех нужных вам permastruct:
class RRSwitcher {
var $rules;
function RRSwitcher(){
add_filter( 'topic_rewrite_rules', array( $this, 'topics' ) );
add_filter( 'rewrite_rules_array', array( $this, 'rules' ) );
}
function topics( $array ){
$this->rules = $array;
return array();
}
function rules( $array ){
return array_merge( (array)$array, (array)$this->rules );
}
}
$RRSwitcher = new RRSwitcher();
global $wp_rewrite;
$wp_rewrite->use_verbose_rules = true;
$wp_rewrite->flush_rules();
Что делает этот код: он удаляет правила перезаписи, сгенерированные из постоянных ссылок тем, из обычного потока массива правил и повторно объединяет их в конце массива. Это предотвращает их конфликт с другими правилами перезаписи. Затем он принудительно включает подробные правила перезаписи (каждая страница получает отдельное правило с конкретным регулярным выражением). Это предотвращает конфликт страниц с правилами ваших тем. Наконец, выполняется жесткий сброс (убедитесь, что ваш файл .htaccess доступен для записи, иначе это не сработает) и сохраняется очень большой и сложный массив правил перезаписи.

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

Не уверен, что это сработает для таксономий, но для пользовательских типов записей сработало
Хотя плагин не обновлялся 2 года, нижеуказанный плагин мне помог: http://wordpress.org/plugins/remove-slug-from-custom-post-type/
Для информации: я использую WP 3.9.1
с WP Types 1.5.7
