Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I created a 'forum' taxonomy, using these rules:

register_taxonomy(
  'forum',
  array('topic'),
  array(
    'public' => true,
    'name' => _a('Forums'),
    'singular_name' => _a('Forum'),
    'show_ui' => true,
    'show_in_nav_menus' => true,
    'hierarchical' => true,

    'labels' => array(
      'name' => _a('Forums'),
      'singular_name' => _a('Forum'),
      'search_items' => _a('Search Forums'),
      'popular_items' => _a('Popular Forums'),
      'all_items' => _a('All Forums'),
      'parent_item' => _a('Parent Forum'),
      'parent_item_colon' => _a('Parent Forum:'),
      'edit_item' => _a('Edit Forum'),
      'update_item' => _a('Update Forum'),
      'add_new_item' => _a('Add New Forum'),
      'new_item_name' => _a('New Forum Name'),
    ),
    'query_var' => true,
    'rewrite' => array('slug' => 'forums', 'with_front' => false, 'hierarchical' => true),  
  )
);

In the front-end the URLs looks like:

forums/general-discussion/sub-forum

How can I remove the front slug ("forums")? Ie, change the URLs to:

general-discussion/sub-forum

If I pass a empty slug argument to register_taxonomy() it works, but that causes issues with the permalinks of the post type associated with this taxonomy

share|improve this question
    
@One Trick Pony - Have you tried instead of leaving 'slug' => 'forums' blank just removing it altogether and just having 'rewrite' => array('with_front' => false, 'hierarchical' => true)? I think that has worked in the past for me. Also make sure you flush the permalinks. –  eileencodes Feb 15 '11 at 18:32
    
tried that, and the permalinks look the same. Adding 'slug' => '' makes it work, but then posts using this taxonomy generate 404s –  onetrickpony Feb 15 '11 at 19:28
    
@One Trick Pony - Besides 'general-discussion' what other top level path segments do you need? –  MikeSchinkel Feb 19 '11 at 5:26
    
any %forum% should be a top level segment –  onetrickpony Feb 19 '11 at 12:14
    
@One Trick Pony - I was just hoping your give me some other top level path segments for context. –  MikeSchinkel Feb 20 '11 at 17:49

8 Answers 8

up vote 8 down vote accepted
+500

URL Design has been important to be for well over a decade; I even wrote a blog about it several years back. And while WordPress is sum is a brilliant bit of software unfortunately it's URL rewrite system is just short of brain dead (IMHO, of course. :) Anyway, glad to see people caring about URL design!

The answer I'm going to provide is a plugin I'm calling WP_Extended that is a proof of concept for this proposal on Trac (Note that proposal started as one thing and evolved into another, so you have to read the entire thing to see where it was headed.)

Basically the idea is to subclass the WP class, override the parse_request() method, and then assign the global $wp variable with an instance of the subclass. Then within parse_request() you actually inspect the path by path segment instead of using a list of regular expressions that must match the URL in their entirety.

So to state it explicitly, this technique inserts logic in front of the parse_request() which checks for URL-to-RegEx matches and instead first looks for taxonomy term matches, but it ONLY replaces parse_request() and leaves the entire rest of the WordPress URL routing system intact including and especially the use of the $query_vars variable.

For your use-case this implementation only compares URL path segments with taxonomy terms since that's all you need. This implementation inspects taxonomy terms respecting parent-child term relationships and when it finds a match it assigns the URL path (minus leading and trailing slashes) to $wp->query_vars['category_name'], $wp->query_vars['tag'] or $wp->query_vars['taxonomy'] & $wp->query_vars['term'] and it bypasses the parse_request() method of the WP class.

On the other hand if the URL path does not match a term from a taxonomy you've specified it delegates URL routing logic to the WordPress rewrite system by calling the parse_request() method of the WP class.

To use WP_Extended for your use-case you'll need to call the register_url_route() function from within your theme's functions.php file like so:

add_action('init','init_forum_url_route');
function init_forum_url_route() {
  register_url_route(array('taxonomy'=>'forum'));
}

What here is the source code for the 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 is 1st code run after WP is created.
    global $wp;
    $wp = new WP_Extended();  // Replace the 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) { // Make sure we test parents
              $taxonomy_term = array();
            } else {
              $parent_id = $term->term_id; // Capture parent ID for verification
              $taxonomy_term[] = $term->slug; // Collect slug as path segment
              unset($terms[$term_index]); // No need to scan it again
            }
            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); // Delegate to WP class
    }
  }
}
WP_Extended::on_load();

P.S. CAVEAT #1

