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:

  1. Using an inline markup and adding additional HTML attributes where appropriate.

  2. 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.

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
offersOPTIONAL An Offer object indicating price

Supported itemListElement @types

We currently support these standard schema.org types:

  • Product for products
  • CollectionPage for categories
  • Brand for brands
  • Article for articles/blogs/pages
  • SearchAction for queries (e.g., if you are suggesting phrases)

instruments (filters)

Element Hint
instrumentREQUIRED This field contains an array of filters.
instrument.@typeREQUIRED Marks this as filter in schema.org vocabulary. Must be ChooseAction.
instrument.nameREQUIRED The filter name. You can use whatever name fits best.
instrument.actionOptionREQUIRED 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
RecommenderClientIdREQUIRED 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).
ItemIdsoptional List of input items of a recommendation request (item_ids from recommendation request).
Recommenderoptional Name of the recommender (recommender from recommendation result).
Typeoptional Type of the recommender (recommendation_type from recommendation result).
_Variantoptional 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 the Luigis.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:

  1. Selector for annotations which must point to a <script> element with JSON+LD annotations

  2. 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.

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.

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:

  1. If you would like to integrate Luigi's Box 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.
  2. 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.

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.

  • 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.

  • 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.

  • 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.

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.

Left side: no errors found.  Right side: linter found some errors.

Support

Troubles? Different nesting? Cannot get it to work? Contact us at support@luigisbox.com, we are glad to help!