Adding Search Features to Drupal with SearchStax Site Search


SearchStax Site Search is a complete search-as-a-service platform that adds powerful site search features to your website. Site Search works with a variety of content management systems, DXPs, and front-end frameworks so you can quickly index your website content and easily display search features and results across your site’s front end.

Site Search can be easily integrated with Drupal using the Search API Solr and SearchStax modules to automatically index and update your Drupal content when it gets added or changed.

There are several options for showing search results and integrating other search features in your Drupal front end.

Building custom Drupal modules gives you the flexibility of dynamic search features and result pages with the reusability and low-code block layout system. Let’s see how to build JavaScript-powered apps with the Search UI Kit and package them into custom Drupal modules.

Contents

This page presents the following topics:

Search UI Kit

The SearchStax Site Search solution’s Search UI Kit is a collection of JavaScript search interface components built for popular front end frameworks including React, Vue, and Angular, and is also available as a vanilla JavaScript package. The Search UI Kit makes it much easier to start building your search result pages and other search features without managing API connections and interfaces.

Our Search UI Kits contain prebuilt search interface components including:

  • Search input box with autocomplete suggestions
  • Search result snippets including promoted search result cards
  • Facet and filters
  • Pagination
  • Related search keywords and popular search terms

Using the Search UI Kit with Drupal

Packaging a JavaScript app into a Drupal module requires some modification to the app build process to generate the correct assets for your Drupal module. The Drupal modules require config files that point to the various JavaScript, CSS, and HTML files needed to load and display the JavaScript app in Drupal.

Search Block or Search Page module?

Depending on how you’re integrating site search features with Drupal you may need to build your search features as individual blocks that get inserted into your Drupal theme and pages or build a stand-alone Page module with a custom URL.

App Config and Build Process

Regardless of which front-end framework you use, you’ll likely need to configure how your app is built and where the browser will actually render the app on a page when it gets loaded.

  1. Target DOM Element – Your JavaScript app will need to load into a unique element with a unique ID. This means you won’t be able to embed your app and module multiple times on the same page.
  2. Static Filenames – Most front-end frameworks include random hashes in the JavaScript and CSS files as a cache-busting method for new builds, but your Drupal module will need a fixed filename for any external JavaScript or CSS that needs to be loaded.
  3. Relative File Paths – Your app’s JavaScript and CSS files will need to be identified in your module config files so they can load correctly. When a visitor’s browser actually loads the files from the module they’ll get loaded from a relative path that includes the module name itself. (This may change depending on how your Drupal site is configured and how modules get added and loaded in your specific instance).
    https://www.yoursite.com/modules/searchstax_site_search_search_page/dist/app.js

React App Build Config

React apps are often built using `npm run build` but the default build process typically creates JavaScript and CSS files with random file names for cache-busting purposes. Vite offers the ability to customize your React build process and create static filenames and consolidate all JavaScript and CSS into single files.

React loads dynamically on the client side. The React JavaScript will look for the appropriate DOM item with the correct ID and load the React app into that element. If the DOM element is not rendered or more than one element has the same ID your React app may not load.

Target DOM Element

In your React app entry file (/src/main.tsx) you’ll need to specify the ID of the DOM element where you want your React app to load.

ReactDOM.createRoot(document.getElementById('searchstax-site-search-search-block')!).render(
  <React.StrictMode>
  	<App />
  </React.StrictMode>,
)

Vite Build Config

Vite provides more customization and flexibility compared to Create React App’s build process. With Vite we can customize the output file names during the build process and include the base URL path that any relative URLs will load from.

vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => {
  if (command === 'build') {
	return {
  	plugins: [react()],
  	server: {
    	host: true,
    	port: 5173,
    	watch: {
      	usePolling: true
    	}
  	},
  	build: {
    	rollupOptions: {
      	output: {
        	entryFileNames: 'app.js',
        	assetFileNames: 'app.css',
        	chunkFileNames: 'chunk.js',
        	manualChunks: undefined,
      	}
    	}
  	},
  	base: '/modules/searchstax_site_search_search_block/dist/',
	}
  }
  else {
	return {
  	plugins: [react()],
  	server: {
    	host: true,
    	port: 5173,
    	watch: {
      	usePolling: true
    	}
  	},
	}
  }
})

Updating package.json

You’ll likely need to update how your dev server and build commands run when switching to Vite. This config includes basic build using the `vite` command to run your React app and the `tsc && vite build` commands to run Typescript checking (if in use) and the `vite build` command using the settings from above.

package.json

{
  "name": "searchstax-site-search-search-block",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
	"dev": "vite",
	"build": "tsc && vite build",
	"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
	"preview": "vite preview"
  },
  "dependencies": {
...

Building the React App

Once your app is set up and configured you can run `npm run build` to build the production version of your React app. If there were no compile or build errors you should see your built React app in the /dist/ folder.

You can now copy these production files into your Drupal module.

Angular App Build Config

Angular apps need only basic modification to load in a specific DOM element and build with fixed filenames.

Target DOM Element

Update your app file (/src/app/app.component.ts) to include a named selector where your Angular app will load.

app.component.ts – update

@Component({
  selector: 'app-root-angular',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'searchstax-accelerator-page';
...
}

Build Config

Update your Angular config file to set ‘outputHashing’ to ‘none’. When building your Angular app your JavaScript and CSS files will no longer include cache-busting hashes in the filenames.

angular.json

{
  ...
  "projects": {
      ...
	"searchstax-accelerator-page": {
             ...
  		"architect": {
                    ...
    			"build": {
                           ...
      				"configurations": {
                                        ...
        					"production": {
                                              ...
          						"outputHashing": "none"
...
}

Building the Angular App

Once your app is set up and configured you can run `ng build` to build the production version of your Angular app. If there were no compile or build errors you should see your built Angular app in the /dist/ folder.

You can now copy these production files into your Drupal module.

Vue App Build Config

Vue apps are pretty easy to customize for building production apps for use in your Drupal modules.

Target DOM Element

In your Vue app entry file (/src/main.js) you can specify the ID of the DOM element where the Vue app will load.

main.js

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router)

app.mount('#searchstax-site-search-search-block')

Vue Build Config

Vue includes Vite as the build engine so it’s easy to set up and customize without major changes. CSS filenames are easily customized along with the relative file path that everything will load from.

JavaScript file names require a callback function to create a static filename that doesn’t include any cache-busting hashes or other unpredictable additions.

vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig(
  {
	transpileDependencies: true,
	chainWebpack : (config) => {
  	config.output.filename('[name].js');
	},
	css: {
  	extract: {
    	filename: '[name].css',
    	chunkFilename: '[name].css'
  	}
	},
	publicPath: '/modules/searchstax_site_search_block/dist/'
  }
)

Building the Vue App

Once your app is set up and configured you can run `npm run build` to build the production version of your Vue app. If there were no compile or build errors you should see your built Vue app in the /dist/ folder.

You can now copy these production files into your Drupal module.

Packaging Your Drupal Module

Drupal modules need specific configuration files so that Drupal can properly load the module, add any blocks or pages to the admin site builder, and appropriately display your block or page when it gets loaded by a visitor to your Drupal site.

Drupal Module Configuration and Files

Drupal modules require specific configuration files and templates so that Drupal can correctly load and display the module and any associated code. Drupal modules need to use consistent naming so that the various configuration files, twig templates, and custom JavaScript and CSS files are properly parsed and loaded by Drupal. The JavaScript apps will also require custom build configurations and targets to ensure the apps can load into the appropriate DOM element in the Drupal module.

The SearchStax Site Search modules will have the following config files.

  • drupal_module_name
    • dist
      • app.css
      • app.js
      • index.html
    • drupal_module_name.Info.yml – Basic module information that’s show in the Drupal admin interface
    • drupal_module_name.libraries.yml – Maps the JavaScript and CSS files that should be loaded with the module
    • drupal_module_name.module – Module configuration file that maps the module controller and template
    • drupal_module_name.routing.yml – The URL config and associated controller config for the module
    • src
      • Controller
        • drupal_module_nameController.php – The controller that loads the HTML twig file
      • Plugin
        • Block
          • drupal_module_nameBlock.php – The block base that loads the HTML twig file
    • templates
      • app.html.twig – The twig file that loads the JavaScript and CSS and contains the target div for the React app
      • drupal_module_name.html.twig – The twig file that attaches the JavaScript and CSS files and points to the App twig file

The /dist/ folder will hold the build code for our JavaScript app. The drupal_module_name.libraries.yml file points to the JavaScript and CSS files in the dist folder so that Drupal can load and access those files. If the file paths in the drupal_module_name.libraries.yml file are incorrect or the names of the files in the /dist/ folder change, the JavaScript app won’t load.

The app.html.twig file in the templates folder gets rendered on your Drupal front end wherever the Block is inserted into a layout.

Dist Folder for Production App

Once you’ve developed, tested, and created your production JavaScript app you can copy the product files from the /dist/ folder in your app project into the /dist/ folder in your Drupal module.

Typically your built app will include the following files

  • app.js
  • app.css
  • index.html

You’ll need to customize the app.html.twig file in your module to ensure it includes the JavaScript and CSS files you need to load as well as the target DOM element where your app will load. This twig file is what will actually get inserted into your Drupal page’s source where your custom module or page is loaded.

The relative paths for the JavaScript and CSS files should include the Drupal module directory (typically /modules/ unless your modules are loaded into a specific theme or custom directory) and the name of your module as well as the /dist/ directory and production files.

<!doctype html>
<html lang="en">
  <head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<script type="module" crossorigin src="/modules/searchstax_site_search_search_bar/dist/app.js"></script>
	<link rel="stylesheet" crossorigin href="/modules/searchstax_site_search_search_bar/dist/app.css">
  </head>
  <body>
	<div id="searchstax-site-search-search-page"></div>
  </body>
</html>

If you’re using Angular you may need to substitute your Angular-specific root element (e.g. <SearchStaxSiteSearchBar></SearchStaxSiteSearchBar>).

Most of the frameworks target a specific DOM element ID so you won’t need to change the app.twig.html file in your module unless you’re using Angular.

Testing in Drupal

Once your module has been built and customized, you can load the module into your Drupal environment and install. This process will likely vary depending on how your Drupal environment and site are configured.

After you have installed your module, you’ll need to go into your Drupal admin to enable the module in the ‘Extend’ section. Once enabled you can navigate directly to the custom module page (if you’ve created one) or add the custom block into your block layout. (Keep in mind you’ll only be able to load each module once per page. Reusing a module on the same layout or page may cause it to render incorrectly or not at all since multiple DOM elements will get created with the same ID).

Production

If your module is working correctly and loading as expected, it can be launched on your production environment. You may need to review any changes or specific configurations for your production environment to ensure file paths are consistent and all resources from your app are able to load correctly.

While not easily recovered from source code, your SearchStax Site Search API keys will be visible when users make search requests. It’s critical to ensure that read-only keys are used in production environments to keep the system secure. Using production keys in your dev and staging environments can lead to low-quality keywords and searches in your analytics data (if analytics are configured) and can exhaust your production API requests.

Additional Integration

Your Drupal instance can send additional contextual data (such node fields, selected language, or user information) to your front end through your custom module. There are several options for passing data to a JavaScript app from the Drupal CMS.

JSON:API

The Drupal JSON:API modules provide a robust REST API that returns JSON objects for many of Drupal’s functions, content types, administrative functions, and other useful endpoints. Your JavaScript front end can call the JSON:API endpoints to obtain additional fields for a specific node, fetch URL aliases and menu items, or access user details to enable access to logged-in content.

Meta Tags & Other On-Page

Your JavaScript-powered front end can typically read other HTML tags from the page that it’s embedded in. This means your JavaScript app can read page title, URLs, meta tags, and other HTML tags that might have relevant contextual information. You can configure Drupal to display specific node fields or other relevant values (such as select language) in a meta tag.

App Root Tag

Another option is to configure your custom module to include your Drupal-specific data and objects in the actual root tag where your app loads. Your JavaScript app will be able to immediately read any contextual data you provide and set up the appropriate front end state without making an API request or modifying other portions of your Drupal instance to provide meta tags.

You’ll need to configure the ‘Block’ object in your module to capture the current node context, load the node’s fields, and pass that data to the Twig template so it will be available to your JavaScript front end.

<?php

namespace Drupal\searchstax_site_search_search_bar\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 *
 * @Block(
 *   id = "searchstax_site_search_search_bar",
 *   admin_label = @Translation("SearchStax Site Search Search Bar"),
 *   category = @Translation("Site Search"),
 *   context_definitions = {
 * 	"node" = @ContextDefinition(
 *    	"entity:node",
 *    	label = @Translation("Node"),
 *    	required = FALSE,
 *  	)
 *   }
 * )
 */
class searchstax_site_search_search_barBlock extends BlockBase {
  /**
   * {@inheritdoc}
   */
  public function build() {
	$nid = -1;
	$fields = array();
	$title = '';
	$node = $this->getContextValue('node');
	if($node) {
  		$nid = $node->id();
  		$title = $node->title->value;
  		foreach ($node->getFields() as $name => $field) {
    			if(str_starts_with($name, 'field_')) {
      				$fields[$name] = $field->getValue();
    			}
  		}
	}

	return [
  		'#theme' => 'searchstax_site_search_search_bar',
  		'#current_language' => \Drupal::languageManager()->getCurrentLanguage()->getId(),
  		'#path' => \Drupal::service('path.current')->getPath(),
  		'#title' => $title,
  		'#fields' => json_encode($fields)
	];
  }
}

This example includes the additional `context_definitions` in the Block’s configuration – this tells Drupal that this block will need access to the current node that is rendering the page. In the Block’s build function we can get access to the current node from the getContextValue function. If the module has loaded on a node-based page then we’ll have a valid node object that includes a node ID< title, and any custom fields.

We can then return this data (along with the Block’s theme) so that our Twig file can render these fields and pass them to our front end app.

<!doctype html>
<html lang="en">
  <head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<script type="module" crossorigin src="/modules/searchstax_site_search_search_bar/dist/app.js"></script>
	<link rel="stylesheet" crossorigin href="/modules/searchstax_site_search_search_bar/dist/app.css">
  </head>
  <body>
	<div
  	id="searchstax-site-search-search-page"
  	data-drupal-language="{{ current_language }}"
  	data-drupal-vars="{{ fields }}"
  	data-drupal-title="{{ title }}"
	data-drupal-path="{{ path }}"
	></div>
  </body>
</html>

Lastly our JavaScript front end just needs to fetch the data from the root tag when it loads.

import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from "react-router-dom";
import App from './components/App/App.tsx'
import './index.css'

let root = document.getElementById('searchstax-site-search-search-page');
const language = root.data-current-language;

ReactDOM.createRoot(root!).render(
  <React.StrictMode>
	<BrowserRouter>
  	<App />
	</BrowserRouter>
  </React.StrictMode>,
)

We now have access to the language, Drupal title and path, as well as all of the fields defined for the node where our module loaded.