Skip to content
Log in

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 dataProduct and ProductGroup JSON-LD (with Offer, 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, or ItemList of products.
  • A services list, store locations, or solution pages → Service, LocalBusiness, or a generic ItemList of those records.
  • A blog or knowledge base → Article, or an ItemList of 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

A product is included in a store based on its Price Book Entry, not a direct or junction lookup on the product record itself. Product2 has no store-scoping field at all; a store’s catalog is the set of products with an active entry in the Pricebook assigned to that store. Use the current_pricebook global, which already resolves to the current store’s assigned Pricebook:

```liquid

{%- paginate current_pricebook.pricebook_entries by 20 -%} {%- for entry in current_pricebook.pricebook_entries -%} {%- assign product = entry.product -%} {{ product.name }} {%- endfor -%} {%- endpaginate -%} ```

:::warning pricebook_entries is a PaginatedList. A plain {% for %} loop over it, without wrapping the loop in paginate, silently returns zero items — it does not lazily fetch results on its own. Wrap the loop in {% paginate current_pricebook.pricebook_entries by <page_size> %} ... {% endpaginate %}, and loop the same collection name inside the block (not a paginate.items accessor). This is confirmed behavior, not a documentation assumption. :::

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 current_pricebook.pricebook_entries by 20 -%} {%- for entry in current_pricebook.pricebook_entries -%} {%- assign i = i | plus: 1 -%} {%- assign product = entry.product -%} { “@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:

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?

Was this article helpful?