Customization
The SDK renders answers, sources and search results for you. When you need a field of your own on screen — a category pill, a brand, a reading time — you change two things:
| Dial | Call | What it decides |
|---|---|---|
| Data | workflow.useApi() |
Which fields the API sends back. |
| Look | workflow.useLayouts() |
How each field is drawn. |
Set the data dial first. This is the step that catches people out. The SDK
asks for a short list of fields, so a field you never requested is undefined
in your template, and your pill renders empty. See the field lists below.
Every example on this page runs inside misocmd, after the SDK loads:
<script>
const misocmd = window.misocmd || (window.misocmd = []);
misocmd.push(async () => {
const client = new MisoClient('YOUR_PUBLISHABLE_KEY');
const workflow = client.ui.asks; // or client.ui.hybridSearch
// your useApi() and useLayouts() calls go here
await client.ui.ready;
});
</script>
Configure the context, not one workflow
Read this before you copy any recipe below. For Ask, the SDK gives you two objects, and they look almost the same:
| You write | You get | Your settings reach |
|---|---|---|
client.ui.asks |
The context | Every answer, follow-ups included. |
client.ui.ask |
One workflow, the first answer | That answer only. |
Each follow-up answer runs as a new workflow inside the context. Settings
resolve as defaults, then context, then workflow, so a follow-up inherits
what you put on the context and never sees what you put on ask.
The difference is easy to miss, because the first answer looks correct either
way. Apply the same customization to client.ui.ask and the first answer's
sources carry it, while the follow-up answer's sources come back plain. On
client.ui.asks, every answer carries it.
| Feature | Context | One workflow |
|---|---|---|
| Ask | client.ui.asks |
client.ui.ask |
| Explore | client.ui.explores |
client.ui.explore |
| Recommendation | client.ui.recommendations |
client.ui.recommendation |
| Hybrid Search | — | client.ui.hybridSearch |
| Search | — | client.ui.search |
Hybrid Search and Search run as a single workflow, so there is nothing to
choose. Use client.ui.hybridSearch and every part of the page follows.
Reach for the singular form only when you want one unit to differ from the rest, such as a second recommendation rail with its own template.
1. Add a category pill to answer sources
Sources under an answer come from the sources role. Two steps, both on the
context so follow-up answers match.
Step 1. Ask for the field. The Ask workflow requests these source fields by default:
['cover_image', 'url', 'created_at', 'updated_at', 'published_at']
title arrives anyway. Everything else you must name, and the list you send
replaces the default, so repeat the fields you still want:
const workflow = client.ui.asks; // the context, not client.ui.ask
workflow.useApi({
source_fl: [
'cover_image', 'url', 'published_at', // keep the defaults you use
'categories', // the field you want to show
],
});
A custom attribute uses its dotted path, such as
custom_attributes.reading_time. See Product Schema.
Step 2. Draw it. Each item draws an info block for the text beside the
image. Override infoBlock, and reuse the built-in blocks for everything you
do not want to change:
workflow.useLayouts({
sources: {
templates: {
infoBlock(layout, data, meta) {
const { className, templates } = layout;
const { escapeHtml } = templates.helpers;
const category = [].concat(data.categories || [])[0];
const pill = category
? `<span class="my-pill">${escapeHtml(category)}</span>`
: '';
return `<div class="${className}__item-info-container">
${pill}
${templates.titleBlock(layout, data, meta)}
${templates.dateBlock(layout, data, meta)}
${templates.descriptionBlock(layout, data, meta)}
</div>`;
},
},
},
});
Then style .my-pill in your own CSS. Constrain the width, because the info
block is a column and a plain inline-block stretches to the full row:
.my-pill {
display: inline-block;
align-self: flex-start; /* without this the pill spans the whole row */
width: fit-content;
padding: 2px 10px;
border-radius: 999px;
background: #e8ecfb;
color: #22307a;
}
Three things make this safe to write:
-
Every default template stays available. Your object is merged over the
defaults, so
templates.titleBlockand the rest still work inside your override. -
escapeHtmlcomes with the layout. Use it on any value from your catalog. It is atlayout.templates.helpers. -
To extend the template you replaced, read it from
layout.constructor.defaultTemplates. Inside your override,layout.templates.infoBlockis your own function, so it repeats forever. -
A missing field returns nothing. The
category ? … : ''guard keeps an empty pill off the page when one record lacks the field. -
Pick the value you mean.
tags[0]is the first tag in the record, which is not always the one a reader expects. Match the tag you want, or map it.
Use infoBlock, not articleInfoBlock. Both the article template and the
product template look for infoBlock first, so one override covers every list.
articleInfoBlock works only where the item type is already article, so the
same code silently does nothing on a search result list.
The blocks you can override
| Template | Draws |
|---|---|
infoBlock |
The text container, for any item type. Start here. |
articleInfoBlock |
The same, for article items only. |
productInfoBlock |
The same, for product items only. |
titleBlock |
The title, with search-term highlighting. |
dateBlock |
The date, from published_at, created_at or updated_at. |
descriptionBlock |
The snippet, and the description when there is no snippet. |
imageBlock |
The cover image. |
article |
The whole item, if you want to start from nothing. |
2. The same pill on hybrid search results
Hybrid Search returns two lists, and they are separate roles:
| Role | What it holds |
|---|---|
products |
The search results. |
sources |
The articles the answer cites. |
The result list uses fl, not source_fl. The default is:
['cover_image', 'url', 'created_at', 'updated_at', 'published_at', 'title']
So the same two steps, against products:
const workflow = client.ui.hybridSearch;
workflow.useApi({
fl: ['cover_image', 'url', 'published_at', 'title', 'categories'],
});
workflow.useLayouts({
products: {
templates: {
infoBlock(layout, data, meta) {
// the same function as above
},
},
},
});
Search results carry no item type of their own, so they draw with the product
template. This is the reason to override infoBlock: the same function then
works for the answer sources and the result list.
To change the cited sources on this page as well, repeat the block under
sources and set source_fl too. Hybrid Search draws its sources in a
horizontal strip with a compact template, so keep them separate: products is
your result list, sources is the strip under the answer.
3. Facets and filters
Two related jobs have their own pages:
- Facets — let a reader narrow the results themselves.
- Filter results (fq) — the rules your product sets.
A full page you can copy
Every snippet above runs in this order. The workflow is configured before
client.ui.ready, and the markup is written after it:
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@miso.ai/client-sdk@1.13.1/dist/css/ui.css">
<div id="miso-hybrid-search-combo"></div>
<script>
const misocmd = window.misocmd || (window.misocmd = []);
misocmd.push(async () => {
const client = new MisoClient('YOUR_PUBLISHABLE_KEY');
const workflow = client.ui.hybridSearch;
workflow.useApi({
fl: ['cover_image', 'url', 'published_at', 'title', 'tags'],
facets: ['tags'],
});
workflow.useLayouts({
products: { templates: { infoBlock } },
facets: { templates: { title, value } }, // see /sdk/facets
});
await client.ui.ready;
document.querySelector('#miso-hybrid-search-combo').innerHTML =
MisoClient.ui.defaults.hybridSearch.templates.root();
});
</script>
<script async
src="https://cdn.jsdelivr.net/npm/@miso.ai/client-sdk@1.13.1/dist/umd/miso.min.js">
</script>
Working examples
The SDK showcase runs each pattern on a live page, with the code beside it. The single search bar example is a good place to start.
Next
- Product Schema — the fields you can request.
-
Hybrid Search API —
fl,facetsandorder_byon the endpoint itself. - Answer API — what each source carries.
