WordPress Rewrite Hooks

Filters and actions for customizing URL rewriting and permalink generation.


Actions

generate_rewrite_rules

Fires after rewrite rules are generated.

do_action_ref_array( 'generate_rewrite_rules', array( &$wp_rewrite ) )

Parameters:

  • $wp_rewrite (WP_Rewrite) – Current WP_Rewrite instance (passed by reference)

Description:

Called at the end of WP_Rewrite::rewrite_rules() after all rules are built. Allows direct manipulation of the WP_Rewrite object before rules are finalized.

Example:

add_action( 'generate_rewrite_rules', function( $wp_rewrite ) {
    // Add custom rules directly
    $wp_rewrite->rules = array_merge(
        array( 'custom/([^/]+)/?$' => 'index.php?custom=$matches[1]' ),
        $wp_rewrite->rules
    );
});

Since: 1.5.0


permalink_structure_changed

Fires after the permalink structure is updated.

do_action( 'permalink_structure_changed', string $old_permalink_structure, string $permalink_structure )

Parameters:

  • $old_permalink_structure (string) – The previous permalink structure
  • $permalink_structure (string) – The new permalink structure

Example:

add_action( 'permalink_structure_changed', function( $old, $new ) {
    // Log permalink changes
    error_log( sprintf( 'Permalinks changed from "%s" to "%s"', $old, $new ) );
    
    // Notify admin
    if ( ! empty( $new ) && empty( $old ) ) {
        // Pretty permalinks just enabled
    }
}, 10, 2 );

Since: 2.8.0


Filters

url_to_postid

Filters the URL before determining the post ID.

apply_filters( 'url_to_postid', string $url )

Parameters:

  • $url (string) – The URL to derive the post ID from

Example:

add_filter( 'url_to_postid', function( $url ) {
    // Handle custom URL redirects
    if ( str_contains( $url, '/old-path/' ) ) {
        $url = str_replace( '/old-path/', '/new-path/', $url );
    }
    return $url;
});

Since: 2.2.0


rewrite_rules_array

Filters the full set of generated rewrite rules.

apply_filters( 'rewrite_rules_array', string[] $rules )

Parameters:

  • $rules (string[]) – Compiled array of rewrite rules, keyed by regex pattern

Description:

The most commonly used hook for modifying rewrite rules. Called after all rules are generated but before they’re saved. Rules are processed in order; first match wins.

Example:

add_filter( 'rewrite_rules_array', function( $rules ) {
    // Add rule at the beginning (highest priority)
    $new_rules = array(
        'api/v1/([^/]+)/?$' => 'index.php?api_version=1&api_action=$matches[1]',
    );
    
    return array_merge( $new_rules, $rules );
});

// Register the query vars
add_filter( 'query_vars', function( $vars ) {
    $vars[] = 'api_version';
    $vars[] = 'api_action';
    return $vars;
});

Since: 1.5.0


mod_rewrite_rules

Filters the mod_rewrite rules formatted for .htaccess output.

apply_filters( 'mod_rewrite_rules', string $rules )

Parameters:

  • $rules (string) – mod_rewrite rules formatted for .htaccess

Example:

add_filter( 'mod_rewrite_rules', function( $rules ) {
    // Add custom Apache directives
    $custom = "
# Custom security rules
RewriteCond %{REQUEST_URI} ^/wp-admin/install.php [NC]
RewriteRule .* - [F]
";
    
    return str_replace( '</IfModule>', $custom . '</IfModule>', $rules );
});

Since: 1.5.0


iis7_url_rewrite_rules

Filters the IIS7 URL Rewrite rules for web.config.

apply_filters( 'iis7_url_rewrite_rules', string $rules )

Parameters:

  • $rules (string) – Rewrite rules formatted for IIS web.config

Since: 2.8.0


flush_rewrite_rules_hard

Filters whether a "hard" rewrite rule flush should be performed.

apply_filters( 'flush_rewrite_rules_hard', bool $hard )

Parameters:

  • $hard (bool) – Whether to flush rewrite rules "hard". Default true.

Description:

A "hard" flush updates .htaccess (Apache) or web.config (IIS). Return false to prevent file modifications.

Example:

