Introduction
I implemented functionality in Mirador, a widely used IIIF (International Image Interoperability Framework) viewer, to meet the following requirements:
- Display the canvas (page) specified by URL parameters on initial load
- Highlight search terms within the specified canvas
This article shares the approach and implementation method for achieving these requirements.
Exploring Approaches
defaultSearchQuery Option
In Mirador 4, you can automatically execute a search at initialization by specifying the defaultSearchQuery option in the window settings:
const miradorViewer = Mirador.viewer({
windows: [{
manifestId: manifestUrl,
canvasId: canvasId,
defaultSearchQuery: 'search term',
}],
});
This option is a convenient feature that automatically executes a search, but for this use case, the following points needed to be considered:
- Page navigation control - Since the specification automatically navigates to the page of the first hit when a search is executed, additional control is needed if you want to stay on the page specified by
canvasId - Knowing when the search completes - Since the search is executed asynchronously, monitoring Redux state is needed if you want to perform processing after completion
A More Direct Approach
After consideration, I adopted an approach that directly uses the receiveSearch action. With this approach:
- Call the Search API directly to get only hits matching the specified canvas
- Build a response in IIIF Search API format
- Register it as search results in Mirador using the
receiveSearchaction
This allows leveraging Mirador’s search highlight feature as-is while maintaining the display of the specified canvas.
Implementation
1. Function to Get Hits from the Search API
const getSearchHitsForCanvas = async (
manifestUrl: string,
canvasId: string,
query: string
): Promise<{ id: string; chars: string; xywh: string }[]> => {
try {
// Get the search service URL from the manifest
const manifestResponse = await fetch(manifestUrl);
if (!manifestResponse.ok) return [];
const manifest = await manifestResponse.json();
// Find the IIIF Search API service
const services = manifest.service || [];
const searchService = (Array.isArray(services) ? services : [services]).find(
(s: { profile?: string }) => s.profile?.includes('search')
);
if (!searchService) return [];
const searchBaseUrl = searchService['@id'] || searchService.id;
const searchUrl = `${searchBaseUrl}?q=${encodeURIComponent(query)}`;
const response = await fetch(searchUrl);
if (!response.ok) return [];
const data = await response.json();
const resources = data.resources || [];
// Extract only hits matching the specified canvas
const hits: { id: string; chars: string; xywh: string }[] = [];
for (const resource of resources) {
const [resourceCanvas, fragment] = resource.on.split('#');
if (resourceCanvas === canvasId && fragment) {
hits.push({
id: resource['@id'] || resource.id,
chars: resource.resource?.chars || query,
xywh: fragment.replace('xywh=', ''),
});
}
}
return hits;
} catch (error) {
console.error('Failed to fetch search hits:', error);
return [];
}
};
This implementation dynamically retrieves the IIIF Search API endpoint from the manifest’s service property. This allows it to work with any IIIF-compliant server.
2. Mirador Initialization and Highlight Registration
// Initialize Mirador
const miradorViewer = window.Mirador.viewer({
id: 'mirador-viewer',
windows: [{
id: 'window-1',
manifestId: manifestUrl,
canvasId: canvasId, // Display the specified canvas
thumbnailNavigationPosition: 'far-right',
}],
window: {
allowFullscreen: true,
allowClose: false,
sideBarOpen: true,
},
workspaceControlPanel: {
enabled: false,
},
});
// Add highlights if search term and canvas are specified
if (searchQuery && canvasId) {
const hits = await getSearchHitsForCanvas(manifestUrl, canvasId, searchQuery);
if (hits.length > 0) {
const M = window.Mirador as Record<string, unknown>;
const receiveSearch = M.receiveSearch as (
windowId: string,
companionWindowId: string,
searchId: string,
searchJson: unknown
) => Record<string, unknown>;
if (receiveSearch) {
// Build a response in IIIF Search API format
const searchResponse = {
'@context': 'http://iiif.io/api/search/1/context.json',
'@id': `${canvasId}/search?q=${encodeURIComponent(searchQuery)}`,
'@type': 'sc:AnnotationList',
within: {
'@type': 'sc:Layer',
total: hits.length,
},
resources: hits.map((hit, index) => ({
'@id': `${canvasId}/search-result-${index}`,
'@type': 'oa:Annotation',
motivation: 'sc:painting',
resource: {
'@type': 'cnt:ContentAsText',
chars: hit.chars,
},
on: `${canvasId}#xywh=${hit.xywh}`,
})),
};
// Add the search panel to the right side
const addCompanionWindow = M.addCompanionWindow as (
windowId: string,
payload: { content: string; position: string }
) => Record<string, unknown>;
const addAction = addCompanionWindow('window-1', {
content: 'search',
position: 'right',
});
miradorViewer.store.dispatch(addAction);
// Get the companionWindowId
const state = miradorViewer.store.getState();
const searchCompanionWindowId = Object.keys(state.companionWindows).find(
id => state.companionWindows[id].content === 'search'
);
if (searchCompanionWindowId) {
// Register the search results
const searchAction = receiveSearch(
'window-1',
searchCompanionWindowId,
`${canvasId}/search?q=${encodeURIComponent(searchQuery)}`,
searchResponse
);
miradorViewer.store.dispatch(searchAction);
}
}
}
}
Key Points
1. Accessing Mirador’s Action Functions
In Mirador 4, action functions can be accessed directly from the global object:
const M = window.Mirador as Record<string, unknown>;
const receiveSearch = M.receiveSearch;
const addCompanionWindow = M.addCompanionWindow;
Note that you access them directly as Mirador.receiveSearch, not through Mirador.actions.
2. IIIF Search API Format Response
The JSON passed to receiveSearch must conform to the IIIF Search API 1.0 format:
{
"@context": "http://iiif.io/api/search/1/context.json",
"@type": "sc:AnnotationList",
"resources": [
{
"@type": "oa:Annotation",
"motivation": "sc:painting",
"resource": {
"@type": "cnt:ContentAsText",
"chars": "matched text"
},
"on": "canvasURI#xywh=x,y,width,height"
}
]
}
3. CompanionWindow Placement
The placement of the search panel can be controlled with the position parameter:
left: Left side (same position as the metadata panel)right: Right side
A natural layout is metadata on the left and search results on the right.
Conclusion
By directly calling the receiveSearch action, the following benefits were achieved:
- Canvas display control - Can add highlights while displaying the specified canvas on initial load
- Immediate highlight display - Highlights are displayed immediately because search results are registered directly
- Panel placement control - The position of the search panel can be freely configured
This method leverages Mirador’s Redux store mechanism and allows for flexible customization.