Удаление слага из URL записей произвольного типа записей
Похоже, что все веб-ресурсы, посвященные теме удаления слага произвольного типа записей, то есть
yourdomain.com/CPT-SLUG/post-name
сейчас содержат устаревшие решения, часто ссылающиеся на версии WordPress до 3.5. Распространенный способ:
'rewrite' => array( 'slug' => false, 'with_front' => false ),
внутри функции register_post_type
. Это больше не работает и вводит в заблуждение. Поэтому я спрашиваю сообщество в 4 квартале 2020...
Какие современные и эффективные способы существуют для удаления слага типа записи из URL записи произвольного типа с помощью аргумента rewrite или любым другим способом?
ОБНОВЛЕНИЕ: Существует несколько способов заставить это работать с помощью регулярных выражений. В частности, ответ от Jan Beck может быть полезен, если вы готовы постоянно отслеживать создание контента, чтобы избежать конфликтующих имен страниц/записей... Однако я убежден, что это серьезный недостаток в ядре WordPress, который должен быть обработан за нас. Как в виде опции/хука при создании CPT, так и в виде расширенного набора настроек для постоянных ссылок. Пожалуйста, поддержите тикет в системе отслеживания.
Примечание: Пожалуйста, поддержите этот trac тикет, отслеживая/продвигая его: https://core.trac.wordpress.org/ticket/34136#ticket

Следующий код будет работать, но нужно помнить, что могут возникнуть конфликты, если ярлык вашего пользовательского типа записи совпадает с ярлыком страницы или записи...
Сначала мы уберем ярлык из постоянной ссылки:
function na_remove_slug( $post_link, $post, $leavename ) {
if ( 'events' != $post->post_type || 'publish' != $post->post_status ) {
return $post_link;
}
$post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
return $post_link;
}
add_filter( 'post_type_link', 'na_remove_slug', 10, 3 );
Просто удалить ярлык недостаточно. Сейчас вы получите ошибку 404, потому что WordPress ожидает такого поведения только от записей и страниц. Также необходимо добавить следующее:
function na_parse_request( $query ) {
if ( ! $query->is_main_query() || 2 != count( $query->query ) || ! isset( $query->query['page'] ) ) {
return;
}
if ( ! empty( $query->query['name'] ) ) {
$query->set( 'post_type', array( 'post', 'events', 'page' ) );
}
}
add_action( 'pre_get_posts', 'na_parse_request' );
Просто замените "events" на ваш пользовательский тип записи, и все готово. Возможно, вам потребуется обновить постоянные ссылки.

спасибо. Как считаете, это лучше, чем создавать редиректы вручную? Я видел это решение, и оно может предотвратить упомянутые конфликты?

Я считаю, что это хорошее автоматизированное решение, если вы уверены, что не создадите конфликтов. Это не самое лучшее решение, если вы предоставляете его... скажем, клиенту, который не разбирается в технологиях.

можете обновить, как использовать этот код для нескольких типов записей

Ошибка возникает с nginx из-за условия 2 != count( $query->query )
. В nginx $query->query может быть таким: array('page' => '', 'name' => '...', 'q' => '...')
. Так что, @NateAllen, в чем смысл этого условия?

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

Только у меня или это ломает некоторые условные теги WordPress, такие как is_single() и is_singular()?

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

Данный код предполагает, что имя post_type
совпадает с slug
пользовательского типа записи, что не обязательно должно быть так в каждом случае. В остальном — отличное решение, хотя согласен, что нативное решение в WordPress было бы лучше.

Шаблон single-{cpt}.php перестает работать при использовании этого подхода

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

Я использовал этот отличный код до выхода WordPress 5.2. После обновления код начал давать сбои в моем плагине для пользовательских типов записей и в плагине Advanced Custom Fields, потому что, как я думаю, они используют ту же функцию pre_get_posts, поэтому вместо групп Advanced Custom Fields я вижу свои пользовательские записи в этом плагине... Также возникают проблемы с плагином CPT UI - невозможно создавать новые записи, они не появляются в списке после создания. Помогите!!

Код работал для одного типа записей. Как использовать этот код для нескольких типов записей?

Ах, наконец-то решение — спасибо! Я целый час не мог понять, почему вдруг начал получать 404 ошибки, пока не осознал, что это из-за параметра 'rewrite' => array( 'slug' => false) :)