// Prevent .htaccess modifications in staging
add_filter( 'flush_rewrite_rules_hard', function( $hard ) {
    if ( defined( 'WP_ENV' ) && WP_ENV === 'staging' ) {
        return false;
    }
    return $hard;
});

Since: 3.7.0


Category-Specific Rule Filters

post_rewrite_rules

Filters rewrite rules used for "post" archives.

apply_filters( 'post_rewrite_rules', string[] $rules )

Parameters:

  • $rules (string[]) – Array of rewrite rules for posts

Since: 1.5.0


date_rewrite_rules

Filters rewrite rules used for date archives.

apply_filters( 'date_rewrite_rules', string[] $rules )

Parameters:

  • $rules (string[]) – Array of rewrite rules for date archives (yyyy, yyyy/mm, yyyy/mm/dd)

Since: 1.5.0


root_rewrite_rules

Filters rewrite rules used for root-level archives.

apply_filters( 'root_rewrite_rules', string[] $rules )

Parameters:

  • $rules (string[]) – Array of root-level rewrite rules (homepage pagination, site feeds)

Since: 1.5.0


comments_rewrite_rules

Filters rewrite rules used for comment feed archives.

apply_filters( 'comments_rewrite_rules', string[] $rules )

Parameters:

  • $rules (string[]) – Array of rewrite rules for site-wide comment feeds

Since: 1.5.0


search_rewrite_rules

Filters rewrite rules used for search archives.

apply_filters( 'search_rewrite_rules', string[] $rules )

Parameters:

  • $rules (string[]) – Array of rewrite rules for search queries

Since: 1.5.0


author_rewrite_rules

Filters rewrite rules used for author archives.

apply_filters( 'author_rewrite_rules', string[] $rules )

Parameters:

  • $rules (string[]) – Array of rewrite rules for author archives

Since: 1.5.0


page_rewrite_rules

Filters rewrite rules used for "page" post type archives.

apply_filters( 'page_rewrite_rules', string[] $rules )

Parameters:

  • $rules (string[]) – Array of rewrite rules for pages

Since: 1.5.0


{$permastructname}_rewrite_rules

Filters rewrite rules for individual permastructs (dynamic hook).

apply_filters( "{$permastructname}_rewrite_rules", string[] $rules )

Parameters:

  • $rules (string[]) – Array of rewrite rules for the permastruct

Description:

Called for each registered permastruct. The $permastructname is the name used when registering the permastruct.

Common Hook Names:

  • category_rewrite_rules – Category archive rules
  • post_tag_rewrite_rules – Tag archive rules
  • post_format_rewrite_rules – Post format archive rules
  • {custom_taxonomy}_rewrite_rules – Custom taxonomy rules
  • {custom_post_type}_rewrite_rules – Custom post type rules

Example:

// Modify category rules
add_filter( 'category_rewrite_rules', function( $rules ) {
    // Add custom rule for category feeds
    $new_rules = array(
        'category/([^/]+)/newsletter/?$' => 'index.php?category_name=$matches[1]&newsletter=1',
    );
    return array_merge( $new_rules, $rules );
});

// Modify custom post type rules
add_filter( 'product_rewrite_rules', function( $rules ) {
    // Remove feed rules for products
    foreach ( $rules as $pattern => $query ) {
        if ( str_contains( $query, 'feed=' ) ) {
            unset( $rules[ $pattern ] );
        }
    }
    return $rules;
});

Since: 3.1.0


tag_rewrite_rules (Deprecated)

Filters rewrite rules used specifically for tags.

apply_filters_deprecated( 'tag_rewrite_rules', array( $rules ), '3.1.0', 'post_tag_rewrite_rules' )

Deprecated: 3.1.0 – Use post_tag_rewrite_rules instead.


rewrite_rules (Deprecated)

Filters the mod_rewrite rules.

apply_filters_deprecated( 'rewrite_rules', array( $rules ), '1.5.0', 'mod_rewrite_rules' )

Deprecated: 1.5.0 – Use mod_rewrite_rules instead.


Hook Usage Patterns

Adding Custom Rewrite Rules

The preferred method for adding rules:

