[ BLOG // 2026-07-26 // 3 MIN ]

Product feeds: getting a bike shop's catalog onto shopping comparators

Why having an e-shop isn't enough — and how a single generated XML feed puts a small bike shop's catalog in front of shopping comparators, twice a day, with no manual work.

PROJECTS nextjseshopfeedmedusacase-study

Building an e-shop is the easy part these days. Getting it in front of people is the hard part. In the Czech market, a large share of purchases start on shopping comparators — the customer isn’t looking for a specific store, they type “Shimano brake pads” and wait to see what comes up. For a shop’s products to show up there at all, the store needs a product feed: a machine-readable list of the entire catalog that the comparator pulls down on a schedule.

That’s exactly what I built for Opravna Bicyklů, a bike repair shop in Liberec with an e-shop running on Next.js and Medusa.js. One XML feed, generated automatically from the live catalog, that keeps pace with stock and prices.

A format comparators already speak

The feed is XML in the format Google Merchant Center uses, which comparators and shopping platforms consume in one form or another. Each product variant becomes one item:

<item>
  <g:id>SKU-123</g:id>
  <g:item_group_id>PROD-45</g:item_group_id>
  <g:title>Shimano brake pads - metallic</g:title>
  <g:link>https://…/shop/shimano-brake-pads</g:link>
  <g:image_link>https://…/img.jpg</g:image_link>
  <g:availability>in_stock</g:availability>
  <g:price>899.00 CZK</g:price>
  <g:condition>new</g:condition>
  <g:brand>Shimano</g:brand>
  <g:mpn>SKU-123</g:mpn>
</item>

The important field is item_group_id — it tells the comparator that several items (say, different colors or sizes) belong to one product, so it shows the variants together instead of scattering them as separate listings. And availability isn’t filled in by hand: it’s read from real stock levels, so when the last unit sells out, the feed tells the comparator before a customer can order thin air.

Three layers, each doing one thing

Feed generation is split into three independent parts. No giant script that mixes everything together.

  • Repository pulls products, variants, prices and stock from the catalog. Nothing else.
  • Mapper is a pure function: it takes the raw data and maps it onto feed items. This is where what’s valid gets decided.
  • Generate assembles the items into XML, escapes special characters, fills in absolute URLs and writes the file.

The mapper is a pure function with no side effects. It can be tested on a bench — no database, no files, just input and output.

The mapper is where the unglamorous work happens that decides the quality of the feed:

  • It skips a product with no image, title or link — the comparator would reject it anyway.
  • It skips a variant without a valid price (zero or nonsense must never reach the feed).
  • It folds the variant title into the product title, but only when it makes sense — internal names like “default” get thrown away.
  • At the end it returns a statistic too: how many products and variants it saw, how many it skipped and why. When the feed suddenly shrinks, it’s immediately obvious where.

Twice a day, with no race

The feed isn’t regenerated on every change — that would be wasteful. A scheduler runs it twice a day. So that two instances can’t write to the same file at once in a multi-instance deployment, generation is guarded by a shared lock (Redis, with an in-memory fallback when Redis isn’t available). One instance generates, the rest wait.

Why it’s interesting

A product feed sounds like routine — “export the catalog to XML”. But the details are the difference between a feed a comparator swallows and one that silently drops your products. Correct variant grouping, availability from live stock, XML escaping and skipping invalid data — these decide whether a store actually shows up on the comparator.

For a small shop it’s also a multiplier: a feed built once means the catalog runs on comparators by itself, with no manual export every time something changes.

// ALL_POSTS
BACK TO BLOG