Просматривая ответы здесь, я думаю, есть место для более элегантного решения, которое объединяет некоторые вещи, изученные выше, и добавляет автоматическое обнаружение и предотвращение дублирования ярлыков записей.
ПРИМЕЧАНИЕ: Убедитесь, что вы заменили 'custom_post_type' на имя вашего собственного CPT во всем примере ниже. Есть много вхождений, и 'найти/заменить' — это простой способ охватить их все. Весь этот код можно поместить в functions.php или в плагин.
Шаг 1: Отключите перезаписи для вашего типа записей, установив rewrites в 'false' при регистрации типа:
register_post_type( 'custom_post_type',
array(
'rewrite' => false
)
);
Шаг 2: Вручную добавляем наши пользовательские перезаписи в конец перезаписей WordPress для нашего custom_post_type
function custom_post_type_rewrites() {
add_rewrite_rule( '[^/]+/attachment/([^/]+)/?$', 'index.php?attachment=$matches[1]', 'bottom');
add_rewrite_rule( '[^/]+/attachment/([^/]+)/trackback/?$', 'index.php?attachment=$matches[1]&tb=1', 'bottom');
add_rewrite_rule( '[^/]+/attachment/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$', 'index.php?attachment=$matches[1]&feed=$matches[2]', 'bottom');
add_rewrite_rule( '[^/]+/attachment/([^/]+)/(feed|rdf|rss|rss2|atom)/?$', 'index.php?attachment=$matches[1]&feed=$matches[2]', 'bottom');
add_rewrite_rule( '[^/]+/attachment/([^/]+)/comment-page-([0-9]{1,})/?$', 'index.php?attachment=$matches[1]&cpage=$matches[2]', 'bottom');
add_rewrite_rule( '[^/]+/attachment/([^/]+)/embed/?$', 'index.php?attachment=$matches[1]&embed=true', 'bottom');
add_rewrite_rule( '([^/]+)/embed/?$', 'index.php?custom_post_type=$matches[1]&embed=true', 'bottom');
add_rewrite_rule( '([^/]+)/trackback/?$', 'index.php?custom_post_type=$matches[1]&tb=1', 'bottom');
add_rewrite_rule( '([^/]+)/page/?([0-9]{1,})/?$', 'index.php?custom_post_type=$matches[1]&paged=$matches[2]', 'bottom');
add_rewrite_rule( '([^/]+)/comment-page-([0-9]{1,})/?$', 'index.php?custom_post_type=$matches[1]&cpage=$matches[2]', 'bottom');
add_rewrite_rule( '([^/]+)(?:/([0-9]+))?/?$', 'index.php?custom_post_type=$matches[1]', 'bottom');
add_rewrite_rule( '[^/]+/([^/]+)/?$', 'index.php?attachment=$matches[1]', 'bottom');
add_rewrite_rule( '[^/]+/([^/]+)/trackback/?$', 'index.php?attachment=$matches[1]&tb=1', 'bottom');
add_rewrite_rule( '[^/]+/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$', 'index.php?attachment=$matches[1]&feed=$matches[2]', 'bottom');
add_rewrite_rule( '[^/]+/([^/]+)/(feed|rdf|rss|rss2|atom)/?$', 'index.php?attachment=$matches[1]&feed=$matches[2]', 'bottom');
add_rewrite_rule( '[^/]+/([^/]+)/comment-page-([0-9]{1,})/?$', 'index.php?attachment=$matches[1]&cpage=$matches[2]', 'bottom');
add_rewrite_rule( '[^/]+/([^/]+)/embed/?$', 'index.php?attachment=$matches[1]&embed=true', 'bottom');
}
add_action( 'init', 'custom_post_type_rewrites' );
ПРИМЕЧАНИЕ: В зависимости от ваших потребностей, вы можете изменить указанные выше перезаписи (отключить трекбэки? фиды? и т.д.). Они представляют 'стандартные' типы перезаписей, которые были бы сгенерированы, если бы вы не отключили перезаписи на шаге 1.
Шаг 3: Снова делаем постоянные ссылки на ваш тип записей 'красивыми'
function custom_post_type_permalinks( $post_link, $post, $leavename ) {
if ( isset( $post->post_type ) && 'custom_post_type' == $post->post_type ) {
$post_link = home_url( $post->post_name );
}
return $post_link;
}
add_filter( 'post_type_link', 'custom_post_type_permalinks', 10, 3 );
ПРИМЕЧАНИЕ: Вы можете остановиться здесь, если вас не беспокоит, что пользователи создадут конфликтующий (дублирующий) пост в другом типе записей, что приведет к ситуации, когда только один из них сможет загрузиться при запросе страницы.
Шаг 4: Предотвращение дублирования ярлыков записей
function prevent_slug_duplicates( $slug, $post_ID, $post_status, $post_type, $post_parent, $original_slug ) {
$check_post_types = array(
'post',
'page',
'custom_post_type'
);
if ( ! in_array( $post_type, $check_post_types ) ) {
return $slug;
}
if ( 'custom_post_type' == $post_type ) {
// Сохранение записи custom_post_type, проверяем дубли в типах POST или PAGE
$post_match = get_page_by_path( $slug, 'OBJECT', 'post' );
$page_match = get_page_by_path( $slug, 'OBJECT', 'page' );
if ( $post_match || $page_match ) {
$slug .= '-duplicate';
}
} else {
// Сохранение POST или PAGE, проверяем дубли в типе custom_post_type
$custom_post_type_match = get_page_by_path( $slug, 'OBJECT', 'custom_post_type' );
if ( $custom_post_type_match ) {
$slug .= '-duplicate';
}
}
return $slug;
}
add_filter( 'wp_unique_post_slug', 'prevent_slug_duplicates', 10, 6 );
ПРИМЕЧАНИЕ: Это добавит строку '-duplicate' в конец любых дублирующихся ярлыков. Этот код не может предотвратить дублирование ярлыков, если они уже существуют до внедрения этого решения. Обязательно проверьте наличие дубликатов заранее.
Буду рад услышать отзывы от тех, кто попробует это решение, чтобы узнать, насколько хорошо оно сработало.

