[TUTORIAL] Recently Viewed Products — AJAX + localStorage + Bricks Template (works with cached pages)

:brick: [TUTORIAL] Recently Viewed Products — AJAX + localStorage + Bricks Template (works with cached pages)

Hey Bricks friends :waving_hand:

Here’s a complete and fully working way to create a Recently Viewed Products section in Bricks Builder —

that works perfectly even on cached pages (no cookies or plugins needed).

This approach uses localStorage (on the client side) to track product visits and then loads a Bricks template via AJAX, so it works with LiteSpeed, Cloudflare, WP Rocket, etc.

No PHP needs to run on the product pages!


:gear: Overview

:white_check_mark: Features:

  • Works on cached or static pages (no cookie or session needed).
  • Tracks views locally via JS (localStorage).
  • Loads a Bricks Template dynamically via admin-ajax.php.
  • Fully compatible with Bricks Query Loop.
  • Keeps product order (latest first).
  • Easy to adapt for a “Favorites” system too!

:puzzle_piece: Step 1 — Add this JavaScript to your Single Product template

(In Bricks: Page Settings → Custom Code → “Body (footer) scripts”)

This script saves the currently viewed postid set on body class into localStorage.

It runs once per product page, even if the page is cached.

<script id="recently-viewed-script">
document.addEventListener('DOMContentLoaded', function() {
  var key = 'recently_viewed';
  var body = (window.top && window.top.document && window.top.document.body) ? window.top.document.body : document.body;
  if (!body) return;

  var cls = body.className || '';
  var clsArray = cls.split(' ');
  var productId = null;

  for (var i = 0; i < clsArray.length; i++) {
    var c = clsArray[i];
    if (c.indexOf('postid-') === 0) {
      productId = parseInt(c.substring(7), 10);
      break;
    }
  }

  if (!productId) {
    console.warn('⚠️ Could not extract postid from classes:', clsArray);
    return;
  }

  console.log('🆔 Detected product ID:', productId);

  // Guardar en localStorage (sin duplicados)
  var viewed = [];
  try {
    var stored = localStorage.getItem(key);
    if (stored) viewed = JSON.parse(stored) || [];
  } catch (e) {}

  var newList = [productId];
  for (var j = 0; j < viewed.length; j++) {
    if (viewed[j] !== productId) newList.push(viewed[j]);
  }

  // Optional: limit to 6
  // newList = newList.slice(0, 6);

  try {
    localStorage.setItem(key, JSON.stringify(newList));
    console.log('✅ recently_viewed stored:', newList);
  } catch (e) {
    console.error('❌ Error storing recently_viewed:', e);
  }
});
</script>

:puzzle_piece: Step 2 — Add PHP code in your theme (or Code Snippets plugin)

You can paste this code either:

  • in your child theme’s functions.php , or
  • in a Code Snippets block (recommended if you don’t want to edit theme files).

This PHP part:

  1. Registers a shortcode [bricks_recently_viewed].
  2. Enqueues a small JS file that loads your Bricks template via AJAX.
  3. Handles the AJAX call that renders your Bricks section template that you will create in step 4, with the product IDs from localStorage.
/* ===========================================================
   Recently Viewed Products (AJAX + Bricks Template)
   =========================================================== */

/**
 * 1️⃣ Shortcode: placeholder container where AJAX content will be injected
 */
add_shortcode('bricks_recently_viewed', function() {
    return '<div id="recently-viewed-products"></div>';
});

/**
 * 2️⃣ Enqueue JavaScript responsible for reading localStorage and requesting AJAX
 */
add_action('wp_enqueue_scripts', function() {
    wp_enqueue_script(
        'recently-viewed-js',
        get_stylesheet_directory_uri() . '/js/recently-viewed.js',
        ['jquery'],
        filemtime(get_stylesheet_directory() . '/js/recently-viewed.js'),
        true
    );

    // Pass data to JS
    wp_localize_script('recently-viewed-js', 'recentlyViewedData', [
        'ajaxurl'     => admin_url('admin-ajax.php'),
        'template_id' => 245499, // 🔧 Replace with your Bricks template ID
    ]);
});

/**
 * 3️⃣ AJAX handler: renders Bricks template with proper CSS (supports all Bricks versions)
 */
add_action('wp_ajax_load_recently_viewed', 'load_recently_viewed_ajax');
add_action('wp_ajax_nopriv_load_recently_viewed', 'load_recently_viewed_ajax');

