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
createSignalfrom SolidJS for managing local component state (new marker inputs). InspectorControlsfrom@wordpress/block-editorprovides a sidebar area for our settings.PanelBody,TextControl,RangeControl, andButtonare UI components from@wordpress/componentsfor building the settings interface.setAttributesis a prop provided by Gutenberg to update the block’s attributes.- We use
Forfrom SolidJS to iterate over themarkersarray. - 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.