Eliminar el slug de taxonomía de un permalink de taxonomía jerárquica personalizada
Creé una taxonomía 'forum', usando estas reglas:
register_taxonomy(
'forum',
array('topic'),
array(
'public' => true,
'name' => _a('Foros'),
'singular_name' => _a('Foro'),
'show_ui' => true,
'show_in_nav_menus' => true,
'hierarchical' => true,
'labels' => array(
'name' => _a('Foros'),
'singular_name' => _a('Foro'),
'search_items' => _a('Buscar Foros'),
'popular_items' => _a('Foros Populares'),
'all_items' => _a('Todos los Foros'),
'parent_item' => _a('Foro Padre'),
'parent_item_colon' => _a('Foro Padre:'),
'edit_item' => _a('Editar Foro'),
'update_item' => _a('Actualizar Foro'),
'add_new_item' => _a('Añadir Nuevo Foro'),
'new_item_name' => _a('Nombre del Nuevo Foro'),
),
'query_var' => true,
'rewrite' => array('slug' => 'forums', 'with_front' => false, 'hierarchical' => true),
)
);
En el front-end las URLs se ven así:
forums/general-discussion/sub-forum
¿Cómo puedo eliminar el slug frontal ("forums")? Es decir, cambiar las URLs a:
general-discussion/sub-forum
Si paso un argumento de slug vacío a register_taxonomy() funciona, pero eso causa problemas con los enlaces permanentes del tipo de publicación asociado con esta taxonomía

