URL to Post ID for Custom Post Types
Three years ago, when I started using WordPress, I was wondering about one thing: “How should we convert a WordPress’s Post URL to its corresponding ID?”. It would be very useful if we could do such a thing, especially when we have to work with AJAX applications.
Fortunately WordPress has a neat function for such purpose, and it’s called url_to_postid1. That function is located in wp-includes/rewrite.php, which does make some sense as we actually want to rewrite a URL so that it becomes an ID.
The magic function above, though, has one major caveat: It does not work with custom post types, which is a shame, really. I do believe that custom post type support will be added in future releases of WordPress, but until then, it’s no harm creating our custom function, right?
We will simply copy url_to_postid from rewrite.php, rename it, and then add a new block of codes. With that in mind, fire up your .php file of choice and add this new conversion function to it:
- /* Post URLs to IDs function, supports custom post types - borrowed and modified from url_to_postid() in wp-includes/rewrite.php */
- function bwp_url_to_postid($url)
- {
- global $wp_rewrite;
- $url = apply_filters('url_to_postid', $url);
- // First, check to see if there is a 'p=N' or 'page_id=N' to match against
- if ( preg_match('#[?&](p|page_id|attachment_id)=(\d+)#', $url, $values) ) {
- $id = absint($values[2]);
- if ( $id )
- return $id;
- }
- // Check to see if we are using rewrite rules
- $rewrite = $wp_rewrite->wp_rewrite_rules();
- // Not using rewrite rules, and 'p=N' and 'page_id=N' methods failed, so we're out of options
- if ( empty($rewrite) )
- return 0;
- // Get rid of the #anchor
- $url_split = explode('#', $url);
- $url = $url_split[0];
- // Get rid of URL ?query=string
- $url_split = explode('?', $url);
- $url = $url_split[0];
- // Add 'www.' if it is absent and should be there
- if ( false !== strpos(home_url(), '://www.') && false === strpos($url, '://www.') )
- $url = str_replace('://', '://www.', $url);
- // Strip 'www.' if it is present and shouldn't be
- if ( false === strpos(home_url(), '://www.') )
- $url = str_replace('://www.', '://', $url);
- // Strip 'index.php/' if we're not using path info permalinks
- if ( !$wp_rewrite->using_index_permalinks() )
- $url = str_replace('index.php/', '', $url);
- if ( false !== strpos($url, home_url()) ) {
- // Chop off http://domain.com
- $url = str_replace(home_url(), '', $url);
- } else {
- // Chop off /path/to/blog
- $home_path = parse_url(home_url());
- $home_path = isset( $home_path['path'] ) ? $home_path['path'] : '' ;
- $url = str_replace($home_path, '', $url);
- }
- // Trim leading and lagging slashes
- $url = trim($url, '/');
- $request = $url;
- // Look for matches.
- $request_match = $request;
- foreach ( (array)$rewrite as $match => $query) {
- // If the requesting file is the anchor of the match, prepend it
- // to the path info.
- if ( !empty($url) && ($url != $request) && (strpos($match, $url) === 0) )
- $request_match = $url . '/' . $request;
- if ( preg_match("!^$match!", $request_match, $matches) ) {
- // Got a match.
- // Trim the query of everything up to the '?'.
- $query = preg_replace("!^.+\?!", '', $query);
- // Substitute the substring matches into the query.
- $query = addslashes(WP_MatchesMapRegex::apply($query, $matches));
- // Filter out non-public query vars
- global $wp;
- parse_str($query, $query_vars);
- $query = array();
- foreach ( (array) $query_vars as $key => $value ) {
- if ( in_array($key, $wp->public_query_vars) )
- $query[$key] = $value;
- }
- // Taken from class-wp.php
- foreach ( $GLOBALS['wp_post_types'] as $post_type => $t )
- if ( $t->query_var )
- $post_type_query_vars[$t->query_var] = $post_type;
- foreach ( $wp->public_query_vars as $wpvar ) {
- if ( isset( $wp->extra_query_vars[$wpvar] ) )
- $query[$wpvar] = $wp->extra_query_vars[$wpvar];
- elseif ( isset( $_POST[$wpvar] ) )
- $query[$wpvar] = $_POST[$wpvar];
- elseif ( isset( $_GET[$wpvar] ) )
- $query[$wpvar] = $_GET[$wpvar];
- elseif ( isset( $query_vars[$wpvar] ) )
- $query[$wpvar] = $query_vars[$wpvar];
- if ( !empty( $query[$wpvar] ) ) {
- if ( ! is_array( $query[$wpvar] ) ) {
- $query[$wpvar] = (string) $query[$wpvar];
- } else {
- foreach ( $query[$wpvar] as $vkey => $v ) {
- if ( !is_object( $v ) ) {
- $query[$wpvar][$vkey] = (string) $v;
- }
- }
- }
- if ( isset($post_type_query_vars[$wpvar] ) ) {
- $query['post_type'] = $post_type_query_vars[$wpvar];
- $query['name'] = $query[$wpvar];
- }
- }
- }
- // Do the query
- $query = new WP_Query($query);
- if ( !empty($query->posts) && $query->is_singular )
- return $query->post->ID;
- else
- return 0;
- }
- }
- return 0;
- }
/* Post URLs to IDs function, supports custom post types - borrowed and modified from url_to_postid() in wp-includes/rewrite.php */
function bwp_url_to_postid($url)
{
global $wp_rewrite;
$url = apply_filters('url_to_postid', $url);
// First, check to see if there is a 'p=N' or 'page_id=N' to match against
if ( preg_match('#[?&](p|page_id|attachment_id)=(\d+)#', $url, $values) ) {
$id = absint($values[2]);
if ( $id )
return $id;
}
// Check to see if we are using rewrite rules
$rewrite = $wp_rewrite->wp_rewrite_rules();
// Not using rewrite rules, and 'p=N' and 'page_id=N' methods failed, so we're out of options
if ( empty($rewrite) )
return 0;
// Get rid of the #anchor
$url_split = explode('#', $url);
$url = $url_split[0];
// Get rid of URL ?query=string
$url_split = explode('?', $url);
$url = $url_split[0];
// Add 'www.' if it is absent and should be there
if ( false !== strpos(home_url(), '://www.') && false === strpos($url, '://www.') )
$url = str_replace('://', '://www.', $url);
// Strip 'www.' if it is present and shouldn't be
if ( false === strpos(home_url(), '://www.') )
$url = str_replace('://www.', '://', $url);
// Strip 'index.php/' if we're not using path info permalinks
if ( !$wp_rewrite->using_index_permalinks() )
$url = str_replace('index.php/', '', $url);
if ( false !== strpos($url, home_url()) ) {
// Chop off http://domain.com
$url = str_replace(home_url(), '', $url);
} else {
// Chop off /path/to/blog
$home_path = parse_url(home_url());
$home_path = isset( $home_path['path'] ) ? $home_path['path'] : '' ;
$url = str_replace($home_path, '', $url);
}
// Trim leading and lagging slashes
$url = trim($url, '/');
$request = $url;
// Look for matches.
$request_match = $request;
foreach ( (array)$rewrite as $match => $query) {
// If the requesting file is the anchor of the match, prepend it
// to the path info.
if ( !empty($url) && ($url != $request) && (strpos($match, $url) === 0) )
$request_match = $url . '/' . $request;
if ( preg_match("!^$match!", $request_match, $matches) ) {
// Got a match.
// Trim the query of everything up to the '?'.
$query = preg_replace("!^.+\?!", '', $query);
// Substitute the substring matches into the query.
$query = addslashes(WP_MatchesMapRegex::apply($query, $matches));
// Filter out non-public query vars
global $wp;
parse_str($query, $query_vars);
$query = array();
foreach ( (array) $query_vars as $key => $value ) {
if ( in_array($key, $wp->public_query_vars) )
$query[$key] = $value;
}
// Taken from class-wp.php
foreach ( $GLOBALS['wp_post_types'] as $post_type => $t )
if ( $t->query_var )
$post_type_query_vars[$t->query_var] = $post_type;
foreach ( $wp->public_query_vars as $wpvar ) {
if ( isset( $wp->extra_query_vars[$wpvar] ) )
$query[$wpvar] = $wp->extra_query_vars[$wpvar];
elseif ( isset( $_POST[$wpvar] ) )
$query[$wpvar] = $_POST[$wpvar];
elseif ( isset( $_GET[$wpvar] ) )
$query[$wpvar] = $_GET[$wpvar];
elseif ( isset( $query_vars[$wpvar] ) )
$query[$wpvar] = $query_vars[$wpvar];
if ( !empty( $query[$wpvar] ) ) {
if ( ! is_array( $query[$wpvar] ) ) {
$query[$wpvar] = (string) $query[$wpvar];
} else {
foreach ( $query[$wpvar] as $vkey => $v ) {
if ( !is_object( $v ) ) {
$query[$wpvar][$vkey] = (string) $v;
}
}
}
if ( isset($post_type_query_vars[$wpvar] ) ) {
$query['post_type'] = $post_type_query_vars[$wpvar];
$query['name'] = $query[$wpvar];
}
}
}
// Do the query
$query = new WP_Query($query);
if ( !empty($query->posts) && $query->is_singular )
return $query->post->ID;
else
return 0;
}
}
return 0;
}As you can see, the new codeblock is from line 81 to line 112, and that’s where the magic happens.
Basically, what we just did was adding some new query vars to the $query variable, which was formerly designed to support only built-in post types, so that WordPress can query for all types of posts through a typical WP_Query2 request later on (line 115) to get the correct post ID.
With the help of this new function, a test like this:
- echo bwp_url_to_postid('http://betterwp.net/wordpress-tips/url_to_postid-for-custom-post-types/');
echo bwp_url_to_postid('http://betterwp.net/wordpress-tips/url_to_postid-for-custom-post-types/');will return 229, which is exactly the ID of this WordPress tip, pretty nice!
Please note that, regardless of what function you would choose to use, custom or original, you should always use it after the init action has been fired, as a lot of plugins and themes will use such action to register post types. If you fail to do so, you will get 0 as the result, obviously.
References
- http://codex.wordpress.org/Function_Reference/url_to_po ... _to_postid [↩]
- http://codex.wordpress.org/Function_Reference/WP_Query [↩]









