• 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 interactive mapping module block for Gutenberg using Svelte standalone templates

Step-by-Step Guide to building a custom interactive mapping module block for Gutenberg using Svelte standalone templates

Project Setup and Svelte Template Configuration

This guide details the creation of a custom Gutenberg block for WordPress, enabling interactive mapping functionality. We’ll leverage Svelte with its standalone template feature for efficient component development, integrating it seamlessly into the WordPress block editor. This approach offers a performant and maintainable solution for complex frontend UIs within WordPress.

Our primary goal is to build a block that allows users to select a location on a map, potentially add markers, and store this data within post meta. We’ll start by setting up the necessary development environment and configuring Svelte for standalone component usage.

WordPress Plugin Structure and Build Process

First, let’s establish the basic WordPress plugin structure. Create a new directory within your wp-content/plugins/ folder, for instance, interactive-map-block. Inside this directory, create a main plugin file, interactive-map-block.php.

<?php
/**
 * Plugin Name: Interactive Map Block
 * Description: A custom Gutenberg block for interactive mapping.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: interactive-map-block
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

/**
 * Register the block.
 */
function interactive_map_block_register_block() {
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'interactive_map_block_register_block' );
?>

Next, we need a build process to compile our Svelte components into JavaScript that WordPress can understand. We’ll use npm and a simple Webpack configuration. Initialize your project with npm:

cd wp-content/plugins/interactive-map-block
npm init -y

Install the necessary development dependencies:

npm install --save-dev @sveltejs/svelte-preprocess svelte-loader webpack webpack-cli @wordpress/scripts

Create a webpack.config.js file in the root of your plugin directory:

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    mode: 'production',
    entry: {
        'index': './src/index.js',
        'editor': './src/editor.js',
        'frontend': './src/frontend.js',
    },
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: '[name].js',
    },
    module: {
        rules: [
            {
                test: /\.svelte$/,
                use: {
                    loader: 'svelte-loader',
                    options: {
                        preprocess: {
                            // Add your preprocessors here if needed, e.g., for TypeScript
                        },
                    },
                },
            },
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                ],
            },
        ],
    },
    plugins: [
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
            filename: '[name].css',
        }),
    ],
    resolve: {
        alias: {
            svelte: path.resolve('node_modules/svelte/src/runtime'),
        },
        extensions: ['.js', '.svelte'],
        mainFields: ['svelte', 'browser', 'module', 'main'],
    },
};

Add build scripts to your package.json:

{
  "name": "interactive-map-block",
  "version": "1.0.0",
  "description": "",
  "main": "build/index.js",
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "dev": "webpack --watch --config webpack.config.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@sveltejs/svelte-preprocess": "^4.0.0",
    "@wordpress/scripts": "^25.0.0",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^6.7.1",
    "mini-css-extract-plugin": "^2.6.0",
    "svelte": "^3.46.4",
    "svelte-loader": "^3.1.2",
    "webpack": "^5.69.0",
    "webpack-cli": "^4.9.2"
  }
}

Now, create the src directory and the entry points: src/index.js, src/editor.js, and src/frontend.js. The index.js will be the main entry point for the block registration, while editor.js will handle the editor-side rendering and frontend.js the public-facing view.

Svelte Component for the Editor Interface

Let’s start with the Svelte component that will be used within the Gutenberg editor. Create src/components/MapEditor.svelte.

<script>
    import { onMount } from 'svelte';
    import L from 'leaflet'; // Assuming Leaflet for mapping

    export let attributes = {};
    export let setAttributes;

    let mapContainer;
    let mapInstance;
    let marker;

    // Default map center and zoom
    const defaultCenter = [51.505, -0.09];
    const defaultZoom = 13;

    onMount(() => {
        mapInstance = L.map(mapContainer).setView(attributes.center || defaultCenter, attributes.zoom || defaultZoom);

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }).addTo(mapInstance);

        // Add existing marker if attributes.marker exists
        if (attributes.marker) {
            marker = L.marker(attributes.marker).addTo(mapInstance)
                .bindPopup('Selected Location')
                .openPopup();
        }

        mapInstance.on('click', (e) => {
            const latlng = e.latlng;
            if (marker) {
                marker.setLatLng(latlng);
            } else {
                marker = L.marker(latlng).addTo(mapInstance)
                    .bindPopup('Selected Location')
                    .openPopup();
            }
            // Update attributes for saving
            setAttributes({
                marker: [latlng.lat, latlng.lng],
                center: [latlng.lat, latlng.lng], // Optionally update center to the marker
                zoom: mapInstance.getZoom()
            });
        });

        // Update zoom level when it changes
        mapInstance.on('zoomend', () => {
            setAttributes({ zoom: mapInstance.getZoom() });
        });
    });

    // Clean up map on component destroy
    import { onDestroy } from 'svelte';
    onDestroy(() => {
        if (mapInstance) {
            mapInstance.remove();
        }
    });
