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.titleBlock and the rest still work inside your override.
  • escapeHtml comes with the layout. Use it on any value from your catalog. It is at layout.templates.helpers.
  • To extend the template you replaced, read it from layout.constructor.defaultTemplates. Inside your override, layout.templates.infoBlock is 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:

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