Created by potrace 1.16, written by Peter Selinger 2001-2019

Miva Developer Docs


Miva 26.03.00 adds JSON-LD structured data output to 3 Shadows flex components. All 3 use the same Structured Data (JSON-LD) fragment when their Structured Data mode is set to Fragment.

Component Schema
mmx-product-details Product
mmx-product-list ItemList of products
mmx-category-list ItemList of categories

Structured Data (JSON-LD) fragment#

The fragment’s output must be a single JSON object without the <script> tag. The component adds the <script type="application/ld+json"> tag. To output nothing for a context, leave the output empty. Use &mvtj: entities so values are encoded for JSON.

The fragment is rendered more than once per page: on the server when the page loads, and again through the JSON API after the component loads its products or categories on the client. l.settings:structured_data:context tells the fragment what it’s rendering:

Context Output Used by
product_details A Product mmx-product-details on the server. The component outputs it as is.
json_api_product_iterator A Product mmx-product-details again once the product loads. mmx-product-list once per loaded product, keeping each product’s name and url for its ListItem.
product_list An ItemList with only a name mmx-product-list on the server. The name is the list’s heading or its category.
category_list An ItemList with only a name mmx-category-list on the server. The name is the list’s heading or the parent category.
json_api_category_iterator A name and url mmx-category-list once per loaded category, for its ListItem.

For JSON API renders, the default fragment sets the context itself. It checks whether s.script_name contains json.mvc, then uses json_api_product_iterator when l.settings:product:id is set, or json_api_category_iterator when l.settings:category:id is set.

After the products or categories load, the list components replace the server-side ItemList with the full list, with 1 ListItem per product or category.

Variables#

Variable Description
l.settings:structured_data:context The render context. See the table above.
l.settings:structured_data:list_name The list name for the product_list and category_list contexts. When it’s empty, the default fragment outputs nothing.
l.settings:product The product record in the product contexts. Fields include id, code, sku, name, descrip, price, sale_price, link, url and inv_level.
l.settings:category The category record. In the category iterator context it’s the category being listed. In the product contexts it’s the current category, when there is one.
g.Store The store record. The default fragment uses g.Store:name for the brand fallback and the seller name.
g.baseurl The store’s base URL. The default fragment uses it to build the image URL.

For a product, the default fragment copies the product into l.settings:json_ld:product and adds description, sku, url, price, image, a brand custom field value and availability.

Default fragment#

The fragment follows Google’s Product snippet, merchant listing and carousel guidance.

<mvt:comment>
|
| Structured Data (JSON-LD)
|
| Use by the "Fragment" Structured Data mode of mmx-product-details, mmx-product-list, and mmx-category-list
|
| Output a single JSON object without the <script> tag, or nothing to omit its output.
|
| l.settings:structured_data:context says what is being rendered.
|
| json_api_category_iterator
|     Format: https://schema.org/ListItem (name and url)
|     mmx-category-list: one ListItem per loaded category.
|
| product_details & json_api_product_iterator
|     Format: https://schema.org/Product
|     mmx-product-details: outputs it as-is, server-side and again once the product loads.
|     mmx-product-list: keeps each product's name and url for its client-side ListItem output.
|
| product_list
|     Format: https://schema.org/ItemList (name only)
|     mmx-product-list: server-side, named after the heading or the list's category. Replaced by the full list once the products load.
|
| category_list
|     Format: https://schema.org/ItemList (name only)
|     mmx-category-list: server-side, named after the heading or the parent category. Replaced by the full list once the categories load.
|
| Follows Google's Product snippet / merchant listing and carousel (summary page) guidance:
| https://developers.google.com/search/docs/appearance/structured-data/product-snippet
| https://developers.google.com/search/docs/appearance/structured-data/merchant-listing
| https://developers.google.com/search/docs/appearance/structured-data/carousel
|
</mvt:comment>

<mvt:assign name="l.is_json_api" value="'json.mvc' IN s.script_name" />

<mvt:if expr="l.is_json_api">
    <mvt:if expr="NOT ISNULL l.settings:product:id">
        <mvt:assign name="l.settings:structured_data:context" value="'json_api_product_iterator'" />
    <mvt:elseif expr="NOT ISNULL l.settings:category:id">
        <mvt:assign name="l.settings:structured_data:context" value="'json_api_category_iterator'" />
    </mvt:if>
</mvt:if>

<mvt:if expr="l.settings:structured_data:context EQ 'json_api_category_iterator'">
{
    "name": "&mvtj:category:name;",
    "url": "&mvtj:category:url;"
}
<mvt:elseif expr="l.settings:structured_data:context EQ 'product_list' OR l.settings:structured_data:context EQ 'category_list'">
    <mvt:if expr="NOT ISNULL l.settings:structured_data:list_name">
{
    "@context": "https://schema.org",
    "@type": "ItemList",
    "name": "&mvtj:structured_data:list_name;"
}
    </mvt:if>
