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 in | Generate 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 backend | The platform's own feed export or feed plugin ecosystem. |
| Spreadsheets or a homegrown database | Feed 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
// 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, '<')
.replace(/>/g, '>');
}
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.
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.
// 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 itemreferenceId, and any pixelViewContent/AddToCartevents your storefront fires. - Variants: if your feed lists variants as separate items (
g:item_group_idgrouping), send the variant id asreferenceId, not the parent product id. - Server-side too: Throttle's
server-side conversions
build
content_idsfrom the order line items'referenceId— a missingreferenceIddrops that item from the join.