Структура URL для произвольного типа записей и таксономии
У меня есть произвольный тип записей products
и произвольная таксономия 'Категории продуктов' со слагом products
.
Моя цель: Я пытаюсь создать структуру URL следующим образом:
Архив типа записей Products: products/
Архивы категорий продуктов верхнего уровня: products/product-category
Архивы категорий продуктов: products/product-category/product-sub-category
Страница продукта: products/product-category/product-sub-category/product-page
Проблема:
Страницы продуктов, похоже, хотят использовать только фактические категории. Поэтому структура URL выглядит как products/uncategorized/product-page
. Страницы архивов категорий не сохраняют базовый путь CPT. Они получаются как category/sub-category
.
Что я пробовал: Бесчисленные поиски в Google, множество сниппетов (те, которые я помню, приведу ниже), несколько пользовательских функций и несколько плагинов. Безрезультатно.
У меня есть следующий код.
add_action( 'init', 'products_cpt', 0);
add_action( 'init', 'register_taxonomies', 0 );
// Произвольный тип записи для всех продуктов
function products_cpt(){
$labels = array(
'name' => _x('Products', 'Post Type General Name'),
'singular_name' => _x('Product', 'Post Type Singluar Name'),
'menu_name' => __('Products'),
'parent_item_colon' => __('Parent Product'),
'all_items' => __('All Products'),
'view_item' => __('View Product'),
'add_new_item' => __('Add New Product'),
'add_new' => __('Add New'),
'edit_item' => __('Edit Product'),
'update_item' => __('Update Product'),
'search_items' => __('Search Products'),
'not_found' => __('Not Found'),
'not_found_in_trash' => __('Not Found in Trash')
);
$supports = array(
'title',
'editor',
'excerpt',
'thumbnail',
'custom-fields',
'revisions'
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'exclude_from_search' => false,
'show_in_admin_bar' => true,
'show_in_nav_menus' => true,
'publicly_queryable' => true,
'query_var' => true,
'taxonomies' => array( 'chemicals' ),
'supports' => $supports,
'has_archive' => 'products'
);
register_post_type('products', $args);
}
function register_taxonomies() {
$taxonomies = array(
'products' => array(
'Product',
'Products'
),
);
foreach($taxonomies as $slug => $name){
create_product_taxonomy($slug,$name[0],$name[1]);
}
}
function create_product_taxonomy($slug, $singular, $plural) {
$labels = array(
'name' => _x( $singular.' Categories', 'Taxonomy General Name', 'text_domain' ),
'singular_name' => _x( $singular.' Category', 'Taxonomy Singular Name', 'text_domain' ),
'menu_name' => __( $singular.' Categories', 'text_domain' ),
'all_items' => __( 'All '.$singular.' Categories', 'text_domain' ),
'parent_item' => __( 'Parent '.$singular.' Category', 'text_domain' ),
'parent_item_colon' => __( 'Parent '.$singular.' Category:', 'text_domain' ),
'new_item_name' => __( 'New '.$singular.' Category Name', 'text_domain' ),
'add_new_item' => __( 'Add New '.$singular.' Category', 'text_domain' ),
'edit_item' => __( 'Edit '.$singular.' Category', 'text_domain' ),
'update_item' => __( 'Update '.$singular.' Category', 'text_domain' ),
'view_item' => __( 'View '.$singular.' Category', 'text_domain' ),
'separate_items_with_commas' => __( 'Separate '.$singular.' Categories with commas', 'text_domain' ),
'add_or_remove_items' => __( 'Add or remove '.$singular.' Categories', 'text_domain' ),
'choose_from_most_used' => __( 'Choose from the most used '.$singular.' Categories', 'text_domain' ),
'popular_items' => __( 'Popular '.$singular.' Categories', 'text_domain' ),
'search_items' => __( 'Search '.$singular.' Categories', 'text_domain' ),
'not_found' => __( 'Not Found', 'text_domain' ),
'no_terms' => __( 'No '.$singular.' Categories', 'text_domain' ),
'items_list' => __( $singular.' Categories list', 'text_domain' ),
'items_list_navigation' => __( $singular.' Categories list navigation', 'text_domain' ),
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => true,
'has_archive' => $plural,
'rewrite' => array(
'slug' => 'products',
'with_front' => true,
'hierarchical' => true
)
);
register_taxonomy( $slug, 'products', $args );
}
Во-первых, я установил настройки постоянных ссылок следующим образом:
Затем я попробовал добавить следующий код, однако он работал только для страниц продуктов. Архивы категорий продуктов по-прежнему выдавали ошибку 404.
add_filter( 'post_type_link', 'products_post_link', 1, 3 );
function products_post_link( $post_link, $id = 0 ){
$post = get_post($id);
if ( is_object( $post ) ){
$terms = wp_get_object_terms( $post->ID, 'products_category' );
$slug_url = '';
$last_id = 0;
if( $terms ){
foreach($terms as $term) {
if ($term === reset($terms)){
foreach($terms as $termInner){
if($termInner->term_id == 0){
$slug_url .= $termInner->slug.'/';
}
}
}elseif ($term === end($terms)){
foreach($terms as $termInner){
if($termInner->parent == $last_id){
$slug_url .= $termInner->slug;
$last_id = $termInner->term_id;
}
}
}else{
foreach($terms as $termInner){
if($termInner->parent == $last_id){
$slug_url .= $termInner->slug.'/';
$last_id = $termInner->term_id;
}
}
}
}
return str_replace( '%category%' , $slug_url , $post_link );
}
}
return $post_link;
}
И это в функцию инициализации CPT:
'rewrite' => array(
'slug' => 'products/%category%',
'with_front' => false,
'hierarchical' => true,
),
Я также пробовал следующее, не помню, что произошло, но помню, что это не сработало:
add_action('init', 'custom_resource_rewrite_rules');
function custom_resource_rewrite_rules() {
add_rewrite_rule('products/([A-Za-z0-9\-\_]+)/?', '$matches[1]', 'top');
}
Я также пытался работать с фильтром term_link
. Безуспешно.
Наконец, я попробовал следующие плагины, тоже безрезультатно.
Есть ли у кого-нибудь решение этой проблемы? Я уже в отчаянии.
После долгих поисков я наконец нашел решение!
Сначала: регистрируем пользовательский тип записи и пользовательскую таксономию:
add_action( 'init', 'register_sps_products_post_type' );
function register_sps_products_post_type() {
register_post_type( 'sps-product',
array(
'labels' => array(
'name' => 'Товары',
'menu_name' => 'Менеджер товаров',
'singular_name' => 'Товар',
'all_items' => 'Все товары'
),
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'comments', 'post-formats', 'revisions' ),
'hierarchical' => false,
'has_archive' => 'products',
'taxonomies' => array('product-category'),
'rewrite' => array( 'slug' => 'products/%product_category%', 'hierarchical' => true, 'with_front' => false )
)
);
register_taxonomy( 'product-category', array( 'sps-product' ),
array(
'labels' => array(
'name' => 'Категории товаров',
'menu_name' => 'Категории товаров',
'singular_name' => 'Категория товара',
'all_items' => 'Все категории'
),
'public' => true,
'hierarchical' => true,
'show_ui' => true,
'rewrite' => array( 'slug' => 'products', 'hierarchical' => true, 'with_front' => false ),
)
);
}
Далее добавляем новое правило перезаписи, чтобы WordPress понимал нашу новую структуру постоянных ссылок:
add_action( 'generate_rewrite_rules', 'register_product_rewrite_rules' );
function register_product_rewrite_rules( $wp_rewrite ) {
$new_rules = array(
'products/([^/]+)/?$' => 'index.php?product-category=' . $wp_rewrite->preg_index( 1 ), // 'products/любые-символы/'
'products/([^/]+)/([^/]+)/?$' => 'index.php?post_type=sps-product&product-category=' . $wp_rewrite->preg_index( 1 ) . '&sps-product=' . $wp_rewrite->preg_index( 2 ), // 'products/любые-символы/слаг-записи/'
'products/([^/]+)/([^/]+)/page/(\d{1,})/?$' => 'index.php?post_type=sps-product&product-category=' . $wp_rewrite->preg_index( 1 ) . '&paged=' . $wp_rewrite->preg_index( 3 ), // совпадение для пагинации в архиве подкатегории
'products/([^/]+)/([^/]+)/([^/]+)/?$' => 'index.php?post_type=sps-product&product-category=' . $wp_rewrite->preg_index( 2 ) . '&sps-product=' . $wp_rewrite->preg_index( 3 ), // 'products/любые-символы/подкатегория/слаг-записи/'
'products/([^/]+)/([^/]+)/([^/]+)/([^/]+)/?$' => 'index.php?post_type=sps-product&product-category=' . $wp_rewrite->preg_index( 3 ) . '&sps-product=' . $wp_rewrite->preg_index( 4 ), // 'products/любые-символы/подкатегория/под-подкатегория/слаг-записи/'
);
$wp_rewrite->rules = $new_rules + $wp_rewrite->rules;
}
Следующая функция исправляет проблему с подкатегориями. Проблема заключается в том, что при попытке загрузить страницу с URL вида faq/category/child-category/, WordPress пытается загрузить запись с слагом "child-category" вместо подкатегории с таким же слагом.
// Костыльное решение для поддержки гибких пользовательских постоянных ссылок
// Есть один случай, когда правила перезаписи из register_kb_rewrite_rules() не работают:
// Когда вы посещаете архивную страницу дочернего раздела (например: http://example.com/faq/category/child-category)
// Суть в том, что в этой ситуации URL парсится как запись Knowledgebase с слагом "child-category" из раздела "category"
function fix_product_subcategory_query($query) {
if ( isset( $query['post_type'] ) && 'sps-product' == $query['post_type'] ) {
if ( isset( $query['sps-product'] ) && $query['sps-product'] && isset( $query['product-category'] ) && $query['product-category'] ) {
$query_old = $query;
// Проверяем, является ли это результатом пагинации (как результаты поиска)
if ( 'page' == $query['product-category'] ) {
$query['paged'] = $query['name'];
unset( $query['product-category'], $query['name'], $query['sps-product'] );
}
// Облегчаем работу БД
$query['fields'] = 'ids';
$query['posts_per_page'] = 1;
// Проверяем, есть ли результаты или нет
$_query = new WP_Query( $query );
if ( ! $_query->posts ) {
$query = array( 'product-category' => $query['sps-product'] );
if ( isset( $query_old['product-category'] ) && 'page' == $query_old['product-category'] ) {
$query['paged'] = $query_old['name'];
}
}
}
}
return $query;
}
add_filter( 'request', 'fix_product_subcategory_query', 10 );
Эта функция позволяет WordPress понимать, как обрабатывать %product_category% в структуре перезаписи вашего пользовательского типа записи:
function filter_post_type_link($link, $post)
{
if ($post->post_type != 'sps-product')
return $link;
if ($cats = get_the_terms($post->ID, 'product-category'))
{
$link = str_replace('%product_category%', get_taxonomy_parents(array_pop($cats)->term_id, 'product-category', false, '/', true), $link); // см. пользовательскую функцию ниже
$link = str_replace('//', '/', $link);
$link = str_replace('http:/', 'http://', $link);
}
return $link;
}
add_filter('post_type_link', 'filter_post_type_link', 10, 2);
Пользовательская функция на основе get_category_parents
. Получает родительские элементы таксономии:
// Моя собственная функция, которая делает то же самое, что get_category_parents для других таксономий
function get_taxonomy_parents($id, $taxonomy, $link = false, $separator = '/', $nicename = false, $visited = array()) {
$chain = '';
$parent = &get_term($id, $taxonomy);
if (is_wp_error($parent)) {
return $parent;
}
if ($nicename)
$name = $parent -> slug;
else
$name = $parent -> name;
if ($parent -> parent && ($parent -> parent != $parent -> term_id) && !in_array($parent -> parent, $visited)) {
$visited[] = $parent -> parent;
$chain .= get_taxonomy_parents($parent -> parent, $taxonomy, $link, $separator, $nicename, $visited);
}
if ($link) {
// ничего, не удалось заставить это работать :(
} else
$chain .= $name . $separator;
return $chain;
}
Источники:

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

