Analytics

Product Feeds Live With Your Catalog

Throttle stores no product catalog by design — feeds for Google Merchant Center, Meta Commerce Manager, and other ad platforms belong to your catalog source.

Why Throttle does not generate feeds

Products are not stored in Throttle. Product and line item data arrives from your cart/checkout payload and is saved as cart and order line items — Throttle is the system of record for transactions, not for your catalog. A product feed needs the full catalog (every product, in stock or not, with images, descriptions, and category data), which only your catalog source has.

Your catalog lives inGenerate the feed with
A CMS (Sanity, Contentful, Payload, ...)A route handler in your storefront that queries the CMS and renders XML — see the example below.
A storefront framework or commerce backendThe platform's own feed export or feed plugin ecosystem.
Spreadsheets or a homegrown databaseFeed middleware (Channable, DataFeedWatch, Feedonomics, GoDataFeed, ...) or a scheduled export to a hosted file.

Reference: Next.js feed route from a CMS

A compact route handler that renders a Google Merchant Center RSS 2.0 feed from a CMS query. Meta Commerce Manager accepts the same format, so one feed URL usually serves both platforms.

app/feeds/google-merchant.xml/route.ts
// app/feeds/google-merchant.xml/route.ts
// Renders a Google Merchant Center product feed from your CMS/catalog.
import { getProducts } from '@/lib/cms'; // your catalog source
export const revalidate = 3600; // regenerate hourly
function esc(value: string): string {
  return value
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}
export async function GET() {
  const products = await getProducts(); // CMS query
  const items = products
    .map(
      (p) => `
    <item>
      <g:id>${esc(p.sku)}</g:id>
      <g:title>${esc(p.name)}</g:title>
      <g:description>${esc(p.description)}</g:description>
      <g:link>https://shop.example.com/products/${esc(p.slug)}</g:link>
      <g:image_link>${esc(p.imageUrl)}</g:image_link>
      <g:availability>${p.inStock ? 'in_stock' : 'out_of_stock'}</g:availability>
      <g:price>${(p.priceMinor / 100).toFixed(2)} ${esc(p.currency)}</g:price>
      <g:brand>${esc(p.brand)}</g:brand>
      <g:condition>new</g:condition>
    </item>`,
    )
    .join('');
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
  <channel>
    <title>Example Shop</title>
    <link>https://shop.example.com</link>
    <description>Product feed</description>${items}
  </channel>
</rss>`;
  return new Response(xml, {
    headers: { 'Content-Type': 'application/xml; charset=utf-8' },
  });
}

The ID-consistency contract

Dynamic remarketing only works when the ids in your purchase events match the ids in your feed. Throttle carries your catalog id through checkout untouched — but only if you send it.

Line-item referenceId must equal the feed item id
The referenceId your cart integration sends Throttle on each line item (surfaced as sku on throttle.completed and used as content_ids / item_id in purchase events ) must equal the feed item id ( <g:id>). If they diverge, Google and Meta cannot join purchases back to catalog items and dynamic remarketing silently degrades. Use one canonical id — typically the SKU — in both places.
Cart line item with referenceId
// Adding a cart line item — referenceId MUST equal the feed's <g:id>
await carts.items.add(cart.id, {
  type: 'product',
  name: 'Premium Widget',
  referenceId: 'WIDGET-PREM-001', // <g:id>WIDGET-PREM-001</g:id> in the feed
  unitPrice: 2599,
  quantity: 1,
});
  • Same id everywhere: feed g:id, cart line item referenceId, and any pixel ViewContent / AddToCart events your storefront fires.
  • Variants: if your feed lists variants as separate items ( g:item_group_id grouping), send the variant id as referenceId, not the parent product id.
  • Server-side too: Throttle's server-side conversions build content_ids from the order line items' referenceId — a missing referenceId drops that item from the join.