{"product_id":"2026-09-14","title":"[Horizon] Reflect metafield content in structured data (JSON-LD) too","description":"\u003cp\u003e\u003cstrong style=\"color:#e0301e;\"\u003e[Why structured data still matters now]\u003c\/strong\u003e In 2026, Shopify introduced UCP (Universal Commerce Protocol) as a dedicated channel for AI agents to fetch product info for agentic commerce — a purpose-built API endpoint where agents can pull price, inventory, and product detail. But UCP is only used by agents that deliberately implement the protocol (every request requires JSON-RPC formatting and an agent profile). Search engine crawlers that don't support UCP, and any AI agent that just fetches the page directly, still rely on the structured data (schema.org \/ JSON-LD) already on the page.\u003c\/p\u003e\n\u003cp\u003eA structured-data specialist makes the same point.\u003cbr\u003e\n\u003cem\u003e\"Structured data remains the main foundation. Before an AI agent can buy a product or book a service, it has to find it.\"\u003c\/em\u003e\u003c\/p\u003e\n\n\u003cp\u003e\u003cspan\u003eIn the previous TIPS (\u003ca href=\"\/en\/products\/2026-09-13\"\u003eshowing different collapsible-row content per product or per category\u003c\/a\u003e), we used product metafields and a metaobject to control what's visible on the page. That's only the \"visible\" side of things, though — it isn't reflected in the structured data (schema.org \/ JSON-LD) that search engines and AI agents read. This TIPS shows how to dynamically merge that same metafield content into the structured data too.\u003c\/span\u003e\u003c\/p\u003e\n\n\u003cp\u003e\u003cstrong style=\"color:#e0301e;\"\u003e[Important] The visible content and the structured data must always come from the same value.\u003c\/strong\u003e Avoid designing a separate description specifically \"for AI agents\" that differs from what's shown on the page. Google explicitly states that structured data must be a true representation of the content on the page, and that you must not mark up content that isn't visible to users. This implementation naturally satisfies that principle, because it reuses the exact same metafield\/metaobject value that's already shown on the page.\u003c\/p\u003e\n\n\u003cp\u003e\u003cimg alt=\"Whatever is shown on the page also flows into the structured data (JSON-LD)\" src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0733\/0381\/8491\/files\/2026-09-14_1_en.png?v=1789305781\" class=\"zoomable-image\" style=\"max-width: 100%; height: auto;\"\u003e\u003c\/p\u003e\n\n\u003cdiv class=\"divider\"\u003e\u003c\/div\u003e\n\n\u003ch3\u003eWhy Horizon's built-in mechanism isn't enough on its own\u003c\/h3\u003e\n\u003cp\u003e\u003cspan\u003eHorizon's structured data is generated with a single line: \u003ccode\u003e{{ closest.product | structured_data }}\u003c\/code\u003e. This filter returns a complete JSON string that already includes price, inventory, and variants — but it just returns it as-is, with no way to add fields afterward. And since Liquid has no filter to parse a string back into JSON (no equivalent of \u003ccode\u003eJSON.parse\u003c\/code\u003e in other languages), there's no way to decompose and rebuild that string in Liquid alone.\u003c\/span\u003e\u003c\/p\u003e\n\u003cp\u003e\u003cspan\u003eThat \u003ccode\u003e\u0026lt;script type=\"application\/ld+json\"\u0026gt;\u003c\/code\u003e tag itself lives inside the core file \u003ccode\u003esections\/product-information.liquid\u003c\/code\u003e — but you don't need to touch it or add an id to it. Inside the new block, \u003ccode\u003edocument.currentScript\u003c\/code\u003e (the currently-executing script element itself) lets you walk up to \u003ccode\u003e.closest('.shopify-section')\u003c\/code\u003e and find the existing ld+json script within the same section, without modifying the core file at all. Because of this mechanism, the block must be placed somewhere inside the \"Product information\" section — placing it in a different section means no ld+json exists in that section, so it silently does nothing.\u003c\/span\u003e\u003c\/p\u003e\n\u003cp\u003e\u003cspan\u003eThe \"extra properties\" side doesn't depend on DOM ordering (e.g. being adjacent to anything) at all. The JSON island carries a unique \u003ccode\u003eid=\"schema-extra-properties-{{ block.id }}\"\u003c\/code\u003e, and \u003ccode\u003edocument.getElementById\u003c\/code\u003e looks it up directly — so no matter how many other blocks (including ones with their own script tags) sit before or after it in the same section, it always finds its own.\u003c\/span\u003e\u003c\/p\u003e\n\n\u003cp\u003e\u003cimg alt=\"How it works: one metafield, two destinations\" src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0733\/0381\/8491\/files\/2026-09-14_2_en.png?v=1789305791\" class=\"zoomable-image\" style=\"max-width: 100%; height: auto;\"\u003e\u003c\/p\u003e\n\n\u003ch3\u003eThe solution: merge into the JSON-LD in the browser\u003c\/h3\u003e\n\u003cp\u003e\u003cspan\u003eWe leave the filter's output untouched, and separately have Liquid output just the \"extra properties\" we want to add as JSON. On page load, JavaScript merges the two together. Google officially states that it can process structured data present in the DOM at render time, so this approach is correctly picked up by both rich results and AI agents.\u003c\/span\u003e\u003c\/p\u003e\n\u003cp\u003e・schema.org's Product type has no dedicated property for \"extra info added via a metafield,\" so we use \u003ccode\u003eadditionalProperty\u003c\/code\u003e (an array of \u003ccode\u003ePropertyValue\u003c\/code\u003e objects), which exists exactly for this purpose.\u003c\/p\u003e\n\u003cp\u003e・Google also notes that dynamically-generated markup can be crawled somewhat less often and less reliably. Fast-changing fields like price and inventory should stay in the filter's static output untouched; appending supplementary info the way we do here keeps the impact limited.\u003c\/p\u003e\n\n\u003ch3\u003eSetup\u003c\/h3\u003e\n\u003cp\u003e\u003cstrong style=\"color:#e0301e;\"\u003e[Key point] This never touches a core file (sections\/product-information.liquid or any other Horizon-provided file).\u003c\/strong\u003e You add one new private block and attach it to the \"Product information\" section in the theme editor — that's the whole change.\u003c\/p\u003e\n\u003cp\u003e◆1. In the code editor, create \u003ccode\u003eschema-additional-property.liquid\u003c\/code\u003e in the \u003ccode\u003eblocks\u003c\/code\u003e directory and paste in the code (shown after purchase).\u003c\/p\u003e\n\n\u003cp\u003e\u003cimg alt=\"New file: blocks\/schema-additional-property.liquid\" src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0733\/0381\/8491\/files\/2026-09-14_3_en_0dcafdc7-ce8c-4174-9d3a-aadde6dceacc.png?v=1789335814\" class=\"zoomable-image\" style=\"max-width: 100%; height: auto;\"\u003e\u003c\/p\u003e\n\n\u003cp\u003e◆2. In the theme editor, open a product page, and under the \"Product information\" section's \"Add block,\" add the \"Reflect additional info in structured data\" block. \u003cstrong style=\"color:#e0301e;\"\u003eIt must be placed somewhere inside the \"Product information\" section\u003c\/strong\u003e — but the exact position within it (relative to other blocks, nesting depth) doesn't matter. This block renders nothing visible.\u003c\/p\u003e\n\n\u003cp\u003e◆3. In the config section at the top of the code, rewrite the metafield keys to match your own store.\u003c\/p\u003e\n\u003cp\u003e・\u003ccode\u003eschema_product_description\u003c\/code\u003e: converts a rich-text product metafield to plain text via the \u003ccode\u003emetafield_text\u003c\/code\u003e filter\u003cbr\u003e\n・\u003ccode\u003eschema_category_guide_text\u003c\/code\u003e: does the same for the rich-text field inside the metaobject referenced by a metaobject-reference metafield\u003c\/p\u003e\n\u003cp\u003e\u003cstrong style=\"color:#e0301e;\"\u003e[Note] A rich-text metafield is NOT correctly converted to plain text by \u003ccode\u003e.value\u003c\/code\u003e or \u003ccode\u003estrip_html\u003c\/code\u003e alone.\u003c\/strong\u003e (During testing, this actually produced the raw rich-text data structure instead of plain text.) Always use the \u003ccode\u003emetafield_text\u003c\/code\u003e filter.\u003c\/p\u003e\n\n\u003ch3\u003eHow to verify it\u003c\/h3\u003e\n\u003cp\u003eAfter deploying, open the product page, open DevTools via \"Inspect,\" and run the following in the Console to see the merged content directly.\u003c\/p\u003e\n\u003cp\u003e\u003ccode\u003eJSON.parse(document.querySelector('script[type=\"application\/ld+json\"]').textContent).additionalProperty\u003c\/code\u003e\u003c\/p\u003e\n\u003cp\u003e\u003cstrong style=\"color:#e0301e;\"\u003e[Note] \"View Page Source\" will NOT show this.\u003c\/strong\u003e The merge happens via JavaScript rewriting the DOM after the page loads, so it never appears in the initial HTML the server returns. Always check the post-execution DOM via \"Inspect\" or the console.\u003c\/p\u003e\n\n\u003ch3\u003eCustomization\u003c\/h3\u003e\n\u003cp\u003e\u003cspan\u003eThis example uses a product metafield and a metaobject-reference metafield, but you can reflect any other metafield the same way by adding another entry to the \u003ccode\u003eadditionalProperty\u003c\/code\u003e array.\u003c\/span\u003e\u003c\/p\u003e\n\n\u003ch3\u003eNotes\u003c\/h3\u003e\n\u003cp\u003e・Google itself notes that dynamically-generated structured data can be somewhat less crawl-frequent and reliable. Don't use this approach for fast-changing data like price or inventory\u003cbr\u003e\n・A metafield with no value simply doesn't output an \u003ccode\u003eadditionalProperty\u003c\/code\u003e entry, so no empty strings are left behind in the schema\u003c\/p\u003e\n\n\u003cp\u003e\u003cspan\u003eSee \u003ca href=\"\/en\/products\/2026-09-13\"\u003ethis TIPS\u003c\/a\u003e for how to show different collapsible-row content per product or per category in the first place.\u003c\/span\u003e\u003c\/p\u003e\n\n\u003cp\u003e\u003cspan\u003eReferences:\u003cbr\u003e\n\u003ca href=\"https:\/\/developers.google.com\/search\/docs\/appearance\/structured-data\/generate-structured-data-with-javascript\" target=\"_blank\" rel=\"noopener\"\u003eGoogle: Generate Structured Data with JavaScript\u003c\/a\u003e\u003cbr\u003e\n\u003ca href=\"https:\/\/developers.google.com\/search\/docs\/appearance\/structured-data\/sd-policies\" target=\"_blank\" rel=\"noopener\"\u003eGoogle: General Structured Data Guidelines\u003c\/a\u003e\u003cbr\u003e\n\u003ca href=\"https:\/\/shopify.dev\/docs\/agents\" target=\"_blank\" rel=\"noopener\"\u003eShopify: Build commerce agents with UCP\u003c\/a\u003e\u003cbr\u003e\n\u003ca href=\"https:\/\/www.structureddata.co.uk\/blog\/structured-data-webmcp-ucp-agentic-ecommerce\" target=\"_blank\" rel=\"noopener\"\u003eHow Structured Data, WebMCP, and UCP Form the Trinity of Agentic E-Commerce\u003c\/a\u003e\u003c\/span\u003e\u003c\/p\u003e\n","brand":"STORE DOJO","offers":[{"title":"Default Title","offer_id":49305294995707,"sku":null,"price":0.0,"currency_code":"USD","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0733\/0381\/8491\/files\/2026-09-14_614d443c-0520-4543-8dad-e09f3d02e211.jpg?v=1789306342","url":"https:\/\/store-dojo.com\/en-us\/products\/2026-09-14","provider":"STORE DOJO","version":"1.0","type":"link"}