WordPress Rewrite API Overview
The WordPress Rewrite API transforms human-readable URLs (permalinks) into query parameters that WordPress can process. It’s the foundation of "pretty permalinks" and allows plugins/themes to create custom URL structures.
Core Concepts
Permalink Structures
A permalink structure is a template string containing rewrite tags that define how URLs map to content:
// Default post permalink structure
/%year%/%monthnum%/%day%/%postname%/
// Example: /2024/01/15/hello-world/
Common Permalink Structures:
| Structure | Example URL | Use Case |
|---|---|---|
/%postname%/ |
/hello-world/ |
Simple, SEO-friendly |
/%year%/%monthnum%/%postname%/ |
/2024/01/hello-world/ |
Date-based archives |
/%category%/%postname%/ |
/news/hello-world/ |
Category-prefixed |
/%post_id%/ |
/123/ |
Minimal, fast |
Rewrite Tags
Rewrite tags are placeholders in permalink structures that get replaced with regex patterns:
| Tag | Regex Pattern | Query Variable |
|---|---|---|
%year% |
([0-9]{4}) |
year= |
%monthnum% |
([0-9]{1,2}) |
monthnum= |
%day% |
([0-9]{1,2}) |
day= |
%hour% |
([0-9]{1,2}) |
hour= |
%minute% |
([0-9]{1,2}) |
minute= |
%second% |
([0-9]{1,2}) |
second= |
%postname% |
([^/]+) |
name= |
%post_id% |
([0-9]+) |
p= |
%author% |
([^/]+) |
author_name= |
%pagename% |
([^/]+?) |
pagename= |
%search% |
(.+) |
s= |
Endpoint Masks
Endpoint masks are bitmask constants that define where endpoints (like /feed/ or /trackback/) are appended:
| Constant | Value | Description |
|---|---|---|
EP_NONE |
0 | Matches nothing |
EP_PERMALINK |
1 | Post permalinks |
EP_ATTACHMENT |
2 | Attachment permalinks |
EP_DATE |
4 | Any date archives |
EP_YEAR |
8 | Yearly archives |
EP_MONTH |
16 | Monthly archives |
EP_DAY |
32 | Daily archives |
EP_ROOT |
64 | Site root |
EP_COMMENTS |
128 | Comment feeds |
EP_SEARCH |
256 | Search results |
EP_CATEGORIES |
512 | Category archives |
EP_TAGS |
1024 | Tag archives |
EP_AUTHORS |
2048 | Author archives |
EP_PAGES |
4096 | Pages |
EP_ALL_ARCHIVES |
7644 | All archive views (EP_DATE | EP_YEAR | EP_MONTH | EP_DAY | EP_CATEGORIES | EP_TAGS | EP_AUTHORS) |
EP_ALL |
8191 | Everything |
Using Endpoint Masks:
// Add endpoint to posts and pages only
add_rewrite_endpoint( 'json', EP_PERMALINK | EP_PAGES );
// Add endpoint to all archives
add_rewrite_endpoint( 'export', EP_ALL_ARCHIVES );
How Rewrite Rules Work
1. Rule Generation
When flush_rewrite_rules() is called, WordPress:
- Reads the permalink structure from
get_option('permalink_structure') - Generates regex patterns for each URL type (posts, pages, archives, feeds)
- Stores compiled rules in
get_option('rewrite_rules') - Optionally updates
.htaccess(Apache) orweb.config(IIS)
2. Request Matching
When a request comes in:
- The web server routes the request to
index.php(via.htaccessor similar) WP::parse_request()iterates through rewrite rules- First matching regex wins
- Captured groups populate query variables
WP_Queryexecutes with those variables
Example Flow:
URL: /2024/01/hello-world/
Rule: ^([0-9]{4})/([0-9]{1,2})/([^/]+)/?$
Query: index.php?year=$matches[1]&monthnum=$matches[2]&name=$matches[3]
Result: WP_Query with year=2024, monthnum=01, name=hello-world
3. Rule Priority
Rules are matched in order. Priority from highest to lowest:
extra_rules_top– Rules added withadd_rewrite_rule($regex, $query, 'top')- Core rules (robots.txt, favicon.ico, sitemap.xml)
- Root rules (homepage pagination, feeds)
- Comments rules
- Search rules
- Author rules
- Date rules
- Post/Page rules (order depends on
use_verbose_page_rules) extra_rules– Rules added withadd_rewrite_rule($regex, $query, 'bottom')
Permalink Structure Components
Front
The static portion before the first rewrite tag:
// Permalink: /blog/%postname%/
// Front: /blog/
// Permalink: /%postname%/
// Front: /
Root
The prefix for all permalink structures. Empty for mod_rewrite; index.php/ for PATHINFO permalinks:
// mod_rewrite: root = ''
// PATHINFO: root = 'index.php/'
Trailing Slashes
Controlled by whether the permalink structure ends with /:
// With trailing slash: /%postname%/
// URL: /hello-world/
// Without trailing slash: /%postname%
// URL: /hello-world
Common URL Patterns
Post URLs
/2024/01/15/hello-world/ → year, monthnum, day, name
/hello-world/ → name
/archives/123 → p (post ID)
Archive URLs
/2024/ → year archive
/2024/01/ → month archive
/2024/01/15/ → day archive
/author/admin/ → author archive
/category/news/ → category archive
/tag/wordpress/ → tag archive
Feed URLs
/feed/ → main feed
/feed/atom/ → Atom feed
/category/news/feed/ → category feed
/hello-world/feed/ → post comments feed
Pagination URLs
/page/2/ → homepage page 2
/category/news/page/3/ → category page 3
/hello-world/2/ → post page 2 (nextpage)
/hello-world/comment-page-2/ → comments page 2
Regex Patterns Reference
Pattern Syntax
WordPress uses PCRE (Perl Compatible Regular Expressions):
| Pattern | Matches |
|---|---|
([0-9]+) |
One or more digits |
([0-9]{4}) |
Exactly 4 digits (year) |
([0-9]{1,2}) |
1-2 digits (month/day) |
([^/]+) |
One or more non-slash characters |
([^/]+?) |
Non-greedy version (pages) |
(.+) |
One or more of anything |
(.*) |
Zero or more of anything |
/?$ |
Optional trailing slash, end of string |
Capture Groups
Each () creates a capture group referenced as $matches[N]:
// Regex: ^category/([^/]+)/page/([0-9]+)/?$
// URL: /category/news/page/2/
// $matches[1] = 'news'
// $matches[2] = '2'
// Query: index.php?category_name=$matches[1]&paged=$matches[2]
Server Configuration
Apache (.htaccess)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
Nginx
location / {
try_files $uri $uri/ /index.php?$args;
}
IIS (web.config)
<rule name="WordPress" patternSyntax="Wildcard">
<match url="*" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="index.php" />
</rule>
Global Variables
$wp_rewrite
The global WP_Rewrite instance:
global $wp_rewrite;
// Check if using permalinks
if ( $wp_rewrite->using_permalinks() ) {
// Pretty permalinks enabled
}
// Get current permalink structure
$structure = $wp_rewrite->permalink_structure;
Best Practices
1. Always Flush Rules on Activation/Deactivation
register_activation_hook( __FILE__, 'myplugin_activate' );
function myplugin_activate() {
myplugin_register_post_type(); // Register CPT first
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );
function myplugin_deactivate() {
flush_rewrite_rules();
}
2. Never Flush on Every Page Load
// BAD - flushes on every request
add_action( 'init', function() {
add_rewrite_rule( '...', '...' );
flush_rewrite_rules(); // DON'T DO THIS
});
// GOOD - flush only on activation
add_action( 'init', function() {
add_rewrite_rule( '...', '...' );
});
3. Use Specific Endpoint Masks
// BAD - adds endpoint everywhere
add_rewrite_endpoint( 'json', EP_ALL );
// GOOD - adds only where needed
add_rewrite_endpoint( 'json', EP_PERMALINK | EP_PAGES );
4. Test Rules with Query Monitor
Use the Query Monitor plugin to inspect matched rewrite rules and query variables.
Debugging
View All Rewrite Rules
global $wp_rewrite;
print_r( $wp_rewrite->wp_rewrite_rules() );
Check Which Rule Matched
add_action( 'parse_request', function( $wp ) {
echo 'Matched rule: ' . $wp->matched_rule;
echo 'Query: ' . $wp->matched_query;
});
Force Regenerate Rules
Visit Settings → Permalinks and click "Save Changes" to trigger a flush.
Or programmatically:
flush_rewrite_rules( true ); // true = hard flush (update .htaccess)