• 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 Elasticsearch search bar block for Gutenberg using React components

Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using React components

Setting Up Your Development Environment

Before we dive into building the Gutenberg block, ensure you have a local WordPress development environment set up. This typically involves:

  • A local server (e.g., Local by Flywheel, MAMP, XAMPP).
  • A WordPress installation.
  • Node.js and npm (or yarn) installed for JavaScript development.

We’ll be using the WordPress Script package for managing our build process, which simplifies asset compilation. Navigate to your WordPress theme or plugin directory in your terminal and run:

If you’re building this as a standalone plugin, create a new plugin directory (e.g., custom-elasticsearch-block) and run the following commands inside it:

mkdir custom-elasticsearch-block
cd custom-elasticsearch-block
npm init -y
npm install @wordpress/scripts --save-dev

Next, add a build script to your package.json file:

{
  "name": "custom-elasticsearch-block",
  "version": "1.0.0",
  "description": "Custom Elasticsearch search bar block for Gutenberg.",
  "main": "index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": ["wordpress", "gutenberg", "elasticsearch"],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/scripts": "^26.10.0"
  }
}

This setup allows you to run npm run build to compile your JavaScript and CSS, and npm run start for continuous compilation during development.

Registering the Gutenberg Block

Gutenberg blocks are registered using PHP. Create a main plugin file (e.g., custom-elasticsearch-block.php) in your plugin’s root directory. This file will enqueue our block’s assets and register the block type.

<?php
/**
 * Plugin Name: Custom Elasticsearch Search Block
 * Description: A Gutenberg block to integrate with Elasticsearch for search functionality.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: custom-elasticsearch-block
 */

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

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

The register_block_type function looks for a block.json file in the specified directory (__DIR__ . '/build' in this case) to define the block’s metadata and assets. Let’s create that next.

Defining Block Metadata (block.json)

Create a block.json file in the root of your plugin directory. This file is crucial for Gutenberg to understand your block, its attributes, and its dependencies.

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "custom-elasticsearch-block/search-bar",
  "version": "0.1.0",
  "title": "Elasticsearch Search Bar",
  "category": "widgets",
  "icon": "search",
  "description": "A search bar powered by Elasticsearch.",
  "keywords": ["elasticsearch", "search", "ajax"],
  "attributes": {
    "placeholderText": {
      "type": "string",
      "default": "Search..."
    },
    "searchEndpoint": {
      "type": "string",
      "default": "/wp-json/custom-elasticsearch/v1/search"
    }
  },
  "textdomain": "custom-elasticsearch-block",
  "editorScript": "file:./build/index.js",
  "editorStyle": "file:./build/index.css",
  "style": "file:./build/style-index.css"
}

Key fields here:

  • name: A unique identifier for your block (namespace/block-name).
  • title: The display name in the Gutenberg editor.
  • category: Where the block appears in the inserter.
  • icon: The icon for the block.
  • attributes: Define editable properties of your block (e.g., placeholder text, API endpoint).
  • editorScript: Points to the compiled JavaScript file for the editor.
  • editorStyle: Points to the compiled CSS for the editor.
  • style: Points to the compiled CSS for the front-end.

Building the Editor Component (React)

Now, let’s create the React component that will render your block in the Gutenberg editor. Create a src directory at the root of your plugin and inside it, create index.js and edit.js.

src/index.js will be the entry point for your block’s JavaScript.

import { registerBlockType } from '@wordpress/blocks';
import './style.scss'; // Front-end styles
import './editor.scss'; // Editor-specific styles
import Edit from './edit';
import metadata from '../block.json';

registerBlockType( metadata.name, {
    edit: Edit,
    save: () => null, // We'll handle saving via PHP or a dynamic block later if needed. For now, null is fine.
} );

src/edit.js will contain the React component for the block’s editor interface.

import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import './editor.scss';

export default function Edit( { attributes, setAttributes } ) {
    const blockProps = useBlockProps();
    const { placeholderText, searchEndpoint } = attributes;

    const onChangePlaceholder = ( newPlaceholder ) => {
        setAttributes( { placeholderText: newPlaceholder } );
    };

    const onChangeSearchEndpoint = ( newEndpoint ) => {
        setAttributes( { searchEndpoint: newEndpoint } );
    };

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( 'Search Settings', 'custom-elasticsearch-block' ) }>
                    <TextControl
                        label={ __( 'Placeholder Text', 'custom-elasticsearch-block' ) }
                        value={ placeholderText }
                        onChange={ onChangePlaceholder }
                    />
                    <TextControl
                        label={ __( 'Search API Endpoint', 'custom-elasticsearch-block' ) }
                        value={ searchEndpoint }
                        onChange={ onChangeSearchEndpoint }
                        help={ __( 'The URL to send search requests to.', 'custom-elasticsearch-block' ) }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps }>
                <input
                    type="text"
                    placeholder={ placeholderText }
                    className="custom-elasticsearch-search-input"
                    readOnly // In the editor, we don't want actual search functionality
                />
                <button className="custom-elasticsearch-search-button">{ __( 'Search', 'custom-elasticsearch-block' ) }</button>
            </div>
        </>
    );
}

In this edit.js:

  • We import necessary components from @wordpress/i18n, @wordpress/block-editor, and @wordpress/components.
  • useBlockProps provides the necessary props for the block’s wrapper element.
  • InspectorControls allows us to add settings to the block’s sidebar.
  • PanelBody and TextControl are used to create input fields for the placeholder text and the search API endpoint.
  • The main div renders a simple input and button, mimicking the front-end appearance. We use readOnly in the editor to prevent actual search requests.

Styling the Block

Create src/style.scss for front-end styles and src/editor.scss for editor-specific styles. These will be compiled by @wordpress/scripts.

/* src/style.scss */
.wp-block-custom-elasticsearch-block-search-bar {
    display: flex;
    align-items: center;
    border: 1px solid #ccc;
    padding: 8px;
    border-radius: 4px;

    .custom-elasticsearch-search-input {
        flex-grow: 1;
        border: none;
        outline: none;
        padding: 5px;
        font-size: 1rem;
    }

    .custom-elasticsearch-search-button {
        background-color: #0073aa;
        color: white;
        border: none;
        padding: 5px 10px;
        border-radius: 3px;
        cursor: pointer;
        margin-left: 8px;
        font-size: 0.9rem;
    }
}
/* src/editor.scss */
.wp-block-custom-elasticsearch-block-search-bar {
    // Editor-specific styles if needed
    background-color: #f0f0f0;
    padding: 15px;
    border: 1px dashed #aaa;
}

Make sure to import these SCSS files in src/index.js.

Compiling Assets

Now, run the build command in your terminal:

npm run build

This will create a build directory containing index.js, index.css, and style-index.css. Your PHP file is configured to load these.

Implementing Front-end Search Functionality

For the front-end, we need to enqueue a JavaScript file that handles the AJAX request to your Elasticsearch endpoint. We can do this dynamically or by enqueuing a separate script. For simplicity, let’s enqueue a script that targets our block.

Create a new file, e.g., assets/js/frontend.js:

document.addEventListener( 'DOMContentLoaded', function() {
    const searchForms = document.querySelectorAll( '.wp-block-custom-elasticsearch-block-search-bar' );

    searchForms.forEach( form => {
        const input = form.querySelector( '.custom-elasticsearch-search-input' );
        const button = form.querySelector( '.custom-elasticsearch-search-button' );

        if ( ! input || ! button ) {
            return;
        }

        // Retrieve the search endpoint from a data attribute set by the block's save function
        // For now, we'll assume it's available. A more robust solution would involve
        // passing this data via wp_localize_script or a dynamic block.
        const searchEndpoint = form.dataset.searchEndpoint || '/wp-json/custom-elasticsearch/v1/search'; // Fallback

        const performSearch = async ( query ) => {
            if ( ! query ) {
                // Clear results or show a message
                console.log( 'No query provided.' );
                return;
            }

            try {
                const response = await fetch( searchEndpoint, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': form.dataset.nonce || '' // Important for security if using WP REST API
                    },
                    body: JSON.stringify( { query: query } ),
                } );

                if ( ! response.ok ) {
                    throw new Error( `HTTP error! status: ${ response.status }` );
                }

                const results = await response.json();
                console.log( 'Search Results:', results );
                // TODO: Display results to the user
                displayResults( results );

            } catch ( error ) {
                console.error( 'Error performing search:', error );
                // TODO: Display error message to the user
            }
        };

        const displayResults = ( results ) => {
            // Clear previous results
            const existingResults = form.parentNode.querySelector( '.elasticsearch-results' );
            if ( existingResults ) {
                existingResults.remove();
            }

            if ( ! results || results.length === 0 ) {
                return; // No results to display
            }

            const resultsContainer = document.createElement( 'div' );
            resultsContainer.className = 'elasticsearch-results';
            resultsContainer.style.marginTop = '10px';
            resultsContainer.style.border = '1px solid #eee';
            resultsContainer.style.padding = '10px';

            const resultsList = document.createElement( 'ul' );
            resultsList.style.listStyle = 'none';
            resultsList.style.padding = '0';
            resultsList.style.margin = '0';

            results.forEach( item => {
                const listItem = document.createElement( 'li' );
                listItem.style.marginBottom = '5px';
                // Assuming results have a 'title' and 'url' field
                const link = document.createElement( 'a' );
                link.href = item.url || '#'; // Use item.url if available
                link.textContent = item.title || 'Untitled Result';
                listItem.appendChild( link );
                resultsList.appendChild( listItem );
            } );

            resultsContainer.appendChild( resultsList );
            form.parentNode.insertBefore( resultsContainer, form.nextSibling );
        };

        // Event listener for button click
        button.addEventListener( 'click', () => {
            performSearch( input.value );
        } );

        // Event listener for Enter key in input field
        input.addEventListener( 'keypress', function( event ) {
            if ( event.key === 'Enter' ) {
                event.preventDefault(); // Prevent form submission if it were a form
                performSearch( input.value );
            }
        } );
    } );
} );

Now, modify your PHP file (custom-elasticsearch-block.php) to enqueue this script. We’ll use wp_enqueue_script and ensure it only loads on the front-end.

<?php
/**
 * Plugin Name: Custom Elasticsearch Search Block
 * Description: A Gutenberg block to integrate with Elasticsearch for search functionality.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: custom-elasticsearch-block
 */

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

/**
 * Enqueue block assets for both the editor and the front-end.
 */
function custom_elasticsearch_block_enqueue_assets() {
    // Enqueue front-end script
    wp_enqueue_script(
        'custom-elasticsearch-block-frontend',
        plugin_dir_url( __FILE__ ) . 'assets/js/frontend.js',
        array( 'wp-element' ), // Dependency on React/WP Element
        filemtime( plugin_dir_path( __FILE__ ) . 'assets/js/frontend.js' ),
        true // Load in footer
    );

    // Localize script to pass data like nonce and endpoint if not using data attributes
    // wp_localize_script( 'custom-elasticsearch-block-frontend', 'customElasticsearchBlock', array(
    //     'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    //     'nonce'   => wp_create_nonce( 'wp_rest' ),
    // ) );
}
add_action( 'wp_enqueue_scripts', 'custom_elasticsearch_block_enqueue_assets' );

/**
 * Register the block.
 */
function custom_elasticsearch_block_register_block() {
    // Ensure the build directory exists and block.json is present
    if ( file_exists( __DIR__ . '/build/block.json' ) ) {
        register_block_type( __DIR__ . '/build' );
    } else {
        // Log an error or display a notice if build files are missing
        error_log( 'Custom Elasticsearch Block: build/block.json not found. Run "npm run build".' );
    }
}
add_action( 'init', 'custom_elasticsearch_block_register_block' );