</script>

<div bind:this="{mapContainer}" style="height: 300px;"></div>

<style>
    /* Basic styling for the map container */
    div {
        width: 100%;
    }
</style>

This component initializes a Leaflet map. It listens for click events to place or move a marker and updates the block’s attributes with the marker’s coordinates, center, and zoom level. The onMount lifecycle hook ensures the map is initialized after the DOM is ready, and onDestroy handles cleanup.

Registering the Block in WordPress

Now, let’s register this Svelte component as a Gutenberg block. We’ll use the @wordpress/scripts package, which provides utilities for block development, including Svelte compilation via Webpack.

Create src/editor.js to register the block for the editor interface:

import { registerBlockType } from '@wordpress/blocks';
import App from './components/MapEditor.svelte'; // Assuming MapEditor.svelte is in src/components

registerBlockType('interactive-map-block/map', {
    title: 'Interactive Map',
    icon: 'location-alt',
    category: 'widgets',
    attributes: {
        marker: {
            type: 'array',
            default: null, // [lat, lng]
        },
        center: {
            type: 'array',
            default: [51.505, -0.09],
        },
        zoom: {
            type: 'number',
            default: 13,
        },
    },
    edit: ({ attributes, setAttributes }) => {
        return React.createElement(App, { attributes, setAttributes });
    },
    save: () => {
        // The frontend component will handle rendering
        return null;
    },
});

And src/frontend.js for the public-facing view. For this example, we’ll keep the frontend simple, just rendering the map based on saved attributes. In a real-world scenario, you might fetch data or add more complex interactions here.

import { registerBlockType } from '@wordpress/blocks';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

// Import Leaflet marker icons and fix paths
import icon from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';

let DefaultIcon = L.Icon.extend({
    options: {
        shadowUrl: iconShadow,
        iconSize:    [25, 41],
        iconAnchor:  [12, 41],
        popupAnchor: [1, -34],
        shadowSize:  [41, 41]
    }
});
let myIcon = new DefaultIcon({iconUrl: icon});
L.Marker.prototype.options.icon = myIcon;


registerBlockType('interactive-map-block/map', {
    edit: () => {
        // No editor-specific UI needed for frontend
        return null;
    },
    save: ({ attributes }) => {
        const { marker, center, zoom } = attributes;

        // We'll render the map using a server-side approach or a separate frontend script
        // For simplicity here, we'll just output a placeholder div that a separate JS can target.
        // In a production setup, you'd likely enqueue a separate JS file for frontend rendering.
        if (marker) {
            return wp.element.createElement('div', {
                className: 'interactive-map-block-frontend',
                'data-marker': JSON.stringify(marker),
                'data-center': JSON.stringify(center),
                'data-zoom': zoom,
                style: 'height: 300px; width: 100%;'
            });
        }
        return null; // Don't render anything if no marker is set
    },
});

The src/index.js file will be the main entry point for the block registration, importing and calling the registration for both editor and frontend views. However, since @wordpress/scripts handles the compilation of editor.js and frontend.js into separate bundles, we can simplify src/index.js or even omit it if @wordpress/scripts is configured to pick up editor.js and frontend.js directly. For clarity, let’s assume @wordpress/scripts is configured to build editor.js and frontend.js as separate entry points, which is common.

To ensure the Svelte component is compiled and the necessary JavaScript files are generated, run the build command:

npm run build

This will create the build directory with index.js, editor.js, and frontend.js (and their corresponding CSS files). The register_block_type( __DIR__ . '/build' ); in your PHP file will automatically enqueue these scripts and styles.

Frontend Map Rendering and Data Handling

The save function in src/editor.js returns null. This is a common pattern when the frontend rendering is handled by a separate JavaScript file that enqueues based on the block’s presence. The save function in src/frontend.js outputs a div with data attributes containing the map information.

We need a separate JavaScript file to initialize Leaflet on the frontend, targeting these divs. Create src/frontend-init.js:

import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

// Import Leaflet marker icons and fix paths
import icon from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';

