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. useBlockPropsprovides the necessary props for the block’s wrapper element.InspectorControlsallows us to add settings to the block’s sidebar.PanelBodyandTextControlare used to create input fields for the placeholder text and the search API endpoint.- The main
divrenders a simple input and button, mimicking the front-end appearance. We usereadOnlyin 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.jsusingwp_enqueue_scripts. - The
custom_elasticsearch_block_render_attributesfunction hooks intorender_block. Since ourblock.json‘ssavefunction returnsnull(meaning it’s a dynamic block or we’re handling saving differently), this filter allows us to add necessarydata-attributes(like the search endpoint and nonce) directly to the block’s wrapper element on the front-end. This makes them accessible to ourfrontend.js. - A placeholder REST API endpoint
custom-elasticsearch/v1/searchis registered. This is where yourfrontend.jswill send POST requests. You’ll need to implement the actual Elasticsearch logic within thecustom_elasticsearch_search_handlerfunction.
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/elasticsearchpackage) installed via Composer. - Logic within the handler to construct and execute Elasticsearch queries based on the incoming
$queryparameter. - 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_callbackin yourblock.jsonor PHP registration. This would render the HTML server-side. - Search Results Display: The current
displayResultsfunction 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.