Только что протестировал, и пока кажется, что это работает.

Был полон надежд на этот подход, но получаю ошибку 404 на своих записях CPT, даже после пересохранения постоянных ссылок.

Жаль, что у вас не сработало, Garconis. Я обсуждал это с другим человеком некоторое время назад, и у него тоже были проблемы на сайте. Кажется, я помню, что важно, есть ли префикс в постоянных ссылках ваших записей блога. На сайте, для которого я это разрабатывал, записи блога используют структуру постоянных ссылок: /blog/%postname%/. Если у вас нет префикса для записей блога, и вам допустимо его добавить, попробуйте и дайте мне знать, как это сработает!

Это сработало для меня. В отличие от других решений на странице, оно не нарушило работу обычных страниц или блога, и не вызвало бесконечных редиректов. Оно даже показывает правильный URL в области "Постоянная ссылка" при редактировании страниц этого пользовательского типа записей. Довольно хорошее решение, единственный нюанс — архивная страница не работает. НЕ ЗАБУДЬТЕ заменить "custom_post_type" и обновить постоянные ссылки после этого.

@MattKeys, стандартные настройки постоянных ссылок имеют пользовательскую структуру /%category%/%postname%/
. При добавлении вашего кода, ЧПУ для пользовательских типов записей выглядят нормально (хотя отсутствует завершающий слеш) ... и проверка конфликтов тоже работает. Но сами записи возвращают 404 ошибку.

Отлично работает! Однако мне пришлось добавить переменную запроса на шаге 2 add_rewrite_tag( "%custom_post_type%", '([^/]+)', "post_type=custom_post_type&name=" );
, чтобы видеть страницы CPT вместо сообщения 404.

Это единственное решение, которое сработало у меня. Мне просто нужно было добавить завершающий слеш в конце return $post_link
в шаге 3.

Работает отлично, но не хватает завершающего слеша. Пожалуйста, обновите ответ, чтобы включить его! Потенциальный недостаток: вы больше не сможете настраивать постоянную ссылку из интерфейса (например, если хотите сделать её короче, чем заголовок).

Это работает для пользовательского типа записи, но вызывает ошибку 404 для всех записей на моем сайте. Я использую это в плагине, поэтому решение должно работать с любой возможной структурой постоянных ссылок, и я не уверен, возможно ли это.

Добавьте следующий код при регистрации таксономии.
'rewrite' => [
'slug' => '/',
'with_front' => false
]
Самое важное, что нужно сделать после изменения кода
После внесения изменений в документ таксономии вашего пользовательского типа записи, перейдите в Настройки > Постоянные ссылки и пересохраните настройки, иначе вы получите ошибку 404 (страница не найдена).

Это действительно работает, я не знаю, как никто не заметил этого раньше. Конечно, это может конфликтовать с другими страницами, если у них одинаковый постоянный URL, но если нет — это отличное решение.

Попробовал это. Это даёт желаемый результат для ссылок моего пользовательского типа записей. Однако, оно "перехватывает" все слаги POST или PAGE и пытается разрешить их как URL для моего пользовательского типа записей, после чего выдает 404 ошибку. (да, я сохранял постоянные ссылки).

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

Это не работает. Выдаёт 404 ошибку, даже если вы обновили постоянные ссылки.

@ChristineCooper Вам нужно выполнить этот шаг
После того как вы изменили таксономию своего пользовательского типа записи, попробуйте перейти в Настройки > Постоянные ссылки и заново сохранить настройки, иначе вы получите ошибку 404 - страница не найдена.

Как я уже отмечал в предыдущем комментарии, вы получите ошибку 404, даже после обновления постоянных ссылок. Пожалуйста, попробуйте сами.

Работает отлично, особенно если прочитать сообщение полностью, включая часть про "пересохранение настроек". +1

Снова, даже после пересохранения настроек постоянных ссылок, записи и страницы перестали работать (404)

Это решение работает для удаления слага из URL. Но архивные страницы больше не работают.

Как уже отмечали другие, это действительно работает для записей CPT. Однако теперь это вызывает ошибку 404 для обычных страниц.

Возможно, это работает для некоторых, но не для других из-за вмешательства других плагинов? Например, у меня это также не работает в среде с настроенным WPML и подкаталогами: www.mysite.com/en/CPT-item/ возвращает 404.

Я недавно пытался разобраться в этом, и краткий ответ, насколько я знаю, — нет. По крайней мере, не через аргумент rewrite.
Длинное объяснение становится очевидным, если взглянуть на реальный код функции register_post_type
в wp-includes/post.php, строка 1454:
add_permastruct( $post_type, "{$args->rewrite['slug']}/%$post_type%", $permastruct_args );
Здесь видно, что функция добавляет $args->rewrite['slug']
перед тегом перезаписи %$post_type%
. Можно подумать: «Давайте просто установим slug в null
», но если посмотреть несколькими строками выше:
if ( empty( $args->rewrite['slug'] ) )
$args->rewrite['slug'] = $post_type;
Видно, что функция всегда ожидает непустое значение slug, а в противном случае использует тип записи.

Спасибо @JanBeck. Есть ли серьезная причина для существования этого правила? Почему бы не модифицировать этот основной файл с условием, чтобы исключить определенные типы записей из этого правила?

Вам следует отметить ответ Jan Beck как решение. WordPress нуждается в slug типа записи (post_type) для правильной маршрутизации запросов. Это правило предотвращает конфликты имен между нативными страницами WP (которые отображаются без slug) и любыми пользовательскими типами записей. Если убрать slug, WordPress не сможет отличить страницу с названием "picnic" от события (пользовательского типа записи) с таким же названием "picnic".

Обзор плагинов
Уже почти 2020 год, и многие из этих ответов не работают. Вот мой собственный обзор текущих вариантов:
- Ответ Мэтта Кейса кажется единственным правильным решением, если вам нужен кастомный код. Ни один из найденных мной плагинов не может сделать всё, что перечислено здесь, особенно проверку на дубликаты. Этот подход выглядит как отличная возможность для плагина, если кто-то захочет его создать.
- Permalink Manager Lite
- Лучший среди бесплатных плагинов, которые я тестировал.
- Даёт полный контроль над структурой постоянных ссылок для всех страниц/записей/CPT и позволяет им быть одинаковыми. GUI здесь самый функциональный.
- Позволяет полностью переопределять ссылки для каждой записи, показывает оригинальную/дефолтную версию и позволяет сбросить настройки.
- Поддерживает мультисайт.
- Не проверяет дубликаты между типами записей, что печально. Если страница и CPT имеют одинаковый URL, загружается страница, а CPT становится недоступным. Никаких предупреждений или ошибок — нужно вручную проверять дубликаты.
- Все функции для таксономий доступны в PRO версии. Напоминания о покупке довольно навязчивые.
- Custom Permalinks
- Бесплатная версия умеет многое. Только таксономии и премиум-поддержка зарезервированы для PRO версии.
- Позволяет изменять полную постоянную ссылку для любой страницы/записи/CPT.
- Поддерживает мультисайт.
- Не позволяет менять дефолтную структуру, так что ваши CPT всё равно будут выглядеть как example.com/cpt-slug/post-title, но их можно изменить вручную.
- Не проверяет дубликаты между типами записей.
- Custom Post Type Permalinks
- Позволяет не-разработчикам менять то, что и так легко меняется через
register_post_type
. - Не позволяет изменять базовый slug CPT — только часть, которая идёт после него, что делает его практически бесполезным для разработчиков и решения вопроса в этой теме.
- Позволяет не-разработчикам менять то, что и так легко меняется через
- remove base slug... — не обновлялся несколько лет... не используйте.

Плагин Permalink Manager Lite — это определенно лучшее решение: стабильное, надежное, чистое, и бесплатная версия позволяет убрать базовый слаг. И он также работает с Polylang! Протестировано на WordPress 5.4 с темой TwentyTwenty, без других активных плагинов. Работает идеально с пользовательскими типами записей, независимо от того, создали ли вы иерархический тип (с дочерними и внучатыми записями). Рекомендую всем, кто хочет чистое решение.