let DefaultIcon = L.Icon.extend({
    options: {
        shadowUrl: iconShadow,
        iconSize:    [25, 41],
        iconAnchor:  [12, 41],
        popupAnchor: [1, -34],
        shadowSize:  [41, 41]
    }
});
let myIcon = new DefaultIcon({iconUrl: icon});
L.Marker.prototype.options.icon = myIcon;


document.addEventListener('DOMContentLoaded', () => {
    const mapContainers = document.querySelectorAll('.interactive-map-block-frontend');

    mapContainers.forEach(container => {
        const markerData = JSON.parse(container.dataset.marker);
        const centerData = JSON.parse(container.dataset.center);
        const zoomData = parseInt(container.dataset.zoom, 10);

        if (markerData) {
            const map = L.map(container).setView(centerData, zoomData);

            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
            }).addTo(map);

            L.marker(markerData, { icon: myIcon }).addTo(map)
                .bindPopup('Selected Location')
                .openPopup();
        }
    });
});

You’ll need to update your webpack.config.js to include frontend-init.js as an entry point and ensure it’s enqueued. Modify the entry in webpack.config.js:

// ... inside webpack.config.js
    entry: {
        'index': './src/index.js', // Main block registration
        'editor': './src/editor.js', // Editor-side
        'frontend': './src/frontend.js', // Frontend save output
        'frontend-init': './src/frontend-init.js', // Frontend map initialization
    },
// ...

And update your PHP file to enqueue frontend-init.js specifically for the frontend:

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

/**
 * Enqueue block assets.
 */
function interactive_map_block_enqueue_assets() {
    // Register block scripts and styles handled by register_block_type
    register_block_type( __DIR__ . '/build' );

    // Enqueue frontend initialization script separately
    wp_enqueue_script(
        'interactive-map-block-frontend-init',
        plugin_dir_url( __FILE__ ) . 'build/frontend-init.js',
        array( 'wp-element', 'leaflet' ), // Dependencies, assuming 'leaflet' is registered elsewhere or you bundle it
        '1.0.0',
        true
    );

    // Enqueue Leaflet CSS if not already handled by block registration
    wp_enqueue_style(
        'leaflet-css',
        plugin_dir_url( __FILE__ ) . 'node_modules/leaflet/dist/leaflet.css', // Adjust path if Leaflet is bundled differently
        array(),
        '1.8.0'
    );
}
add_action( 'enqueue_block_assets', 'interactive_map_block_enqueue_assets' );

/**
 * Register the block.
 */
function interactive_map_block_register_block() {
    // This will register the block and enqueue editor scripts/styles
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'interactive_map_block_register_block' );

Note: In a production environment, you would typically bundle Leaflet itself within your plugin’s assets rather than relying on an external enqueue. For this example, we’re assuming Leaflet is available. You might need to adjust the wp_enqueue_script dependencies and paths accordingly.

Advanced Considerations and Enhancements

This setup provides a solid foundation. For more advanced use cases:

  • Server-Side Rendering (SSR): For better performance and SEO, consider server-side rendering of the map. This involves PHP logic to generate the initial map HTML and data.
  • Custom Markers and Popups: Extend the Svelte component to allow users to add multiple markers, customize their appearance, and add dynamic content to popups.
  • Data Storage: Integrate with WordPress post meta to save complex map data (e.g., GeoJSON).
  • External Data Sources: Fetch map data from external APIs or WordPress custom post types.
  • Styling: Implement more robust styling for the map container and controls.
  • Accessibility: Ensure the map and its controls are accessible to users with disabilities.
  • Internationalization: Use WordPress i18n functions for translatable strings.
  • TypeScript: Integrate TypeScript into your Svelte components for improved type safety. This would involve configuring svelte-preprocess in webpack.config.js.

By following these steps, you can build a powerful and interactive mapping module for Gutenberg using Svelte’s standalone templates, offering a modern and efficient development experience for complex WordPress block editor extensions.

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 and Resolving deep-seated hook priority conflicts in third-party Firebase Realtime DB connectors
  • Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using Alpine.js lightweight states
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Sage Roots modern environments
  • How to design secure Zapier dynamic webhooks webhook listeners using signature validation and payload queues
  • WordPress Development Recipe: Real-time custom event triggers using WebSockets and Metadata API (add_post_meta)

Categories

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

Recent Posts

  • Debugging and Resolving deep-seated hook priority conflicts in third-party Firebase Realtime DB connectors
  • Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using Alpine.js lightweight states
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Sage Roots modern environments

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

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