// Method 1: Using add_rewrite_rule() on init
add_action( 'init', function() {
    add_rewrite_rule(
        '^events/([0-9]{4})/([0-9]{2})/?$',
        'index.php?post_type=event&year=$matches[1]&monthnum=$matches[2]',
        'top'
    );
});

// Method 2: Using rewrite_rules_array filter
add_filter( 'rewrite_rules_array', function( $rules ) {
    $custom = array(
        '^events/([0-9]{4})/([0-9]{2})/?$' => 'index.php?post_type=event&year=$matches[1]&monthnum=$matches[2]',
    );
    return array_merge( $custom, $rules );
});

Removing Specific Rules

add_filter( 'rewrite_rules_array', function( $rules ) {
    // Remove all feed rules
    foreach ( $rules as $pattern => $query ) {
        if ( preg_match( '/feed/', $query ) ) {
            unset( $rules[ $pattern ] );
        }
    }
    return $rules;
});

Modifying Category Rules

add_filter( 'category_rewrite_rules', function( $rules ) {
    $modified = array();
    
    foreach ( $rules as $pattern => $query ) {
        // Change category base from 'category' to 'topics'
        $new_pattern = str_replace( 'category/', 'topics/', $pattern );
        $modified[ $new_pattern ] = $query;
    }
    
    return $modified;
});

Debugging Rewrite Rules

// Log all rules when they're generated
add_filter( 'rewrite_rules_array', function( $rules ) {
    if ( WP_DEBUG ) {
        error_log( 'Rewrite rules generated: ' . count( $rules ) );
        
        // Log first 10 rules
        $i = 0;
        foreach ( $rules as $pattern => $query ) {
            error_log( sprintf( 'Rule %d: %s => %s', ++$i, $pattern, $query ) );
            if ( $i >= 10 ) break;
        }
    }
    return $rules;
});

Conditional Rule Modification

add_filter( 'rewrite_rules_array', function( $rules ) {
    // Only modify for specific conditions
    if ( ! is_multisite() ) {
        return $rules;
    }
    
    // Remove author archives for security
    foreach ( $rules as $pattern => $query ) {
        if ( str_contains( $query, 'author_name=' ) ) {
            unset( $rules[ $pattern ] );
        }
    }
    
    return $rules;
});

Full Rewrite Replacement

// Complete control over rewrite rules
add_action( 'generate_rewrite_rules', function( $wp_rewrite ) {
    // Replace all rules with minimal set
    $wp_rewrite->rules = array(
        '^$'                      => 'index.php',
        '^([^/]+)/?$'             => 'index.php?name=$matches[1]',
        '^([^/]+)/([0-9]+)/?$'    => 'index.php?name=$matches[1]&page=$matches[2]',
        '^page/([0-9]+)/?$'       => 'index.php?paged=$matches[1]',
        '^feed/(feed|rdf|rss|rss2|atom)/?$' => 'index.php?feed=$matches[1]',
    );
});

Performance Considerations

Rule Count Impact

More rules = slower request parsing. Keep rules minimal:

// Monitor rule count
add_filter( 'rewrite_rules_array', function( $rules ) {
    $count = count( $rules );
    
    if ( $count > 500 ) {
        error_log( "Warning: High rewrite rule count: $count" );
    }
    
    return $rules;
});

Caching Considerations

Rules are cached in wp_options. Avoid flushing unnecessarily:

// BAD: Flushes on every page load
add_action( 'init', function() {
    add_rewrite_rule( '...', '...' );
    flush_rewrite_rules(); // DON'T
});

// GOOD: Flush only on activation/deactivation
register_activation_hook( __FILE__, function() {
    add_rewrite_rule( '...', '...' );
    flush_rewrite_rules();
});

Rule Priority Optimization

Put most-matched rules first:

add_filter( 'rewrite_rules_array', function( $rules ) {
    // Move frequently-accessed patterns to the top
    $priority = array();
    $normal = array();
    
    foreach ( $rules as $pattern => $query ) {
        // Single posts are most common
        if ( str_contains( $query, 'name=' ) && ! str_contains( $query, 'category' ) ) {
            $priority[ $pattern ] = $query;
        } else {
            $normal[ $pattern ] = $query;
        }
    }
    
    return array_merge( $priority, $normal );
});