Хороший обзор. Код из моего ответа работает на сайте клиента уже несколько лет без каких-либо проблем. Единственный недостаток, на который указывали некоторые в моем решении, — он не работает с 'умолчальными' постоянными ссылками для встроенного типа записи 'блог' (post). Вместо этого должен быть префикс, например: /blog/%postname%/. Для тех, кто использует WordPress как CMS, это может быть уже привычно, но для других это, к сожалению, может стать препятствием.

В ответ на мой предыдущий ответ:
вы, конечно, можете установить параметр rewrite
в false
при регистрации нового типа записи и обрабатывать правила перезаписи самостоятельно, например так:
<?php
function wpsx203951_custom_init() {
$post_type = 'event';
$args = (object) array(
'public' => true,
'label' => 'Events',
'rewrite' => false, // всегда устанавливайте это в false
'has_archive' => true
);
register_post_type( $post_type, $args );
// это ваши реальные параметры перезаписи
$args->rewrite = array(
'slug' => 'calendar'
);
// всё, что следует ниже, взято из функции register_post_type
if ( is_admin() || '' != get_option( 'permalink_structure' ) ) {
if ( ! is_array( $args->rewrite ) )
$args->rewrite = array();
if ( empty( $args->rewrite['slug'] ) )
$args->rewrite['slug'] = $post_type;
if ( ! isset( $args->rewrite['with_front'] ) )
$args->rewrite['with_front'] = true;
if ( ! isset( $args->rewrite['pages'] ) )
$args->rewrite['pages'] = true;
if ( ! isset( $args->rewrite['feeds'] ) || ! $args->has_archive )
$args->rewrite['feeds'] = (bool) $args->has_archive;
if ( ! isset( $args->rewrite['ep_mask'] ) ) {
if ( isset( $args->permalink_epmask ) )
$args->rewrite['ep_mask'] = $args->permalink_epmask;
else
$args->rewrite['ep_mask'] = EP_PERMALINK;
}
if ( $args->hierarchical )
add_rewrite_tag( "%$post_type%", '(.+?)', $args->query_var ? "{$args->query_var}=" : "post_type=$post_type&pagename=" );
else
add_rewrite_tag( "%$post_type%", '([^/]+)', $args->query_var ? "{$args->query_var}=" : "post_type=$post_type&name=" );
if ( $args->has_archive ) {
$archive_slug = $args->has_archive === true ? $args->rewrite['slug'] : $args->has_archive;
if ( $args->rewrite['with_front'] )
$archive_slug = substr( $wp_rewrite->front, 1 ) . $archive_slug;
else
$archive_slug = $wp_rewrite->root . $archive_slug;
add_rewrite_rule( "{$archive_slug}/?$", "index.php?post_type=$post_type", 'top' );
if ( $args->rewrite['feeds'] && $wp_rewrite->feeds ) {
$feeds = '(' . trim( implode( '|', $wp_rewrite->feeds ) ) . ')';
add_rewrite_rule( "{$archive_slug}/feed/$feeds/?$", "index.php?post_type=$post_type" . '&feed=$matches[1]', 'top' );
add_rewrite_rule( "{$archive_slug}/$feeds/?$", "index.php?post_type=$post_type" . '&feed=$matches[1]', 'top' );
}
if ( $args->rewrite['pages'] )
add_rewrite_rule( "{$archive_slug}/{$wp_rewrite->pagination_base}/([0-9]{1,})/?$", "index.php?post_type=$post_type" . '&paged=$matches[1]', 'top' );
}
$permastruct_args = $args->rewrite;
$permastruct_args['feed'] = $permastruct_args['feeds'];
add_permastruct( $post_type, "%$post_type%", $permastruct_args );
}
}
add_action( 'init', 'wpsx203951_custom_init' );
Теперь вы видите, что вызов add_permastruct
больше не включает slug.
Я протестировал два сценария:
- Когда я создал страницу с slug "calendar", эта страница перезаписывается архивом типа записи, который также использует slug "calendar".
- Когда я создал страницу с slug "my-event" и событие (CPT) с таким же slug "my-event", отображается пользовательский тип записи.
- Любые другие страницы также не работают. Если посмотреть на изображение выше, становится понятно почему: правило для пользовательского типа записи всегда будет совпадать с slug страницы. Поскольку WordPress не может определить, является ли это страницей или несуществующим пользовательским типом записи, он вернет ошибку 404. Вот почему вам нужен slug для идентификации либо страницы, либо CPT. Возможным решением может быть перехват ошибки и поиск существующей страницы, как показано в этом ответе.

