If we want to create our own permalinks for custom post types, we can do so by using the WP_Rewrite WordPress object.
We can also create our own permalink rules from scratch, by using the same object.
One very important function while creating permalink rules is the $wp_rewrite->flush_rules() function. This function does the following –
- Deletes the ‘rewrite_rules’ option. This is where all the permalink rules for our blog are stored. When someone clicks on one of our blog pages, the parse_request function retrieves these rewrite rules and translates them into a query request.
- Regenerates new rewrite rules from scratch using the wp_rewrite_rules function. This function regenerates the rules and updates the ‘rewrite_rules’ option.
- It may also write to the .htaccess file if necessary.
When we add new permalink structures with register_post_type or add_permastruct, the structures only get added to the global $wp_rewrite object. It does not manifest as a rewrite rule until after we execute the $wp_rewrite->flush_rules() command.
That is why if we have a plugin that adds new permalink rules (including through register_post_type) we want to run $wp_rewrite->flush_rules() –
- During the activation process to ensure that our new permalink structures show up in the blog rewrite rules.
- During the deactivation process to remove our permalink structures from the blog rewrite rules.
1 | function _activate() { |
2 | global $wp_rewrite ; |
3 | $wp_rewrite ->add_rewrite_tag( "%gallery%" , '([^/]+)' , "gallery=" ); |
4 | $wp_rewrite ->add_permastruct( 'gallery' , $gallery_structure , false); |
5 | $wp_rewrite ->flush_rules(); |
6 | } |
7 |
8 | function _deactivate() { |
9 | global $wp_rewrite ; |
10 | $wp_rewrite ->add_permastruct( 'gallery' , '' ); |
11 | $wp_rewrite ->flush_rules(); |
12 | } |
Note – flush_rules is also run by the WordPress system when we Save Changes in Settings >> Permalinks. Therefore, if our blog permalinks are not working properly, we can manually flush our permalink rules by going into Settings >> Permalinks and doing a save.
Flushing Permalink Rules in WordPress Multi-Site
In WordPress multi-site, we may need to flush rewrite rules for multiple blogs during network activation and deactivation.
However, there is a fly in the ointment. When we change blogs using set_blog_id, we are only changing the database context. The global $wp_rewrite object remains the same, i.e., it is the $wp_rewrite object of the original blog from where we executed the network activate or deactivate.
If we cycle through the blogs of a multi-site configuration and simply execute flush_rules, the wrong permalink rules from the non-updated $wp_rewrite object will be written into the ‘rewrite_rules’ option of each of the blogs. In effect, we would mess up the permalink rules of all our blogs except for the original activation/deactivation blog.
To properly flush permalink rules, we must limit our changes to the database and not access blog specific data stored within the $wp_rewrite object. Below, we do this by adding new permalink rules directly into the ‘rewrite_rules’ database option instead of using the flush_rules function.
1 | function shiba_add_rewrite_rules( $permastruct , $ep_mask =EP_NONE) { |
2 | if (! $permastruct ) return ; |
3 | global $wp_rewrite ; |
4 | $wp_rewrite ->matches = 'matches' ; // this is necessary to write the rules properly |
5 | $new_rules = $wp_rewrite ->generate_rewrite_rules( $permastruct , $ep_mask ); |
6 | $rules = get_option( 'rewrite_rules' ); |
7 | $rules = array_merge ( $new_rules , $rules ); |
8 | update_option( 'rewrite_rules' , $rules ); |
9 | } |
10 |
11 | // remove all rewrite rules for a given permastruct |
12 | function shiba_remove_rewrite_rules( $permastruct , $ep_mask =EP_NONE) { |
13 | // replace all tags within permastruct |
14 | if (! $permastruct ) return ; |
15 | global $wp_rewrite ; |
16 | $wp_rewrite ->matches = 'matches' ; |
17 | $remove_rules = $wp_rewrite ->generate_rewrite_rules( $permastruct ); |
18 | $num_rules = count ( $remove_rules ); |
19 | // Get first rule |
20 | $rule1 = reset( $remove_rules ); $key_rule1 = key( $remove_rules ); |
21 | |
22 | $rules = get_option( 'rewrite_rules' ); |
23 | $i = $num_rules ; |
24 | foreach ( $rules as $pretty_link => $query_link ) { |
25 | // find the first rule |
26 | if (( $pretty_link == $key_rule1 ) && ( $query_link == $rule1 )) { $i = 0; } |
27 | if ( $i < $num_rules ) { |
28 | // Delete next $num_rules |
29 | unset( $rules [ $pretty_link ]); $i ++; |
30 | } |
31 | } |
32 | update_option( 'rewrite_rules' , $rules ); |
33 | } |
Line 5 – Generate permalink rules for our plugin permalink structures. Even though we are using $wp_rewrite here, we are only using one of its functions. Most importantly, that function does not rely on any blog specific data or state stored in the $wp_rewrite object.
Line 6 – Get the ‘rewrite rules’ for the current blog. This is a database operation which does get updated with set_blog_id.
Line 7 – Merge our new permalink rules with the previous blog rules.
Line 8 – Update the ‘rewrite rules’ for the current blog.
In the shiba_remove_rewrite_rules function we use a similar process to remove the permalink rules added by our plugin.
We Are Done!
Now we can flush permalink rules in our regular and multi-site blogs!
A simpler solution for Multisite is to re-initialize the rewrite rules before flushing them:
https://gist.github.com/iandunn/c5a983d1a9d4b09d6d20
Thanks! Will have to try it out.
Hi Shiba,
Thanks for this information. How/where do we call the rewrite functions for multisite? Is it part of the activation routine, and where is the $permalink variable is being passed from?
Thanks!
(actually it’s the $permastruct var that’s being passed, sorry about that)
I pass in the permastruct variable myself. It is simply the permalink structure I want to set my object to, for example-
“/gallery/%monthnum%/%category%/%gallery%”
I call the flush functions during activation and deactivation.
Great! That makes sense. Thank you for the speedy reply. You are pretty, awesome, you know, right? : )
I’m back … I was having trouble with WordPress adding yet another “/blog/” before my custom post type slug. I had to set the “with_front” property of the custom post type rewrite setting to “False” (and not put it in quotes!!! yeah, that was kinda stupid). Anyway, I just though I’d mention it in case anyone else crazy enough to use both Multisite and Custom Post Types comes along.
You can also add rewrite rules within the rewrite_rules_array filter directly. I wrote up this gem earlier today with the purpose of manually creating callback linked action pages. The usage would be something like:
new action_page(‘cart’, array(‘items’));
add_action(‘admin_post_nopriv_cart’, array($this, ‘display_cart’));
add_action(‘admin_post_cart’, array($this, ‘display_cart’));
1
<?php
2
action_page::init();
3
class action_page {
4
5
protected $action;
6
protected $vars;
7
8
public static function init() {
9
add_action('template_redirect', array(__CLASS__, 'action_redirect'));
10
}
11
12
public function __construct( $action, $vars = array()) {
13
14
$this->action = $action;
15
$this->vars = $vars;
16
17
add_filter('query_vars', array($this, 'add_query_vars'));
18
add_filter('rewrite_rules_array', array($this, 'add_rewrite_rules'));
19
add_filter("{$action}_query_vars", array($this, 'import_vars'));
20
}
21
22
public function add_query_vars( $vars ) {
23
$vars[] = 'action';
24
return array_merge($vars, $this->vars);
25
}
26
27
public function add_rewrite_rules( $rules ) {
28
$regex = implode("/", $this->regex());
29
$regex = !empty($regex) ? "{$regex}/?$" : "?$";
30
$rules = array("{$this->action}/{$regex}" => $this->match()) + $rules;
31
return $rules;
32
}
33
34
protected function regex() {
35
$m = array_fill_keys($this->vars, "([^/]+)");
36
return apply_filters("{$this->action}_regex", $m);
37
}
38
39
protected function match() {
40
$size = sizeof($this->vars);
41
$match = "/index.php?action={$this->action}";
42
for($i = 0; $i < $size; $i++) {
43
$m = $i + 1;
44
$match .= "&{$this->vars[$i]}=\$matches[{$m}]";
45
}
46
return apply_filters("{$this->action}_match", $match);
47
}
48
49
public function import_vars( $vars = array() ) {
50
global $wp_query;
51
$vars += array_intersect_key($wp_query->query_vars, array_flip($this->vars));
52
return $vars;
53
}
54
55
56
public static function action_redirect() {
57
if($action = get_query_var('action')) {
58
$vars = apply_filters("{$action}_query_vars", array());
59
$action = wp_validate_auth_cookie() ? "admin_post_{$action}" : "admin_post_nopriv_{$action}";
60
do_action($action, $vars);
61
}
62
}
63
}
Still a bit confuse here.. I am trying to add a new category using the WP API (multiple sites), the permalinks are not refreshing.
So when a visitor goes to the page on the site it throws a 404 error. I have to manually go to to wp-admin -> settings -> permalinks and save to solve the problem.
Here is what I am doing that is not working:
add_action(‘init’, ‘flush_permalinks’);
function flush_permalinks(){
global $wp_rewrite;
$wp_rewrite->set_permalink_structure(‘/%postname%/’);
//$wp_rewrite->flush_rules();
// flush_rewrite_rules();
}
Could you find my error? Thanks.
what the $gallery_structure, in $wp_rewrite->add_permastruct(‘gallery’, $gallery_structure, false); contains, (I understand its a struct based on google search). BUt its initialized or assigned a value?
$gallery_structure contains a regular WP permalink structure, e.g. /galleries/%year%/%monthnum%/%gallery%.
http://shibashake.com/wordpress-theme/custom-post-type-permalinks-part-2