• 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 Svelte standalone templates

Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using Svelte standalone templates

Setting Up the WordPress Plugin and Development Environment

We’ll begin by establishing the foundational structure for our custom Gutenberg block. This involves creating a new WordPress plugin and configuring the necessary build tools. For this project, we’ll leverage the official WordPress `@wordpress/scripts` package, which provides a robust build pipeline for JavaScript, CSS, and other assets, including support for modern JavaScript features and React. We’ll also use Svelte for our block’s frontend interface, opting for a standalone template approach to keep the Svelte compilation separate from the main WordPress build process.

First, create a new directory for your plugin within the wp-content/plugins/ directory of your WordPress installation. Let’s name it custom-elasticsearch-search.

Plugin Structure and `plugin.php`

Inside the custom-elasticsearch-search directory, create the main plugin file, custom-elasticsearch-search.php. This file will contain the plugin header and the necessary hooks to register our Gutenberg block.





Configuring the Build Process with `package.json`

Next, we need to set up our build tools. Navigate into your plugin directory in your terminal and initialize npm. Then, install the necessary development dependencies.

cd wp-content/plugins/custom-elasticsearch-search
npm init -y
npm install @wordpress/scripts @wordpress/blocks @wordpress/components @wordpress/i18n svelte svelte-loader --save-dev

Now, create a package.json file in the root of your plugin directory. This file will define our build scripts. We'll configure it to use `@wordpress/scripts` for the main WordPress block compilation and add a separate script for compiling our Svelte component.

{
  "name": "custom-elasticsearch-search",
  "version": "1.0.0",
  "description": "A Gutenberg block for searching Elasticsearch.",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
    "build:svelte": "svelte-kit build --dir build/svelte-components",
    "dev:svelte": "svelte-kit dev --port 5173 --hot --watch --dir build/svelte-components"
  },
  "keywords": ["wordpress", "gutenberg", "elasticsearch", "svelte"],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/blocks": "^12.0.0",
    "@wordpress/components": "^26.0.0",
    "@wordpress/i18n": "^7.0.0",
    "@wordpress/scripts": "^26.0.0",
    "svelte": "^4.0.0",
    "svelte-loader": "^3.0.0",
    "svelte-preprocess": "^5.0.0",
    "svelte-kit": "^1.0.0"
  },
  "dependencies": {
    "axios": "^1.0.0"
  },
  "browserslist": [
    "file:./.browserslistrc"
  ]
}

Create a .browserslistrc file in the root of your plugin directory to specify browser compatibility for autoprefixing.

defaults
not IE 11
not op_mini all

Defining the Block with `block.json`

Create a block.json file in the root of your plugin directory. This file describes your block to WordPress, including its name, category, attributes, and the scripts and styles to enqueue.

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

Building the Gutenberg Block Editor Interface (React)

The editorScript in block.json points to build/index.js. This is where the main JavaScript for the block editor will be compiled. We'll use React for the editor-side interface, leveraging WordPress components.

Create a src/index.js file. This will be the entry point for the block editor script.

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import Edit from './edit';
import save from './save';

registerBlockType( 'custom-elasticsearch-search/block', {
    edit: Edit,
    save: save,
} );

Create src/edit.js for the block's editor interface.

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

const 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-search' ) }>
                    <TextControl
                        label={ __( 'Placeholder Text', 'custom-elasticsearch-search' ) }
                        value={ placeholderText }
                        onChange={ onChangePlaceholder }
                    />
                    <TextControl
                        label={ __( 'Search API Endpoint', 'custom-elasticsearch-search' ) }
                        value={ searchEndpoint }
                        onChange={ onChangeSearchEndpoint }
                        help={ __( 'The REST API endpoint to query Elasticsearch.', 'custom-elasticsearch-search' ) }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps }>
                <input
                    type="text"
                    placeholder={ placeholderText }
                    readOnly
                    aria-label={ __( 'Search', 'custom-elasticsearch-search' ) }
                />
                <button disabled>{ __( 'Search', 'custom-elasticsearch-search' ) }</button>
                <p>{ __( 'This is a preview. The actual search will be powered by Svelte on the frontend.', 'custom-elasticsearch-search' ) }</p>
            </div>
        </>
    );
};

export default Edit;

Create src/save.js. This function determines how the block is saved to the database. Since our Svelte component will handle the dynamic rendering on the frontend, this save function will output static HTML that acts as a container for our Svelte app.

import { useBlockProps } from '@wordpress/block-editor';

const save = ( { attributes } ) => {
    const blockProps = useBlockProps.save();
    const { placeholderText, searchEndpoint } = attributes;

    return (
        <div { ...blockProps }
             data-placeholder-text={ placeholderText }
             data-search-endpoint={ searchEndpoint }
             id="custom-elasticsearch-search-svelte-app"
        ></div>
    );
};

export default save;

Building the Frontend Search Interface with Svelte

Now, let's integrate Svelte for the dynamic frontend search experience. We'll create a separate Svelte application that will be mounted onto the container element generated by our save.js function. This approach keeps the Svelte build process distinct and allows for more complex Svelte features.

Create a new directory svelte-src in your plugin's root. Inside, create main.js, which will be the entry point for your Svelte application.

// svelte-src/main.js
import App from './App.svelte';

// Find all instances of our block container
const searchBlocks = document.querySelectorAll('#custom-elasticsearch-search-svelte-app');

searchBlocks.forEach(blockElement => {
    // Extract data attributes from the container
    const placeholderText = blockElement.dataset.placeholderText || 'Search...';
    const searchEndpoint = blockElement.dataset.searchEndpoint || '/wp-json/custom-elasticsearch-search/v1/search';

    // Mount the Svelte app into the container
    new App({
        target: blockElement,
        props: {
            initialPlaceholderText: placeholderText,
            initialSearchEndpoint: searchEndpoint
        }
    });
});

Create the main Svelte component, svelte-src/App.svelte.

<script>
    import { onMount } from 'svelte';
    import axios from 'axios';

    export let initialPlaceholderText = 'Search...';
    export let initialSearchEndpoint = '/wp-json/custom-elasticsearch-search/v1/search';

    let searchTerm = '';
    let searchResults = [];
    let isLoading = false;
    let error = null;

    // Use a local variable for the endpoint to allow potential updates if needed
    let searchEndpoint = initialSearchEndpoint;

    async function performSearch() {
        if (!searchTerm.trim()) {
            searchResults = [];
            error = null;
            return;
        }

        isLoading = true;
        error = null;
        searchResults = [];

        try {
            const response = await axios.get(searchEndpoint, {
                params: {
                    q: searchTerm
                }
            });
            // Assuming Elasticsearch returns results in a 'hits.hits' array
            searchResults = response.data.hits.hits.map(hit => hit._source);
        } catch (e) {
            console.error("Search error:", e);
            error = 'An error occurred during the search.';
            searchResults = [];
        } finally {
            isLoading = false;
        }
    }

    // Handle Enter key press for search
    function handleKeyPress(event) {
        if (event.key === 'Enter') {
            performSearch();
        }
    }

    // Initialize placeholder text from props
    $: placeholder = initialPlaceholderText;

    // If you wanted to allow dynamic endpoint changes via attributes, you'd need
    // to re-evaluate searchEndpoint here or use a reactive statement.
    // $: searchEndpoint = initialSearchEndpoint; // This would update if initialSearchEndpoint changes

</script>

<div class="elasticsearch-search-wrapper">
    <div class="search-input-group">
        <input
            type="text"
            bind:value={searchTerm}
            on:keypress={handleKeyPress}
            placeholder={placeholder}
            aria-label={placeholder}
            disabled={isLoading}
        />
        <button on:click={performSearch} disabled={isLoading || !searchTerm.trim()} >
            { isLoading ? 'Searching...' : 'Search' }
        </button>
    </div>

    {#if isLoading}
        <p>Loading results...</p>
    {:else if error}
        <p class="error-message">{error}</p>
    {:else if searchResults.length > 0}
        <ul class="search-results">
            {#each searchResults as result (result.id || JSON.stringify(result))}
                <li>
                    <h3>{result.title || 'No Title'}</h3>
                    <p>{result.excerpt || result.content || 'No description available.'}</p>
                    {#if result.url}
                        <a href={result.url} target="_blank" rel="noopener noreferrer">Read More</a>
                    {/if}
                </li>
            {/each}
        </ul>
    {:else if searchTerm.trim()}
        <p>No results found for "{searchTerm}".</p>
    {/if}
</div>

<style>
    .elasticsearch-search-wrapper {
        font-family: sans-serif;
        width: 100%;
        max-width: 600px;
        margin: 20px auto;
        border: 1px solid #ccc;
        padding: 15px;
        border-radius: 5px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    .search-input-group {
        display: flex;
        gap: 10px;
        margin-bottom: 15px;
    }
    .search-input-group input[type="text"] {
        flex-grow: 1;
        padding: 10px;
        border: 1px solid #ccc;
        border-radius: 3px;
        font-size: 1rem;
    }
    .search-input-group button {
        padding: 10px 15px;
        background-color: #0073aa;
        color: white;
        border: none;
        border-radius: 3px;
        cursor: pointer;
        font-size: 1rem;
        transition: background-color 0.2s ease;
    }
    .search-input-group button:hover:not(:disabled) {
        background-color: #005177;
    }
    .search-input-group button:disabled {
        background-color: #ccc;
        cursor: not-allowed;
    }
    .search-results {
        list-style: none;
        padding: 0;
        margin: 0;
    }
    .search-results li {
        margin-bottom: 15px;
        padding-bottom: 15px;
        border-bottom: 1px solid #eee;
    }
    .search-results li:last-child {
        border-bottom: none;
        margin-bottom: 0;
        padding-bottom: 0;
    }
    .search-results h3 {
        margin: 0 0 5px 0;
        font-size: 1.2rem;
        color: #333;
    }
    .search-results p {
        margin: 0 0 10px 0;
        color: #555;
        line-height: 1.5;
    }
    .search-results a {
        color: #0073aa;
        text-decoration: none;
        font-weight: bold;
    }
    .search-results a:hover {
        text-decoration: underline;
    }
    .error-message {
        color: red;
        font-weight: bold;
    }
</style>

Configuring SvelteKit for Standalone Compilation

To use SvelteKit for building our standalone component, we need a minimal SvelteKit configuration. Create a svelte.config.js file in your plugin's root directory.

// svelte.config.js
import adapter from '@sveltejs/adapter-static'; // Or use adapter-auto if deploying to a server with Node.js

/** @type {import('@sveltejs/kit').Config} */
const config = {
    kit: {
        adapter: adapter({
            // Options for adapter-static
            pages: 'build/svelte-components', // Output directory for static assets
            assets: 'build/svelte-components', // Assets directory
            fallback: null // No fallback page needed for this use case
        }),
        paths: {
            base: '', // Relative paths for assets
        },
        prerender: {
            entries: [] // No prerendering needed for this dynamic component
        }
    },
    preprocess: {
        // Add any Svelte preprocessors here if needed (e.g., for TypeScript, SCSS)
        // Example: import sveltePreprocess from 'svelte-preprocess';
        // script: async () => sveltePreprocess(),
        // style: async () => sveltePreprocess(),
    }
};

export default config;

We also need to tell SvelteKit where to find our main Svelte entry point. Create a src/routes/+page.svelte file. This file won't be directly rendered by SvelteKit in this setup, but it's required for SvelteKit's build process.

<!-- src/routes/+page.svelte -->
<script>
    // This file is a placeholder for SvelteKit's build process.
    // The actual application is mounted in svelte-src/main.js.
</script>
<h1>Elasticsearch Search Block</h1>
<p>This is a placeholder page. The search component is loaded dynamically.</p>

And a minimal src/routes/+layout.svelte.

<!-- src/routes/+layout.svelte -->
<slot />

Building the Assets

With the structure and code in place, we can now build our assets. Run the following commands in your plugin's root directory:

npm run build
npm run build:svelte

This will generate the necessary JavaScript and CSS files in the build/ directory for the WordPress editor, and the Svelte components in build/svelte-components/. The `wp-scripts build` command compiles src/index.js into build/index.js and build/index.css. The `build:svelte` command, using our SvelteKit configuration, compiles the Svelte application into build/svelte-components/.

Enqueuing the Svelte View Script

Our block.json specifies a viewScript: file:./build/view.js. This script will be enqueued on the frontend when our block is present. We need to create this file and ensure it loads our compiled Svelte application.

Create a src/view.js file. This script will be responsible for finding our block's container and mounting the Svelte application.

import('./svelte-components/index.js'); // This path assumes adapter-static outputting to build/svelte-components

Now, we need to tell `@wordpress/scripts` to compile this src/view.js into build/view.js. We can achieve this by adding a custom script to our package.json and running it after the main build.

Modify your package.json scripts:

{
  // ... other scripts
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
    "build:svelte": "svelte-kit build --dir build/svelte-components",
    "dev:svelte": "svelte-kit dev --port 5173 --hot --watch --dir build/svelte-components",
    "build:view": "wp-scripts build --input=src/view.js --output-path=build",
    "build:all": "npm run build && npm run build:svelte && npm run build:view"
  }
  // ... other configurations
}

Now, run the combined build command:

npm run build:all

This ensures that src/view.js is compiled and placed correctly in the build/ directory, and that the Svelte application is also built and placed in build/svelte-components/. The `import('./svelte-components/index.js');` in src/view.js will correctly resolve to the compiled Svelte output.

Creating the WordPress REST API Endpoint

To make the search functional, we need a WordPress REST API endpoint that will proxy requests to your Elasticsearch instance. Create a new file, includes/rest-api.php, within your plugin directory.

<?php
/**
 * Custom Elasticsearch Search REST API Endpoint.
 */

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

// Define your Elasticsearch connection details.
// In a production environment, these should be stored securely (e.g., environment variables or wp-config.php).
define( 'ELASTICSEARCH_HOST', 'http://localhost:9200' ); // Replace with your Elasticsearch host
define( 'ELASTICSEARCH_INDEX', 'your_index_name' ); // Replace with your Elasticsearch index name
// define( 'ELASTICSEARCH_USERNAME', 'elastic' ); // Uncomment and set if authentication is required
// define( 'ELASTICSEARCH_PASSWORD', 'changeme' ); // Uncomment and set if authentication is required

/**
 * Registers the REST API route for Elasticsearch search.
 */
function custom_elasticsearch_register_search_route() {
    register_rest_route( 'custom-elasticsearch-search/v1', '/search', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'custom_elasticsearch_handle_search',
        'permission_callback' => '__return_true', // Adjust permissions as needed for security
        'args'                => array(
            'q' => array(
                'required'          => true,
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
                'description'       => 'The search query.',
            ),
        ),
    ) );
}
add_action( 'rest_api_init', 'custom_elasticsearch_register_search_route' );

/**
 * Handles the Elasticsearch search request.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
 */
function custom_elasticsearch_handle_search( WP_REST_Request $request ) {
    $query = $request->get_param( 'q' );

    if ( empty( $query ) ) {
        return new WP_Error( 'missing_param', 'Search query parameter "q" is required.', array( 'status' => 400 ) );
    }

    // Basic Elasticsearch query structure. Customize this based on your needs.
    // This example searches across a 'content' field.
    $search_body = array(
        'query' => array(
            'multi_match' => array(
                'query' => $query,
                'fields' => array( 'title^3', 'content', 'excerpt' ), // Boost title field
                'fuzziness' => 'AUTO', // Enable fuzzy matching
            ),
        ),
        'size' => 10, // Number of results to return
    );

    $args = array(
        'method'  => 'GET',
        'body'    => json_encode( $search_body ),
        'headers' => array(
            'Content-Type' => 'application/json',
        ),
    );

    // Add authentication if defined
    if ( defined( 'ELASTICSEARCH_USERNAME' ) && defined( 'ELASTICSEARCH_PASSWORD' ) ) {
        $args['headers']['Authorization'] = 'Basic ' . base64_encode( ELASTICSEARCH_USERNAME . ':' . ELASTICSEARCH_PASSWORD );
    }

    $es_url = ELASTICSEARCH_HOST . '/' . ELASTICSEARCH_INDEX . '/_search';

    $response = wp_remote_request( $es_url, $args );

    if ( is_wp_error( $response ) ) {
        return new WP_Error( 'elasticsearch_error', 'Error communicating with Elasticsearch.', array( 'status' => 500, 'details' => $response->get_error_message() ) );
    }

    $response_code = wp_remote_retrieve_response_code( $response );
    $response_body = wp_remote_retrieve_body( $response );
    $data = json_decode( $response_body, true );

    if ( $response_code >= 400 ) {
        return new WP_Error( 'elasticsearch_api_error', 'Elasticsearch API returned an error.', array( 'status' => $response_code, 'details' => $data ) );
    }

    // Return only the relevant parts of the Elasticsearch response
    // Adjust this based on what your Svelte component expects.
    // For example, you might want to map fields to a consistent structure.
    $formatted_results = array(
        'hits' => array(
            'hits' => $data['hits']['hits'] ?? array(),
        ),
        'took' => $data['took'] ?? 0,
        'timed_out' => $data['timed_out'] ?? false,
    );

    return rest_ensure_response( $formatted_results );
}

Make sure to include this file in your main plugin file:

// In custom-elasticsearch-search.php
require_once plugin_dir_path( __FILE__ ) . 'includes/rest-api.php';

Final Steps and Testing

Activate your "Custom Elasticsearch Search Block" plugin in the WordPress admin area. Then, add the "Elasticsearch Search Bar" block to a post or page. Configure the placeholder text and the search API endpoint in the block inspector. Save the post and view it on the frontend. As you type in the search bar and press Enter, the Svelte component should query your configured Elasticsearch endpoint via the WordPress REST API and display the results.

Important Considerations:

  • Security: The provided REST API endpoint has 'permission_callback' => '__return_true' for simplicity. In a production environment, you MUST implement proper authentication and authorization to prevent unauthorized access to your Elasticsearch instance. Consider using WordPress nonces or custom API keys.
  • Elasticsearch Indexing: Ensure your WordPress content is properly indexed in Elasticsearch. You might need a separate plugin or process to push content to Elasticsearch. The example REST API assumes fields like title, content, and excerpt are available in your Elasticsearch documents.
  • Error Handling: The Svelte component includes basic error handling, but you may want to enhance this for a better user experience.
  • Styling: The Svelte component includes inline styles. For better maintainability, consider extracting these into a separate CSS file and importing it into your Svelte component, or enqueueing a dedicated stylesheet via WordPress.
  • SvelteKit Adapter: For a purely static frontend deployment (e.g., serving HTML, CSS, JS directly from WordPress without a Node.js server), @sveltejs/adapter-static is appropriate. If your WordPress site is hosted on a server with Node.js capabilities, you could explore @sveltejs/adapter-auto or other server-side adapters.

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 securely integrate Mailchimp Newsletter endpoints into WordPress custom plugins using WordPress Database Class ($wpdb)
  • Implementing automated compliance reporting for custom member profile directories ledgers using mpdf engine
  • Step-by-Step Guide: Refactoring legacy hooks to use Factory Method design structures pattern in theme layers
  • Building secure B2B pricing grids with custom Rewrite API custom endpoints endpoints and role overrides
  • WordPress Development Recipe: Staggered database writes for high-volume custom form fields using WordPress Options API

Categories

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

Recent Posts

  • How to securely integrate Mailchimp Newsletter endpoints into WordPress custom plugins using WordPress Database Class ($wpdb)
  • Implementing automated compliance reporting for custom member profile directories ledgers using mpdf engine
  • Step-by-Step Guide: Refactoring legacy hooks to use Factory Method design structures pattern in theme layers

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (842)
  • Debugging & Troubleshooting (637)
  • Security & Compliance (617)
  • 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