Итак, если цель — убрать slug для пользовательских типов записей (CPT), не могли бы мы просто дать CPT уникальное имя, которое не будет конфликтовать, раз оно всё равно не будет отображаться в URL? Или конфликт возможен из-за post-name, если он совпадает с названием страницы?

Я обновил свой ответ, чтобы показать, что это на самом деле ломает все страницы. Без slug WordPress будет искать CPT вместо страницы, и если не найдёт, вернёт ошибку. Так что это на самом деле не связано с post-name.

Понял. Должны быть правила перезаписи, которые добавляют '-1' к конфликтующим URL, как это делается в родных записях WordPress vs страницах. Я создал тикет в треке https://core.trac.wordpress.org/ticket/34136#ticket, буду рад услышать ваше мнение.

Предыстория
Даже после долгих поисков я не смог найти работоспособного решения для удаления слага (slug) типа записи (CPT) из постоянных ссылок, которое бы корректно работало и соответствовало тому, как WordPress на самом деле обрабатывает запросы. Как оказалось, все, кто ищет такое же решение, находятся в той же лодке, что и я.
На самом деле, решение состоит из двух частей.
- Удаление слага CPT из постоянных ссылок
- Объяснение WordPress, как находить записи по новым постоянным ссылкам
Первая часть довольно проста, и многие существующие решения делают это правильно. Вот как это выглядит:
// удаляем slug CPT из постоянных ссылок
function remove_cpt_slug( $post_link, $post, $leavename ) {
if ( $post->post_type != 'custom_post_type' ) {
return $post_link;
} else {
$post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
return $post_link;
}
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );
Теперь вторая часть, где всё становится сложнее. После решения первой части в постоянных ссылках вашего CPT больше нет слага CPT. Но теперь проблема в том, что WordPress не знает, как находить ваши записи по этим новым ссылкам, потому что он ожидает, что в ссылках CPT будет присутствовать слаг CPT. Без него WordPress не может найти вашу запись, и при запросе вы получаете ошибку 404.
Теперь вам нужно объяснить WordPress, как находить записи по новым постоянным ссылкам. Но именно здесь существующие решения работают не очень хорошо. Давайте рассмотрим несколько примеров:
Следующая функция работает неплохо, но только если структура постоянных ссылок установлена как "Название записи".
function parse_request_remove_cpt_slug( $query ) {
if ( ! $query->is_main_query() || 2 != count( $query->query ) || ! isset( $query->query['page'] ) ) {
return;
}
if ( ! empty( $query->query['name'] ) ) {
global $wpdb;
$cpt = $wpdb->get_var("SELECT post_type FROM $wpdb->posts WHERE post_name = '{$query->query['name']}'");
// Добавляем CPT в список типов записей, которые WordPress будет учитывать при запросе по названию записи.
$query->set( 'post_type', $cpt );
}
}
add_action( 'pre_get_posts', 'parse_request_remove_cpt_slug' );
Следующая функция хорошо работает для вашего CPT независимо от структуры постоянных ссылок, но вызывает ошибки для всех других типов записей.
function rewrite_rule_remove_cpt_slug() {
add_rewrite_rule(
'(.?.+?)(?:/([0-9]+))?/?$',
'index.php?custom_post_type=$matches[1]/$matches[2]&post_type=custom_post_type',
'bottom'
);
}
add_action( 'init', 'rewrite_rule_remove_cpt_slug', 1, 1 );
Есть ещё одно решение, которое должно работать самостоятельно, но приводит к ещё большим проблемам, вызывая ошибки как в записях CPT, так и в других. Оно требует изменения аргумента rewrite при регистрации CPT:
'rewrite' => array( 'slug' => '/', 'with_front' => false )
Все найденные мной существующие решения похожи на приведённые выше. Они либо работают частично, либо вообще не работают. Вероятно, это связано с тем, что WordPress не предоставляет стандартного способа удаления слага CPT из постоянных ссылок, поэтому эти решения основаны либо на частных случаях, либо на хакерских методах.
Решение
Вот что у меня получилось в процессе поиска решения, которое работает в большинстве, если не во всех, случаях. Этот код правильно удаляет слаг CPT из постоянных ссылок и объясняет WordPress, как находить записи CPT по новым ссылкам. Он не перезаписывает правила в базе данных, поэтому вам не нужно сохранять структуру постоянных ссылок заново. Кроме того, это решение согласуется с тем, как WordPress на самом деле обрабатывает запросы для поиска записей по постоянным ссылкам, что делает его более приемлемым.
Обязательно замените custom_post_type
на название вашего типа записи. Оно встречается один раз в каждой функции, всего два раза.
// удаляем slug CPT из постоянных ссылок
function remove_cpt_slug( $post_link, $post, $leavename ) {
if ( $post->post_type != 'custom_post_type' ) {
return $post_link;
} else {
$post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
return $post_link;
}
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );
// объясняем WordPress, как находить записи по новым постоянным ссылкам
function parse_request_remove_cpt_slug( $query_vars ) {
// возвращаем, если это админ-панель
if ( is_admin() ) {
return $query_vars;
}
// возвращаем, если "красивые" постоянные ссылки не включены
if ( ! get_option( 'permalink_structure' ) ) {
return $query_vars;
}
$cpt = 'custom_post_type';
// сохраняем значение слага записи в переменную
if ( isset( $query_vars['pagename'] ) ) {
$slug = $query_vars['pagename'];
} elseif ( isset( $query_vars['name'] ) ) {
$slug = $query_vars['name'];
} else {
global $wp;
$path = $wp->request;
// используем путь URL в качестве слага
if ( $path && strpos( $path, '/' ) === false ) {
$slug = $path;
} else {
$slug = false;
}
}
if ( $slug ) {
$post_match = get_page_by_path( $slug, 'OBJECT', $cpt );
if ( ! is_admin() && $post_match ) {
// удаляем ошибку 404 из массива query_vars, так как запись найдена в CPT
if ( isset( $query_vars['error'] ) && $query_vars['error'] == 404 ) {
unset( $query_vars['error'] );
}
// удаляем ненужные элементы из исходного массива query_vars
unset( $query_vars['pagename'] );
// добавляем необходимые элементы в массив query_vars
$query_vars['post_type'] = $cpt;
$query_vars['name'] = $slug;
$query_vars[$cpt] = $slug; // создаём элемент "cpt=>post_slug"
}
}
return $query_vars;
}
add_filter( 'request', "parse_request_remove_cpt_slug" , 1, 1 );
Важные замечания:
Это решение намеренно не учитывает структуру постоянных ссылок "Простая", так как она не является "красивой". Таким образом, код будет работать со всеми структурами, кроме "Простой".
Поскольку WordPress автоматически не предотвращает создание дублирующихся слагов в разных типах записей, у вас могут возникнуть проблемы с доступом к записям, имеющим одинаковые слаги, из-за потери уникальности в постоянных ссылках CPT после удаления слагов CPT. Этот код не включает функциональность для предотвращения такого поведения, поэтому вам может потребоваться отдельное решение.
Если существует дублирующаяся постоянная ссылка, этот код отдаст приоритет вашему CPT, и поэтому будет отображаться запись из вашего CPT.

