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

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:

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?