SEF urls: Friendly URLs for a Dual Multi-level Taxonomy
The problem (Task):
We have:
company
— Flat post type.catalog
— Hierarchical taxonomy.location
— Hierarchical taxonomy.
We need permalinks:
/company/
prefix for all! Combined queryes for the two taxes. Working pagination!
Examples:
/company/bmw7/
— post./company/almaty/
— location term archive (top level)./company/almaty/auezo/
— location term archive (level 2)./company/almaty/auezo/three/
— location term archive (level 3)./company/avto/
— catalog term archive (top level)./company/avto/light/
— catalog term archive (level 2)./company/avto/light/lyuks/
— catalog term archive (level 3)./company/avto/almaty/
— catalog + location terms archive (top levels)./company/avto/almaty/auezo/
— catalog + location terms archive (level 1 + level 2)./company/avto/light/almaty/auezo/three/
— catalog + location terms archive (level 2 + level 3).
All of these must support pagination. Ex:
/company/almaty/page/2/
— location term pagination page 2./company/avto/light/almaty/auezo/three/page/2/
— catalog + location terms page 2.- etc.
The code which do all things:
GitHub<?php new Same_Prefix_Post_Taxes_Rewrite(); /** * How it works. * * We have: * - ``company`` — Flat post type. * - ``catalog`` — Hierarchical taxonomy. * - ``location`` — Hierarchical taxonomy. * * We need permalinks: * ``/company/`` prefix for all! Combined queryes for the two taxes. Working pagination! * Examples: * - ``/company/bmw7/`` — post. * - ``/company/almaty/`` — location term archive (top level). * - ``/company/almaty/auezo/`` — location term archive (level 2). * - ``/company/almaty/auezo/three/`` — location term archive (level 3). * - ``/company/avto/`` — catalog term archive (top level). * - ``/company/avto/light/`` — catalog term archive (level 2). * - ``/company/avto/light/lyuks/`` — catalog term archive (level 3). * - ``/company/avto/almaty/`` — catalog + location terms archive (top levels). * - ``/company/avto/almaty/auezo/`` — catalog + location terms archive (level 1 + level 2). * - ``/company/avto/light/almaty/auezo/three/`` — catalog + location terms archive (level 2 + level 3). * * All of these must support pagination. Ex: * - ``/company/almaty/page/2/`` — location term pagination page 2. * - ``/company/avto/light/almaty/auezo/three/page/2/`` — catalog + location terms page 2. * - etc. * * For posts we need additional permalinks. Ex: * - ``site.kz/company/bmw7/reviews/`` — post. * - ``site.kz/company/bmw7/gallery/`` — post. * - ``site.kz/company/bmw7/edit/`` — post. * - ``site.kz/company/bmw7/price/`` — post. * * @version 1.4 * * @author Kama (wp-kama.com) */ class Same_Prefix_Post_Taxes_Rewrite { static $post_type = 'company'; static $catalog_tax = 'catalog'; static $location_tax = 'location'; static $post_subpages = [ 'edit' => 'Редактирование', 'price' => 'Цена', 'gallery' => 'Галлерея', 'reviews' => 'Обзоры', ]; static $request = []; function __construct(){ add_action( 'init', [ __CLASS__, 'register_post_types' ] ); add_action( 'request', [ __CLASS__, 'fix_request' ] ); add_filter( self::$post_type . '_rewrite_rules', [ __CLASS__, 'post_rewrite_rules' ] ); add_filter( 'saved_' . self::$location_tax, [ __CLASS__, '_location_top_slugs_refresh_cache' ] ); add_filter( 'delete_' . self::$location_tax, [ __CLASS__, '_location_top_slugs_refresh_cache' ] ); add_filter( 'pre_term_link', [ __CLASS__, 'make_term_link' ], 10, 2 ); add_filter( 'query_vars', static function( $vars ){ $vars[] = 'catalog_location'; $vars[] = 'post_subpage'; return $vars; } ); add_filter( 'get_canonical_url', [ __CLASS__, 'subpages_canonical' ], 10, 2 ); add_filter( 'single'.'_template_hierarchy', [ __CLASS__, 'subpages_template_hierarchy' ] ); isset( $_GET['rwdebug'] ) && add_action( 'wp', [ __CLASS__, '_debug' ] ); // debug } static function is_multi_tax_query(){ return count( self::$request ) > 1; } static function register_post_types(){ register_taxonomy( self::$catalog_tax, [ self::$post_type ], [ 'label' => '', // определяется параметром $labels->name 'labels' => [ 'name' => 'Каталог', 'singular_name' => 'Каталог', ], 'public' => true, 'hierarchical' => true, 'rewrite' => false, //[ 'slug'=>self::$post_type, 'hierarchical'=>true, 'with_front'=>false, 'feed'=>false ], 'show_admin_column' => false, ] ); register_taxonomy( self::$location_tax, [ self::$post_type ], [ 'label' => '', // определяется параметром $labels->name 'labels' => [ 'name' => 'Локация', 'singular_name' => 'Локация', ], 'public' => true, 'hierarchical' => true, 'rewrite' => false, //[ 'slug'=>self::$post_type, 'hierarchical'=>true, 'with_front'=>false, 'feed'=>false ], 'show_admin_column' => false, ] ); register_post_type( self::$post_type, [ 'label' => null, 'labels' => [ 'name' => 'Компании', // основное название для типа записи 'singular_name' => 'Компания', // название для одной записи этого типа ], 'public' => true, 'menu_position' => null, 'menu_icon' => null, 'hierarchical' => false, 'supports' => [ 'title', 'editor' ], // 'title','editor','author','thumbnail','excerpt','trackbacks','custom-fields','comments','revisions','page-attributes','post-formats' 'taxonomies' => [ self::$catalog_tax, self::$location_tax ], 'has_archive' => true, 'rewrite' => [ 'with_front'=>false, 'feeds'=>false, 'endpoints'=>false ], 'query_var' => true, ] ); } static function fix_request( $vars ){ // post request like catalog|location if( ! empty( $vars['name'] ) && self::$post_type === $vars['post_type'] ){ $name = $vars['name']; $location_top = self::_location_top_slugs(); $unset_vars__fn = static function( & $vars ){ unset( $vars['post_type'], $vars['name'], $vars[ self::$post_type ] ); // clear }; // location if( false !== strpos( $location_top, "|$name|" ) ){ $unset_vars__fn( $vars ); // clear $vars[ self::$location_tax ] = $name; } // catalog elseif( $term = get_term_by( 'slug', $name, self::$catalog_tax ) ){ $unset_vars__fn( $vars ); // clear $vars[ self::$catalog_tax ] = $name; } } // special catalog_location request elseif( ! empty( $vars['catalog_location'] ) ){ $parts = explode( '/', $vars['catalog_location'] ); // maybe company post with post_subpage if( count( $parts ) === 2 && isset( self::$post_subpages[ $parts[1] ] ) && $post = get_page_by_path( $parts[0], OBJECT, [ self::$post_type ] ) ){ $vars[ self::$post_type ] = $parts[0]; $vars['post_type'] = self::$post_type; $vars['name'] = $parts[0]; $vars['post_subpage'] = $parts[1]; } // catalog_location request else { $location_top = self::_location_top_slugs(); $catalogs = $locations = []; $lnk = & $catalogs; foreach( $parts as $part ){ if( false !== strpos( $location_top, "|$part|" ) ) $lnk = & $locations; $lnk[] = $part; } unset( $lnk ); $catalog_slug = end( $catalogs ); $location_slug = end( $locations ); // for check reliability of the URL to redirect to right one $build_catalog = $catalog_slug ? self::_build_tax_uri( $catalog_slug, self::$catalog_tax ) : ''; $build_location = $location_slug ? self::_build_tax_uri( $location_slug, self::$location_tax ) : ''; // two taxs if( $catalog_slug && $location_slug ){ // 301 redirect to correct URL if( ( implode( '/', $catalogs ) !== $build_catalog ) || ( implode( '/', $locations ) !== $build_location ) ){ wp_redirect( user_trailingslashit( '/'. self::$post_type ."/$build_catalog/$build_location" ), 301 ); exit; } self::$request['catalog'] = get_term_by( 'slug', $catalog_slug, self::$catalog_tax ); self::$request['location'] = get_term_by( 'slug', $location_slug, self::$location_tax ); $vars[ self::$catalog_tax ] = $catalog_slug; $vars[ self::$location_tax ] = $location_slug; } // catalog elseif( $catalog_slug ){ // 301 redirect to correct URL if( implode( '/', $catalogs ) !== $build_catalog ){ $url = get_term_link( $catalog_slug, self::$catalog_tax ); if( ! is_wp_error( $url ) ){ wp_redirect( $url, 301 ); exit; } } self::$request['catalog'] = get_term_by( 'slug', $catalog_slug, self::$catalog_tax ); $vars[ self::$catalog_tax ] = $catalog_slug; } // location elseif( $location_slug ){ // 301 redirect to correct URL if( implode( '/', $locations ) !== $build_location ){ wp_redirect( get_term_link( $location_slug, self::$location_tax ), 301 ); exit; } self::$request['location'] = get_term_by( 'slug', $location_slug, self::$location_tax ); $vars[ self::$location_tax ] = $location_slug; } } unset( $vars['catalog_location'] ); } return $vars; } static function post_rewrite_rules( $rules ){ $_first_part = self::$post_type . "/(.+?)"; $_page_part = 'page/?([0-9]{1,})'; $more_riles = [ "$_first_part/$_page_part/?$" => 'index.php?catalog_location=$matches[1]&paged=$matches[2]', "$_first_part/?$" => 'index.php?catalog_location=$matches[1]', ]; // delete conflict attachment rules foreach( $rules as $regex => $rule ){ if( false === strpos( $regex, '/attachment/' ) && false !== strpos( $rule, 'attachment=' ) ) unset( $rules[ $regex ] ); } $rules += $more_riles; return $rules; } static function make_term_link( $url, $term ){ if( $term->taxonomy === self::$catalog_tax || $term->taxonomy === self::$location_tax ){ return user_trailingslashit( '/'. self::$post_type .'/'. self::_build_tax_uri( $term ) ); } return $url; } /** * Gets parent terms (including current). * * @param WP_Term|string $term * @param string $taxonomy IF term passed as string. * * @return WP_Term[]|array */ static function parent_terms( $term, $taxonomy = null ){ if( is_string( $term ) ) $term = get_term_by( 'slug', $term, $taxonomy ); if( ! $term ) return []; $path = [ $term ]; while( $term->parent ){ $term = get_term( $term->parent ); $path[] = $term; } return array_reverse( $path ); } /** * Build URL part of term of specified taxonomy. EX: 'parent/child/childchild' * * @param WP_Term|string $term * @param string $taxonomy IF term passed as string. * * @return string */ static function _build_tax_uri( $term, $taxonomy = null ){ $slugs = wp_list_pluck( self::parent_terms( $term, $taxonomy ), 'slug' ); return implode( '/', $slugs ); } /** * Get top level location taxonomy slugs as array. Cache the result. * * @param bool $refresh */ static function _location_top_slugs( $refresh = false ){ $trans_name = 'location_top_slugs'; $top_slugs = get_transient( $trans_name ); if( $refresh || ! $top_slugs ){ $top_slugs = get_terms( [ 'taxonomy' => self::$location_tax, 'parent' => 0, 'hide_empty' => false, ] ); $top_slugs = '|'. implode( '|', wp_list_pluck( $top_slugs, 'slug' ) ) .'|'; set_transient( $trans_name, $top_slugs, HOUR_IN_SECONDS ); } return $top_slugs; } static function _location_top_slugs_refresh_cache(){ self::_location_top_slugs( 'refresh' ); } static function subpages_template_hierarchy( $templates ){ if( $subpage = get_query_var('post_subpage') ){ $subpage_file = 'single-'. self::$post_type ."-$subpage.php"; array_unshift( $templates, $subpage_file ); } return $templates; } static function subpages_canonical( $canonical_url, $post ){ if( $subpage = get_query_var('post_subpage') ){ $canonical_url = user_trailingslashit( rtrim( get_permalink( $post ), '/' ) ."/$subpage" ); } return $canonical_url; } static function _debug(){ print_r( get_queried_object() ); print_r( $GLOBALS['wp'] ); print_r( $GLOBALS['wp_query'] ); print_r( $GLOBALS['wp_rewrite'] ); exit; } }