Marking Rendered Search Results
The HTML contract that lets the Data Collector measure autocomplete and search results you render yourself.
When you call the Perselio Search API and render autocomplete and the search results page (SERP) yourself, the Data Collector cannot see your API traffic. It only sees the rendered page. A lightweight DOM contract tells it which parts of the page are Perselio search, which query produced them, and which items they show.
Without it, search performance — click-through rate, add-to-cart and revenue from search, searches without results — cannot be reported, and search cannot learn from what visitors click.
The contract covers both search surfaces:
- Autocomplete — suggestions shown while the visitor types.
- Search results page — the full list of results after the visitor submits a query.
If your markup already carries some of this information under different attributes, tell us. The collector is configured per site and can read what is already there — the attributes below are the recommended convention, not a requirement to restructure your templates.
1. Mark the search surface
Add the attributes to the element that wraps the results — the autocomplete panel and the results list:
<div data-per-search="autocomplete" data-per-search-query="push-up bra">
<!-- suggestions rendered by you -->
</div>
<div data-per-search="serp" data-per-search-query="push-up bra">
<!-- results rendered by you -->
</div>data-per-search— required. Marks the element as a Perselio search surface. Value:autocompleteorserp.data-per-search-query— required. The query that produced the results currently shown, exactly as you sent it to the API insearchQuery.
The query must belong to the results on screen, not to the input. While the visitor types, the search input is always ahead of the rendered suggestions. Setdata-per-search-queryin the same render that replaces the results — never copy the live input value.
If the autocomplete panel is rendered outside the search form — for example appended to <body> — put the attributes on the panel itself.
2. Group autocomplete results
Autocomplete usually shows several kinds of results side by side. Wrap each group and say what it contains:
<div data-per-search="autocomplete" data-per-search-query="push-up bra">
<div data-per-search-group="products">…</div>
<div data-per-search-group="categories">…</div>
<div data-per-search-group="brands">…</div>
<div data-per-search-group="articles">…</div>
<div data-per-search-group="queries">…</div>
</div>Values: products, categories, brands, articles, queries. Use only the groups you render. Positions are counted within each group, so the first category is position 1 even when it is shown below five products.
On the search results page, groups are needed only if you show more than the item list — for example a block of matching categories above the results.
3. Identify every result
Every rendered result carries its identifier. Search returns more than items, so results other than items also say what they are:
<!-- item -->
<div class="product-card" data-per-item-id="12345">…</div>
<!-- other entities -->
<a data-per-item-type="category" data-per-item-id="Women > Lingerie > Bras" href="…">Bras</a>
<a data-per-item-type="brand" data-per-item-id="Triumph" href="…">Triumph</a>
<a data-per-item-type="article" data-per-item-id="size-guide" href="…">Size guide</a>
<a data-per-item-type="query" data-per-item-id="push-up bra black" href="…">push-up bra black</a>| Result | data-per-item-type | data-per-item-id |
|---|---|---|
| Item | omit (default) | the item ID, same as everywhere else on your site — see Identifying Products on Your Site |
| Category | category | the category value returned by the API (facetCounts value, or id from the category scenario) |
| Brand | brand | the brand value returned by the API (facetCounts value, or id from the brand scenario) |
| Article | article | the article id returned by the API — the same ID as in the Text Feed |
| Suggested query | query | the suggested query text |
Put the attributes on the root element of the result — the element that wraps its link, image and name. For an item this is the card itself, exactly as on a category listing.
Position on paginated results
If the results page is paginated or uses "load more", add the absolute position of each result across all pages:
<div class="product-card" data-per-item-id="12345" data-per-position="25">…</div>The first result of page 2 with 24 results per page is 25. Without pagination, the position is read from the order of the results and the attribute is not needed.
4. Mark the no-results state
Keep the surface wrapper on the page even when the API returns no results, and mark the message you show instead:
<div data-per-search="serp" data-per-search-query="xyzzy">
<p data-per-search-no-results>We found nothing for "xyzzy".</p>
</div>Searches without results are among the most useful signals for tuning — synonyms and redirects in Search Curation start from them.
5. Mark the search box
Each submitted search is recorded: pressing Enter, clicking the search button, or clicking "show all results" in autocomplete.
<form data-per-search-form action="/search">
<input type="search" name="q" />
<button type="submit">Search</button>
</form>
<a data-per-search-show-all href="/search?q=push-up+bra">Show all results</a>data-per-search-form— the form containing the search input.data-per-search-show-all— the link from autocomplete to the full results page.
Recommendations inside search
Recommended items shown within a search surface — for example popular items in autocomplete before the visitor types, or recommendations on a page without results — are recommendations, not search results. Mark their block as described in Marking Rendered Recommendations.
Prerequisites
The Data Collector script must be present on the page. It observes the marked surfaces and sends the events — it is required even when every search API call is made from your backend.
Validation checklist
- Autocomplete and the results page each carry
data-per-searchon the element that wraps the results. data-per-search-querymatches the query of the results currently shown, including while the visitor types.- Each autocomplete group carries
data-per-search-group. - Every result carries
data-per-item-id; results other than items also carrydata-per-item-type. - Paginated results carry
data-per-positionwith the absolute position. - The no-results state keeps the wrapper and marks the message with
data-per-search-no-results. - The search form and the "show all results" link are marked.
- The attributes are present before the surface becomes visible.
Related
- Search Onboarding — where this step fits in the API integration
- Identifying Products on Your Site — the item identifiers the result cards carry
- Marking Rendered Recommendations — the same contract for recommendation blocks
- Data Collector — the script that observes the marked surfaces
Updated about 13 hours ago