На момент написания это решение является лучшим. Оно простое и корректное. Его логику легко понять и настроить. Фактически, я адаптировал это решение, чтобы также удалять слаги таксономий.

Чтобы предотвратить создание дублирующих слэгов, вы можете прочитать и настроить код в "Шаге 4" ответа от Matt Keys.

Я объединил этот код и код Matt Keys в этом gist-е, требуется больше тестирования, но вроде работает нормально. В основном я только изменил обработку дубликатов для слэга на что-то вроде -1, -2 и т.д. Наверняка есть более лучший способ сделать это. Было бы даже неплохо создать небольшой интерфейс для установки 'location' в желаемый слаг CPT. https://gist.github.com/cdsaenz/291d9599e1f20313c3a87edf48233176

и мы можем внести некоторые изменения в упомянутую выше функцию:
function na_parse_request( $query ) {
if ( ! $query->is_main_query() || 2 != count( $query->query ) || ! isset( $query->query['page'] ) ) {
return;
}
if ( ! empty( $query->query['name'] ) ) {
$query->set( 'post_type', array( 'post', 'events', 'page' ) );
}
}
на:
function na_parse_request( $query ) {
if ( ! $query->is_main_query() || 2 != count( $query->query ) || ! isset( $query->query['page'] ) ) {
return;
}
if ( ! empty( $query->query['name'] ) ) {
global $wpdb;
$pt = $wpdb->get_var(
"SELECT post_type FROM `{$wpdb->posts}` " .
"WHERE post_name = '{$query->query['name']}'"
);
$query->set( 'post_type', $pt );
}
}
чтобы установить правильное значение post_type.

