Documentation

Integration manual

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?”
Search results

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

  • 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., image a scenario when using an advanced search and the user types no query, but chooses to search for all products in stock. 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 send it.

Filters

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.

Not 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:
    • 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.
    In all cases, the canonical URL should be
    https://www.example.com/products/black-shirt
  • 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 be always 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 any price.

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 results will have position 1, the last 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 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.
Searchbox

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

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 get a report about data quality in the console tab of the developer tools.

A basic example

The sample document below 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. Click the hints on the right to learn more.

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",
        "position": "1",
        "name": "Android Phone PX100",
        "url": "https://myshop.com/products/236",
        "offers": {
          "@type": "Offer",
          "priceCurrency": "EUR",
          "price": "120"
        }
      },
      {
        "@type": "ListItem",
        "position": "2",
        "name": "iPhone X",
        "url": "https://myshop.com/products/293",
        "offers": {
          "@type": "Offer",
          "priceCurrency": "EUR",
          "price": "999"
        }
      }
    ]
  },
  "instrument": [{
    "@type": "ChooseAction",
    "name": "Sort by",
    "actionOption": "Relevance"
  },
  {
    "@type": "ChooseAction",
    "name": "Color",
    "actionOption": ["Black", "Silver"]
  }]
}
</script>
Hints
The JSON+LD document must be embedded inside a script tag with type="application/ld+json"
The special @context attributes marks this as schema.org-compatible
The special @type attributes marks this as search-related information in the shema.org vocabulary
The query that the user entered.
This field is called result in schema.org, which is a bit confusing since it will contain an array of results.
The list name. Valid values here are "Search Results" for regular search results andAutocomplete" for autocomplete results.
The instrument field contains an array of filters.
A special @type attribute marks this as filter in schema.org vocabulary.
This is the filter name.
When a single filter has multiple values, you can include all values at once in an array.

Conversions

Conversion actions cannot be embedded directly in the JSON+LD document, so you'll need to add HTML data- attributes to conversion elements.

Conversion
<div data-action="buy">
    Add to cart
</div>
<div data-action="wishlist">
    Add to wishlist
</div>
Hints
The data-action="buy" code will consider clicks anywhere within the element as conversion action with type "buy".
The data-action="wishlist" code will consider clicks anywhere within the element as conversion action with type "wishlist".

Autocomplete

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.

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.

Inline schema.org annotations

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 get a report about data quality in the console tab of the developer tools.

A basic example

Simple product
<div id="product-123">
    <div class="prod-name">Milk</div>
    <span class="prod-price">25</div>
</div>

can be made readable by software by adding just a few simple annotations telling what it describes — a product with a price:

Machine-readable product
<div id="product123" itemscope itemtype="http://schema.org/Product">
    <div class="prod-name" itemprop="name">Milk</div>
    <div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
        <span class="prod-price" itemprop="price">25</div>
        <meta itemprop="priceCurrency" content="EUR">
    </div>
</div>
Hints
These two HTML attributes mark that this page (and nested HTML) contains a product. There are many types of entities that you can declare (e.g., an Article, Book, Recipe…), but in the context of e-shop, we'll work with just the Product entity.
This itemprop attribute declares that the Product you are annotating has a name and it is Milk. There are multiple ways that you can add value to an attribute, the easiest way is to use the textContent of the HTML node, like we did in this example.
Annotating the price is a bit more complicated since it involves declaring a price as a numeric value, and declaring the currency the price is in. Schema.org reserves a special itemtype Offer for this purpose.
This declares a price using the same mechanism as we used for product name annotation.
We were not displaying the currency in the original markup, so let's not display it in the annotated machine-readable version either. The meta HTML tag is hidden by default. Also notice that unlike in the previous examples, we are assigning the priceCurrency value not by using a textContent of the meta element, but instead by using another attribute called content.

The example above has shown all the basics. Using annotations:

  • itemscope & itemtype – You tell 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 or link element (currency in the example).

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.

Find a suitable HTML element which includes the list of results and place the following markup.

Query
<body>
    <nav>…</nav>
    <section class="container-fluid">
        <input type="text" name="q" value="phone"/>
        <div itemscope itemtype="http://schema.org/SearchAction">
            <meta itemprop="query">phone</meta>
            <div itemprop="result" itemscope itemtype="http://schema.org/ItemList">
                …
            </div>
    </section>
</body>
Hints
SearchAction is a base schema.org entity encapsulating search. Everything related to search - query, results and filters must be nested under this entity.
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. You can also use the content attribute, e.g.:
<meta itemprop="query" content="phone">
Results should be nested somewhere inside the SearchAction (they don't need to have a class '.result', it's just an example here).

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.

Results

Results
<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>
Hints
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.
A name for the list of search results. For search analytics purposes, this needs to be set to "Search Results".
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.
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.
Product name - this is what you'll see in Luigi's Box dashboards.
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.
schema.org requires that the price is nested inside Offer itemtype.

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.

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.

Filters
<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>
Hints
Each filter is an instrument of the SearchAction.
Each filter is a nested schema.org entity — a ChooseAction.
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.
The actionOption is a filter value. Again, the value is arbitrary, but it's best to use the exact same value that the user sees.
Here we have declared a filter called 'Sort'. The name is arbitrary, but notice that even a sort is a filter. Anything that has influence on which items you show is a filter.

When you have a multi-value filter, e.g., a facet with checkboxes, just repeat the actionOption meta tag.

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>
Hints
It's ok to repeat actionOptions, but it's not ok to have multiple ChooseAction-s with the same name.

Make sure that in the HTML structure, the ChooseActions are nested under SearchAction!

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>
Hints
Autocomplete annotations must be nested under their own SearchAction.
On the rare occasion that your autocomplete uses filters, include them here. See the image above for an example of autocomplete with filters.
In the image above, you can see that the filter has no visible name, but we name it 'category'.
The ItemList must be named Autocomplete.
Autocomplete items are usually not paginated, so the explicit item position is not necessary.
If your autocomplete items show prices, feel free to anotate them, they are not required in this context though.

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:

Conversions
<button type="submit" itemscope itemtype="http://schema.org/FindAction">
    <meta itemprop="name" content="buy">
    <span …>
        Add to cart
    </span>
</button>
Hints
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 of the action can be replaced with any text of choice, e.g. "favorite", "wishlist".

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. Annotate each one with a different name.

Individual products

Although our collector framework does not need to know about individual products on their product pages apart from the annotations of the converting actions (buy, etc.), since we derive all the attributes from search results we saw, 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.

Support

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