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#
priceCurrencyis hard-coded toUSD. 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
nameandurlfrom each product or category render, so extra product fields don’t change theListItemoutput. - Keep the output valid JSON. A trailing comma or an unescaped value makes the whole block invalid, and search engines ignore it.