ACTUALIZACIÓN
Desde que escribí esto, el núcleo de WordPress ha añadido el hook 'do_parse_request'
que permite manejar el enrutamiento de URLs de manera elegante sin necesidad de extender la clase WP
. Cubrí el tema en profundidad en mi charla de WordCamp Atlanta 2014 titulada "Hardcore URL Routing"; las diapositivas están disponibles en el enlace.
RESPUESTA ORIGINAL
El diseño de URLs ha sido importante para mí por más de una década; incluso escribí un blog sobre ello hace varios años. Y aunque WordPress en conjunto es un software brillante, desafortunadamente su sistema de reescritura de URLs deja bastante que desear (en mi humilde opinión, claro está. :) ¡En fin, me alegra ver que la gente se preocupa por el diseño de URLs!
La respuesta que voy a proporcionar es un plugin que llamo WP_Extended
, que es una prueba de concepto para esta propuesta en Trac (Nota: esa propuesta empezó siendo una cosa y evolucionó en otra, así que hay que leerla entera para ver a dónde llegó.)
Básicamente, la idea es crear una subclase de la clase WP
, sobrescribir el método parse_request()
, y luego asignar la variable global $wp
con una instancia de la subclase. Dentro de parse_request()
se inspecciona la ruta por segmentos en lugar de usar una lista de expresiones regulares que deben coincidir con la URL completa.
Para ser explícitos, esta técnica inserta lógica antes de parse_request()
que busca coincidencias de URL contra expresiones regulares y en su lugar primero busca coincidencias con términos de taxonomías, pero SÓLO reemplaza parse_request()
y deja intacto el resto del sistema de enrutamiento de WordPress, especialmente el uso de la variable $query_vars
.
Para tu caso de uso, esta implementación sólo compara segmentos de URL con términos de taxonomías, ya que eso es todo lo que necesitas. Esta implementación inspecciona los términos de taxonomías respetando las relaciones padre-hijo y cuando encuentra una coincidencia asigna la ruta de la URL (sin barras iniciales ni finales) a $wp->query_vars['category_name']
, $wp->query_vars['tag']
o $wp->query_vars['taxonomy']
& $wp->query_vars['term']
, y omite el método parse_request()
de la clase WP
.
Por otro lado, si la ruta de la URL no coincide con un término de las taxonomías especificadas, delega la lógica de enrutamiento al sistema de reescritura de WordPress llamando al método parse_request()
de la clase WP
.
Para usar WP_Extended
en tu caso, necesitarás llamar a la función register_url_route()
desde el archivo functions.php
de tu tema así:
add_action('init','init_forum_url_route');
function init_forum_url_route() {
register_url_route(array('taxonomy'=>'forum'));
}
Aquí está el código fuente del plugin:
<?php
/*
Filename: wp-extended.php
Plugin Name: WP Extended for Taxonomy URL Routes
Author: 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() { // Setup theme es el 1er código ejecutado tras crear WP.
global $wp;
$wp = new WP_Extended(); // Reemplaza la variable global $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) { // Asegurarse de verificar padres
$taxonomy_term = array();
} else {
$parent_id = $term->term_id; // Capturar ID padre para verificación
$taxonomy_term[] = $term->slug; // Coleccionar slug como segmento de ruta
unset($terms[$term_index]); // No necesitamos escanearlo de nuevo
}
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); // Delegar a la clase WP
}
}
}
WP_Extended::on_load();
P.D. ADVERTENCIA #1
Aunque para un sitio dado esta técnica funciona brillantemente, esta técnica NUNCA debería usarse en un plugin para distribuir en WordPress.org para que otros lo usen. Si está en el núcleo de un paquete de software basado en WordPress, entonces podría ser aceptable. De lo contrario, esta técnica debería limitarse a mejorar el enrutamiento de URLs para un sitio específico.
¿Por qué? Porque sólo un plugin puede usar esta técnica. Si dos plugins intentan usarla, entrarán en conflicto.
Como nota aparte, esta estrategia puede expandirse para manejar genéricamente prácticamente cualquier patrón de caso de uso que pueda requerirse, y eso es lo que pretendo implementar tan pronto como encuentre el tiempo libre o un cliente que patrocine el tiempo necesario para construir implementaciones completamente genéricas.
ADVERTENCIA #2
Escribí esto para sobrescribir parse_request()
, que es una función muy grande, y es bastante posible que me haya olvidado de establecer una propiedad o dos del objeto global $wp
. Así que si algo se comporta de manera extraña, házmelo saber y estaré encantado de investigarlo y revisar la respuesta si es necesario.
En fin...

Después de escribir esto me di cuenta de que probé para categorías en lugar de términos de taxonomía en general, así que lo anterior no funcionará para la taxonomía 'forum'
, sin embargo lo revisaré para que funcione más tarde hoy...

Así que he actualizado el código para abordar el problema que mencioné en el comentario anterior.

no puedo hacer que esto funcione... ¿necesito cambiar las reglas de reescritura?

@One Trick Pony - Un poco más de información de diagnóstico ayudaría. :) ¿Qué intentaste? ¿Qué sucede cuando ingresas las URLs en tu navegador? ¿Acaso llamaste a tu taxonomía 'forums'
en lugar de 'forum'
? ¿Esperas que las URLs que enlazan a estas páginas cambien (si es así, no me extraña, mi código no aborda la impresión de URLs, solo el enrutamiento de URLs.)

no, puedo cambiar las URLs (creo que es la función term_link en la que necesito engancharme para eso). site/rootforum/
funciona, pero site/rootforum/subforum/
no (error 404) ...

@One Trick Pony - ¿Es subforum
un término de taxonomía hijo de rootforum
? Por favor, proporciona más información sobre tu configuración actual; es muy difícil darte algo que funcione cuando no estamos seguros exactamente de cómo es tu configuración.

no, es la misma taxonomía. subforum
es un término hijo de rootforum

@One Trick Pony - ¿Qué sucede cuando "no funciona?" ¿Qué URLs (completas) no están funcionando? ¿Cuál es el dominio real donde esto no funciona? ¿La URL para /forums/foo/bar
funciona pero no /foo/bar
? Dame más información y haré que esto funcione para ti, pero en este momento siento que no tengo suficiente para depurar ya que funciona perfectamente para mí.

mira - http://dev.digitalnature.ro/wp99420/forum1/ - luego intenta acceder a http://dev.digitalnature.ro/wp99420/forum1/general-discussion/ (general-discussion
es un sub-foro de forum1
)

si es relevante, aquí está el código completo del sistema de foro: http://pastebin.com/N509zCh3

@One Trick Pony - ¿Qué es wp99420? ¿Es esa la raíz del sitio? Mi código actualmente no maneja la ubicación de la raíz de un sitio en un subdirectorio; ¿es eso lo que necesitas?

@One Trick Pony - He actualizado el código para manejar sitios en un subdominio.

realmente no funciona, todavía recibo el error 404, pero gracias de todos modos. Acabo de descubrir el plugin bbpress, así que no usaré esto más

@One Trick Pony - Realmente desearía que pudiéramos descubrir por qué esto no funciona para ti. Quiero convertirlo en un plugin que cualquiera pueda usar para cualquier URL que desee, así que descubrir por qué no funciona para ti sería útil.

bueno, probándolo en localhost/wp
obtengo esto como $path_segments: Array ( [0] => wp [1] => rootforum [2] => subforum )
. $taxonomy_term y $taxonomy_slug también están bien, así que parse_request() parece hacer su trabajo. Tal vez el problema es que parent::parse_request() está reiniciando el array query_vars (ver línea 123 en class-wp)?

@One Trick Pony - ¿Cuál es tu valor para WP_SITEURL
? ¿$_SERVER['SERVER_NAME']
devuelve 'localhost'
? Tiendo a configurar dominios "locales" para pruebas (ej. http://mytest.dev
vs. http://localhost/wp
) por lo que a menudo no detecto los errores que ocurren en configuraciones no estándar.

@One Trick Pony - He actualizado el código para que no dependa de WP_SITEURL; ¿puedes intentarlo de nuevo?

lo mismo... De todos modos encontré el problema, al menos en mi sistema - es la cadena $path
implosionada que pasas a query_vars['term']. Se ve como rootforum/subform
(debería ser solo la última parte, subforum
)

@One Trick Pony - ¿rootforum
no es un término padre de 'subforum'
en la taxonomía 'forum'
?

rootforum es un término, y subforum es un término hijo de rootforum. ambos son parte de la taxonomía forum

@MikeSchinkel: realmente deseo que ese ticket de trac se incluya en el núcleo. Me parece una gran idea (con mi limitado entendimiento). Por cierto, ¿tienes este plugin en el repositorio de WP?

Oye @MikeSchinkel, ¿esto llegó a algún lado? Tus ideas son geniales y abordan mi mayor dolor con WP. ¿Llegó a existir ese plugin?

@PeterB - Gracias por los amables comentarios. Todavía estoy trabajando en un plugin de uso general, pero preparé una charla detallada en WordCamp sobre Enrutamiento de URLs, las diapositivas están aquí: http://hardcorewp.com/2014/hardcore-url-routing-for-wordpress/

Sencillo, de verdad.
Paso 1: Deja de usar completamente el parámetro rewrite. Vamos a crear tus propias reglas de reescritura.
'rewrite'=>false;
Paso 2: Establece reglas de páginas detalladas. Esto fuerza a que las Páginas normales tengan sus propias reglas en lugar de ser un comodín al final.
Paso 3: Crea algunas reglas de reescritura para manejar tus casos de uso.
Paso 4: Fuerza manualmente un vaciado de reglas. La forma más fácil: ve a Ajustes->Enlaces permanentes y haz clic en el botón guardar. Prefiero esto sobre un método de activación de plugin para mi propio uso, ya que puedo forzar el vaciado de reglas cuando cambio cosas.
Entonces, hora del código:
function test_init() {
// crea una nueva taxonomía
register_taxonomy(
'forum',
'post',
array(
'query_var' => true,
'public'=>true,
'label'=>'Foro',
'rewrite' => false,
)
);
// fuerza reglas detalladas... esto hace que cada Página tenga su propia regla en lugar de ser un
// comodín, que usaremos para la taxonomía del foro
global $wp_rewrite;
$wp_rewrite->use_verbose_page_rules = true;
// dos reglas para manejar feeds
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]');
// una regla para manejar la paginación de posts en la taxonomía
add_rewrite_rule('(.+)/page/?([0-9]{1,})/?$','index.php?forum=$matches[1]&paged=$matches[2]');
// una regla para mostrar la taxonomía forum normalmente
add_rewrite_rule('(.+)/?$', 'index.php?forum=$matches[1]');
}
add_action( 'init', 'test_init' );
Recuerda que después de agregar este código, necesitas tenerlo activo cuando vacíes las reglas de enlaces permanentes (¡guardando la página en Ajustes->Enlaces permanentes)!
Después de haber vaciado las reglas y guardado en la base de datos, entonces /cualquier-cosa debería ir a tu página de taxonomía forum=cualquier-cosa.
Las reglas de reescritura no son tan difíciles si entiendes expresiones regulares. Uso este código para ayudarme al depurarlas:
function test_foot() {
global $wp_rewrite;
echo '<pre>';
var_dump($wp_rewrite->rules);
echo '</pre>';
}
add_action('wp_footer','test_foot');
De esta manera, puedo ver las reglas actuales de un vistazo en mi página. Solo recuerda que dada cualquier URL, el sistema comienza en la parte superior de las reglas y recorre hacia abajo hasta encontrar una que coincida. La coincidencia se usa para reescribir la consulta en un conjunto más normal de ?clave=valor. Esas claves se analizan en lo que va al objeto WP_Query. Simple.
Edición: Nota al margen, este método probablemente solo funcionará si tu estructura normal de posts personalizados comienza con algo que no sea un comodín, como %categoría% o algo por el estilo. Necesitas comenzarlo con una cadena estática o numérica, como %año%. Esto es para evitar que capture tu URL antes de llegar a tus reglas.

Si deseas una depuración más sencilla de tus reglas de reescritura, (de nuevo) recomiendo mi plugin analizador de reescrituras, que te permite probar las reglas y ver las variables de consulta sobre la marcha.

Desafortunadamente, el sistema actual de reescritura de URLs obliga a aplanar todos los patrones potenciales de URLs en una gran lista, en lugar de seguir la estructura de árbol inherente a las rutas de URLs. La configuración actual no puede coincidir convenientemente con un array de literales como categorías o nombres de foros; como sabes, obliga a que todas las URLs de "Página" se evalúen primero. Coincidir por segmento de ruta y hacer coincidencias de múltiples formas (array de literales, categorías, etiquetas, términos de taxonomía, nombres de usuario, tipos de entrada, nombres de entrada, callbacks, hooks de filtro y finalmente RegEx) escalaría mejor para la complejidad y sería más fácil de entender.

Mike: En realidad, eso no es más fácil de entender en absoluto, porque no tengo la menor idea de qué diablos estás hablando allí. Tus ideas sobre el enrutamiento de URLs son confusas y difíciles, y como probablemente sabes, no estoy de acuerdo con ellas. La búsqueda plana tiene más sentido y es más flexible de lo que tiendes a reconocer. La mayoría de la gente no quiere toda esa complejidad innecesaria en sus URLs, y casi nadie la necesita tampoco.

Gracias, pero creo que ya intenté esto antes (http://wordpress.stackexchange.com/questions/9455/custom-post-type-permalinks-giving-404s)

Afortunadamente, WordPress Answers ahora permite que las personas que sí quieren control sobre sus URLs finalmente tengan voz, y parecen ser muchas (más de 100). Pero respeto que quizás no puedas seguir mi ejemplo antes de una implementación completa. Predigo que, una vez que el enfoque que estoy defendiendo se implemente completamente en un plugin y después de unos 6-12 meses, se habrá convertido en la forma preferida para que los sitios CMS basados en WordPress gestionen sus URLs. Así que retomemos este debate en unos 9 meses.

No podrás hacer esto usando solo WP_Rewrite, ya que no puede distinguir entre los slugs de términos y los slugs de publicaciones.
También debes engancharte a 'request' y evitar el error 404, estableciendo la variable de consulta de publicación en lugar de la de taxonomía.
Algo como esto:
function fix_post_request( $request ) {
$tax_qv = 'forum';
$cpt_name = 'post';
if ( !empty( $request[ $tax_qv ] ) ) {
$slug = basename( $request[ $tax_qv ] );
// si esto generaría un 404
if ( !get_term_by( 'slug', $slug, $tax_qv ) ) {
// establecer las variables de consulta correctas
$request[ 'name' ] = $slug;
$request[ 'post_type' ] = $cpt_name;
unset( $request[$tax_qv] );
}
}
return $request;
}
add_filter( 'request', 'fix_post_request' );
Ten en cuenta que la taxonomía debe definirse antes que el tipo de publicación.
Este sería un buen momento para señalar que tener una taxonomía y un tipo de publicación con la misma variable de consulta es una Mala Idea.
Además, no podrás acceder a publicaciones que tengan el mismo slug que uno de los términos.

Está acordado que tener una taxonomía y un tipo de contenido con el mismo query var es una Mala Idea, pero eso podría implicar para la gente que tener una taxonomía y un tipo de contenido con el mismo nombre es una mala idea, lo cual no es el caso. Si usas el mismo nombre entonces solo uno de los dos debería tener un query var.

Echaría un vistazo al código del plugin Top Level Cats:
http://fortes.com/projects/wordpress/top-level-cats/
Podrías adaptarlo fácilmente para que busque el slug de tu taxonomía personalizada cambiando
$category_base = get_option('category_base');
en la línea 74 por algo como:
$category_base = 'forums';

Te sugiero que eches un vistazo al plugin Custom Post Permalinks. No tengo tiempo para probarlo ahora mismo, pero podría ayudarte con tu situación.

Como estoy familiarizado con tu otra pregunta, responderé teniendo eso en cuenta.
No he probado esto en absoluto, pero podría funcionar si lo ejecutas una vez justo después de registrar todas las estructuras permanentes que desees:
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();
Lo que hace esto: elimina las reglas de reescritura generadas desde el enlace permanente de temas del flujo normal del array de reglas y las vuelve a fusionar al final del array. Esto evita que esas regras interfieran con cualquier otra regla de reescritura. Luego, fuerza reglas de reescritura detalladas (cada página obtiene una regla individual con una expresión regular específica). Esto evita que las páginas interfieran con las reglas de tus temas. Finalmente, ejecuta un reinicio completo (asegúrate de que tu archivo .htaccess tenga permisos de escritura, de lo contrario esto no funcionará) y guarda el array muy grande y complicado de reglas de reescritura.

Elimina el slug del tipo añadiendo una regla específica para cada página de tipo de entrada personalizada.

No estoy seguro si esto funcionará para taxonomías, pero funcionó para tipos de contenido personalizados
Aunque no ha sido actualizado en 2 años, el siguiente plugin funcionó para mí: http://wordpress.org/plugins/remove-slug-from-custom-post-type/
Para tu información, estoy ejecutando WP 3.9.1
con WP Types 1.5.7
