Loading posts via AJAX in a hidden div with updated URL?

Updating the url via javascript, is not related to WordPress, however you just need to modify window.location.hash in your jQuery code.

Regarding the usage of admin-ajax.php it is recommended way to do tasks via AJAX that need to load WordPress environment, instead of manually requiring wp-load.php.

In your case you are sending a request to a regular WordPress url, so it’s not too bad, however your current code have some issues.

The “$_POST['id']” Issue

In your singular post you have

$post = get_post($_POST['id']);

that is not needed at all, because you are sending a request to singular post url, it means post variable is automatically set by WordPress.

Moreover, $_POST['id'] may be not set, especially if you use pretty permalinks, that will cause a PHP warning to be thrown.

The “Single View” Issue

Your singular posts will still have own permalinks. If you visit one of those permalinks, you’ll see a broken html page that doesn’t contain any <html>, <head> or <body> tag, but just the content of the page.

You can fix that issue in 2 ways:

  • use the 'post_link' hook to change the permalink of posts and make it pointing to index page with a proper hash, and also put in place a redirect to send users that enter singular url in the address bar to index page with a proper hash. That may be fine, but in that way your post can never be seen in singular view.

  • second way is to edit your single post template and conditionally output proper html tags when the page is loaded in a regular (i.e. non-AJAX) request.

    function is_ajax() {
      return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
             && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
    }
    
    if (is_ajax()) {
       // open here <html>, <head> and <body> tags
    }
    
    // page content here
    
    if (is_ajax()) {
       // close here <html>, <head> and <body> tags
    }
    

In this way when requested using regular requests your posts are shown in well formed html page.

Performance Implications

Using one of the two ways suggested above you can solve main issue of your current code, but there is still a performance issue.

Requests to WordPress urls are a bit more expensive in performance than requests to admin-ajax.php. Even if both load full WP environment, for regular requests WordPress need to resolve the permalink to a query. That is done loading a set of rewrite rules from database and programmatically performing a regex check against any of the rules untill one of them matches current url.

If you can send an ajax request that contains post ID as part of the request itself, you will be able to output the post content skipping that check and so improving performance.

To obtain that, you should use WordPress AJAX API. Something like:

in functions.php

add_action('wp_enqueue_scripts', function() {
  // $script_url is the full url to your js file
  wp_enqueue_script('myjs', $script_url, array('jquery'), true, null, true);
  wp_localize_script('myjs', 'myData', array('ajaxurl' => admin_url('admin-ajax.php));
});

add_action('wp_ajax_load-single-post', 'prefix_ajax_single_post');
add_action('wp_ajax_nopriv_load-single-post', 'prefix_ajax_single_post');

function prefix_ajax_single_post() {
  $pid = (int) filter_input(INPUT_GET, 'pID', FILTSER_SANITIZE_NUMBER_INT);
  if ($pid > 0) {
    global $post;
    $post = get_post($pid);
    setup_postdata($post);
    printf('<div id="single-post post-%d">', $pid);
    the_title();
    the_content();
    echo '</div>';
  }
  exit();
}

jQuery (goes in the file whose url is the $script_url var in code above)

(function($, D){

  $.ajaxSetup({cache:false});

  $(".post-link").click(function(){

    var postID = $(this).attr('rel');
    var $container = $("#single-post-container");
    $container.html("content loading");
    $.get(D.ajaxurl, {action: 'load-single-post', pID: postID}, function(content) {
      $container.html(content);
    });

  });

})(jQuery, myData);

Your index template code can stay the same.

Using this approach all issues are solved and, as you can see, the single post template is not involved at all, it means you can use it to display the singular post view as you like. (If you don’t want any singular post view, the first suggestion under The “Single View” Issue can be still used).