Although for a given site I think this technique works brilliantly but this technique should NEVER be used for a plugin to be distributed on WordPress.org for others to use. If it is at the core of a software package based on WordPress then that might be okay. Otherwise this technique should be limited to improving the URL routing for a specific site.

Why? Because only one plugin can use this technique. If two plugins try to use it they will conflict with each other.

As an aside this strategy can be expanded to generically handle practically every use-case pattern that could be required and that's what I intend to implement as soon as I either find the spare time or a client who can sponsor the time that it would take to build fully generic implementations.

CAVEAT #2

I wrote this to override parse_request() which is a very large function, and it is quite possible that I missed a property or two of the global $wp object that I should have set.. So if something acts wonky let me know and I'll be happy to research it and revise the answer if need be.

Anyway...

UPDATE

Since writing this WordPress core has added the 'do_parse_request' hook that allows URL routing to be handled elegantly and without the need to extend the WP class. I covered the topic in-depth in my 2014 Atlanta WordCamp talk entitled "Hardcore URL Routing"; the slides are available at the link.

share|improve this answer
    
After writing this I realized that I tested for categories instead of for taxonomy terms in general so the above will not work for the 'forum' taxonomy however I will revise it to work later today... –  MikeSchinkel Feb 23 '11 at 12:58
    
So I've updated the code to address the problem I mention in the prior comment. –  MikeSchinkel Feb 23 '11 at 23:55
    
can't get this work... do I need to change the rewrite rules? –  onetrickpony Feb 24 '11 at 10:51
    
@One Trick Pony - A little more diagnostic information would help. :) What did you try? What happens when you enter the URLs into your browser? Did you per chance call your taxonomy 'forums' rather than 'forum'? Are you expecting the URLs that link to these pages to change (if yes, no wonder, my code does not address the printing of URLs, only routing of URLs.) –  MikeSchinkel Feb 24 '11 at 19:38
    
no, I can change the URLs (I think its the term_link function that I need to hook into for that). site/rootforum/ works, but site/rootforum/subforum/ doesn't (404 error) ... –  onetrickpony Feb 24 '11 at 19:56

Not sure if this will work for taxonomies, but it worked for custom post types

Although it hasn't been updated for 2 years, the below plugin worked for me: http://wordpress.org/plugins/remove-slug-from-custom-post-type/

FYI I'm running WP 3.9.1 with WP Types 1.5.7

share|improve this answer

There is a plugin for this.

It removes the type slug by adding a specific rule for each custom post type page.

share|improve this answer

Simple, really.

Step 1: Stop using the rewrite parameter at all. We're going to roll your own rewrites.

'rewrite'=>false;

Step 2: Set verbose page rules. This forces normal Pages to have their own rules instead of being a catch-all at the bottom of the page.

Step 3: Create some rewrite rules to handle your use cases.

Step 4: Manually force a flush rules to happen. Easiest way: go to settings->permalink and click the save button. I prefer this over a plugin activation method for my own usage, since I can force the rules to flush whenever I change things around.

So, code time:

function test_init() {
    // create a new taxonomy
    register_taxonomy(
        'forum',
        'post',
        array(
            'query_var' => true,
            'public'=>true,
            'label'=>'Forum',
            'rewrite' => false,
        )
    );

    // force verbose rules.. this makes every Page have its own rule instead of being a 
    // catch-all, which we're going to use for the forum taxo instead
    global $wp_rewrite;
    $wp_rewrite->use_verbose_page_rules = true;

    // two rules to handle 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]');

    // one rule to handle paging of posts in the taxo
    add_rewrite_rule('(.+)/page/?([0-9]{1,})/?$','index.php?forum=$matches[1]&paged=$matches[2]');

    // one rule to show the forum taxo normally
    add_rewrite_rule('(.+)/?$', 'index.php?forum=$matches[1]');
}

add_action( 'init', 'test_init' );

Remember that after adding this code, you need to have it active when you go flush the permalink rules (by Saving the page on Settings->Permalinks)!

After you've flushed the rules and saved to the database, then /whatever should go to your forum=whatever taxonomy page.

Rewrite rules really aren't that difficult if you understand regular expressions. I use this code to help me when debugging them:

function test_foot() {
    global $wp_rewrite;
    echo '<pre>';
    var_dump($wp_rewrite->rules);
    echo '</pre>';
}
add_action('wp_footer','test_foot');

This way, I can see the current rules at a glance on my page. Just remember that given any URL, the system starts at the top of the rules and goes down through them until it finds one that matches. The match is then used to rewrite the query into a more normal looking ?key=value set. Those keys get parsed into what goes into the WP_Query object. Simple.

Edit: Side note, this method will probably only work if your normal custom post structure starts with something that isn't a catchall, like %category% or some such thing like that. You need to start it with a static string or a numeric, like %year%. This is to prevent it catching your URL before it gets to your rules.

share|improve this answer
    
If you want easier debugging of your rewrite rules, I (again) recommend my rewrite analyzer plugin, which allows you to try out the rules and see the query variables on the fly. –  Jan Fabry Feb 24 '11 at 12:28
    
Unfortunately the current URL rewrite system forces the flattening of all potential URL patterns into a large list vs. following the inherent tree structure of URL paths. The current setup can't conveniently match an array of literals such as categories, or forum names; as you know it forces all "Page" URLs to be evaluated first. Matching by path segment and matching in multiple ways (array of literals, categories, tags, tax-terms, user names, post types, post names, callbacks, filter hooks, and finally RegEx) would scale for complexity better, and would be easier to understand. –  MikeSchinkel Feb 25 '11 at 4:42
    
Mike: Actually, that is not easier to understand at all, because I haven't got the first clue WTF you're talking about there. Your ideas of URL routing are confusing and difficult, and as you probably know, I disagree with them. The flat search makes more sense and is more flexible than you tend to give it credit for. Most people don't want all that needless complexity in their URLs, and almost nobody needs it either. –  Otto Feb 25 '11 at 17:03
    
Thanks, but I think I already tried this before (wordpress.stackexchange.com/questions/9455/…) –  onetrickpony Feb 25 '11 at 19:00
    
Fortunately WordPress Answers now allows people who do want control of their URLs to finally have a voice, and they seem to be many (100+). But I respect that you may not be able to follow my example prior to a full implementation. I predict that, once the approach I am advocating is fully implemented in a plugin and after about 6-12 months it will have become the preferred way for WordPress-based CMS sites to route their URLs. So lets resume this debate in about 9 months. –  MikeSchinkel Mar 1 '11 at 17:38

You will not be able to do this using WP_Rewrite alone, since it can't distinguish between term slugs and post slugs.

You have to also hook into 'request' and prevent the 404, by setting the post query var instead of the taxonomy one.

Something like this:

function fix_post_request( $request ) {
    $tax_qv = 'forum';
    $cpt_name = 'post';

    if ( !empty( $request[ $tax_qv ] ) ) {
        $slug = basename( $request[ $tax_qv ] );

        // if this would generate a 404
        if ( !get_term_by( 'slug', $slug, $tax_qv ) ) {
            // set the correct query vars
            $request[ 'name' ] = $slug;
            $request[ 'post_type' ] = $cpt_name;
            unset( $request[$tax_qv] );
        }
    }

    return $request;
}
add_filter( 'request', 'fix_post_request' );

Note that the taxonomy has to be defined before the post type.

This would be a good time to point out that having a taxonomy and a post type with the same query var is a Bad Idea.

Also, you won't be able to reach posts that have the same slug as one of the terms.

share|improve this answer
    
Agreed that having a taxonomy and a post type with the same query var is a Bad Idea, but that might imply to people having a taxonomy and a post type with the same name is a bad idea, which is not the case. If using the same name then only one of the two should have a query var. –  MikeSchinkel Feb 20 '11 at 17:51

Since I'm a familiar with your other question, I'll answer with that in mind.

I haven't tested this at all, but it might work if you execute this once right after you register all the permastructs you want.:

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();

What this does: it removes the rewrite rules generated from the topics permalink from the normal flow of the rules array and re-merges them at the end of the array. This prevents those rules from interfering with any other rewrite rules. Next, it forces verbose rewrite rules (each page gets an individual rule with a specific regular expression). This prevents the pages from interfering with your topic's rules. Finally, it executes a hard flush (make sure your .htaccess file is writable, otherwise this won't work) and saves the very large very complicated array of rewrite rules.

share|improve this answer
    
tried it, nothing changes –  onetrickpony Feb 17 '11 at 23:21

I'd suggest taking a look at the Custom Post Permalinks plugin. I don't have time to test right now, but it may help with your situation.

share|improve this answer
    
it doesn't, it only handles posts, not taxonomies, and even if it would, I'd have to add some kind of prefix before %forum%, which is exactly what I'm trying to avoid... –  onetrickpony Feb 17 '11 at 22:14

I'd take a look at the code of the top level cats plugin:

http://fortes.com/projects/wordpress/top-level-cats/

You could easily adapt that so it's looking for your custom taxonomy slug by changing the

$category_base = get_option('category_base');

on line 74 to something like like:

$category_base = 'forums';
share|improve this answer
    
Might work for categories, but it doesn't for custom taxonomies (at least in wp 3.1)... I managed to change the URLs, but I get 404 errors –  onetrickpony Feb 15 '11 at 19:29

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.