Javascript collector
We provide starter integration that allows you to start using Luigi's Box immediately and without any major programming effort on your side. This integration relies on the specific HTML structure that was used on your site at the time you signed up for Luigi's Box. As you keep improving your site, it is very likely that the HTML structure will change and your Luigi's Box integration will break and we will receive incorrect data, which will negatively impact your data quality and negatively impact the process of learning the optimal rankings (if you are using Luigi's Box Autocomplete or Luigi's Box Search).
To maintain high data quality and remove the risk of breaking the data collection process when changing HTML structure, you should explicitely annotate search data in your HTML. We provide a tracker script which can read structured data about search and search results from standard schema.org annotations (we support both inline annotations or embedded json+ld). These annotations are not tied to your design and HTML structure and therefore robust against redesign and other front-end changes.
Note that the starter script does not support schema.org/json+ld annotations and even if you annotate your searches, the starter script will keep collecting data using hardcoded HTML structure. If you are using our starter integration and want to switch to robust schema.org/json+ld annotations, contact us and we will prepare you a standard tracking script.
Script inclusion ¶
When you sign up for Luigi's Box you will receive a tracking script and a Tracker ID. You must include this script in the HTML code of your website to start tracking search data.
The Tracker ID is a unique identifier of your domain and is tied to the tracking script. You should not use the same script for different domains; if you do, your data will get mixed into a single view inside Luigi's Box, making any relevant analysis very difficult. If you want to track different domains, contact us for separate Tracker IDs and tracking scripts.
Inside <head> element ¶
We recommend that you insert the script tag inside the <head>
element in your
HTML code. Note, that the script is marked as async
and thus will not impact
your page load speed in any way.
Just for reference, the script tag that you should have received upon signing up will look like the following example.
<script async src="https://scripts.luigisbox.com/LBX-123.js"></script>
Google Tag Manager integration ¶
If you are unable to insert the script tag directly into your HTML, you can use
Google Tag Manager. Tracking script you insert into your HTML is the same you
would use with Google Tag Manager, however using Google Tag Manager will impact
the data collection process and some advanced features (e.g., fixit rules) will
appear to be working slowly. We recommend that you always insert the script to
<head>
element for full experience.
Content Security Policy ¶
If your website is using Content Security Policy, you need to add following rules to enable Luigi's Box search analytics script.
CSP directive | Value |
---|---|
connect-src |
https://api.luigisbox.com |
script-src |
https://scripts.luigisbox.com |
script-src |
https://cdn.luigisbox.com |
Annotations Introduction ¶
Luigi’s Box recognizes search and product microdata annotations in schema.org format - a standard and accepted way to give structure to data and make it understandable to machines.
You want to use schema.org annotations, because:
Google, Bing, Yahoo and other search engines use them to show rich snippets about your product directly on the search results page. Compare the following results and see how the first one with metadata stands out. Using schema.org is an amazing way to draw attention to your listing and stand out from the competition.
Apple’s Siri and Google’s Assistant can use them to answer questions about shopping. “Hey Siri, where can I buy a new phone?”
There are 2 ways to add schema.org annotations to your web, and both are supported by Luigi's Box:
Using an inline markup and adding additional HTML attributes where appropriate.
Embedding a separate JSON object which includes all semantic information in one place.
Both options are equivalent, as they convey the same semantic information. They only differ in the used notation. When you choose inline markup, you have to intersperse the annotations throughout the HTML, but on the other hand, there is no duplicated information. When you use a JSON object, the semantic data is contained in a single place, but duplicates the information that is already present in HTML.
Based on our experience, inline annotations are easier to maintain, but the embedded JSON document is somewhat easier to implement and understand, especially when you are adding schema.org annotations for the first time.
Concepts
Regardless of which implementation you choose (inline annotations or JSON+LD), there are several concepts that you need to know about and which, together, form a complete information about the search performed.
Concept | |
---|---|
Query | what the user typed into your searchbox |
Filters | additional restrictions used, e.g. search only in "Accessories" department |
Search results | specifically title, URL address, position in the list of results and price (if applicable). |
Conversion intent | did the user add an item to the shopping cart? |
Query ¶
Query is what the user typed into the search box to get search results.
It is important that you don't encode the query in any way (e.g., you should do no percent-encoding, or whitespace trimming). Use excactly the same query string that the user typed, even if you do some internal preprocessing before using the query for search.
Note, that a valid search may not have a query at all. E.g., imagine a scenario when using an advanced search and the user types no query, but chooses to search for all products from a certain brand. In this case you would use empty query with filters.
See below for filters explanation.
Filters ¶
Anything that influences which products are displayed should be annotated as filters. Sorting, or facets, or sometimes even the category have effect on what is displayed.
Luigi's Box only cares about active filters. When you are showing a facet where a user can limit phones by the manufacturer, but the user did not select anything yet, it's not a filter and you should not annotate it.
Take a look at the picture above. There are 4 filters:
Color, Price, Brand and Sort by. Notice that the Brand filter is unused so there is no need to annotate it. In this case, you should send:
- Color: Black,
- Price: 20-800,
- Sort: Price.
Note that some filters are implicit and not visible or modifiable by the user.
Imagine a scenario where your users are assigned specific access levels and you are limiting the search results to only show results the user can access. In this case, you should send the access level as a filter.
Search results ¶
The relevant information about search result is:
Title — this will usually be the same title as you are showing to the user, e.g. a product title. The title is not required to be unique (and in many cases won't, but that's ok).
URL — the canonical link to the search result (e.g. a product). It should be valid URL, including http/https protocol. The URLs should be unique, and each URL should link to exactly one product. In our experience, the most common problem that we see is that the URLs are not canonical. The URL should only contain enough parameters that it still links to the product and nothing more. Some examples of URLs that violate this requirement are:
URL | |
---|---|
https://www.example.com/products/black-shirt?ref=search | notice the ref parameter which is not necessary for the link to work and is only used for analytical purposes. |
https://www.example.com/products/black-shirt?q=shirt&page=2 | notice the q and page parameters which are not necessary for the link to work and are only used to construct a link back to search. |
https://www.example.com/products/black-shirt?gclid=283h1bxz81jzgj | notice the gclid parameter which is not necessary for the link to work and is only used for tracking purposes. |
https://www.example.com/products/black-shirt✔ | This is the correct canonical URL that should be used in all cases |
- Position — a number indicating a search result position in the full list of search results, considering pagination.
- Price (optional) — price of the product. This may not always be available, e.g., the product is not in stock anymore, or the search result is a blog post, which is not sellable and thus has no price. It's ok to not annotate price if the search result does not have a price (i.e. the search result is an article and not a product).
Luigi's Box only cares about search results that are visible. Imagine a scenario where your search found thousands of search results, but you only present a list of first 20 search results, along with a pagination component, which lets the user scroll through the search results and view additional results.
In this case, you should only annotate the 20 visible search results, and ignore the rest. The first result will have position 1, the last result position 20. When the user clicks through to page 2, annotate the visible results, and the first result will have position 21, the last one position 40.
Conversions ¶
Everything that is important to you from the business perspective can be tracked as “conversion”, regardless of whether it is an action of “buying” an item, “liking” it, “favoriting” it, or anything else.
There are usually many different types of conversions found at different places.
It is usually possible to convert both from the list of search results and the product detail itself. You should annotate both conversion actions.
It's ok to have several conversion actions for the same product, e.g. buy a product, or add a product to favorite list. Just make sure to give each conversion a different name.
Autocomplete ¶
The process of annotating autocomplete results is the same as with the regular Search Results. Here are the notable differences:
- Autocomplete is usually not paginated so you can safely skip the position annotations
- In case your Autocomplete results don't show prices you can safely skip the price attribute.
- Sometimes autocomplete can have filters too. See the image below for a an example. In this case, you must annotate this filter, e.g. Category: Phones.
Product listings ¶
Any display of product list which is of interest can be considered a “search”. Therefore, if the user clicks a menu element and is taken to the list of products which he/she can manipulate through filters and/or sort, this is considered a “search” too (albeit without a query, only with filters, if any) and you should send it to Luigi's Box to be analyzed. It makes sense to send the category name as a filter.
Embedded JSON+LD
Search example
Sample JSON+LD document
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "SearchAction",
"query": "phone",
"result": {
"@type": "ItemList",
"name": "Search Results",
"itemListElement": [
{
"@type": ["ListItem", "Product"],
"position": "1",
"name": "Android Phone PX100",
"url": "https://myshop.com/products/236",
"offers": {
"@type": "Offer",
"priceCurrency": "EUR",
"price": "120"
}
},
{
"@type": ["ListItem", "Product"],
"position": "2",
"name": "iPhone X",
"url": "https://myshop.com/products/293",
"offers": {
"@type": "Offer",
"priceCurrency": "EUR",
"price": "999"
}
},
{
"@type": ["ListItem", "CollectionPage"],
"position": "3",
"name": "Apple iPhone",
"url": "https://myshop.com/category/iphone"
}
]
},
"instrument": [{
"@type": "ChooseAction",
"name": "Sort by",
"actionOption": "Relevance"
},
{
"@type": "ChooseAction",
"name": "Color",
"actionOption": ["Black", "Silver"]
}]
}
</script>
The sample document shows all concepts in a JSON+LD format. You should include a document similar to this, wrapped in a script tag somewhere in your page HTML code.
Top-level attributes ¶
Element | Hint |
---|---|
<script type="application/ld+json"> |
The JSON+LD document must be embedded inside a this script tag. |
@context |
This attribute marks this as schema.org-compatible. |
@type |
This attribute marks this as search-related information in the shema.org vocabulary. You must use SearchAction here (even for recommendation). |
query |
The query that the user entered (required only for search results and autocomplete). |
result |
This element contains the list of search results. The name is a bit confusing, but in the schema.org nomenclature, results of the SearchAction is an ItemList of search results. |
result.name |
Valid values here are "Search Results" for regular search results, "Autocomplete" for autocomplete results and "Recommendation" for recommendation results. |
itemListElement ¶
itemListElement
represents a single result either in autocomplete widget, or on regular search results page. Below is a list of attributes that we rely on for analytics purposes. While you can add more attributes from the schema.org spec, it will have no effect and they will be ignored by the parser.
Element | Hint |
---|---|
@type |
An array of types. One of the elements has to be "ListItem", which will mark the element as a search resut in the schema.org vocabulary. You should use additional type which will help us differentiate between results that are products, categories, brands, or articles, or anything else that you might show on the search results page. See below for supported types. |
position |
Position of the result in the results list. |
name |
The user-visible title of the result |
url |
Canonical URL of the result |
offers OPTIONAL |
An Offer object indicating price |
Supported itemListElement @types ¶
We currently support these standard schema.org types:
Product
for productsCollectionPage
for categoriesBrand
for brandsArticle
for articles/blogs/pagesSearchAction
for queries (e.g., if you are suggesting phrases)
instruments (filters) ¶
Element | Hint |
---|---|
instrument REQUIRED |
This field contains an array of filters. |
instrument.@type REQUIRED |
Marks this as filter in schema.org vocabulary. Must be ChooseAction . |
instrument.name REQUIRED |
The filter name. You can use whatever name fits best. |
instrument.actionOption REQUIRED |
When a single filter has multiple values, you can include all values at once in an array. |
Recommendation example
Sample JSON+LD document for Recommendation
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "SearchAction",
"result": {
"@type": "ItemList",
"name": "Recommendation",
"itemListElement": [
{
"@type": ["ListItem", "Product"],
"position": "1",
"name": "Android Phone PX100",
"url": "https://myshop.com/products/236",
"offers": {
"@type": "Offer",
"priceCurrency": "EUR",
"price": "120"
}
},
{
"@type": ["ListItem", "Product"],
"position": "2",
"name": "iPhone X",
"url": "https://myshop.com/products/293",
"offers": {
"@type": "Offer",
"priceCurrency": "EUR",
"price": "999"
}
}
]
},
"instrument": [{
"@type": "ChooseAction",
"name": "RecommenderClientId",
"actionOption": "basket"
},
{
"@type": "ChooseAction",
"name": "ItemIds",
"actionOption": ["/products/123", "/products/129"]
}]
}
</script>
For the recommendation, you can use the following filters (and you must always include at least RecommenderClientId
):
name | actionOption |
---|---|
RecommenderClientId REQUIRED |
Unique identifier of the recommender (recommender_client_identifier from recommendation result). Its value should define type of recommender user along with its position on the site (e.g., product_detail_bottom_alternatives ). |
ItemIds optional |
List of input items of a recommendation request (item_ids from recommendation request). |
Recommender optional |
Name of the recommender (recommender from recommendation result). |
Type optional |
Type of the recommender (recommendation_type from recommendation result). |
_Variant optional |
Determines variant in A/B testing (e.g., Original, Luigis). |
Explicit trigger
An example to show you the logical flow of results rendering. In the example below, the script requests results asynchronously, then renders the results with annotations and only after the results are rendered, calls
Luigis.Scan
. You only need to use theLuigis.Scan
call with appropriate arguments, the rest of the code is just for demonstration.
<div id="search-results">
<div>
<script id="search-results-annotations" type="application/ld+json">
</script>
<script>
$.get('/search/results?q=iphone', function(data) {
renderResults(data);
generateJsonLd(data);
Luigis.Scan('#search-results-annotations', '#search-results');
});
</script>
After filling in the JSON+LD, you must call a notification function which will trigger data collection.
The notification function is called Luigis.Scan
and accepts 2 arguments:
Selector for annotations which must point to a
<script>
element with JSON+LD annotationsSelector for an element which contains the actual user-visible search results. We need to find the actual search result elements so we can detect user interactions (clicks, conversions). This selector is optional, and by default set to
body
, meaning we are searching all body for search result elements, but we strongly suggest that you provide this selector as narrowly scoped as possible.
Since you are (and should be) loading the analytics script asynchronously, there is a possibility that when you call Luigis.Scan, the analytics script is not yet loaded and the function does not exist.
To prevent this situation, you must add the following code to the <head>
element of your website.
<script> window._lbcq = []; window.Luigis = window.Luigis || {}; window.Luigis.User = '...'; // Optionally, set your user identifier here; see below window.Luigis.Scan = window.Luigis.Scan || function(a, r) { window._lbcq.push([a, r]); } </script>
The code will define a simple implementation of the Luigis.Scan
function,
which will just add "scan" commands to a queue. When the integration script is
loaded it redefines the function with real implementation and processes the
queued commands.
Defining the simple implementation early will allow you to load the integration script asynchronously, without impacting your page load speed.
This is also the place to customize user ID used for search analytics via window.Luigis.User
property. Even though it is not necessary in most use cases, see User identifiers section for examples when it might be better for you.
Conversions
Conversion actions cannot be embedded directly in the JSON+LD document, so you'll need to add HTML data-action
attributes to conversion elements. Make sure that you are adding the annotation to all places where users can convert. This usually includes:
Conversion place | Example | |
---|---|---|
Search results list | Add the data-action attribute to all buttons on the search results page. |
Most sites include an "Add to cart" button next to each product directly in the list of search results. |
Product detail | Add the data-action annotation to appropriate buttons on the product detail page. |
This will usually be an "Add to cart" button. |
It is acceptable to use several different conversion types. Most common conversion types in e-commerce are:
- "buy"
- "wishlist"
Choose whatever names are convenient for you, you will be able to filter and segment on conversion type in Luigi's Box Search Analytics.
In some cases, you may want to send a negative conversion (i.e. user
clicks a thumbs-down button). We use negative conversions as additional signal
when learning optimal search ranking. Negative conversions must be annotated
with a data-action-attitude="negative"
annotation.
Conversion
<div data-action="buy">
Add to cart
</div>
<div data-action="wishlist">
Add to wishlist
</div>
Element | Hint |
---|---|
data-action="buy" |
Clicks anywhere within the element will be considered as conversion action with type "buy". |
data-action="wishlist" |
Clicks anywhere within the element will be considere as conversion action with type "wishlist". |
Negative Conversion
<div data-action="thumbs-down" data-action-attitude="negative">
Add to cart
</div>
Element | Hint |
---|---|
data-action-attitude="negative" |
Clicks anywhere within the element will be considere as a negative conversion actions. Note that you must still include data-action attribute for the click to be considered (negative) conversion. |
Autocomplete
Sample JSON+LD document for Autocomplete
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "SearchAction",
"query": "phone",
"result": {
"@type": "ItemList",
"name": "Autocomplete",
"itemListElement": [
{
"@type": "ListItem",
"position": "1",
"name": "Android Phone PX100",
"url": "https://myshop.com/products/236",
},
{
"@type": "ListItem",
"position": "2",
"name": "iPhone X",
"url": "https://myshop.com/products/293",
}
]
}
}
</script>
You should annotate Autocomplete results in the same way as regular search
results. We recommend that you create a separate <script
type="application/ld+json">
tag where you will describe your autocomplete
results. When you update Autocomplete results, you should also update the
JSON+LD document for the Autocomplete search. A good strategy is to assign the
script tag containing the Autocomplete JSON+LD a special id attribute and then
replace its contents with new JSON+LD when autocomplete results change.
Element | Hint |
---|---|
name |
It is important that the name attribute is set to "Autocomplete" to mark this as an autocomplete list. |
No search results
JSON+LD for no search results
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "SearchAction",
"query": "skirt",
"result": {
"@type": "ItemList",
"name": "Search Results",
"itemListElement": []
},
"instrument": [{
"@type": "ChooseAction",
"name": "Color",
"actionOption": "Black"
}]
}
</script>
When your search returns no results, you need to add a json+ld markup anyway. You must annotate the query, the used filters and the search name (Search Results/Autocomplete). Since your search returned no results, set itemListElement to empty array.
Element | Hint |
---|---|
itemListElement |
Notice that this attribute is present, but set to empty array since there are no results. |
Infinite scrolling
When your site is using infinite scrolling, you should update the JSON+LD document for regular search results. It is not necessary to build JSON+LD document for all visible search results — only build the JSON+LD for the search results that were freshly loaded.
Using custom identifiers to pair analytics data with catalog
Changing the identifiers requires several changes.
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "SearchAction",
"query": "phone",
"result": {
"@type": "ItemList",
"name": "Search Results",
"itemListElement": [
{
"@type": "ListItem",
"position": "1",
"name": "Android Phone PX100",
"url": "https://myshop.com/products/236",
"identifier": "SKU_9293"
},
{
"@type": "ListItem",
"position": "2",
"name": "iPhone X",
"url": "https://myshop.com/products/293",
"identifier": "SKU_1929"
}
]
}
}
</script>
1. Update the JSON+LD annotations to also include the item identifier. Use the identifier
key for each item inside itemListElement
. The value of the identifier will be used for pairing with catalog data and must be present in the catalog data.
<div data-lb-id="SKU_9293" class="product-result">
<h1>Android Phone PX100</h1>
<img src="thumbnail.png"/>
<p class="description">A nice and reliable phone</p>
</div>
</script>
2. Mark the HTML for each particular search result with the data-lb-id
annotation. Mark the element that is wrapping the item "tile" in search results.
<html>
<head>
<meta property="lb:id" content="SKU_9293">
...
</script>
3. On the item detail page (e.g. the product detail page), insert a <meta>
tag in the <head>
section to associate the item with the identifier.
All of the above changes are necessary to correctly link conversion attributions to searches using your provided item identifiers instead of URLs.
Inline schema.org annotations
A basic example
Simple product HTML code
<div id="product-6">
<a href="/products/6?ref=search" class="prod-name">Milk</a>
<span class="prod-price">25</div>
</div>
Inline schema.org annotations allow you to turn an unstructured HTML code, like the one in this example.
Machine-readable search results
<div itemscope itemtype="http://schema.org/SearchAction">
<meta itemprop="query" content="phone">
<div itemprop="result" itemscope itemtype="http://schema.org/ItemList">
<meta itemprop="name" content="Search Results">
<div id="product-6" itemprop="itemListElement" itemscope itemtype="http://schema.org/Product http://schema.org/ListItem">
<a href="/products/6?ref=search" class="prod-name" itemprop="name">Milk</a>
<meta itemprop="url" content="http://example.com/products/226">
<div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
<span class="prod-price" itemprop="price">25</div>
<meta itemprop="priceCurrency" content="EUR">
</div>
</div>
</div>
<div style="display:none !important;" itemprop="instrument" itemscope itemtype="http://schema.org/ChooseAction">
<meta itemprop="name" content="Color">
<meta itemprop="actionOption" content="Black">
</div>
</div>
Into a machine understandable code by adding just a few annotations
(itemscope
and itemprop
) telling what it describes — a list of search
results showing single product with a price.
This example is quite complex because it shows all concepts. The HTML annotations encode information about query, filters and search results. Please see the subsequent sections for more details.
Using annotations ¶
The itemscope
, itemtype
and itemprop
HTML attributes, you can add
deeper meaning and structure to the data. When using the schema.org
annotations, keep in mind these rules:
You create new scopes (contexts) by using
itemscope
&itemtype
attributes. These attributes convey that the element describes an item of a type (e.g, a Product) and its descendant nodes describe its properties.itemprop
– You tell that this element contains a property of the parent item. This can be a “text content” of a user visible element (price in the example), or it can be - defined using an invisible meta element (currency in the example).
Nesting structure
> SearchAction
- itemprop="query"
> itemprop="result", ItemList
- itemprop="name"
> itemprop="itemListElement", ListItem
- itemprop="name"
- itemprop="url"
- itemprop="position"
> itemprop="offers", Offer
- itemprop="price"
> itemprop="itemListElement", ListItem
...
> itemprop="instrument", ChooseAction
- itemprop="name"
- itemprop="actionOption"
> itemprop="instrument", ChooseAction
...
- Make sure that you adhere to the nesting rules. The top-level itemscope must be http://schema.org/SearchAction.
itemprop
values are extracted either from textContent of the element, e.g. give the HTML<span itemprop="name">Milk</span>
Milk will be used as"name"
attribute. Alternatively, you can use"content"
attribute to overide textContent, e.g.<span itemprop="name" content="Cheese">Milk</span>
.Visibility is not taken into account when parsing the data. The attributes must not be user-visible and can be safely hidden with CSS. You can also use a
<meta>
tag which is invisible by default. Just keep in mind that the meta tag must be open and has no textContent so you must use an explicit"content"
attribute.
Explicit trigger
An example to show you the logical flow of results rendering. In the example below, the script requests results asynchronously, then renders the results with annotations and only after the results are rendered, calls
Luigis.Scan
. You only need to use theLuigis.Scan
call with appropriate arguments, the rest of the code is just for demonstration.
<div id="search-results">
<div>
<script>
$.get('/search/results?q=iphone', function(data) {
renderResultsWithAnnotations(data);
Luigis.Scan('#search-results', '#search-results');
});
</script>
After rendering the inline schema.org annotation, you must call a notification function which will trigger data collection.
The notification function is called Luigis.Scan
and accepts 2 arguments:
Selector which must point to an HTML element which contains the root
SearchAction
annotation.Selector for an element which contains the actual user-visible search results. We need to find the actual search result elements so we can detect user interactions (clicks, conversions). This selector is optional, and by default set to
body
, meaning we are searching all body for search result elements, but we strongly suggest that you provide this selector as narrowly scoped as possible.
Since you are (and should be) loading the analytics script asynchronously, there is a possibility that when you call Luigis.Scan, the analytics script is not yet loaded and the function does not exist.
To prevent this situation, you must add the following code to the <head>
element of your website.
<script> window._lbcq = []; window.Luigis = window.Luigis || {}; window.Luigis.User = '...'; // Optionally, set your user identifier here; see below window.Luigis.Scan = window.Luigis.Scan || function(a, r) { window._lbcq.push([a, r]); } </script>
The code will define a simple implementation of the Luigis.Scan
function,
which will just add "scan" commands to a queue. When the integration script is
loaded it redefines the function with real implementation and processes the
queued commands.
Defining the simple implementation early will allow you to load the integration script asynchronously, without impacting your page load speed.
This is also the place to customize user ID used for search analytics via window.Luigis.User
property. Even though it is not necessary in most use cases, see User identifiers section for examples when it might be better for you.
Query
Schema.org annotations require strict nesting. The markup that defines search must be placed somewhere on top of the page so the results can be nested inside.
To annotate query, find a suitable HTML element which includes the list of results and place the query markup.
If your site has autocomplete, make sure that the autocomplete results/suggestions are not nested under the same SearchAction
as the regular search. It's usually not a problem, since most of the autocomplete libraries place the HTML markup for autocomplete results at the bottom of the HTML, just before the closing body tag.
Query markup
<body>
<nav>…</nav>
<section class="container-fluid">
<input type="text" name="q" value="phone"/>
<div itemscope itemtype="http://schema.org/SearchAction">
<meta itemprop="query" content="phone">
<div itemprop="result" itemscope itemtype="http://schema.org/ItemList">
…
</div>
</section>
</body>
Element | Hint |
---|---|
SearchAction |
This is a base schema.org entity encapsulating search. Everything related to search - query, results and filters must be nested under this entity. |
itemprop="query" |
We are showing the query back to the user in the text input field. It is repeated here just for the purpose of annotation. There's no need to show the query twice — we used an invisible meta tag. |
ItemList |
Results should be nested somewhere inside this SearchAction . |
Results
Make sure that in the HTML structure, the ItemList is nested under SearchAction
.
Sometimes the user can switch view type from list to grid. Make sure that items in all views are annotated.
Results markup
<div … itemprop="result" itemscope itemtype="http://schema.org/ItemList">
<meta itemprop="name" content="Search Results">
<a href="/products/236" … itemprop="itemListElement" itemscope itemtype="http://schema.org/Product http://schema.org/ListItem">
<meta itemprop="position" content="1">
<div itemprop="name">Android Phone PX100</div>
<meta itemprop="url" content="https://myshop.com/products/236">
<div style="display:none !important;" itemprop="offers" itemscope itemtype="http://schema.org/Offer">
<meta itemprop="priceCurrency" content="EUR">
<meta itemprop="price" content="120.00">
</div>
</a>
<a href="/products/237" … itemprop="itemListElement" itemscope itemtype="http://schema.org/Product http://schema.org/ListItem">
<meta itemprop="position" content="2">
…
</a>
<a href="/products/123" … itemprop="itemListElement" itemscope itemtype="http://schema.org/Product http://schema.org/ListItem">
<meta itemprop="position" content="3">
…
</a>
</div>
Element | Hint |
---|---|
ItemList |
This indicates a list of results. Notice that there are actually two things happening at once. We are annotating a result itemprop of the parent SearchAction and at the same time declare that the results are an ItemList entity. |
meta itemprop="name" |
A name for the list of search results. For search analytics purposes, this needs to be set to "Search Results" or "Autocomplete". |
ListItem |
This indicates an item in the list of search results — a search result. Each result has two schema.org types. It is a Product but also a ListItem at the same time. We only require that the item has ListItem type. You can also add additonal types, such as Product . Consult schema.org reference for list of supported types. |
position |
Invisible element with the result position. When you use pagination, this should be the position in the paginated list, e.g., assuming pages of 10 items, the first item on a 2nd page should be 11. |
name |
Product name - this is what you'll see in Luigi's Box dashboards. |
url |
Use canonical product URL here. Do not include tracking parameters, query, or other parameters that have no effect on which product is displayed. It's ok to use those parameters on the user clickable URL though, just don't use them here. |
offers |
schema.org requires that the price is nested inside Offer itemtype. |
price |
Make sure that the price parses as a floating point number. That means using dot (.) instead of comma (,) to separate the fractional part of the price. E.g., "24,3" is wrong and "24.3" is correct. Also using currency symbols here is invalid, the currency is already specified. |
Filters
Filters can get complicated very fast. When you use facets, price range selectors, sorting etc., there can be many filters each with a different HTML structure. You could annotate the filters in-place, but there is a much easier way. Put the markup for all active filters in a single place, inside invisible elements, somewhere inside the SearchAction
.
When you have a multi-value filter, e.g., a facet with checkboxes, just repeat the actionOption
meta tag.
Make sure that in the HTML structure, the ChooseActions
are nested under SearchAction
.
Filters markup
<div style="display:none !important;" itemprop="instrument"
itemscope itemtype="http://schema.org/ChooseAction">
<meta itemprop="name" content="Color">
<meta itemprop="actionOption" content="Black">
</div>
<div style="display:none !important;" itemprop="instrument"
itemscope itemtype="http://schema.org/ChooseAction">
<meta itemprop="name" content="Price">
<meta itemprop="actionOption" content="20-800">
</div>
<div style="display:none !important;" itemprop="instrument"
itemscope itemtype="http://schema.org/ChooseAction">
<meta itemprop="name" content="Sort">
<meta itemprop="actionOption" content="Price">
</div>
Element | Hint |
---|---|
instrument |
Each filter is an instrument of the SearchAction. |
ChooseAction |
Each filter is a nested schema.org entity — a ChooseAction. |
name |
The name itemprop relates to the ChooseAction scope and is a name of the filter. The value can be arbitrary, whatever you use, we'll show you in Luigi's Box analytics verbatim, so it's best to use a name that will make sense to you later. |
ActionOption |
The filter value. Again, the value is arbitrary, but it's best to use the exact same value that the user is seeing. |
Multi-value filters
<div style="display:none !important;" itemprop="instrument"
itemscope itemtype="http://schema.org/ChooseAction">
<meta itemprop="name" content="Brand">
<meta itemprop="actionOption" content="Samsung">
<meta itemprop="actionOption" content="Lenovo">
<meta itemprop="actionOption" content="Apple">
</div>
Element | Hint |
---|---|
actionOption/ChooseAction |
It's ok to repeat actionOptions, but it's not ok to have multiple ChooseAction-s with the same name. |
Autocomplete
The process of annotating autocomplete results is the same as with the regular Search Results.
You must create a separate SearchAction
that cannot be nested inside the SearchAction
for regular Search Results.
Autocomplete
<div itemscope itemtype="http://schema.org/SearchAction">
<div itemprop="query">phone</div>
<div itemprop="instrument" itemscope itemtype="http://schema.org/ChooseAction">
<meta itemprop="name" content="category">
<meta itemprop="actionOption" content="Phones">
</div>
<div itemprop="result" itemscope itemtype="http://schema.org/ItemList">
<meta itemprop="name" content="Autocomplete">
<a href="/products/236" … itemprop="itemListElement" itemscope itemtype="http://schema.org/Product http://schema.org/ListItem">
<meta itemprop="position" content="1">
<div itemprop="name">Android Phone PX100</div>
<meta itemprop="url" content="https://myshop.com/products/236">
<div style="display:none !important;" itemprop="offers" itemscope itemtype="http://schema.org/Offer">
<meta itemprop="priceCurrency" content="EUR">
<meta itemprop="price" content="120.00">
</div>
</a>
<a href="/products/237" … itemprop="itemListElement" itemscope
itemtype="http://schema.org/Product http://schema.org/ListItem">
…
</a>
<a href="/products/123" … itemprop="itemListElement" itemscope
itemtype="http://schema.org/Product http://schema.org/ListItem">
…
</a>
</div>
</div>
Element | Hint |
---|---|
SearchAction |
Autocomplete annotations must be nested under their own SearchAction. |
ChooseAction |
On the rare occasion that your autocomplete uses filters, include them here. See the image above for an example of autocomplete with filters. |
name |
We name the filter 'category'. |
ItemList |
The ItemList must be named Autocomplete. |
position |
Autocomplete items are usually not paginated, so the explicit item position is not necessary. |
offers OPTIONAL |
If your autocomplete items show prices, feel free to anotate them, they are not required in this context though. |
No results
No results markup
<div itemscope itemtype="http://schema.org/SearchAction">
<div itemprop="query">phone</div>
<div itemprop="instrument" itemscope itemtype="http://schema.org/ChooseAction">
<meta itemprop="name" content="category">
<meta itemprop="actionOption" content="Phones">
</div>
<div itemprop="result" itemscope itemtype="http://schema.org/ItemList">
<meta itemprop="name" content="Search Results">
<span>No results found!</span>
</div>
</div>
When your search returns no results, you need to add schema.org annotations anyway. You must annotate the query, the used filters and the search name (Search Results/Autocomplete). Since your search returned no results, do not include any ListItems
.
Make sure that you include the ItemList
scope though and set the correct name
itemprop
(either Search Results or Autocomplete).
Conversions
Use a FindAction
from schema.org to annotate conversion elements. Any element where click should be tracked can be annotated in the following manner, either in search results or on individual product pages:
There are usually many different types of conversions found at different places.
Conversion place | Example | |
---|---|---|
Search results list | Add the FindAction annotations to all buttons on the search results page. |
Most sites include an "Add to cart" button next to each product directly in the list of search results. |
Product detail | Add the FindAction annotation to appropriate buttons on the product detail page. |
This will usually be an "Add to cart" button. |
It is acceptable to use several different conversion types. Most common conversion types in e-commerce are "buy" and "wishlist". Choose whatever names are convenient for you, you will be able to filter and segment on conversion type in Luigi's Box Search Analytics.
Conversions markup
<button type="submit" itemscope itemtype="http://schema.org/FindAction">
<meta itemprop="name" content="buy">
<span>
Add to cart
</span>
</button>
Element | Hint |
---|---|
FindAction |
Each conversion must be nested under a FindAction . Think about it as an action you can do when you have found what you have been looking for. |
name |
Name of the action can be replaced with any text of choice, e.g. "favorite", "wishlist". |
In some cases, you may want to send a negative conversion (i.e. user clicks a thumbs-down button). We use negative conversions as additional signal when learning optimal search ranking.
Negative conversions markup
<button type="submit" itemscope itemtype="http://schema.org/FindAction">
<meta itemprop="name" content="thumbs-down">
<meta itemprop="additionalType" content="http://schema.org/DislikeAction">
<span …>
Add to cart
</span>
</button>
Element | Hint |
---|---|
name |
You can choose any conversion name that you like. |
additionalType |
To mark the conversion as negative, set additionalType to DislikeAction . |
Individual products
Although we do not need to know about individual products on their product pages apart from the annotations of the converting actions (buy, etc.), it may be beneficial to also annotate product pages, since external search engines and other tools do use them. Refer to example in the intro. Further information can be found here:
https://developers.google.com/search/docs/data-types/products
User identifiers
By default Luigi's Box Search Analytics script assigns each user a unique identifier and saves it in a _lb
cookie. We use this cookie to count various usage metrics in Luigi's Box application and it serves also as a basis for personalization of search and recommendation services. However, there are some use cases when it might be better to use your own unique user identifiers:
- If you would like to integrate our Search as a Service with personalization enabled or Recommender on backend without using our JavaScript libraries, using you own unique user identifiers enables you to use the services up to their full potential by sending user identifier also for the first visit of a user, when you do not have our
_lb
cookie identifier available on your backend. - If you know that most or all of your users are logged in while browsing your site, you may leverage your user identifiers to get insight into their behavior cross-device.
If you would like to set your own unique user identifiers add the following code to the
element of your website.<script> window._lbcq = []; window.Luigis = window.Luigis || {}; window.Luigis.User = '...'; // Your user identifier goes here window.Luigis.Scan = window.Luigis.Scan || function(a, r) { window._lbcq.push([a, r]); } </script>
If the window.Luigis.User
property is not set or empty the default behavior will be triggered and Luigi's Box Search Analytics script will assign a unique identifier as describe.
The window._lcbq
and window.Luigis.Scan
are part of asynchronous loading of the script and help to make sure everything works even in case Luigi's Box Search Analytics script is not yet loaded. See more details when implementing via embedded JSON+LD or via inline Schema.org annotations.
Frequent problems
Here's a list of frequent problems that we have noticed with implementation.
Malformed JSON in JSON+LD integration. We've seen many cases of malformed JSON, but the most common error is a trailing comma at the end of the JSON array. If you suspect that your generated JSON+LD is not valid, open the browser developer tools (usually by pressing F12 key) and switch to the Console tab. If we cannot parse the JSON, we will show an error message.
Bad or no results list name. We collect and analyze only searches where the ItemList name is present and set to "Search Results" or "Autocomplete" (mind the capitalization). If you set the list name to something else we will ignore that list altogether.
No annotations for "no search results". When your search returns no results, the backend system very often renders a completely different HTML template which is missing the annotations. Please check the corresponding section for JSON+LD no search results or inline annotations no search results.
Are you setting product positions with respect to pagination? Result position should be relative to full search results list. When you are showing 10 results per page, then on 2nd page in pagition, the products should be numbered 11–20. On 3rd page 21–30. A very frequent problem that we are seeing is that the positions on each page in pagination start from 1 again.
Query string is encoded. Sometimes we see that the query is URL encoded (percent-encoding) and it leads to double encoding since we encode the query again if necessary. It's best to use the query as is and leave encoding to us.
No conversion annotations. Very often, clients implement search results tracking and then forget about conversion tracking. Please check the corresponding section for JSON+LD conversions or inline annotations conversions.
No conversion annotations on product detail. Make sure to implement conversion tracking on both search results page and product detail page. Please check the corresponding section for JSON+LD conversions or inline annotations conversions.
No filter annotations. Very often filter annotations are missing, but they are as important as the query string. Consider that your query "skirt" is returning results and converting very well, but as soon as users check "Color: black" in your facets, your search returns no results. Filter are a natural part of the query and you should track them. Please check the corresponding section for JSON+LD filters or inline annotations filters.
Troubleshooting
While you are developing the integration, we recommend that you turn on data linter to see debugging information. Make sure that Luigi's Box integration script is included in the page and then, in your web browser, open the developer console (usually by pressing the F12 key), find the "Console" tab and type in the following command: Luigis.lint = true
After that, reload the page, but do not close the developer console. Each time, the integration collects search-related data, you will see what was parsed from your site and you'll get a report about data quality in the console tab of the developer tools.
If you've done everything correctly, you should see a blue Luigi's Box logo. If there were some problems with the data, you will see a red logo and a list of errors.
If you are not seeing the linter messages and logos, the most probable cause is that you are already running an early version of integration that does not support linting. Let us know and we will upgrade your integration.
Support
Troubles? Different nesting? Cannot get it to work? Contact us at support@luigisbox.com, we are glad to help!