<mvt:elseif expr="( l.settings:structured_data:context EQ 'json_api_product_iterator' OR l.settings:structured_data:context EQ 'product_details' ) AND NOT ISNULL l.settings:product:id">
    <mvt:comment>Core Fields</mvt:comment>
    <mvt:assign name="l.settings:json_ld:product" value="l.settings:product" />
    <mvt:assign name="l.settings:json_ld:product:description" value="miva_html_strip( l.settings:json_ld:product:descrip, '' )" />
    <mvt:assign name="l.settings:json_ld:product:sku" value="ternary( ISNULL l.settings:json_ld:product:sku, l.settings:json_ld:product:code, l.settings:json_ld:product:sku )" />
    <mvt:assign name="l.settings:json_ld:product:url" value="ternary( ISNULL l.settings:json_ld:product:link, l.settings:json_ld:product:url, l.settings:json_ld:product:link )" />
    <mvt:assign name="l.settings:json_ld:product:price" value="rnd( ternary( ISNULL l.settings:json_ld:product:sale_price, l.settings:json_ld:product:price, l.settings:json_ld:product:sale_price ), 2 )" />

    <mvt:comment>Load "main" Image</mvt:comment>
    <mvt:do file="g.Module_Library_DB" name="l.loaded_image_type" value="ImageType_Load_Code( 'main', l.image_type )" />
    <mvt:if expr="l.loaded_image_type">
        <mvt:do file="g.Module_Library_DB" name="l.loaded_product_image" value="ProductImage_Load_Type( l.settings:json_ld:product:id, l.image_type:id, l.product_image )" />
        <mvt:if expr="l.loaded_product_image">
            <mvt:do file="g.Module_Library_DB" name="l.null" value="Image_Load_ID( l.product_image:image_id, l.image )" />
            <mvt:assign name="l.settings:json_ld:product:image" value="g.baseurl $ l.image:image" />
        </mvt:if>
    </mvt:if>

    <mvt:comment>Brand</mvt:comment>
    <mvt:item name="customfields" param="Read_Product_ID( l.settings:json_ld:product:id, 'brand', l.settings:json_ld:product:customfield_values:customfields:brand )" />
    <mvt:if expr="ISNULL l.settings:json_ld:product:customfield_values:customfields:brand">
        <mvt:assign name="l.settings:json_ld:product:customfield_values:customfields:brand" value="g.Store:name" />
    </mvt:if>

    <mvt:comment>Availability</mvt:comment>
    <mvt:if expr="l.settings:json_ld:product:inv_level EQ 'low'">
        <mvt:assign name="l.settings:json_ld:product:availability" value="'https://schema.org/LimitedAvailability'" />
    <mvt:elseif expr="l.settings:json_ld:product:inv_level EQ 'out'">
        <mvt:assign name="l.settings:json_ld:product:availability" value="'https://schema.org/OutOfStock'" />
    <mvt:else>
        <mvt:assign name="l.settings:json_ld:product:availability" value="'https://schema.org/InStock'" />
    </mvt:if>

{
    "@context": "https://schema.org",
    "@type": "Product",
    "name": "&mvtj:json_ld:product:name;",
    "description": "&mvtj:json_ld:product:description;",
    "sku": "&mvtj:json_ld:product:sku;",
    <mvt:if expr="NOT ISNULL l.settings:json_ld:product:image">
        "image": [ "&mvtj:json_ld:product:image;" ],
    </mvt:if>
    <mvt:if expr="NOT ISNULL l.settings:category:name">
        "category": "&mvtj:category:name;",
    </mvt:if>
    "brand": {
        "@type": "Brand",
        "name": "&mvtj:json_ld:product:customfield_values:customfields:brand;"
    },
    "offers": {
        "@type": "Offer",
        <mvt:if expr="NOT ISNULL l.settings:json_ld:product:url">
            "url": "&mvtj:json_ld:product:url;",
        </mvt:if>
        "price": "&mvtj:json_ld:product:price;",
        "priceCurrency": "USD",
        "availability": "&mvtj:json_ld:product:availability;",
        "itemCondition": "https://schema.org/NewCondition",
        "seller": {
            "@type": "Organization",
            "name": "&mvtj:global:Store:name;"
        }
    }
}
</mvt:if>

Customizing the fragment#

  • priceCurrency is hard-coded to USD. Stores that sell in another currency need to change it.
  • The brand comes from a product custom field with the code brand. If the field is empty, the store name is used.
  • The list components use only name and url from each product or category render, so extra product fields don’t change the ListItem output.
  • Keep the output valid JSON. A trailing comma or an unescaped value makes the whole block invalid, and search engines ignore it.