/**
 * Add data attributes to the block's wrapper for front-end script.
 * This is an alternative to wp_localize_script for passing data.
 */
function custom_elasticsearch_block_render_attributes( $attributes, $content, $block ) {
    if ( isset( $block['blockName'] ) && 'custom-elasticsearch-block/search-bar' === $block['blockName'] ) {
        $wrapper_attributes = get_block_wrapper_attributes(); // Gets classes and styles
        $search_endpoint = isset( $attributes['searchEndpoint'] ) ? esc_url( $attributes['searchEndpoint'] ) : '/wp-json/custom-elasticsearch/v1/search';
        $nonce = wp_create_nonce( 'wp_rest' ); // Create nonce for REST API calls

        return $wrapper_attributes . sprintf(
            ' data-search-endpoint="%s" data-nonce="%s"',
            esc_attr( $search_endpoint ),
            esc_attr( $nonce )
        );
    }
    return $content;
}
// Note: The save function in block.json is set to () => null.
// This means the block is rendered dynamically or we need to hook into render_block.
// For simplicity, let's use the render_block filter to add data attributes.
add_filter( 'render_block', 'custom_elasticsearch_block_render_attributes', 10, 3 );

// --- REST API Endpoint for Search ---
// This is a placeholder. You'll need a robust Elasticsearch integration here.
function custom_elasticsearch_search_handler( WP_REST_Request $request ) {
    $query = $request->get_json_params()['query'] ?? '';

    if ( empty( $query ) ) {
        return new WP_Error( 'empty_query', 'Search query cannot be empty.', array( 'status' => 400 ) );
    }

    // TODO: Replace this with actual Elasticsearch query logic
    // Example: Using a hypothetical Elasticsearch client library
    /*
    try {
        $client = new ElasticsearchClient(); // Your Elasticsearch client instance
        $params = [
            'index' => 'your_index_name',
            'body'  => [
                'query' => [
                    'multi_match' => [
                        'query' => $query,
                        'fields' => [ 'title^3', 'content' ] // Example fields
                    ]
                ]
            ]
        ];
        $response = $client->search( $params );

        $results = array_map( function( $hit ) {
            return [
                'title' => $hit['_source']['title'] ?? 'N/A',
                'url'   => $hit['_source']['url'] ?? '#',
                'excerpt' => $hit['_source']['excerpt'] ?? ''
            ];
        }, $response['hits']['hits'] );

        return new WP_REST_Response( $results, 200 );

    } catch ( Exception $e ) {
        return new WP_Error( 'elasticsearch_error', 'Error communicating with Elasticsearch: ' . $e->getMessage(), array( 'status' => 500 ) );
    }
    */

    // Dummy response for demonstration
    $dummy_results = array();
    if ( strpos( strtolower( $query ), 'test' ) !== false ) {
        $dummy_results = [
            [ 'title' => 'Test Result 1', 'url' => '/test-1' ],
            [ 'title' => 'Another Test Result', 'url' => '/test-2' ],
        ];
    } else {
         $dummy_results = [
            [ 'title' => 'General Result A', 'url' => '/result-a' ],
            [ 'title' => 'General Result B', 'url' => '/result-b' ],
        ];
    }

    return new WP_REST_Response( $dummy_results, 200 );
}

add_action( 'rest_api_init', function () {
    register_rest_route( 'custom-elasticsearch/v1', '/search', array(
        'methods' => 'POST',
        'callback' => 'custom_elasticsearch_search_handler',
        'permission_callback' => '__return_true', // Adjust permissions as needed
    ) );
} );
?>

In the PHP file:

  • We enqueue assets/js/frontend.js using wp_enqueue_scripts.
  • The custom_elasticsearch_block_render_attributes function hooks into render_block. Since our block.json‘s save function returns null (meaning it’s a dynamic block or we’re handling saving differently), this filter allows us to add necessary data-attributes (like the search endpoint and nonce) directly to the block’s wrapper element on the front-end. This makes them accessible to our frontend.js.
  • A placeholder REST API endpoint custom-elasticsearch/v1/search is registered. This is where your frontend.js will send POST requests. You’ll need to implement the actual Elasticsearch logic within the custom_elasticsearch_search_handler function.

