Finally after a few months of searching and learning AJAX with pagination PHP and History.pushState() I created AJAX PHP pagination.

Test page

1. Ajax(index.php)

$(window).on('load', function () { // makes sure that whole site is loaded

            var url_string = window.location.href;
            var url_parts = url_string.split('/');
            var piece = url_parts[url_parts.length -1]; //get last part of url (it should be page=n)
            var page;
            if(piece.substring(0, 5) === 'page=') {
               page = parseInt(piece.substr(5));
            } else { //if it's the first time you landed in the page you haven't page=n
               page = 1;
     // load_data(1);  
      function load_data(page)  
        $('#load_data').html('<div id="status" style="" ></div>');

      $(document).on('click', '.pagination_link', function(event) 
            var page = $(this).attr("id"); 
            //Now push PAGE to URL
            window.history.pushState({page}, `Selected: ${page}`, `${'page=' +  page}`)
            //window.history.pushState({page}, `Selected: ${page}`, `./selected=${page}`)
            return event.preventDefault(); 
            window.addEventListener('popstate', e => {
            var page = $(this).attr("id");
            console.log('popstate error!');

2. pagination2.php


$rowperpage = 10;
$page = 1;
if($_POST['page'] > 1)
  $p = (($_POST['page'] - 1) * $rowperpage);
  $page = $_POST['page'];
  $p = 0;

$visible = $visible ?? true;
$products_count = count_all_products(['visible' => $visible]);        
$sql = "SELECT * FROM products ";
$sql .= "WHERE visible = true ";
$sql .= "ORDER BY position ASC ";
$sql .= "LIMIT ".$p.", ".$rowperpage."";  
$product_set = find_by_sql($sql);
$product_count = mysqli_num_rows($product_set);
if($product_count == 0) {
      echo "<h1>No products</h1>";

while($run_product_set = mysqli_fetch_assoc($product_set)) { ?>

    <div style="float: left; margin-left: 20px; margin-top: 10px; class="small">

      <a href="<?php echo url_for('/show.php?id=' . htmlspecialchars(urlencode($run_product_set['id'])));   ?>">
          <img src="staff/images/<?php echo htmlspecialchars($run_product_set['filename']); ?> " width="150">

      <p><?php echo htmlspecialchars($run_product_set['prod_name']); ?></p>


    <?php }

<section id="pagination">
if($_POST['page'] > 1)
      $page_nb = $_POST['page'];
      $page_nb = 1;
$check = $p + $rowperpage;
$prev_page = $page_nb - 1;

$limit = $products_count / $rowperpage;

$limit = ceil($limit);
$current_page = $page_nb;
if($page_nb > 1) {
    echo "<span class='pagination_link' style='cursor:pointer;'  id='".$prev_page."'>Back</span>";
if ( $products_count > $check ) {   
    for ( $i = max( 1, $page_nb - 5 ); $i <= min( $page_nb + 5, $limit ); $i++ ) {
        if ( $current_page == $i ) {
            echo "<span class=\"selected\">{$i}</span>";
        } else { 
            echo "<span class='pagination_link' style='cursor:pointer;' id='".$i."'>".$i."</span>"; 
if ($products_count > $check) { 

$next_page = $page_nb + 1;  
    echo "<span class='pagination_link' style='cursor:pointer;' id='".$next_page."'>Next</span>";

3. htaccess

<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-l
    RewriteRule ^(.*)$ index.php?/$1 [L]

4. Functions

function url_for($script_path) {
  // add the leading '/' if not present
  if($script_path[0] != '/') {
    $script_path = "/" . $script_path;
  return WWW_ROOT . $script_path;
  • 2
    \$\begingroup\$ Why should we continue to review your code if you aren't going to take the past advice that we give you? Why am I still seeing incomprehensible function names like h() and u()?!? Please read the previous reviews, upvote the helpful ones, accept answers, adjust your future codes, do not ask reviewers to repeat good advice. In recent history: #1: codereview.stackexchange.com/a/240604/141885 #2: codereview.stackexchange.com/a/242782/141885 \$\endgroup\$ Nov 17, 2020 at 23:03
  • \$\begingroup\$ @mickmackusa I did not know that 'closed' is public visible, I thought it was just for me. I was convinced the old one could be thrown away. I corrected h + u and added function url_for. \$\endgroup\$ Nov 18, 2020 at 0:06
  • \$\begingroup\$ Ouch! I have never seen a need for nesting these: htmlspecialchars(urlencode..) \$\endgroup\$
    – Rick James
    Mar 29, 2021 at 6:01
  • \$\begingroup\$ Pagination via OFFSET has problems -- missing/duplicated data, and performance. Instead, "remember where you left off". mysql.rjweb.org/doc.php/pagination \$\endgroup\$
    – Rick James
    Mar 29, 2021 at 6:04
  • \$\begingroup\$ @Rick James I can't check - I have only 34,898 items :) \$\endgroup\$ Mar 29, 2021 at 7:52

1 Answer 1


Overall Assessment

It appears to work, although when a user on the demo site (presuming the code is the same) goes to page 10, only the link with text BACK is present. Should other page numbers exist? I understand why no other numbered links appear after having looked at the PHP logic but my experience with other paging systems makes me question this UI experience.

The practice of sending HTML in an AJAX response and using that to directly set the content of a DOM element is an antiquated practice, and could be an XSS avenue. While the practice of sending data from the API and having the front end code construct the HTML dynamically may have been a new construct eight years ago it is much more common in today’s web.

The indentation is inconsistent - especially in the JavaScript code - which makes it difficult to read.

I see that you have a previous post with identical code, thus this post is a follow-up to that one. That post has an accepted answer, though not all suggestions were taken into account. This is fine but you might want to consider those suggestions.

By the way you might not need jQuery...



DOM ready callback

If you are using jQuery 3.0 or newer then the format for .ready() can be simplified to "the recommended syntax"3:

$(function() { ... })

Instead of:


Parsing page parameter

Instead of using window.location.href the search property could be used. On that MDN page it states:

Modern browsers provide URLSearchParams and URL.searchParams to make it easy to parse out the parameters from the querystring.

Your code could utilize the URLSearchParams interface - something along the lines of:

const params = new URLSearchParams(document.location.search.substring(1));
const name = params.get("page") || 1;

jQuery AJAX functions

There is a convenience method .post() that could be used to simplify the code to run the AJAX request - for example, I haven't tested this but it could be as simple as this:

const container = $(#load_data);
container.html('<div id="status" style="" ></div>');
$.post("pagination2.php", { page }, container.html);

unused variable page in event listener function

window.addEventListener('popstate', e => {
        var page = $(this).attr("id");
        console.log('popstate error!');

The variable page isn't used within the callback function. It can be eliminated.



Things that shouldn't be changed (e.g. number of rows per page) can be declared as a constant - e.g. with define() or const.

const ROWS_PER_PAGE = 10; 

One thing that was already mentioned in the answer by @mickmackusa to your previous question is that:

The LIMIT clause string can be written without concatenation:
$sql .= "LIMIT $p, $rowperpage";

I see that code is still unchanged. Perhaps it be helpful to know the reason: Variables in double-quoted (and heredoc syntaxes) are expanded when they occur in double quoted strings.

Closing and re-opening PHP tags

I see this code at the top of pagination2.php:

<?php require_once('../private/initialize.php'); ?>
$page = ''; 

There is little need to close and re-open the PHP tags.

$page = ''; 
  • \$\begingroup\$ Long ago, I chose to always write PHP, with echo to get HTML. And never go the other way (HTML with embedded PHP). The code in the Question is hard to read since it does both! \$\endgroup\$
    – Rick James
    Mar 29, 2021 at 16:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.