function load_recently_viewed_ajax() {
    // 💾 Retrieve product IDs sent via GET or POST
    $raw_ids = $_REQUEST['ids'] ?? '';
    $ids = array_filter(array_map('intval', explode(',', $raw_ids)));

    if (empty($ids)) {
        wp_send_json_success('<p>No recently viewed products.</p>');
    }

    // 🧩 Bricks template ID (must be provided)
    $template_id = intval($_REQUEST['template_id'] ?? 0);
    if (!$template_id) {
        wp_send_json_error('Missing template ID');
    }

    // ⚡ Pass product IDs to Bricks Query Loop via $_GET
    $_GET['ids'] = implode(',', $ids);

    // 🧱 Render the Bricks template (HTML only)
    $html = do_shortcode('[bricks_template id="' . $template_id . '"]');

    // 📁 Locate Bricks CSS directory inside uploads
    $upload_dir = wp_upload_dir();
    $base_dir   = trailingslashit($upload_dir['basedir']) . 'bricks/css/';
    $base_url   = trailingslashit($upload_dir['baseurl']) . 'bricks/css/';

    // 🧩 Check possible Bricks CSS file naming formats (old/new)
    $candidates = [
        "bricks-template-{$template_id}.css", // older Bricks versions
        "post-{$template_id}.min.css",        // modern minified format
        "post-{$template_id}.css"             // non-minified fallback
    ];

    $css_link = '';

    // 🔍 Look for the first existing CSS file
    foreach ($candidates as $file) {
        $path = $base_dir . $file;
        if (file_exists($path)) {
            $css_link = '<link rel="stylesheet" href="' . esc_url($base_url . $file) . '" />';
            break;
        }
    }

    // 🪄 If no physical file exists, fall back to inline CSS stored in DB
    if (!$css_link) {
        $css = get_post_meta($template_id, '_bricks_css', true);
        if ($css) {
            $css_link = '<style id="bricks-template-' . esc_attr($template_id) . '-css">' . $css . '</style>';
        }
    }

    // 🚀 Return combined CSS + HTML in AJAX response
    wp_send_json_success($css_link . $html);
}

:wrench: IMPORTANT: On step 4, replace ‘template_id’ on the code above (around line 25) with your Bricks template ID

:puzzle_piece: Step 3 — Create the JS file in your child theme

Create this file:

/wp-content/themes/your-child-theme/js/recently-viewed.js

It loads the Bricks section template on step 4 dynamically once the page is ready.

(function($) {
  $(function() {
    const key = 'recently_viewed';
    const stored = localStorage.getItem(key);
    if (!stored) return;

    let ids;
    try {
      ids = JSON.parse(stored);
    } catch {
      return;
    }

    if (!Array.isArray(ids) || !ids.length) return;

    // 🚫 Exclude current product
    const body = (window.top && window.top.document && window.top.document.body)
      ? window.top.document.body
      : document.body;

    const cls = body ? body.className : '';
    const match = cls.match(/postid-(\d+)/);
    const currentId = match && match[1] ? parseInt(match[1], 10) : null;

    if (currentId) {
      ids = ids.filter(id => id !== currentId);
    }

    if (!ids.length) return;

    // 🔧  AJAX request to admin-ajax.php
    $.post(recentlyViewedData.ajaxurl, {
      action: 'load_recently_viewed',
      ids: ids.join(','),
      template_id: recentlyViewedData.template_id
    })
    .done(function(res) {
      if (res.success && res.data) {
        $('#recently-viewed-products').html(res.data);
      } else {
        console.warn('No recently viewed data received', res);
      }
    })
    .fail(function(err) {
      console.error('AJAX error loading recently viewed', err);
    });
  });
})(jQuery);

:puzzle_piece: Step 4 — Create the Bricks Section Template
1. Create a new Bricks Template of type Section (for example, name it “Recently Viewed Products”).
2. Inside it, Create a Query Loop.
3. On the Query editor (PHP) paste this code:

$ids = [];
if (isset($_GET[‘ids’])) {
$ids = array_map(‘intval’, explode(’,’, sanitize_text_field($_GET[‘ids’])));
}
return [
‘post_type’      => ‘product’,
‘posts_per_page’ => 6, 
‘no_found_rows’  => true,
‘post__in’       => $ids,
‘orderby’        => ‘post__in’,
];
  • Change ‘posts_per_page’ value to the number of results you wish.

  • :warning: Save the template and copy the id of your new created template. Then paste the id to ‘template_id’ on the code on Step 2.

  • Now design the loop visually (image, title, price, add-to-cart, etc.) —
    exactly as you would with any Bricks product grid.

:puzzle_piece: Step 5 — Add the shortcode anywhere

Place this shortcode anywhere in Bricks using the shortcode element, or Gutenberg:

[bricks_recently_viewed]

This will render the section Template “Recently Viewed” wherever you want.

When the page loads:

  • The script reads product IDs from localStorage.
  • It fetches the template HTML from admin-ajax.php.
  • The “Recently Viewed Products” section appears dynamically, even on cached pages.

:light_bulb: Tip

You can easily extend this same system to create a “Favorites” feature:

just replace the key name (recently_viewed) with something like user_favorites,

and trigger the save logic from a “heart” button click instead of DOMContentLoaded.

4 Likes

I like this idea, how is it working on your site? I plan to implement it.

1 Like

AJAX and Query Loop works flawless.

It is quite straightfoward to implement. Go ahead.

Embedded it into the Nested Slider element, it looks great. Just set the right “options” values on Bricks element that make you feel that the slider behaves natural.

A conditional to set visibility of the “Recently Viewed” slider when viewed products is higher than a given number. This will avoid to show the slider when there are only one or two posts viewed.

IMPORTANT: on Step 4 on the query loop, post type is set to “product”, as it was created for woocommerce. You may need to change the post type there to “post” or anything else.

1 Like

IMPORTANT : TO MAKE IT WORK!!!

On Step 3, on /wp-content/themes/your-child-theme/js/recently-viewed.js

Template ID need to be declared in the JS as well,

Please replace on line 3:

const key='recently_viewed;

With this: (being 251429 your ‘Recently Viewed’ template ID)

const key='recently_viewed', tpl=251429; // < Add your Recently Viewed template

So, the template ID needs to be declared both in PHP, as we did in step 2, and also in JS (step 3)

1 Like

Since Bricks 2.2, the Splide library is no longer enqueued globally on every page. Bricks now only loads Splide when a native slider element (e.g., Nestable Slider) is present on the page. This means that pages without sliders may have Splide undefined, returning this console error:

Uncaught ReferenceError: Splide is not defined

To fix this and keep the AJAX “Recently Viewed Products” system working site-wide, we need to load Splide dynamically only if it’s not already available.

Here’s an updated snippet for your recently-viewed.js that:

  1. Checks whether window.Splide exists.

  2. If missing, loads Splide from CDN.

  3. Once Splide is available, initializes the slider.

  4. Prevents double initialization with a flag.

Find this line on /wp-content/themes/your-child-theme/js/recently-viewed.js

$('#recently-viewed-products').html(res.data);

and REPLACE it with this snippet

if (container.classList.contains('is-initialized')) return;

function initSplideWhenReady() {
  if (typeof window.Splide === 'undefined') {
    // Splide not present → load it once from CDN
    if (!document.querySelector('script[src*="splide"]')) {
      const script = document.createElement('script');
      script.src = 'https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.3/dist/js/splide.min.js';
      script.onload = initSplideWhenReady;
      document.body.appendChild(script);
    }
    return;
  }

  try {
    new Splide(container, cfg).mount();
    container.classList.add('is-initialized');
  } catch (e) {
    console.warn('[Recently Viewed] ⚠️ Error initializing Splide:', e);
  }
}

initSplideWhenReady();

This fallback ensures that:

  • If Bricks already enqueued Splide (because a slider is on the page), the existing version is used.

  • If not, Splide is loaded manually and then used without errors.

  • Cached pages, cart pages, checkout pages, and other non-slider views work without console errors.


This update is fully compatible with the original tutorial’s intent (works on cached pages, uses localStorage + AJAX + Bricks template), but ensures it survives Bricks 2.2’s optimized asset loading.

This has always been true. It’s always been conditional (no splide JS unless a slider element on the page)

Ok i don’t doubt you, but after upgrading to bricks 2.2 I had to declare the Splide load. Before it was working without it.
As Recently Viewed Posts Slider is started through AJAX, any little change can change the architecture, so is safer to declare it as above if not present on the page.