Step-by-Step Guide to building a custom real-time activity logs block for Gutenberg using PHP block-render callbacks
Leveraging PHP Block-Render Callbacks for Real-Time Activity Logs in Gutenberg
For e-commerce platforms, real-time visibility into user activity is paramount for security, performance monitoring, and understanding customer behavior. Integrating this data directly into the WordPress admin interface via Gutenberg offers a powerful, context-aware dashboard. This guide details the construction of a custom Gutenberg block that dynamically displays real-time activity logs, utilizing PHP block-render callbacks for efficient server-side rendering.
Prerequisites and Setup
Before we begin, ensure you have a local development environment set up with WordPress, PHP (version 7.4+ recommended), and Node.js/npm for asset compilation. Familiarity with the WordPress Plugin API and basic JavaScript is assumed.
Defining the Gutenberg Block
We’ll create a custom plugin to house our Gutenberg block. The block’s registration and server-side rendering logic will be defined in PHP. The block’s metadata, including its name, title, and editor script/style handles, is defined in a JavaScript file.
Plugin Structure
Create a new directory for your plugin, e.g., wp-content/plugins/ecommerce-activity-log. Inside, create the following files and directories:
ecommerce-activity-log.php(main plugin file)build/(directory for compiled JS/CSS)src/(directory for source JS/CSS)src/index.js(main JavaScript entry point for the block)src/block.json(block metadata)
Block Metadata (src/block.json)
This file describes our block to WordPress. We’ll specify a render_callback here, pointing to a PHP function that will handle the server-side rendering.
{
"apiVersion": 2,
"name": "ecommerce-activity-log/realtime-logs",
"title": "Real-time Activity Log",
"category": "widgets",
"icon": "chart-line",
"description": "Displays a real-time feed of user activity.",
"keywords": [ "ecommerce", "activity", "log", "real-time" ],
"attributes": {
"logCount": {
"type": "number",
"default": 10
}
},
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css",
"render_callback": "ecommerce_activity_log_render_callback"
}
JavaScript Entry Point (src/index.js)
This file registers the block for the editor. For a block with a render_callback, the editor representation can be quite simple, often just a placeholder or a static preview. We’ll use a basic placeholder here.
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import './style.scss'; // Editor styles
import './editor.scss'; // Editor-specific styles
registerBlockType( 'ecommerce-activity-log/realtime-logs', {
title: __( 'Real-time Activity Log', 'ecommerce-activity-log' ),
icon: 'chart-line',
category: 'widgets',
description: __( 'Displays a real-time feed of user activity.', 'ecommerce-activity-log' ),
attributes: {
logCount: {
type: 'number',
default: 10,
},
},
edit: ( { attributes, setAttributes } ) => {
// Simple placeholder for the editor.
// The actual content is rendered server-side.
return (
<div>
<h3>{ __( 'Real-time Activity Log', 'ecommerce-activity-log' ) }</h3>
<p>{ __( 'Displaying the last', 'ecommerce-activity-log' ) } { attributes.logCount } { __( ' activities.', 'ecommerce-activity-log' ) }</p>
<p><em>{ __( '(Content will be rendered on the front-end)', 'ecommerce-activity-log' ) }</em></p>
</div>
);
},
save: () => {
// This function is not used when a render_callback is defined.
// WordPress will use the output of the render_callback instead.
return null;
},
} );
Asset Compilation
To compile the JavaScript and SCSS files, you’ll need a build process. A common setup uses `@wordpress/scripts`. Add a package.json file to your plugin’s root directory:
{
"name": "ecommerce-activity-log",
"version": "1.0.0",
"description": "Custom Gutenberg block for e-commerce activity logs.",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": [],
"author": "",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/scripts": "^26.0.0"
}
}
Run npm install to install dependencies, then npm run build to compile your assets. The compiled files will appear in the build/ directory.
Server-Side Rendering with PHP Block-Render Callbacks
The core of our real-time functionality lies in the PHP render_callback. This function receives the block’s attributes and is responsible for outputting the HTML that will be displayed on the front-end.
Plugin Main File (ecommerce-activity-log.php)
This file registers the block type and defines the render callback function.
<?php
/**
* Plugin Name: E-commerce Activity Log
* Description: Displays a real-time feed of user activity for e-commerce.
* Version: 1.0.0
* Author: Your Name
* License: GPL-2.0-or-later
* Text Domain: ecommerce-activity-log
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Registers the block using the metadata loaded from the `block.json` file.
* Behind the scenes, it registers also all assets so they can be enqueued
* through the block editor in the corresponding context.
*
* @see https://developer.wordpress.org/reference/functions/register_block_type/
*/
function ecommerce_activity_log_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'ecommerce_activity_log_block_init' );
/**
* Render callback function for the Real-time Activity Log block.
*
* @param array $attributes The block attributes.
* @return string The HTML output for the block.
*/
function ecommerce_activity_log_render_callback( $attributes ) {
$log_count = isset( $attributes['logCount'] ) ? intval( $attributes['logCount'] ) : 10;
// Ensure we don't try to fetch a negative number of logs.
if ( $log_count <= 0 ) {
$log_count = 10;
}
// In a real-world scenario, you would fetch logs from a custom table,
// an external service, or a more sophisticated logging mechanism.
// For this example, we'll simulate fetching recent WordPress actions.
$logs = ecommerce_activity_log_get_simulated_logs( $log_count );
if ( empty( $logs ) ) {
return '<p>' . __( 'No activity logs found.', 'ecommerce-activity-log' ) . '</p>';
}
ob_start();
?>
<div class="ecommerce-activity-log-container">
<h3><?php esc_html_e( 'Recent Activity', 'ecommerce-activity-log' ); ?></h3>
<ul class="ecommerce-activity-log-list">
<?php foreach ( $logs as $log ) : ?>
<li class="ecommerce-activity-log-item">
<span class="ecommerce-activity-log-timestamp"><?php echo esc_html( $log['timestamp'] ); ?></span>
<span class="ecommerce-activity-log-message"><?php echo esc_html( $log['message'] ); ?></span>
<span class="ecommerce-activity-log-user">(<?php echo esc_html( $log['user'] ); ?>)</span>
</li>
<?php endforeach; ?>
</ul>
<!-- Add a placeholder for real-time updates -->
<div id="ecommerce-activity-log-realtime-indicator"></div>
</div>
<?php
return ob_get_clean();
}
/**
* Simulates fetching activity logs.
* In a production environment, this would query a dedicated log table or API.
*
* @param int $count Number of logs to retrieve.
* @return array Array of log entries.
*/
function ecommerce_activity_log_get_simulated_logs( $count = 10 ) {
$sample_logs = [
[ 'timestamp' => '2023-10-27 10:00:05', 'message' => 'User "john_doe" logged in.', 'user' => 'System' ],
[ 'timestamp' => '2023-10-27 10:01:15', 'message' => 'Product "Awesome Gadget" added to cart by user "jane_smith".', 'user' => 'jane_smith' ],
[ 'timestamp' => '2023-10-27 10:02:30', 'message' => 'Order #12345 placed by user "john_doe".', 'user' => 'john_doe' ],
[ 'timestamp' => '2023-10-27 10:03:00', 'message' => 'User "admin" updated product "Super Widget".', 'user' => 'admin' ],
[ 'timestamp' => '2023-10-27 10:04:45', 'message' => 'User "jane_smith" completed checkout for order #12346.', 'user' => 'jane_smith' ],
[ 'timestamp' => '2023-10-27 10:05:10', 'message' => 'New user registered: "peter_jones".', 'user' => 'System' ],
[ 'timestamp' => '2023-10-27 10:06:20', 'message' => 'Product "Basic T-Shirt" stock updated.', 'user' => 'admin' ],
[ 'timestamp' => '2023-10-27 10:07:00', 'message' => 'User "john_doe" viewed product "Awesome Gadget".', 'user' => 'john_doe' ],
[ 'timestamp' => '2023-10-27 10:08:15', 'message' => 'Comment on product "Super Widget" by user "jane_smith".', 'user' => 'jane_smith' ],
[ 'timestamp' => '2023-10-27 10:09:30', 'message' => 'User "peter_jones" added "Basic T-Shirt" to wishlist.', 'user' => 'peter_jones' ],
];
// Return the latest logs up to the requested count.
return array_slice( array_reverse( $sample_logs ), 0, $count );
}
?>
Implementing Real-Time Updates
The PHP render callback provides the initial HTML. For true real-time updates, we need a client-side mechanism. This typically involves WebSockets or Server-Sent Events (SSE). For simplicity in this example, we’ll simulate updates using JavaScript polling, which is less efficient but easier to implement without a dedicated WebSocket server.
Client-Side JavaScript for Updates (src/index.js – Modified)
We’ll add JavaScript to the editor and front-end to periodically fetch new log entries. For the front-end, we’ll enqueue a separate script.
Enqueueing Front-End Script
Add this to your ecommerce-activity-log.php file:
/**
* Enqueue front-end scripts.
*/
function ecommerce_activity_log_enqueue_scripts() {
// Only enqueue on the front-end if the block is present.
// A more robust check would involve checking post content for the block signature.
if ( is_admin() ) {
return;
}
wp_enqueue_script(
'ecommerce-activity-log-frontend',
plugin_dir_url( __FILE__ ) . 'build/frontend.js',
array( 'wp-element' ), // Dependencies
filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' ),
true // Load in footer
);
// Pass data to the script, e.g., the initial log count and nonce for AJAX.
wp_localize_script( 'ecommerce-activity-log-frontend', 'ecommerceActivityLog', array(
'logCount' => 10, // Default or fetched from block attributes if possible
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ecommerce_activity_log_nonce' ),
) );
}
add_action( 'wp_enqueue_scripts', 'ecommerce_activity_log_enqueue_scripts' );
Front-End JavaScript (src/frontend.js)
Create a new file src/frontend.js and add the following:
document.addEventListener( 'DOMContentLoaded', () => {
const logContainer = document.querySelector( '.ecommerce-activity-log-container' );
if ( ! logContainer ) {
return; // Block not present on this page
}
const logList = logContainer.querySelector( '.ecommerce-activity-log-list' );
const realtimeIndicator = document.getElementById( 'ecommerce-activity-log-realtime-indicator' );
const initialLogCount = typeof ecommerceActivityLog !== 'undefined' ? ecommerceActivityLog.logCount : 10;
let currentLogCount = initialLogCount;
// Function to fetch and display new logs
const fetchAndUpdateLogs = () => {
fetch( ecommerceActivityLog.ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams( {
action: 'ecommerce_activity_log_fetch_logs',
nonce: ecommerceActivityLog.nonce,
count: currentLogCount, // Requesting current count to get *new* logs
} ),
} )
.then( response => response.json() )
.then( data => {
if ( data.success && data.data.logs && data.data.logs.length > 0 ) {
// Prepend new logs to the list
data.data.logs.forEach( log => {
const listItem = document.createElement( 'li' );
listItem.className = 'ecommerce-activity-log-item';
listItem.innerHTML = `
<span class="ecommerce-activity-log-timestamp">${ log.timestamp }</span>
<span class="ecommerce-activity-log-message">${ log.message }</span>
<span class="ecommerce-activity-log-user">(${ log.user })</span>
`;
logList.prepend( listItem ); // Add to the top
} );
currentLogCount += data.data.logs.length; // Update count
// Keep the list size manageable (optional)
const maxItems = 50; // Example limit
while ( logList.children.length > maxItems ) {
logList.removeChild( logList.lastChild );
}
if ( realtimeIndicator ) {
realtimeIndicator.textContent = `(${ data.data.logs.length } new activities) ${ new Date().toLocaleTimeString() }`;
setTimeout(() => { realtimeIndicator.textContent = ''; }, 5000); // Clear indicator after 5 seconds
}
} else if ( data.success && data.data.logs && data.data.logs.length === 0 ) {
// No new logs, do nothing
if ( realtimeIndicator ) {
realtimeIndicator.textContent = `(No new activities) ${ new Date().toLocaleTimeString() }`;
setTimeout(() => { realtimeIndicator.textContent = ''; }, 5000);
}
} else {
console.error( 'Failed to fetch logs:', data.data.message || 'Unknown error' );
if ( realtimeIndicator ) {
realtimeIndicator.textContent = `(Error fetching logs) ${ new Date().toLocaleTimeString() }`;
}
}
} )
.catch( error => {
console.error( 'Error fetching logs:', error );
if ( realtimeIndicator ) {
realtimeIndicator.textContent = `(Network error) ${ new Date().toLocaleTimeString() }`;
}
} );
};
// Poll for new logs every 15 seconds
const pollingInterval = 15000;
setInterval( fetchAndUpdateLogs, pollingInterval );
// Initial fetch on load (optional, as render_callback already provides initial logs)
// fetchAndUpdateLogs();
} );
AJAX Handler for Log Fetching
We need a WordPress AJAX endpoint to serve new log entries to the front-end JavaScript. Add this to ecommerce-activity-log.php:
/**
* AJAX handler to fetch recent activity logs.
*/
function ecommerce_activity_log_fetch_logs_ajax() {
check_ajax_referer( 'ecommerce_activity_log_nonce', 'nonce' );
$current_log_count = isset( $_POST['count'] ) ? intval( $_POST['count'] ) : 10;
// Fetch logs *since* the last known log.
// This requires a way to track the latest log timestamp or ID.
// For simulation, we'll fetch a slightly larger batch and return only the newest ones.
$fetch_batch_size = 5; // Fetch a few more than requested to find new ones
$simulated_all_logs = ecommerce_activity_log_get_simulated_logs( $current_log_count + $fetch_batch_size ); // Get more logs than currently displayed
$new_logs = [];
if ( ! empty( $simulated_all_logs ) ) {
// Assuming logs are sorted by time descending, the first $current_log_count are already displayed.
// We take the next $fetch_batch_size logs as potentially new.
$new_logs = array_slice( $simulated_all_logs, 0, $fetch_batch_size );
}
if ( ! empty( $new_logs ) ) {
wp_send_json_success( [
'logs' => $new_logs,
'message' => sprintf( __( '%d new activity logs fetched.', 'ecommerce-activity-log' ), count( $new_logs ) ),
] );
} else {
wp_send_json_success( [
'logs' => [],
'message' => __( 'No new activity logs found.', 'ecommerce-activity-log' ),
] );
}
}
add_action( 'wp_ajax_ecommerce_activity_log_fetch_logs', 'ecommerce_activity_log_fetch_logs_ajax' );
// For non-logged-in users, though unlikely for activity logs.
// add_action( 'wp_ajax_nopriv_ecommerce_activity_log_fetch_logs', 'ecommerce_activity_log_fetch_logs_ajax' );
Styling the Block (src/style.scss and src/editor.scss)
Add some basic styling to make the log readable. Create src/style.scss for front-end styles and src/editor.scss for editor styles. Ensure your package.json includes these in the build process (@wordpress/scripts handles this automatically if they are in the src/ directory).
/* src/style.scss */
.ecommerce-activity-log-container {
border: 1px solid #ddd;
padding: 15px;
background-color: #f9f9f9;
border-radius: 4px;
font-family: sans-serif;
margin-bottom: 20px;
}
.ecommerce-activity-log-container h3 {
margin-top: 0;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 15px;
}
.ecommerce-activity-log-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 300px; /* Limit height and allow scrolling */
overflow-y: auto;
}
.ecommerce-activity-log-item {
padding: 8px 0;
border-bottom: 1px dashed #eee;
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 0.9em;
color: #555;
}
.ecommerce-activity-log-item:last-child {
border-bottom: none;
}
.ecommerce-activity-log-timestamp {
color: #888;
flex-shrink: 0;
}
.ecommerce-activity-log-message {
flex-grow: 1;
color: #333;
}
.ecommerce-activity-log-user {
color: #777;
font-style: italic;
flex-shrink: 0;
}
#ecommerce-activity-log-realtime-indicator {
margin-top: 10px;
font-size: 0.8em;
color: #999;
text-align: right;
min-height: 1.2em; /* Prevent layout shift */
}
/* src/editor.scss */
.wp-block-ecommerce-activity-log-realtime-logs {
border: 1px dashed #ccc;
padding: 15px;
background-color: #f0f0f0;
text-align: center;
font-family: sans-serif;
}
.wp-block-ecommerce-activity-log-realtime-logs h3 {
margin-top: 0;
color: #555;
}
Real-World Considerations and Enhancements
The provided solution uses simulated logs and polling. For a production e-commerce environment, consider these improvements:
- Dedicated Logging System: Implement a custom database table (e.g.,
wp_ecommerce_activity_logs) to store structured log data. This allows for more efficient querying and filtering than parsing WordPress actions. - WebSockets/SSE: Replace polling with WebSockets (e.g., using Socket.IO or a WordPress plugin like WP-Websockets) or Server-Sent Events for true real-time, push-based updates. This significantly reduces server load and improves responsiveness.
- Security: Implement robust nonce checks and sanitize all data before outputting it. Ensure user roles and capabilities are checked if certain logs are sensitive.
- Performance: For high-traffic sites, optimize log retrieval. Consider caching strategies or using a dedicated logging service. The AJAX endpoint should be highly efficient.
- Filtering and Search: Add controls within the block (either in the editor or on the front-end) to filter logs by user, activity type, or date range.
- Error Handling: Enhance error handling in both PHP and JavaScript to provide informative feedback to administrators.
- User Interface: Improve the visual presentation of logs, perhaps with icons or different styling for different event types.
Conclusion
By utilizing PHP block-render callbacks, you can create dynamic, server-rendered Gutenberg blocks that seamlessly integrate complex functionality into the WordPress admin and front-end. This approach ensures that the block’s content is always up-to-date and efficiently generated. For real-time features like activity logs, combining server-side rendering with a client-side update mechanism (even a simple polling one for demonstration) provides a powerful solution for e-commerce businesses seeking immediate insights into user interactions.