This is great. Can you provide more info on the init function? I’m getting all zeroes so I need to know either how to fire the action manually, or where to place my function so it gets processed properly. Thank you!
I figured it out. It wasn’t the init function. I wasn’t using permalink / rewrite rules and this function depends on rewrites being turned on to work, otherwise it returns a 0. Great function. Thanks again.
Thanks buddy this ended up helping me. Works like a charm. Know how the overhead comparison turns out between the two functions?
Lifesaver! Thanks!
Thank you so much! Lifesaver.
Thanks, works really well. Lifesaver for my wordpress ajax website too!
Thanks.
The function return me the custom post type ID (so, OK), but :
Warning: array_map() [function.array-map]: Argument #2 should be an array in /www/wp-includes/query.php on line 2162
Warning: implode() [function.implode]: Invalid arguments passed in /www/wp-includes/query.php on line 2162
PS: WordPress 3.4.1
I’m sorry… it’s OK now.
Great function!!
Hi, I tried to use this function but no success, maybe I’ve done something wrong,
so I copied the function in functions.php, and in the file of interest, I added an action
called the function:
add_action( 'init', 'bwp_url_to_postid' ); $postid = bwp_url_to_postid('hxxp://betterwp.net/wordpress-tips/url_to_postid-for-custom-post-types/');My permalinks are set to:
/%category%/%postname%/You don’t need to use add_action for this to work. All you have to do is this:
echo bwp_url_to_postid('hxxp://yourdomain.com/category/postname/');Replace the URL in quotes with your actual URLs.
Great stuff, works like a charm! Thank you for this!
Hi,
love this “magic” function.
Works perfectly, but I have a question, maybe an issue…
I use it to “auto-embed” a preview of a post related to the links pasted in comments.
So, when someone past an internal link in it comment, a small block with the cover and the title of the page is displayed.
The problem is, when I change the name of this post, it also change it url… and so the function return 0. Wherease I click the link, wordpress detect the related post and redirect me correctly.
Is there a way to detect the original post after it url changed ???
Hope I was clear and my english not to bad
Tks a lot.
I would not say impossible, but it’s not trivial to do that. What I recommend is: when someone inserts a comment with a link to a post in it, you can fetch the ID and then store the ID as a comment meta (perhaps as an array so you can store many IDs per comment). When displaying comments, simply get the IDs and put them somewhere your script can use. More info here: http://codex.wordpress.org/Function_Reference/add_comment_meta
If you don’t like such approach, maybe you should take a look at canonical redirect: http://codex.wordpress.org/Function_Reference/redirect_canonical. Please read that function’s codes carefully, it will be helpful.
Thank’s for your answer.
Dealing with comment meta seem’s to be complicate in my case. I’m using this function on blog comments, buddypress activity comments, buddypress updates, etc.
Maybe the second approach with canonical redirect should be more efficient for me.
Currently I’m cheatting by updating post title but keeping the same url… booooooo, it’s bad
In that case I highly recommend that you go with the second approach, it should work
Hi,
I tried for houres to figure how it works but without any results…
I tried this code :
<?php var_dump(redirect_canonical( "my_old_url")); ?>or
<?php var_dump(redirect_canonical( "my_new_url")); ?>and the result is always false.
If I understant, the function is testing by default the current URL, but if I spicify the URL I want it to test, it should work and return me something. Isn’t it ?
Thanks in advance for your help!
Have you taken a look at the function’s code? You will need to call the function like this:
echo redirect_canonical( "my_old_url", false)Yes I have read all the doc, and try everything.
echo redirect_canonical( "my_new_url", false)result : nothingecho redirect_canonical( "my_new_url", true)result : nothing (no redirection)echo redirect_canonical( "my_new_url", false)result : nothingecho redirect_canonical( "my_new_url", true)result : nothing (no redirection)The only result I can get is by using var_dump…this is not a “result” but I can see something in my screen.
I also tried to read the entire function (which is very long) but can’t understand how it work.
Thanks for your help
If you see nothing it could be that the redirect_canonical function could not guess the new url based on the old url (sorry in my previous comment it should be my_old_url instead of my_new_url). In such case, not much can be done I believe…
Thanks for your answers.
I’ll try to find another solution.
Cheers
Thank you so much!!!
But it retrieves only public custom post types. I need to check also over private custom post types. I’m sure your function can work fine as well, but I think I have to change some parameters. Can you tell me what to change?
thanks in advance!
bye
Mirko
I found the issue. WP_query doesn’t include the private posts if the user is not authenticated in wordpress but only if the user is authenticated.
In addition, wp_query has a parameter also to check the user capabilities (if the user is authenticated): perm => readable | editable.
I solved using access directly to db via wpdb.
thanks
Mirko