Step-by-Step Guide to building a custom real-time activity logs block for Gutenberg using Svelte standalone templates
Setting Up the Development Environment
Before diving into code, ensure your WordPress development environment is ready. This involves having a local WordPress installation (e.g., using LocalWP, Docker, or a LAMP/LEMP stack) and Node.js with npm or yarn installed. We’ll be using npm for package management and the WordPress Script package for building our Gutenberg block.
Create a new directory for your plugin. Navigate into this directory via your terminal and initialize a Node.js project:
mkdir custom-activity-logs cd custom-activity-logs npm init -y
Next, install the necessary development dependencies. We’ll need `@wordpress/scripts` for compiling our JavaScript and Svelte components, and `svelte` itself.
npm install @wordpress/scripts svelte --save-dev
In your `package.json` file, add a `scripts` section to leverage `@wordpress/scripts` for building and watching your assets:
{
"name": "custom-activity-logs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wordpress/scripts": "^26.10.0",
"svelte": "^4.2.12"
}
}
Plugin Structure and Registration
Create the main plugin file (e.g., `custom-activity-logs.php`) in the root of your plugin directory. This file will register your Gutenberg block.
<?php
/**
* Plugin Name: Custom Activity Logs
* Description: A custom block to display real-time activity logs.
* Version: 1.0.0
* Author: Your Name
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Register the block.
*/
function custom_activity_logs_register_block() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_activity_logs_register_block' );
?>
Now, create the necessary directories for your block’s source files. We’ll need a `src` directory for our Svelte components and JavaScript, and a `build` directory where the compiled assets will be placed by `@wordpress/scripts`.
mkdir src build
Inside the `src` directory, create an `index.js` file. This will be the entry point for your block’s JavaScript.
// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import App from './App.svelte';
registerBlockType( 'custom-activity-logs/realtime-logs', {
title: 'Real-time Activity Logs',
icon: 'chart-line', // WordPress Dashicon
category: 'widgets',
edit: ( { attributes, setAttributes } ) => {
// This component will be rendered in the editor.
// For this example, we'll keep it simple and focus on the frontend.
return <div>Real-time Activity Logs (Editor Preview)</div>;
},
save: () => {
// This function determines what is saved to the database.
// We'll render our Svelte component on the frontend via PHP.
return null; // Return null as we'll render dynamically.
},
} );
The `registerBlockType` function is crucial. It tells WordPress about our new block. We define its `title`, `icon`, and `category`. For the `edit` function, we’ll provide a placeholder for now. The `save` function returns `null` because we intend to render the Svelte component dynamically on the frontend using PHP, rather than saving static HTML. This is key for real-time updates.
Creating the Svelte Component
In the `src` directory, create your main Svelte component, `App.svelte`. This component will handle fetching and displaying the activity logs.
<!-- src/App.svelte -->
<script>
import { onMount, onDestroy } from 'svelte';
let logs = [];
let websocket = null;
// Replace with your actual WebSocket endpoint
const WEBSOCKET_URL = 'ws://localhost:8080/ws'; // Example WebSocket server
onMount(() => {
connectWebSocket();
});
onDestroy(() => {
if (websocket) {
websocket.close();
}
});
function connectWebSocket() {
websocket = new WebSocket(WEBSOCKET_URL);
websocket.onopen = () => {
console.log('WebSocket connection opened');
// You might want to send a message to the server to subscribe to logs
// websocket.send(JSON.stringify({ action: 'subscribe', topic: 'activity_logs' }));
};
websocket.onmessage = (event) => {
try {
const newLogEntry = JSON.parse(event.data);
// Assuming the server sends log entries one by one
logs = [newLogEntry, ...logs]; // Add new log to the top
// Limit the number of logs displayed to prevent performance issues
if (logs.length > 50) {
logs.pop();
}
} catch (e) {
console.error('Error parsing WebSocket message:', e);
}
};
websocket.onerror = (error) => {
console.error('WebSocket Error:', error);
};
websocket.onclose = () => {
console.log('WebSocket connection closed. Attempting to reconnect...');
// Implement a reconnection strategy with backoff
setTimeout(connectWebSocket, 5000); // Reconnect after 5 seconds
};
}
</script>
<div class="activity-logs-container">
<h3>Recent Activity</h3>
<ul>
{#each logs as log (log.id)}
<li>
<span class="timestamp">{new Date(log.timestamp).toLocaleString()}</span>
<span class="message">{log.message}</span>
</li>
{/each}
</ul>
</div>
<style>
.activity-logs-container {
border: 1px solid #eee;
padding: 15px;
border-radius: 5px;
background-color: #f9f9f9;
font-family: sans-serif;
}
.activity-logs-container h3 {
margin-top: 0;
color: #333;
}
.activity-logs-container ul {
list-style: none;
padding: 0;
margin: 0;
max-height: 300px; /* Limit height and enable scrolling */
overflow-y: auto;
}
.activity-logs-container li {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed #ddd;
display: flex;
flex-direction: column;
font-size: 0.9em;
}
.activity-logs-container li:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.timestamp {
color: #777;
font-size: 0.8em;
margin-bottom: 5px;
}
.message {
color: #555;
}
</style>
This Svelte component:
- Initializes an empty `logs` array and a `websocket` variable.
- Uses `onMount` to establish a WebSocket connection when the component is added to the DOM.
- Uses `onDestroy` to close the WebSocket connection when the component is removed.
- The `connectWebSocket` function handles the WebSocket lifecycle: opening, receiving messages, errors, and closing.
- On receiving a message, it parses the JSON data, prepends the new log entry to the `logs` array, and enforces a maximum number of displayed logs.
- Includes basic styling for the log display.
Important: You will need a separate backend service that provides a WebSocket endpoint (e.g., using Node.js with `ws`, or a PHP WebSocket server) to push activity log data. The `WEBSOCKET_URL` should point to this service.
Integrating Svelte with WordPress Block Editor
To render the Svelte component on the frontend, we need to modify our `src/index.js` and create a PHP file to enqueue the compiled Svelte component.
First, update `src/index.js` to import and render the Svelte component within the `edit` function for the block editor preview. For the frontend, we’ll rely on PHP.
// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import { render } from 'svelte/internal'; // Import render for dynamic mounting
import App from './App.svelte';
registerBlockType( 'custom-activity-logs/realtime-logs', {
title: 'Real-time Activity Logs',
icon: 'chart-line',
category: 'widgets',
edit: ( { clientId } ) => {
// Render the Svelte component in the editor
// We need a DOM element to mount to. Gutenberg provides a placeholder.
// For simplicity, we'll just show a message. A more advanced approach
// would involve creating a temporary div and rendering into it.
return <div>Real-time Activity Logs (Editor Preview - requires dynamic rendering setup)</div>;
},
save: () => {
// Return null as we will render dynamically via PHP.
return null;
},
} );
Now, let’s create a PHP file to enqueue the compiled JavaScript and CSS for our block and to handle the dynamic rendering on the frontend.
Create a file named `custom-activity-logs-frontend.php` inside your plugin directory.
<?php
/**
* Frontend rendering and script enqueuing.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Enqueue block assets.
*/
function custom_activity_logs_enqueue_block_assets() {
// Enqueue the compiled JavaScript and CSS for the block editor and frontend.
// The path is relative to the plugin's root directory.
wp_enqueue_script(
'custom-activity-logs-block-js',
plugins_url( 'build/index.js', __FILE__ ),
array( 'wp-blocks', 'wp-element', 'wp-editor' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
wp_enqueue_style(
'custom-activity-logs-block-css',
plugins_url( 'build/index.css', __FILE__ ),
array(),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
);
}
add_action( 'enqueue_block_assets', 'custom_activity_logs_enqueue_block_assets' );
/**
* Render the Svelte component on the frontend.
*
* This function is called by the block's save() returning null.
* We use a filter to inject our dynamic rendering logic.
*/
function custom_activity_logs_render_frontend_block( $block_content, $block ) {
if ( isset( $block['blockName'] ) && 'custom-activity-logs/realtime-logs' === $block['blockName'] ) {
// We need a container element for our Svelte app to mount onto.
// The ID should be unique for each instance of the block.
$container_id = 'custom-activity-logs-app-' . uniqid();
// Enqueue the Svelte app's JavaScript and CSS if not already enqueued.
// This ensures the necessary assets are loaded only when the block is present.
wp_enqueue_script( 'custom-activity-logs-frontend-app', plugins_url( 'build/frontend-app.js', __FILE__ ), array(), filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend-app.js' ) );
wp_enqueue_style( 'custom-activity-logs-frontend-app-css', plugins_url( 'build/frontend-app.css', __FILE__ ), array(), filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend-app.css' ) );
// The actual Svelte component will be mounted client-side.
// We need to pass the container ID to the JavaScript.
// This requires a separate entry point for the frontend Svelte app.
ob_start();
?>
<div id="" data-websocket-url=""></div>
<script>
// This script will be handled by frontend-app.js
// It will find the container and mount the Svelte app.
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('');
if (container) {
// Assuming frontend-app.js exports a function to mount the app
// e.g., mountActivityLogApp(container, websocketUrl);
if (window.mountActivityLogApp) {
window.mountActivityLogApp(container, container.dataset.websocketUrl);
}
}
});
</script>
<?php
return ob_get_clean();
}
return $block_content;
}
add_filter( 'render_block', 'custom_activity_logs_render_frontend_block', 10, 2 );
We need to adjust our build process to create a separate entry point for the frontend rendering. Modify `src/index.js` to include this:
// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import App from './App.svelte'; // Our main Svelte component
// Editor-specific registration
registerBlockType( 'custom-activity-logs/realtime-logs', {
title: 'Real-time Activity Logs',
icon: 'chart-line',
category: 'widgets',
edit: ( { clientId } ) => {
// For the editor, we'll just show a placeholder.
// Real-time updates are typically not desired or feasible in the editor.
return <div>Real-time Activity Logs (Editor Preview)</div>;
},
save: () => {
// Return null to indicate dynamic rendering on the frontend.
return null;
},
} );
// Frontend-specific entry point (if needed for direct mounting)
// This part is more for demonstration; the PHP filter handles the mounting.
// If you were to use a different approach, you might export a mount function here.
export function mountFrontendApp(containerId, websocketUrl) {
const container = document.getElementById(containerId);
if (container) {
new App({
target: container,
props: {
initialWebsocketUrl: websocketUrl // Pass URL as a prop
}
});
}
}
// If you want to automatically mount the app on DOMContentLoaded
// when this script is loaded directly (e.g., via wp_enqueue_script directly)
// document.addEventListener('DOMContentLoaded', () => {
// const containers = document.querySelectorAll('[data-custom-activity-logs-app]');
// containers.forEach(container => {
// const websocketUrl = container.dataset.websocketUrl;
// new App({
// target: container,
// props: {
// initialWebsocketUrl: websocketUrl
// }
// });
// });
// });
We also need a dedicated entry point for the frontend rendering. Create `src/frontend.js`:
// src/frontend.js
import App from './App.svelte';
// Expose a function to mount the Svelte app
export function mountActivityLogApp(container, websocketUrl) {
new App({
target: container,
props: {
initialWebsocketUrl: websocketUrl // Pass the URL as a prop to the Svelte component
}
});
}
Update `App.svelte` to accept the WebSocket URL as a prop:
<!-- src/App.svelte -->
<script>
import { onMount, onDestroy } from 'svelte';
export let initialWebsocketUrl = 'ws://localhost:8080/ws'; // Default URL, can be overridden by prop
let logs = [];
let websocket = null;
onMount(() => {
connectWebSocket(initialWebsocketUrl); // Use the prop
});
onDestroy(() => {
if (websocket) {
websocket.close();
}
});
function connectWebSocket(url) {
websocket = new WebSocket(url);
websocket.onopen = () => {
console.log('WebSocket connection opened to:', url);
};
websocket.onmessage = (event) => {
try {
const newLogEntry = JSON.parse(event.data);
logs = [newLogEntry, ...logs];
if (logs.length > 50) {
logs.pop();
}
} catch (e) {
console.error('Error parsing WebSocket message:', e);
}
};
websocket.onerror = (error) => {
console.error('WebSocket Error:', error);
};
websocket.onclose = () => {
console.log('WebSocket connection closed. Attempting to reconnect...');
setTimeout(() => connectWebSocket(url), 5000); // Reconnect using the same URL
};
}
</script>
<div class="activity-logs-container">
<h3>Recent Activity</h3>
<ul>
{#each logs as log (log.id)}
<li>
<span class="timestamp">{new Date(log.timestamp).toLocaleString()}</span>
<span class="message">{log.message}</span>
</li>
{/each}
</ul>
</div>
<style>
/* ... (styles remain the same) ... */
.activity-logs-container {
border: 1px solid #eee;
padding: 15px;
border-radius: 5px;
background-color: #f9f9f9;
font-family: sans-serif;
}
.activity-logs-container h3 {
margin-top: 0;
color: #333;
}
.activity-logs-container ul {
list-style: none;
padding: 0;
margin: 0;
max-height: 300px; /* Limit height and enable scrolling */
overflow-y: auto;
}
.activity-logs-container li {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px dashed #ddd;
display: flex;
flex-direction: column;
font-size: 0.9em;
}
.activity-logs-container li:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.timestamp {
color: #777;
font-size: 0.8em;
margin-bottom: 5px;
}
.message {
color: #555;
}
</style>
Finally, configure `@wordpress/scripts` to build both `index.js` (for the editor) and `frontend.js` (for the frontend). Edit your `package.json`:
{
"name": "custom-activity-logs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "wp-scripts build --experimental-modules --module-format=esm --output-path=build && wp-scripts build src/frontend.js --experimental-modules --module-format=esm --output-path=build",
"start": "wp-scripts start --experimental-modules"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wordpress/scripts": "^26.10.0",
"svelte": "^4.2.12"
}
}
The `–experimental-modules` and `–module-format=esm` flags are important for modern JavaScript module handling. The `build` script now explicitly builds both `index.js` and `frontend.js` into the `build` directory.
Building and Activating the Plugin
Run the build command in your terminal:
npm run build
This will compile your Svelte components and JavaScript into the `build` directory, creating `index.js`, `index.css`, `frontend.js`, and `frontend.css`. Make sure your plugin directory (`custom-activity-logs`) is placed inside the `wp-content/plugins/` directory of your WordPress installation.
Activate the “Custom Activity Logs” plugin from your WordPress admin dashboard.
Now, go to the WordPress editor (e.g., create or edit a post/page), and search for “Real-time Activity Logs”. Add the block to your content.
When viewing the post/page on the frontend, you should see the “Real-time Activity Logs” block rendered. It will attempt to connect to your WebSocket server. If the server is running and sending data, the logs will appear in real-time.
Testing and Further Development
To test this, you’ll need a WebSocket server. A simple Node.js server using the `ws` library can be set up:
// server.js (Node.js example)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
// Send a log entry every 5 seconds
const interval = setInterval(() => {
const logEntry = {
id: Date.now(),
timestamp: new Date().toISOString(),
message: `User activity detected: ${Math.random().toString(36).substring(7)}`,
level: 'info'
};
ws.send(JSON.stringify(logEntry));
}, 5000);
ws.on('message', (message) => {
console.log(`Received: ${message}`);
});
ws.on('close', () => {
console.log('Client disconnected');
clearInterval(interval);
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
clearInterval(interval);
});
});
console.log('WebSocket server started on port 8080');
Install the `ws` package (`npm install ws`) and run this server (`node server.js`). Ensure the `WEBSOCKET_URL` in your Svelte component (or passed via props/data attributes) matches the server’s address and port.
For production, consider:
- A more robust WebSocket server implementation.
- Authentication and authorization for WebSocket connections.
- Error handling and reconnection strategies with exponential backoff.
- Server-side logging aggregation to feed the WebSocket.
- Security considerations for WebSocket endpoints.
- Optimizing Svelte compilation for production builds.
This setup provides a foundation for building dynamic, real-time features within WordPress using modern JavaScript frameworks like Svelte, integrated seamlessly with the Gutenberg block editor.