• 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 SolidJS high-performance reactive components

Step-by-Step Guide to building a custom interactive mapping module block for Gutenberg using SolidJS high-performance reactive components

Setting Up the WordPress Development Environment

Before diving into component development, ensure your local WordPress environment is configured for plugin development. This typically involves a local server stack (e.g., Local by Flywheel, Docker with a WordPress image, or a manual LAMP/LEMP setup) and a code editor with PHP and JavaScript support. For this guide, we’ll assume you have a functional WordPress installation accessible via localhost or a similar development domain.

We’ll be creating a custom Gutenberg block. The standard approach involves using the WordPress `@wordpress/scripts` package for asset compilation. This package handles Babel, Webpack, and other build tools necessary for modern JavaScript development within WordPress.

Plugin Structure and `plugin.php`

Create a new directory for your plugin within the wp-content/plugins/ directory of your WordPress installation. Let’s name it interactive-map-block. Inside this directory, create the main plugin file, interactive-map-block.php.

<?php
/**
 * Plugin Name:       Interactive Map Block
 * Plugin URI:        https://example.com/plugins/interactive-map-block/
 * Description:       A custom Gutenberg block for displaying interactive maps using SolidJS.
 * Version:           1.0.0
 * Author:            Your Name
 * Author URI:        https://yourwebsite.com/
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       interactive-map-block
 * Domain Path:       /languages
 */

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 interactive_map_block_init() {
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'interactive_map_block_init' );

Next, create a block.json file in the root of your plugin directory. This file describes your block to WordPress, including its name, attributes, and script/style dependencies.

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "interactive-map-block/map",
    "version": "1.0.0",
    "title": "Interactive Map",
    "category": "widgets",
    "icon": "location-alt",
    "description": "Display an interactive map with custom markers.",
    "keywords": ["map", "interactive", "location", "ecommerce"],
    "attributes": {
        "mapCenter": {
            "type": "object",
            "default": { "lat": 40.7128, "lng": -74.0060 }
        },
        "zoomLevel": {
            "type": "number",
            "default": 12
        },
        "markers": {
            "type": "array",
            "default": []
        }
    },
    "supports": {
        "html": false
    },
    "textdomain": "interactive-map-block",
    "editorScript": "file:./build/index.js",
    "editorStyle": "file:./build/index.css",
    "style": "file:./build/style-index.css",
    "viewScript": "file:./build/view.js"
}

Project Setup with `@wordpress/scripts` and SolidJS

To manage our JavaScript build process and integrate SolidJS, we’ll use WordPress’s official `@wordpress/scripts` package. This package provides a pre-configured Webpack setup that’s compatible with Gutenberg development. We’ll also need to configure SolidJS’s JSX support.

Initialize your project with npm or yarn. Navigate to your plugin directory in the terminal and run:

npm init -y

Install the necessary development dependencies:

npm install --save-dev @wordpress/scripts solid-js @babel/preset-react @babel/preset-env @babel/plugin-transform-react-jsx @babel/core webpack webpack-cli babel-loader @babel/plugin-proposal-class-properties @babel/plugin-syntax-jsx @babel/plugin-transform-react-jsx

Create a package.json file (if you didn’t run npm init -y) and add the following scripts:

{
  "name": "interactive-map-block",
  "version": "1.0.0",
  "description": "Interactive Map Block for Gutenberg",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
    "packages-update": "wp-scripts packages-update"
  },
  "keywords": ["wordpress", "gutenberg", "block", "solidjs"],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@babel/core": "^7.23.7",
    "@babel/plugin-proposal-class-properties": "^7.18.6",
    "@babel/plugin-syntax-jsx": "^7.14.5",
    "@babel/plugin-transform-react-jsx": "^7.23.6",
    "@wordpress/scripts": "^27.0.0",
    "babel-loader": "^9.1.3",
    "solid-js": "^1.8.12",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4"
  }
}

We need to configure Babel to handle JSX. Create a .babelrc file in the root of your plugin directory:

{
  "presets": [
    "@babel/preset-env",
    ["@babel/preset-react", { "runtime": "automatic", "importSource": "solid-js" }]
  ],
  "plugins": [
    "@babel/plugin-syntax-jsx",
    "@babel/plugin-transform-react-jsx",
    "@babel/plugin-proposal-class-properties"
  ]
}

The key here is the React preset configuration: "runtime": "automatic" and "importSource": "solid-js". This tells Babel to use SolidJS’s JSX transform, which is compatible with React’s JSX syntax.

Building the Editor Component with SolidJS

Create a src directory in your plugin’s root. Inside src, create index.js, which will be the entry point for your block’s editor script. We’ll also create MapEditor.jsx for our SolidJS component.

// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import MapEditor from './MapEditor';
import metadata from '../block.json';

registerBlockType( metadata.name, {
    edit: MapEditor,
    save: () => null, // We'll handle saving the map data via attributes
} );

Now, let’s create the MapEditor.jsx component. This component will render in the Gutenberg editor and allow users to configure the map’s center, zoom level, and add/edit markers. For simplicity, we’ll use a basic input form. For actual map rendering in the editor, you’d typically integrate a lightweight mapping library or use a placeholder.

// src/MapEditor.jsx
import { createSignal, For } from 'solid-js';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, RangeControl, Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

function MapEditor( { attributes, setAttributes } ) {
    const { mapCenter, zoomLevel, markers } = attributes;

    const [newMarkerLat, setNewMarkerLat] = createSignal('');
    const [newMarkerLng, setNewMarkerLng] = createSignal('');
    const [newMarkerTitle, setNewMarkerTitle] = createSignal('');

    const updateMapCenter = (lat, lng) => {
        setAttributes( { mapCenter: { lat: parseFloat(lat), lng: parseFloat(lng) } } );
    };

    const updateZoomLevel = (level) => {
        setAttributes( { zoomLevel: level } );
    };

    const addMarker = () => {
        const lat = parseFloat(newMarkerLat());
        const lng = parseFloat(newMarkerLng());
        const title = newMarkerTitle();

        if ( !isNaN(lat) && !isNaN(lng) && title ) {
            setAttributes( {
                markers: [
                    ...(markers || []),
                    { lat, lng, title }
                ]
            } );
            setNewMarkerLat('');
            setNewMarkerLng('');
            setNewMarkerTitle('');
        } else {
            alert('Please enter valid latitude, longitude, and a title for the marker.');
        }
    };

    const removeMarker = (index) => {
        setAttributes( {
            markers: markers.filter((_, i) => i !== index)
        } );
    };

    return (
        <>
            <InspectorControls>
                <PanelBody title={__('Map Settings', 'interactive-map-block')} initialOpen={true}>
                    <TextControl
                        label={__('Latitude', 'interactive-map-block')}
                        value={mapCenter.lat}
                        onChange={(value) => updateMapCenter(value, mapCenter.lng)}
                        type="number"
                        step="any"
                    />
                    <TextControl
                        label={__('Longitude', 'interactive-map-block')}
                        value={mapCenter.lng}
                        onChange={(value) => updateMapCenter(mapCenter.lat, value)}
                        type="number"
                        step="any"
                    />
                    <RangeControl
                        label={__('Zoom Level', 'interactive-map-block')}
                        value={zoomLevel}
                        onChange={updateZoomLevel}
                        min={1}
                        max={20}
                    />
                </PanelBody>
                <PanelBody title={__('Markers', 'interactive-map-block')} initialOpen={false}>
                    <h3>{__('Add New Marker', 'interactive-map-block')}</h3>
                    <TextControl
                        label={__('Latitude', 'interactive-map-block')}
                        value={newMarkerLat()}
                        onChange={setNewMarkerLat}
                        type="number"
                        step="any"
                    />
                    <TextControl
                        label={__('Longitude', 'interactive-map-block')}
                        value={newMarkerLng()}
                        onChange={setNewMarkerLng}
                        type="number"
                        step="any"
                    />
                    <TextControl
                        label={__('Title', 'interactive-map-block')}
                        value={newMarkerTitle()}
                        onChange={setNewMarkerTitle}
                    />
                    <Button isPrimary onClick={addMarker}>
                        {__('Add Marker', 'interactive-map-block')}
                    </Button>

                    <h3>{__('Existing Markers', 'interactive-map-block')}</h3>
                    <ul>
                        <For each={markers || []}>
                            {(marker, index) => (
                                <li>
                                    {marker.title} ({marker.lat}, {marker.lng})
                                    <Button isDestructive onClick={() => removeMarker(index())}>
                                        {__('Remove', 'interactive-map-block')}
                                    </Button>
                                </li>
                            )}
                        </For>
                    </ul>
                </PanelBody>
            </InspectorControls>

            <div style={{ padding: '20px', border: '1px dashed #ccc', textAlign: 'center' }}>
                <h3>{__('Interactive Map Preview (Editor)', 'interactive-map-block')}</h3>
                <p>{__('Configure map settings and markers in the sidebar.', 'interactive-map-block')}</p>
                <p>Center: {mapCenter.lat}, {mapCenter.lng}</p>
                <p>Zoom: {zoomLevel}</p>
                <p>Markers: {markers && markers.length}</p>
            </div>
        </>
    );
}

export default MapEditor;

In this component:

  • We use createSignal from SolidJS for managing local component state (new marker inputs).
  • InspectorControls from @wordpress/block-editor provides a sidebar area for our settings.
  • PanelBody, TextControl, RangeControl, and Button are UI components from @wordpress/components for building the settings interface.
  • setAttributes is a prop provided by Gutenberg to update the block’s attributes.
  • We use For from SolidJS to iterate over the markers array.
  • The main editor area displays a placeholder message and the current attribute values.

Building the Frontend View Script

The viewScript defined in block.json (build/view.js) will be responsible for rendering the actual interactive map on the frontend. This script will be enqueued only when the block is present on the page.

Create src/view.js:

// src/view.js
import { createRoot } from 'solid-js/web';
import MapFrontend from './MapFrontend';

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

    mapBlockElements.forEach(blockElement => {
        const dataset = blockElement.dataset;
        const mapCenter = JSON.parse(dataset.mapCenter || '{}');
        const zoomLevel = parseInt(dataset.zoomLevel || '12', 10);
        const markers = JSON.parse(dataset.markers || '[]');

        // Create a mount point for the SolidJS app within the block element
        const mountPoint = document.createElement('div');
        blockElement.appendChild(mountPoint);

        const root = createRoot(() => (
            <MapFrontend
                initialCenter={mapCenter}
                initialZoom={zoomLevel}
                initialMarkers={markers}
            />
        ));
        root.render(mountPoint);
    });
});

Now, create the MapFrontend.jsx component. This is where you’d integrate your chosen mapping library (e.g., Leaflet, Mapbox GL JS, Google Maps API). For this example, we’ll use a placeholder and display the map configuration.

// src/MapFrontend.jsx
import { createSignal, For, onMount } from 'solid-js';
// Import your mapping library here, e.g.:
// import L from 'leaflet';
// import 'leaflet/dist/leaflet.css';

function MapFrontend( props ) {
    const [mapInstance, setMapInstance] = createSignal(null);

    onMount(() => {
        // Initialize your map here using props.initialCenter, props.initialZoom, props.initialMarkers
        // Example with Leaflet (requires Leaflet to be installed and imported):
        /*
        const map = L.map('map-container').setView([props.initialCenter.lat, props.initialCenter.lng], props.initialZoom);
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }).addTo(map);

        (props.initialMarkers || []).forEach(marker => {
            L.marker([marker.lat, marker.lng]).addTo(map)
                .bindPopup(marker.title)
                .openPopup();
        });
        setMapInstance(map);
        */

        // Placeholder for actual map initialization
        console.log("Map Frontend Mounted", props);
        // Simulate map instance for demonstration
        setMapInstance({ id: 'map-container-placeholder' });
    });

    return (
        <div id="map-container" style={{ height: '400px', width: '100%', border: '1px solid #ccc', background: '#e0e0e0' }}>
            <div style={{ padding: '20px', textAlign: 'center', paddingTop: '150px' }}>
                <h3>Interactive Map Display</h3>
                <p>Center: {props.initialCenter.lat}, {props.initialCenter.lng}</p>
                <p>Zoom: {props.initialZoom}</p>
                <p>Markers: {props.initialMarkers && props.initialMarkers.length}</p>
                <p>Map would be rendered here.</p>
            </div>
        </div>
    );
}

export default MapFrontend;

In view.js, we select all instances of our block on the page. For each instance, we parse the attributes stored in the element’s dataset and then create a root for our SolidJS MapFrontend component, rendering it inside the block’s main element.

The MapFrontend.jsx component uses onMount to perform side effects, such as initializing the mapping library. You’ll need to install and configure your preferred mapping library. For example, if using Leaflet, you’d add npm install leaflet to your dependencies and uncomment the Leaflet-related code.

Handling Block Saving

In Gutenberg, the save function of a block determines its HTML output on the frontend. For dynamic blocks or blocks that rely on JavaScript for rendering, it’s common to return null from the save function and handle frontend rendering entirely via the viewScript. This approach allows us to pass block attributes as data attributes to the frontend element.

In our src/index.js, we have:

registerBlockType( metadata.name, {
    edit: MapEditor,
    save: () => null, // This tells Gutenberg not to save static HTML.
} );

When Gutenberg saves the post, it will render an empty wrapper element for our block. The viewScript then finds these elements, reads their attributes (which Gutenberg automatically serializes into data-* attributes), and hydrates the SolidJS application.

To ensure attributes are correctly serialized as data attributes, we need to modify the block.json to include a render property that specifies how the block should be rendered on the frontend. For blocks that use a viewScript and no static HTML, this is crucial.

{
    // ... other properties
    "editorScript": "file:./build/index.js",
    "editorStyle": "file:./build/index.css",
    "style": "file:./build/style-index.css",
    "viewScript": "file:./build/view.js",
    "render": "file:./render.php"
}

Create a render.php file in your plugin’s root directory:

<?php
/**
 * Server-side rendering for the Interactive Map Block.
 *
 * @package InteractiveMapBlock
 */

$map_center_json = wp_json_encode( $attributes['mapCenter'] ?? [] );
$zoom_level_json = wp_json_encode( $attributes['zoomLevel'] ?? 12 );
$markers_json    = wp_json_encode( $attributes['markers'] ?? [] );

// The class name should match the block's namespace and name.
// e.g., 'wp-block-your-namespace-your-block-name'
$wrapper_attributes = get_block_wrapper_attributes();

?>
<div
    <?= $wrapper_attributes ?>
    data-map-center="<?= esc_attr( $map_center_json ) ?>"
    data-zoom-level="<?= esc_attr( $zoom_level_json ) ?>"
    data-markers="<?= esc_attr( $markers_json ) ?>"
>
    <!-- The SolidJS view script will hydrate this element -->
    <!-- You can optionally add a placeholder or loading indicator here -->
    <div class="map-placeholder"><?php esc_html_e( 'Loading map...', 'interactive-map-block' ); ?></div>
</div>

This render.php file is executed server-side. It takes the block’s attributes, serializes them into JSON, and outputs a wrapper div with these JSON strings as data-* attributes. get_block_wrapper_attributes() ensures proper block class names and attributes are applied.

Building and Activation

Navigate to your plugin’s root directory in the terminal and run the build command:

npm run build

This command will compile your SolidJS JSX code, transpile it using Babel, and bundle it into build/index.js and build/view.js, along with generating CSS files. After the build process completes, activate the “Interactive Map Block” plugin from your WordPress admin area.

Testing the Block

Create a new post or page in WordPress. You should now see the “Interactive Map” block available in the Gutenberg editor. Add the block, and use the sidebar controls to set the map center, zoom level, and add a few markers. Save the post and view it on the frontend. The map should render with the configured settings, and the markers should be visible (assuming you’ve integrated a mapping library).

For debugging the frontend rendering, you can inspect the div element generated by render.php. It will contain the data-map-center, data-zoom-level, and data-markers attributes. Your view.js script reads these attributes to initialize the SolidJS component and the mapping library.

Performance Considerations and E-commerce Integration

SolidJS’s fine-grained reactivity and compilation to efficient JavaScript make it an excellent choice for performance-critical components like interactive maps. By using a viewScript, the JavaScript for the map is only loaded on pages where the block is actually used, reducing initial page load times.

For e-commerce applications, this block can be extended to display store locations, product distribution maps, or event venues. You could fetch marker data dynamically from custom post types (e.g., “Store Locations”) or product attributes, making the map highly dynamic and integrated with your store’s data. Ensure that any external mapping API keys are handled securely, perhaps via WordPress options or environment variables.

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

  • How to build custom WooCommerce core overrides extensions utilizing modern Rewrite API custom endpoints schemas
  • Reducing database query bloat in ACF Pro dynamic fields layouts using custom lazy loaders
  • Debugging and Resolving complex WP_DEBUG notice floods issues during heavy concurrent database traffic
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency GitHub API repositories handlers
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Mailchimp Newsletter connectors

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 (42)
  • 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 (96)
  • WordPress Plugin Development (99)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • How to build custom WooCommerce core overrides extensions utilizing modern Rewrite API custom endpoints schemas
  • Reducing database query bloat in ACF Pro dynamic fields layouts using custom lazy loaders
  • Debugging and Resolving complex WP_DEBUG notice floods issues during heavy concurrent database traffic

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