Step-by-Step Guide to building a custom real-time activity logs block for Gutenberg using Alpine.js lightweight states
Gutenberg Block Structure and Initialization
We’ll start by defining the basic structure of our Gutenberg block. This involves registering the block type and setting up its initial attributes. For a real-time activity log, we’ll need attributes to store the log entries, pagination data, and potentially a loading state.
The block registration happens in JavaScript, typically within a plugin’s main JavaScript file. We’ll use the wp.blocks.registerBlockType function. For the frontend rendering, we’ll leverage Alpine.js to manage the dynamic state of the activity log.
Registering the Block Type
First, let’s define the block’s metadata and its initial state. We’ll create a JavaScript file (e.g., src/index.js) and enqueue it properly in WordPress.
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import Edit from './edit';
import save from './save';
registerBlockType('antigravity/realtime-activity-log', {
title: __('Real-time Activity Log', 'antigravity'),
icon: 'list-view',
category: 'widgets',
attributes: {
logEntries: {
type: 'array',
default: [],
},
currentPage: {
type: 'number',
default: 1,
},
totalPages: {
type: 'number',
default: 1,
},
isLoading: {
type: 'boolean',
default: false,
},
},
edit: Edit,
save: save,
});
Frontend Rendering with Alpine.js
The save function in Gutenberg defines the static HTML that gets saved to the post content. For dynamic content like real-time logs, we’ll use the edit function to render the block in the editor and a separate JavaScript file for the frontend. This frontend script will initialize Alpine.js and fetch/display the logs.
src/edit.js (Editor View)
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
// In the editor, we might show a placeholder or a limited view.
// For simplicity, we'll just show a message.
return (
<div { ...blockProps }>
<p>{ __('Real-time Activity Log (Frontend will display live data)', 'antigravity') }</p>
<p>Current Page: { attributes.currentPage }</p>
<p>Total Pages: { attributes.totalPages }</p>
</div>
);
}
src/save.js (Static HTML for Post Content)
import { useBlockProps } from '@wordpress/block-editor';
export default function save({ attributes }) {
// The save function should output static HTML.
// The dynamic fetching and rendering will be handled by frontend JS.
// We'll add a data attribute to hook into with Alpine.js.
return (
<div { ...useBlockProps.save() } data-antigravity-activity-log="">
<!-- Placeholder for dynamic content -->
<p>Loading activity log...</p>
</div>
);
}
Frontend JavaScript with Alpine.js
This is where the real-time magic happens. We’ll create a separate JavaScript file (e.g., assets/js/frontend.js) that initializes Alpine.js and fetches log data. This script needs to be enqueued to run on the frontend of your WordPress site.
assets/js/frontend.js
document.addEventListener('alpine:init', () => {
const activityLogBlocks = document.querySelectorAll('[data-antigravity-activity-log]');
activityLogBlocks.forEach(blockElement => {
Alpine.data('activityLog', () => ({
logEntries: [],
currentPage: 1,
totalPages: 1,
isLoading: false,
perPage: 10, // Configurable per page
init() {
this.fetchLogs();
// Optional: Poll for new logs every X seconds
// setInterval(() => this.fetchLogs(), 30000);
},
async fetchLogs(page = 1) {
this.isLoading = true;
try {
// Replace with your actual API endpoint
const response = await fetch(`/wp-json/antigravity/v1/activity-logs?page=${page}&per_page=${this.perPage}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
this.logEntries = data.logs || [];
this.currentPage = data.current_page || 1;
this.totalPages = data.total_pages || 1;
} catch (error) {
console.error('Error fetching activity logs:', error);
this.logEntries = [{ timestamp: new Date().toISOString(), message: 'Failed to load logs.', level: 'error' }];
} finally {
this.isLoading = false;
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.fetchLogs(this.currentPage + 1);
}
},
prevPage() {
if (this.currentPage > 1) {
this.fetchLogs(this.currentPage - 1);
}
},
formatTimestamp(isoString) {
if (!isoString) return '';
const date = new Date(isoString);
return date.toLocaleString(); // Adjust formatting as needed
}
}));
// Initialize Alpine.js for this specific block
// We need to ensure Alpine is loaded and then apply it.
// A common pattern is to have a global Alpine instance or load it dynamically.
// For simplicity here, assuming Alpine is globally available.
// If not, you'd need to load Alpine.js script first.
if (typeof Alpine !== 'undefined') {
// Find the element within the block that will be the x-data root
// For this example, we'll assume the blockElement itself is the root.
// If not, you might need a specific child element.
Alpine.initTree(blockElement);
} else {
console.error('Alpine.js not found. Please ensure it is loaded.');
}
});
});
WordPress Plugin Setup and Enqueuing
To make this work, you need a WordPress plugin. The plugin will register the block type and enqueue the necessary JavaScript and CSS files. We’ll use the standard WordPress plugin structure.
real-time-activity-log.php (Main Plugin File)
<?php
/**
* Plugin Name: Real-time Activity Log
* Description: A custom Gutenberg block for displaying real-time activity logs.
* Version: 1.0.0
* Author: Antigravity
* Text Domain: antigravity
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Register the Gutenberg block.
*/
function antigravity_register_activity_log_block() {
// Register block on server-side.
register_block_type( __DIR__ . '/build' );
// Enqueue frontend scripts and styles.
wp_enqueue_script(
'antigravity-activity-log-frontend',
plugin_dir_url( __FILE__ ) . 'build/frontend.js',
array( 'alpinejs' ), // Dependency on Alpine.js
filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' )
);
// Enqueue Alpine.js if not already present (e.g., via theme or another plugin)
// It's generally better to rely on theme/plugin providing it or use a CDN.
// For this example, we'll assume it's available or you'll enqueue it.
// If you need to enqueue it:
/*
wp_enqueue_script(
'alpinejs',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js', // Use a specific version
array(),
'3.x.x',
true
);
*/
}
add_action( 'init', 'antigravity_register_activity_log_block' );
/**
* Register REST API endpoint for activity logs.
*/
function antigravity_register_activity_log_api() {
register_rest_route( 'antigravity/v1', '/activity-logs', array(
'methods' => 'GET',
'callback' => 'antigravity_get_activity_logs',
'permission_callback' => '__return_true', // Adjust permissions as needed
) );
}
add_action( 'rest_api_init', 'antigravity_register_activity_log_api' );
/**
* Callback function to fetch activity logs.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
function antigravity_get_activity_logs( WP_REST_Request $request ) {
$page = $request->get_param( 'page' ) ? intval( $request->get_param( 'page') ) : 1;
$per_page = $request->get_param( 'per_page' ) ? intval( $request->get_param( 'per_page') ) : 10;
// --- Placeholder for actual log fetching ---
// In a real-world scenario, you would query your database or log storage.
// For demonstration, we'll generate dummy data.
$logs = array();
$total_logs = 100; // Assume 100 total log entries
$offset = ($page - 1) * $per_page;
for ( $i = 0; $i < $per_page; $i++ ) {
$log_index = $offset + $i + 1;
if ($log_index > $total_logs) break;
$logs[] = array(
'id' => $log_index,
'timestamp' => gmdate( 'Y-m-d\TH:i:s', time() - ( $total_logs - $log_index ) * 60 * 5 ), // Simulate time
'message' => 'User ' . rand(100, 999) . ' performed an action.',
'level' => (rand(0, 10) < 2) ? 'warning' : ((rand(0, 10) < 1) ? 'error' : 'info'),
);
}
// --- End Placeholder ---
$total_pages = ceil( $total_logs / $per_page );
return new WP_REST_Response( array(
'logs' => $logs,
'current_page' => $page,
'total_pages' => $total_pages,
'per_page' => $per_page,
'total_logs' => $total_logs,
), 200 );
}
/**
* Enqueue editor scripts.
*/
function antigravity_editor_scripts() {
wp_enqueue_script(
'antigravity-activity-log-editor',
plugin_dir_url( __FILE__ ) . 'build/index.js',
array( 'wp-blocks', 'wp-editor', 'wp-components', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
}
add_action( 'enqueue_block_editor_assets', 'antigravity_editor_scripts' );
Build Process and File Structure
You’ll need a build process to compile your JavaScript (e.g., using Babel and Webpack or Vite) and handle asset management. A typical structure would look like this:
real-time-activity-log/(Plugin Root)real-time-activity-log.php(Main Plugin File)build/(Compiled Assets)index.js(Editor JS)frontend.js(Frontend JS)style-index.css(Optional: Editor Styles)index.css(Optional: Frontend Styles)
src/(Source Files)index.js(Block Registration)edit.js(Editor Component)save.js(Save Component)
assets/js/(Frontend JS Source)frontend.js(Alpine.js Logic)
package.json(NPM Dependencies)webpack.config.js(or Vite config)
Your package.json would include dependencies like @wordpress/blocks, @wordpress/i18n, @wordpress/block-editor, and potentially a build tool like Webpack or Vite.
Example package.json
{
"name": "real-time-activity-log",
"version": "1.0.0",
"description": "Gutenberg block for real-time activity logs.",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": ["wordpress", "gutenberg", "block"],
"author": "Antigravity",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/scripts": "^26.0.0"
},
"dependencies": {
"alpinejs": "^3.13.0"
}
}
Run npm install to install dependencies, and then npm run build to compile your assets. The wp-scripts package simplifies the Webpack configuration for WordPress development.
Styling the Activity Log
You’ll want to add some CSS to make the activity log presentable. This can be done in a style-index.css (for editor and frontend) or index.css (for frontend only) file, enqueued by your plugin.
Example CSS (assets/css/style.css, to be compiled into build/index.css)
.wp-block-antigravity-realtime-activity-log {
border: 1px solid #ddd;
padding: 15px;
border-radius: 4px;
background-color: #f9f9f9;
font-family: sans-serif;
}
.wp-block-antigravity-realtime-activity-log p {
margin-bottom: 10px;
}
.activity-log-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.activity-log-table th,
.activity-log-table td {
border: 1px solid #eee;
padding: 8px;
text-align: left;
}
.activity-log-table th {
background-color: #f2f2f2;
font-weight: bold;
}
.activity-log-table tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
.log-level-info {
color: #31708f;
}
.log-level-warning {
color: #8a6d3b;
font-weight: bold;
}
.log-level-error {
color: #a94442;
font-weight: bold;
}
.log-pagination {
margin-top: 15px;
display: flex;
justify-content: center;
align-items: center;
}
.log-pagination button {
background-color: #0073aa;
color: white;
border: none;
padding: 8px 12px;
margin: 0 5px;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.log-pagination button:hover {
background-color: #005177;
}
.log-pagination button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.log-pagination span {
margin: 0 10px;
font-weight: bold;
}
.loading-indicator {
text-align: center;
padding: 20px;
font-style: italic;
color: #777;
}
Integrating Alpine.js Data Attributes
The Alpine.js logic in frontend.js uses Alpine.data('activityLog', ...) to define a reusable component. This component is then initialized on elements with the data-antigravity-activity-log attribute. We need to ensure our Alpine.js component is correctly applied to the block’s root element in the frontend.
Modifying assets/js/frontend.js for Initialization
document.addEventListener('alpine:init', () => {
const activityLogBlocks = document.querySelectorAll('[data-antigravity-activity-log]');
activityLogBlocks.forEach(blockElement => {
// Define the Alpine component data
const activityLogComponent = {
logEntries: [],
currentPage: 1,
totalPages: 1,
isLoading: false,
perPage: 10,
init() {
this.fetchLogs();
},
async fetchLogs(page = 1) {
this.isLoading = true;
try {
const response = await fetch(`/wp-json/antigravity/v1/activity-logs?page=${page}&per_page=${this.perPage}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
this.logEntries = data.logs || [];
this.currentPage = data.current_page || 1;
this.totalPages = data.total_pages || 1;
} catch (error) {
console.error('Error fetching activity logs:', error);
this.logEntries = [{ timestamp: new Date().toISOString(), message: 'Failed to load logs.', level: 'error' }];
} finally {
this.isLoading = false;
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.fetchLogs(this.currentPage + 1);
}
},
prevPage() {
if (this.currentPage > 1) {
this.fetchLogs(this.currentPage - 1);
}
},
formatTimestamp(isoString) {
if (!isoString) return '';
const date = new Date(isoString);
return date.toLocaleString();
}
};
// Apply Alpine.js to the block element
// We need to ensure Alpine is loaded and then apply it.
if (typeof Alpine !== 'undefined') {
// Assign the component data to the element using x-data
// We can do this programmatically or by ensuring the block's save function
// includes x-data="{ ...componentDefinition }"
// For dynamic initialization, we can use Alpine.bind or set properties.
// A simpler approach for this structure is to ensure the block's save function
// outputs the correct x-data attribute.
// Let's assume the save function outputs:
// ...
// If not, we can try to bind it:
// Alpine.bind(blockElement, 'x-data', activityLogComponent); // This might be too late or complex.
// The most robust way is to ensure the save function outputs the x-data attribute.
// Let's adjust the save.js to include x-data.
// If save.js outputs `data-antigravity-activity-log=""`, we can initialize it like this:
Alpine.data('activityLogComponent', activityLogComponent); // Register the component globally
Alpine.initTree(blockElement); // Initialize Alpine on the block element
} else {
console.error('Alpine.js not found. Please ensure it is loaded.');
}
});
});
Adjusting src/save.js for Alpine.js Initialization
import { useBlockProps } from '@wordpress/block-editor';
export default function save({ attributes }) {
// Add x-data attribute to initialize Alpine.js
// We'll pass initial attributes if needed, but for dynamic data,
// we rely on the frontend script to fetch and update.
// The 'activityLogComponent' name should match the one registered in frontend.js
return (
<div
{ ...useBlockProps.save() }
data-antigravity-activity-log=""
x-data="activityLogComponent" // This tells Alpine to use our component
>
<!-- Placeholder for dynamic content -->
<div x-show="isLoading" class="loading-indicator">Loading activity log...</div>
<div x-show="!isLoading">
<table class="activity-log-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Message</th>
<th>Level</th>
</tr>
</thead>
<tbody>
<template x-for="log in logEntries" :key="log.id">
<tr>
<td><span x-text="formatTimestamp(log.timestamp)"></span></td>
<td><span x-text="log.message"></span></td>
<td><span :class="'log-level-' + log.level" x-text="log.level.toUpperCase()"></span></td>
</tr>
</template>
<tr x-show="logEntries.length === 0 && !isLoading">
<td colspan="3" style="text-align: center;">No activity logs found.</td>
</tr>
</tbody>
</table>
<div class="log-pagination" x-show="totalPages > 1">
<button @click="prevPage()" :disabled="currentPage === 1">« Prev</button>
<span>Page <span x-text="currentPage"></span> of <span x-text="totalPages"></span></span>
<button @click="nextPage()" :disabled="currentPage === totalPages">Next »</button>
</div>
</div>
</div>
);
}
Final Considerations and Enhancements
This setup provides a robust foundation for a real-time activity log block. For production environments, consider:
- Security: The REST API endpoint
antigravity_get_activity_logshas'permission_callback' => '__return_true'. This is insecure for sensitive logs. Implement proper authentication and authorization checks (e.g., checking user capabilities) based on who should view the logs. - Log Storage: The current log fetching is a placeholder. Integrate with a proper logging mechanism (e.g., custom database table, external logging service).
- Real-time Updates: For true real-time updates without polling, consider WebSockets or Server-Sent Events (SSE). This would require a more complex backend setup.
- Error Handling: Enhance error handling in both the PHP backend and the frontend JavaScript.
- Performance: For very large log volumes, optimize database queries and consider caching strategies.
- Configuration: Allow users to configure the number of logs per page, log levels to display, etc., via block attributes or plugin settings.
- Accessibility: Ensure the table and pagination are accessible using ARIA attributes and proper semantic HTML.
By combining Gutenberg’s block API with Alpine.js’s declarative state management, you can build dynamic and interactive user interfaces within WordPress efficiently, keeping your JavaScript lean and maintainable.