Problem/Motivation
The current Typesense search block implementation only supports one search block per page because all block instances share the same drupalSettings['search_api_typesense']
configuration. When multiple blocks are placed on the same page, the settings from the last rendered block overwrite the previous ones, causing conflicts and preventing proper initialization of multiple search instances.
This limitation prevents users from creating different search experiences on the same page, such as having separate search blocks for different content types or collections with different configurations.
Steps to reproduce
1. Create two or more Typesense search blocks with different configurations (different collections, facets, or settings)
2. Place both blocks on the same page
3. Observe that only the last rendered block works correctly
4. Check browser developer tools to see that drupalSettings.search_api_typesense
contains only the configuration from the last block
Proposed resolution
Implement a namespaced configuration system that allows multiple search blocks to coexist on the same page:
1. Update TypesenseSearchBlock::build() method:
php
public function build(): array {
// ... existing code ...
$block_id = $this->getDerivativeId() ?: $this->getPluginId() . '_' . uniqid();
return [
'content' => [
'#theme' => 'search_api_typesense_search',
'#block_id' => $block_id,
'#collection_render_parameters' => $collection_render_parameters,
'#facets' => $facets,
'#attached' => [
'drupalSettings' => [
'search_api_typesense' => [
'blocks' => [
$block_id => [
'server' => [
'apiKey' => $this->configuration['search_only_key'] ?? '',
'nodes' => $configuration['nodes'],
],
'collection_specific_search_parameters' => $collection_specific_search_parameters,
'collection_render_parameters' => $collection_render_parameters,
'current_langcode' => $this->getCurrentLanguage()->getId(),
'hits_per_page' => $this->configuration['hits_per_page'],
'facets' => $facets,
],
],
],
],
],
],
];
}
2. Update search.js to handle multiple blocks:
javascript
((Drupal, TypesenseInstantSearchAdapter, instantsearch) => {
Drupal.behaviors.search = {
attach(context, settings) {
if (!settings.search_api_typesense?.blocks) {
return;
}
Object.entries(settings.search_api_typesense.blocks).forEach(([blockId, blockSettings]) => {
const searchboxSelector = `#searchbox-${blockId}`;
const [searchbox] = once('searchbox', searchboxSelector, context);
if (searchbox === undefined) {
return;
}
this.initializeSearch(blockId, blockSettings);
});
},
initializeSearch(blockId, blockSettings) {
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: blockSettings.server,
additionalSearchParameters: {
exclude_fields: 'embedding',
exhaustive_search: true,
},
collectionSpecificSearchParameters: blockSettings.collection_specific_search_parameters,
});
const searchClient = typesenseInstantsearchAdapter.searchClient;
const collections = blockSettings.collection_render_parameters;
const firstCollection = collections[0];
const search = instantsearch({
searchClient,
indexName: firstCollection.collection_name,
});
const getCollectionWidgets = (collection) => [
instantsearch.widgets.stats({
container: `#stats-${collection.collection_name}-${blockId}`,
}),
instantsearch.widgets.infiniteHits({
container: `#hits-${collection.collection_name}-${blockId}`,
cssClasses: {
list: 'search-results',
loadMore: 'btn btn--load-more',
},
templates: {
item(hit, { html, components }) {
if (blockSettings.debug) {
return html`
<article class="search-result-debug">
<ul>
${Object.keys(hit).map(
(key) =>
html`<li>
<strong>${key}:</strong> ${JSON.stringify(
hit[key],
null,
2,
)}
</li>`,
)}
</ul>
</article>
`;
}
if (hit.rendered_item) {
return html`${hit.rendered_item}`;
}
return html`
<article>
<h3>${components.Highlight({ hit, attribute: 'title' })}</h3>
<small> (${hit.id})</small>
</article>
`;
},
},
}),
];
search.addWidgets([
instantsearch.widgets.configure({
hitsPerPage: blockSettings.hits_per_page,
...(blockSettings.current_langcode && {
facetFilters: [`langcode:${blockSettings.current_langcode}`],
}),
}),
instantsearch.widgets.searchBox({
container: `#searchbox-${blockId}`,
placeholder: 'Search...',
}),
...getCollectionWidgets(firstCollection),
]);
if (collections.length > 1) {
collections.slice(1).forEach((collection) => {
search.addWidgets([
instantsearch.widgets
.index({ indexName: collection.collection_name })
.addWidgets(getCollectionWidgets(collection)),
]);
});
}
const facetWidgets = {
string: (facet) =>
instantsearch.widgets.refinementList({
container: `#facet-${facet.name}-${blockId}`,
attribute: facet.name,
searchable: true,
}),
number: (facet) =>
instantsearch.widgets.rangeSlider({
container: `#facet-${facet.name}-${blockId}`,
attribute: facet.name,
}),
bool: (facet) =>
instantsearch.widgets.toggleRefinement({
container: `#facet-${facet.name}-${blockId}`,
attribute: facet.name,
templates: {
labelText: facet.label,
},
}),
};
blockSettings.facets.forEach((facet) => {
if (facetWidgets[facet.type]) {
search.addWidgets([facetWidgets[facet.type](facet)]);
}
});
search.start();
},
};
})(Drupal, TypesenseInstantSearchAdapter, instantsearch);
3. Update Twig template to include block-specific IDs:
twig
<div id="searchbox-{{ block_id }}"></div>
{% for collection in collection_render_parameters %}
<div id="stats-{{ collection.collection_name }}-{{ block_id }}"></div>
<div id="hits-{{ collection.collection_name }}-{{ block_id }}"></div>
{% endfor %}
{% for facet in facets %}
<div id="facet-{{ facet.name }}-{{ block_id }}"></div>
{% endfor %}
Remaining tasks
- [ ] Update TypesenseSearchBlock::build()
method to generate unique block IDs and namespace settings
- [ ] Modify search.js
to handle multiple block configurations and initialize separate search instances
- [ ] Update Twig template to include block ID in HTML element attributes
- [ ] Update facet widget initialization to use block-specific container selectors
- [ ] Add automated tests for multiple blocks on the same page
- [ ] Update documentation to reflect multi-block support
- [ ] Test federated search across different block instances
User interface changes
- HTML elements will include block-specific IDs (e.g., #searchbox-{block_id}
, #facet-{facet_name}-{block_id}
)
- No visible changes to the user interface - multiple search blocks will function independently
- Each block maintains its own search state and results display
API changes
- drupalSettings.search_api_typesense
structure will change from a flat configuration object to a nested structure with a blocks
property
- JavaScript Drupal.behaviors.search
will be updated to handle multiple block configurations
- Template variables will include a new block_id
parameter
Data model changes
No database or entity model changes are required. The changes are limited to:
- Block configuration structure in drupalSettings
- HTML template structure to include block IDs
- JavaScript initialization logic