• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom real-time activity logs block for Gutenberg using Svelte standalone templates

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.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (726)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala