On a couple of recent Next.js projects, the design called for highlighted excerpts to be displayed in the search results:
On the Drupal side of things, both sites are using Search API, and while excerpts are supported in Search API, they weren't coming through in the JSON:API results.
While looking through the Drupal issue queues, I found an issue that described this problem exactly. In the issue, someone provided a patch to JSON:API Search API which would include the excerpts. The same developer later on provided a patch-less method to do the same thing. In this post, I'll show how this is implemented in Drupal, and how to merge the excerpts with the JSON:API entities in Next.js using Next-Drupal.
Getting the excerpts into the JSON:API results
The developer in the before mentioned issue provided some code for an event subscriber. Add this class to an existing custom module (or create a new module), and add its service definition, and you're good to go. I put the event subscriber and service definition in a GitHub repo for brevity sake.
Once that module (or your existing module) is enabled, and assuming you enabled the "highlight" processor on your Search API index, the excerpts should available in the retrieved search data.
Integrating the excerpts with the JSON:API results
Now that things on the Drupal side are set up, the excerpts need to be merged into the individual JSON objects (aka Drupal node entities).
Typically when fetching data from Drupal, you would request that the JSON returned from the fetch get deserialized into a javascript object. However, the excerpt data will be lost if you do it this way. Instead, skip the deserialization during the fetch, and do it manually after the data is returned from Drupal. You can use deserialize
from the next-drupal
package to do this.
Now, just loop through the results, and add the excerpts to the node results. Here is the relevant code:
import { deserialize } from 'next-drupal'
const response = await fetch('/api/search/[your_search_index]', {
method: 'POST',
body: JSON.stringify({
params: {
page: {
// `page` is the current page of results
// `PAGER_SIZE` is the number of results to get/display on each page
offset: page * PAGER_SIZE,
limit: PAGER_SIZE,
},
filter: {
// `searchWords` is the word/phrase the user is searching for
fulltext: searchWords
},
},
deserialize: false,
}),
})
const json = await response.json()
const items = deserialize(json)
// Merge excerpts from search_api into results
// Note: not all `items` will have a corresponding excerpt
const results = items.map((result) => {
const nid = result.drupal_internal__nid
if (json.meta?.extra_data?.hasOwnProperty(nid)) {
return { ...result, ...json.meta?.extra_data[nid] }
} else {
return result
}
})
// `quantity` is the number of total results from the search query
const quantity = json.meta?.count
// `results` contains the data needed to display the search results to the user
// Example:
results.map(result => {
return (
<div key={result.id}>
<h3>{result.title}</h3>
<div>{result.excerpt}</div>
</div>
)
})
Conclusion
Each excerpt will have the highlighted words wrapped in HTML. The only thing left to do is add some CSS to differentiate the highlighted words from the rest of the text.
Bonus tip
Instead of using Drupal's HTML wrapped search words, remove the "highlight prefix" and "highlight suffix" from the Search API "highlight" processor, and use a custom highlighter instead, like react-highlight-words. You'll then be able to highlight words in the title in addition to the excerpt. But I'll leave that up to you to figure out.