Back to Top

URL to Post ID for Custom Post Types

Previous Post:

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

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.


  1. ... _to_postid []
  2. []

Take Social Sharing to
the Next Level with Monarch!

Take Social Sharing to the Next Level with Monarch!
Print Article Trackback Trackback to this Article   Subscribe to Comments RSS Subscribe to Comments RSS

30 Opinions for URL to Post ID for Custom Post Types (6 Trackbacks)

  1. User's Gravatar
    JB Hansen February 7, 2012 at 11:05 am – Permalink

    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!

  2. User's Gravatar
    JB Hansen February 7, 2012 at 12:32 pm – Permalink

    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.

  3. User's Gravatar
    Hudson April 4, 2012 at 4:58 am – Permalink

    Thanks buddy this ended up helping me. Works like a charm. Know how the overhead comparison turns out between the two functions?

  4. User's Gravatar
    elba May 28, 2012 at 7:58 pm – Permalink

    Lifesaver! Thanks!

  5. User's Gravatar
    Thiago June 27, 2012 at 5:32 am – Permalink

    Thank you so much! Lifesaver. 🙂

  6. User's Gravatar
    Grsmto August 6, 2012 at 5:41 am – Permalink

    Thanks, works really well. Lifesaver for my wordpress ajax website too!

  7. User's Gravatar
    Guicara August 18, 2012 at 2:57 am – Permalink


    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

    • User's Gravatar
      Guicara August 18, 2012 at 3:10 am – Permalink

      I’m sorry… it’s OK now.

  8. User's Gravatar
    Rein Van Leirsberghe November 2, 2012 at 10:22 pm – Permalink

    Great function!!

  9. User's Gravatar
    Vlad December 19, 2012 at 2:51 pm – Permalink

    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://');

    My permalinks are set to: /%category%/%postname%/

    • User's Gravatar
      OddOneOut December 19, 2012 at 11:25 pm – Permalink

      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://');

      Replace the URL in quotes with your actual URLs.

  10. User's Gravatar
    Dario January 4, 2013 at 5:05 pm – Permalink

    Great stuff, works like a charm! Thank you for this!

  11. User's Gravatar
    Brice February 8, 2013 at 8:06 pm – Permalink

    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.

    • User's Gravatar
      OddOneOut February 11, 2013 at 11:36 pm – Permalink

      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:

      If you don’t like such approach, maybe you should take a look at canonical redirect: Please read that function’s codes carefully, it will be helpful.

      • User's Gravatar
        Brice February 12, 2013 at 3:21 pm – Permalink

        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 😉

        • User's Gravatar
          OddOneOut February 12, 2013 at 10:16 pm – Permalink

          In that case I highly recommend that you go with the second approach, it should work 🙂

  12. User's Gravatar
    Brice February 21, 2013 at 4:20 am – Permalink

    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")); ?>
    <?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!

    • User's Gravatar
      OddOneOut February 21, 2013 at 8:34 pm – Permalink

      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)

      • User's Gravatar
        Brice February 21, 2013 at 8:45 pm – Permalink

        Yes I have read all the doc, and try everything.
        echo redirect_canonical( "my_new_url", false) result : nothing
        echo redirect_canonical( "my_new_url", true) result : nothing (no redirection)
        echo redirect_canonical( "my_new_url", false) result : nothing
        echo 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

        • User's Gravatar
          OddOneOut February 21, 2013 at 8:50 pm – Permalink

          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…

          • User's Gravatar
            Brice February 21, 2013 at 8:55 pm – Permalink

            Thanks for your answers.
            I’ll try to find another solution.

  13. User's Gravatar
    Mirko March 29, 2013 at 1:02 am – Permalink

    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!

  14. User's Gravatar
    Mirko March 29, 2013 at 10:02 pm – Permalink

    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.

  15. User's Gravatar
    David Johnson December 22, 2013 at 8:37 pm – Permalink

    Thank you for sharing this tip. Informative and detailed.

Speak Up Your Mind!

An asterisk (*) indicates a required field and must be filled.

  • Web page and e-mail addresses turn into links automatically.
  • Wrap codes in: <code lang=""></code> or <pre lang="" extra="">
  • Lines and paragraphs break automatically.

Next Post: