Build structured data feeds for AI answer engines
On this page
Use this to build a custom structured data feed, in whatever schema.org format an AI answer engine expects, from any content your store publishes. The pattern below applies equally to a product catalog, a services list, store locations, solution or landing pages, or any other Salesforce-backed content, not only products.
What StoreConnect generates automatically
Two things are automatic today, and only apply where the underlying content exists:
- Product structured data —
ProductandProductGroupJSON-LD (withOffer,Brand, and variant fields) is rendered automatically on product detail pages. This only applies if your store sells products through the StoreConnect catalog. See Product rich data snippet to override the built-in output. BreadcrumbList— rendered automatically on product, category, and search pages, to describe site navigation hierarchy.
If your site is service-based, location-based, or otherwise doesn’t rely on the product catalog, neither of these applies to you. Everything below is how you build the equivalent yourself, for any content type.
Before you begin
Decide what content you want to expose and to which schema.org type it maps, for example:
- A product catalog →
Product,ProductGroup, orItemListof products. - A services list, store locations, or solution pages →
Service,LocalBusiness, or a genericItemListof those records. - A blog or knowledge base →
Article, or anItemListof articles. - Common questions on a page →
FAQPage.
None of these beyond Product/ProductGroup/BreadcrumbList are generated automatically. You build them the same way regardless of content type: query the backing records, then render valid schema.org output yourself.
Enumerate products for the current store
Product2 has no store-scoping field, so there is no single lookup that tells you which products belong to a store. A product is on a store only when both are true: it has an active Price Book Entry in the Pricebook assigned to that store, and it is assigned to a Product Category whose Taxonomy belongs to that store. A Price Book Entry on its own is not enough.
The all_products global already applies both rules, so it is the collection to build a feed from:
```liquid
{%- paginate all_products by 20 -%} {%- for product in all_products -%} {{ product.name }} {%- endfor -%} {%- endpaginate -%} ```
If you need the price record itself rather than the product, loop current_pricebook.pricebook_entries and read entry.product. The Pricebook is tied to the store, but its entries are not filtered by Taxonomy, so this collection can include products that are not on the store. Check each product against the store’s categories before emitting it:
```liquid
{%- paginate current_pricebook.pricebook_entries by 20 -%} {%- for entry in current_pricebook.pricebook_entries -%} {%- assign product = entry.product -%} {{ product.name }} {%- endfor -%} {%- endpaginate -%} ```
:::warning
all_products and pricebook_entries are both PaginatedList collections. A plain {% for %} loop over one, without wrapping the loop in paginate, silently returns zero items; it does not lazily fetch results on its own. Wrap the loop in {% paginate <collection> by <page_size> %} ... {% endpaginate %}, and loop the same collection name inside the block, not a paginate.items accessor.
:::
For a custom object backing services, locations, or other non-catalog content, use the query tag instead, since those objects don’t go through a Pricebook:
```liquid
{%- query ‘Your_Custom_Object__c’ as records, s_c__store__c: current_store.id -%} {%- for record in records -%} {{ record.name }} {%- endfor -%} ```
The store-scoping field shown here (s_c__store__c) is illustrative. Confirm the actual field name for your object against a live org’s schema before relying on it; it varies by object, and some objects scope to a store through a junction rather than a direct lookup.
Serve the feed
Create a content page with a file extension in its Path field (for example .json or .xml) so it renders as raw, layout-free output instead of themed HTML. Build the schema.org structure directly in the page’s Content Body using Liquid:
```liquid
{ “@context”: “https://schema.org”, “@type”: “ItemList”, “itemListElement”: [ {%- assign i = 0 -%} {%- paginate all_products by 20 -%} {%- for product in all_products -%} {%- assign i = i | plus: 1 -%} { “@type”: “ListItem”, “position”: {{ i }}, “name”: “{{ product.name }}”, “url”: “{{ product.url }}” }{% unless forloop.last %},{% endunless %} {%- endfor -%} {%- endpaginate -%} ] } ```
:::warning
StoreConnect does not automatically serialize this output. Your template is responsible for producing syntactically valid JSON or XML, including correct comma placement between loop iterations (as in the forloop.last check above) and escaping any field value that could contain a quote or control character.
:::
This example builds an ItemList of products, but the same page-plus-query pattern (see the previous section) builds a feed of services, locations, or any other record type. A non-catalog example (a location or service listing) follows the identical shape: query the object holding that content, loop, and emit the schema.org type that matches it (LocalBusiness, Service, or a generic ItemList) instead of Product.
Validate your output
The tools are the same regardless of content type or schema used:
- Google’s Rich Results Test checks eligibility for Google’s rich results.
- The Schema.org Validator checks any schema.org JSON-LD against the specification generally.
Related patterns
An llms.txt or companion feed built this way can link to, or be linked from, other feeds you create the same way. See Answer engine and generative engine optimization for the broader AEO/GEO picture, including how to control which AI crawlers can reach these feeds at all.
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.