Integrating with Elasticsearch

The most critical part is the actual Elasticsearch integration. The provided custom_elasticsearch_search_handler function is a placeholder. You will need:

  • An Elasticsearch instance running and accessible.
  • An index populated with your data.
  • A PHP Elasticsearch client library (e.g., the official elasticsearch/elasticsearch package) installed via Composer.
  • Logic within the handler to construct and execute Elasticsearch queries based on the incoming $query parameter.
  • Proper error handling and security measures (e.g., sanitizing input, checking user capabilities if necessary).

For example, to install the Elasticsearch PHP client:

cd your-wordpress-plugin-directory
composer require elasticsearch/elasticsearch

Then, in your PHP file, you would instantiate the client and perform searches:

// ... inside custom_elasticsearch_search_handler function ...

use Elasticsearch\ClientBuilder;

// Ensure you have Composer's autoloader included if using Composer packages
// require_once plugin_dir_path( __FILE__ ) . 'vendor/autoload.php';

try {
    $client = ClientBuilder::create()
        // ->setHosts(array('localhost:9200')) // Configure your Elasticsearch hosts
        // ->setBasicAuthentication('user', 'password') // If authentication is enabled
        ->build();

    $params = [
        'index' => 'your_wordpress_content_index', // Your Elasticsearch index name
        'body'  => [
            'query' => [
                'multi_match' => [
                    'query' => $query,
                    'fields' => [ 'title^3', 'content', 'tags' ] // Example fields to search
                ]
            ],
            '_source' => ['title', 'url', 'excerpt'] // Specify fields to return
        ]
    ];

    $response = $client->search( $params );

    $results = array_map( function( $hit ) {
        return [
            'title' => $hit['_source']['title'] ?? 'N/A',
            'url'   => $hit['_source']['url'] ?? '#', // Ensure you have URLs indexed
            'excerpt' => $hit['_source']['excerpt'] ?? ''
        ];
    }, $response['hits']['hits'] );

    return new WP_REST_Response( $results, 200 );

} catch ( \Exception $e ) {
    // Log the error for debugging
    error_log( 'Elasticsearch Search Error: ' . $e->getMessage() );
    return new WP_Error( 'elasticsearch_error', 'An internal error occurred while searching.', array( 'status' => 500 ) );
}
// ... rest of the handler ...

Remember to configure the Elasticsearch client with your host(s), authentication details, and the correct index name.

Final Steps and Considerations

After completing these steps:

  • Activate your plugin in WordPress.
  • Go to the WordPress editor, search for “Elasticsearch Search Bar”, and add it to your post or page.
  • Configure the placeholder text and search endpoint in the block’s sidebar.
  • Test the search functionality on the front-end.

Further Enhancements:

  • Dynamic Rendering: Instead of relying on front-end JavaScript for the save function, you could make this a dynamic block by defining a render_callback in your block.json or PHP registration. This would render the HTML server-side.
  • Search Results Display: The current displayResults function is basic. You’ll want to create a more sophisticated UI for displaying search results, potentially with pagination or highlighting.
  • Debouncing/Throttling: For a smoother user experience, especially with live search, implement debouncing or throttling on the input field to avoid excessive API calls.
  • Security: Always sanitize user input and ensure your REST API endpoint has appropriate permissions. Use nonces correctly.
  • Configuration: Consider adding more configuration options to the block’s inspector controls, such as fields to search, minimum characters before search, etc.
  • Indexing Strategy: Plan how your WordPress content will be indexed into Elasticsearch. This might involve a separate indexing process or a plugin that syncs content.

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

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Carbon Fields custom wrappers
  • Building secure B2B pricing grids with custom REST API Controllers endpoints and role overrides

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 (48)
  • 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 (182)
  • WordPress Plugin Development (197)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins

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