Для тех, кто столкнулся с проблемами дочерних записей, как и я, лучшим решением оказалось добавление собственных правил перезаписи URL.
Основная проблема заключалась в том, что WordPress по-разному обрабатывает редиректы для страниц второго уровня (дочерние записи) и третьего уровня (дочерние записи дочерних записей).
Это означает, что для URL вида /тип-записи/название-записи/дочерняя-запись/ можно использовать /название-записи/дочерняя-запись, и WordPress перенаправит на полный URL с указанием типа записи. Но для URL вида /тип-записи/название-записи/дочерняя-запись/дочерняя-дочерней-записи/ сокращенный вариант /название-записи/дочерняя-запись/дочерняя-дочерней-записи уже не работает.
Анализируя правила перезаписи, можно заметить, что WordPress сопоставляет не только имя страницы на первом и втором уровнях (на втором уровне, вероятно, проверяются вложения), а затем выполняет перенаправление. На третьем уровне это уже не работает.
Первое, что нужно сделать - удалить тип записи из ссылок дочерних элементов. Это можно сделать с помощью кода, подобного тому, что предложил Nate Allen в своем ответе:
$post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
Лично я использовал комбинацию различных условных проверок, чтобы определить наличие дочерних записей и сформировать правильные постоянные ссылки. Эта часть не слишком сложная, и примеры подобных решений можно найти в интернете.
Следующий шаг отличается от предложенного в других ответах. Вместо добавления условий в основной запрос (что работало для произвольных типов записей и их дочерних элементов, но не для более глубоких уровней), я добавил правило перезаписи в конец списка правил WordPress. Таким образом, если проверка по имени страницы не сработала и система собиралась вернуть 404, выполняется последняя проверка на соответствие имени страницы в рамках произвольного типа записи.
Вот правило перезаписи, которое я использовал (предполагая, что 'event' - это название вашего произвольного типа записи):
function rewrite_rules_for_removing_post_type_slug()
{
add_rewrite_rule(
'(.?.+?)(?:/([0-9]+))?/?$',
'index.php?event=$matches[1]/$matches[2]&post_type=event',
'bottom'
);
}
add_action('init', 'rewrite_rules_for_removing_post_type_slug', 1, 1);
Надеюсь, это поможет другим разработчикам, так как я не смог найти решений, касающихся именно дочерних записей второго уровня и удаления слага из их URL.

Похоже, в регулярном выражении есть опечатка. Между '(:' необходимо добавить '?', чтобы использовать его как не захватывающую подгруппу => '(?:'. Третий знак '?' кажется расположен неверно, так как позволяет первой подгруппе быть пустой. Вероятно, он должен находиться между ( и :. Без этой опечатки выражение будет идентично тому, которое используется для встроенного типа записи 'page'.

Столкнулся с такими же проблемами, и на сайте WordPress, похоже, ничего не меняется. В моем конкретном случае, когда для отдельных записей блога требовалась структура /blog/%postname%/, это решение
https://kellenmace.com/remove-custom-post-type-slug-from-permalinks/
привело к куче ошибок 404.
Но вместе с этим замечательным подходом, который не использует структуру постоянных ссылок в админке для записей блога, всё, наконец, заработало как надо. https://www.bobz.co/add-blog-prefix-permalink-structure-blog-posts/
Огромное спасибо.

Я попробовал, и это сработало.
Вот простой код, который нужно использовать:
register_post_type('wporg_product', array( 'labels' => array( 'name' => 'Портфолио', 'singular_name' => 'Портфолио', ), 'menu_icon' => 'dashicons-id', 'rewrite' => array( 'slug' => 'portfolio', 'with_front' => false ), // мой пользовательский ярлык
)
);
После изменения этого кода необходимо один раз сохранить настройки постоянных ссылок.

Вот что сработало у меня. Замените podcast
на слаг вашего CPT:
add_action('init', function () {
register_post_type(
'podcast',
[
'rewrite' => false,
]
);
});
add_filter('post_type_link', function ($post_link, $post, $leavename) {
if (isset($post->post_type) && $post->post_type === 'podcast') {
$post_link = home_url($post->post_name);
}
return $post_link;
}, 10, 3);
add_action('init', function () {
add_rewrite_rule('(.+?)/?$', 'index.php?podcast=$matches[1]', 'bottom');
});

Плагин "Remove CPT Base" работает.

Вам не нужно так много хардкода. Просто используйте лёгкий плагин:
У него есть настраиваемые параметры.

Теперь я понимаю, почему вас заминусовали — это мешает нормальному разрешению ссылок на страницы. Я не заметил этого, потому что получал кешированные копии существующих страниц, несмотря на обновления.

Переход по ссылкам на страницы (которые не были пользовательским типом записи) из главного меню выдавал ошибки 404, как будто страницы не существует; вот и всё.

@Walf, можешь привести какой-нибудь пример URL из твоего случая? (можешь скрыть доменное имя, если хочешь, мне просто нужен пример) спасибо, я обновлю его