Я решил первую проблему, но касательно второй - функция fix_product_subcategory_query() ломает шаблон single для моего пользовательского типа записей. Конкретно, она загружает общий шаблон "single.php", а не предназначенный для моего CPT. Можете предложить решение?

На самом деле, это ужасный способ сделать это.
Если вы добавите таксономию перед типом записи, используя тайминг add_action
, вы сможете использовать правило перезаписи типа записи для добавления префикса к таксономии.
Пример:
// Регистрация пользовательской таксономии
function type_taxonomy() {
$labels = array(
'name' => _x( 'Типы', 'Общее название таксономии', 'asw' ),
'singular_name' => _x( 'Тип', 'Уникальное название таксономии', 'asw' ),
);
$rewrite = array(
'slug' => 'update/type',
'with_front' => false,
'hierarchical' => false,
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => true,
'rewrite' => $rewrite,
'show_in_rest' => true,
);
register_taxonomy( 'type', array( 'updates' ), $args );
}
// Устанавливаем тайминг перед типом записи
add_action( 'init', 'type_taxonomy', $timing = 0 );
}
if ( ! function_exists('updates_post_type') ) {
// Регистрация пользовательского типа записи
function updates_post_type() {
$labels = array(
'name' => _x( 'Обновления', 'Общее название типа записи', 'asw' ),
'singular_name' => _x( 'Обновление', 'Уникальное название типа записи', 'asw' ),
);
$rewrite = array(
'slug' => 'update',
'with_front' => true,
'pages' => true,
'feeds' => true,
);
$args = array(
'label' => __( 'Обновления', 'asw' ),
'description' => __( 'Записи должны содержать важные обновления о здоровье, благополучии и сообществе', 'asw' ),
'labels' => $labels,
'supports' => array( 'title', 'editor', 'thumbnail' ),
'taxonomies' => array( 'type', 'category', 'post_tag', 'brands', 'markets' ),
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'menu_position' => 5,
'menu_icon' => 'dashicons-heart',
'show_in_admin_bar' => true,
'show_in_nav_menus' => true,
'can_export' => true,
'has_archive' => true,
'exclude_from_search' => false,
'publicly_queryable' => true,
'rewrite' => $rewrite,
'capability_type' => 'page',
'show_in_rest' => true,
);
register_post_type( 'updates', $args );
}
// Устанавливаем тайминг после таксономии
add_action( 'init', 'updates_post_type', $timing = 10 );
}
