Platforms
Product feeds
Connect your store's Google Shopping or Meta catalog feed, or a JSON file, and Findlane reads it every hour and keeps your index in step, with no code on your server.
A product feed is the file most shop platforms make for Google Shopping or Meta's catalog ads: every product, with its title, link, photo, price and stock. Give Findlane its address, and Findlane reads it every hour and keeps your index in step. Nothing runs on your server, and nobody writes a sync.
With the search box for any site, a store on any platform with a feed gets search without a developer. Stores on Shopify use the Shopify app instead, which syncs by itself.
Find your feed
| Platform | Where the feed is |
|---|---|
| WooCommerce | Our plugin, which connects the store and reads it for you; or a free feed plugin that publishes an address, such as CTX Feed or Product Feed PRO |
| Shopware 6 | A Google Shopping product export, under Sales Channels. Its address ends with the export's access key |
| Wix | Marketing Integrations, Google Catalog, Get Catalog Link |
| Magento | A feed extension, which writes the file to an address such as /media/feed/google.xml |
| PrestaShop | A Google feed module |
| Your own platform | Any of the formats below. A JSON file of records, as the API takes them, needs no mapping |
The Google for WooCommerce plugin sends products to Google itself and has no address, so it can't be used.
To try it first, use our sample feed: the demo store's 286 products, with shoes and clothes in sizes, at https://cdn.findlane.dev/demo-store/feed.xml.
Connect it
- On the index's Records page, choose Connect a feed.
- Paste the feed's address. If it needs a username and password, turn on It needs a sign-in: the password is stored encrypted and never shown again.
- Choose Check the feed. Findlane reads its first 2,000 items and shows the products they make, the fields they have, and any items it refused, with why.
- Choose how often to read it: every hour, every 6 hours, or once a day at an hour you pick (UTC). Then Connect and import.
The first import starts at once. If the index still has its default settings, it gets settings for the feed's fields: titles, brands, categories and descriptions searched; brands, categories, colors and sizes as filters; price sorts; and variants turned on.
What it reads
| Format | What it is |
|---|---|
| XML | Google's and Meta's feeds: RSS 2.0 or Atom, with attributes such as g:price |
| Tab-separated text | Google's text feeds, with a header row of attribute names (image_link or image link) |
| Comma-separated text | Meta's text feeds, the same way; semicolons work too |
| JSON | An array of records, or { "records": [...] }, each with an objectID (or an id) |
| JSON Lines | A record a line |
Any of them may be gzipped. Findlane tells the format from the file's first bytes, not its name.
A feed can be up to 1 GB and 1,000,000 items. An index has one feed. Zip and bz2 files, FTP and SFTP aren't read.
How items become records
Google's and Meta's attributes become the fields the Shopify app's records have, so the search box, merchandising and recommendations treat every store alike:
| Feed attribute | Record field | How |
|---|---|---|
id | objectID | |
title, description | title, description | HTML removed |
link | url | |
image_link, additional_image_link | image, images | |
price | price, currency | "15.00 USD" is 15 and USD; "1.990 ISK" is 1990, since krónur have no decimals |
sale_price | price, compare_at_price | While the sale is on, by sale_price_effective_date, the sale price is price and the regular one compare_at_price |
availability | available | in_stock, preorder and backorder are true; out_of_stock is false |
product_type | product_type, categories | "Women > Dresses > Maxi": product_type is Maxi, categories lists all three, so each can be a category page |
brand, gtin, mpn, color, size, material, pattern, gender, age_group, condition, custom_label_0–4 | The same names | |
product_detail | specs | Each detail by its name, such as specs.width_cm |
product_highlight | highlights | |
| Shipping, tax and ads-only attributes | Left out | |
| Anything else | Its own name | Up to 40 of them, as text |
JSON and JSON Lines files are records as the API takes them, without mapping.
Variants
Google wants an item for each variant, such as each size and color of a shirt, all sharing an item_group_id. By default the items of a group become one product:
- Its
objectIDis theitem_group_id, and its title the words the variants' titles share, without their color or size. priceis the lowest,price_maxthe highest, andavailableistruewhen any variant is.color,sizeand the other attributes that vary are lists, for filters.variantslists each variant'sid,url,image,price,available, color and size. With the variants setting on, each hit says which variant the search matched, so "blue shirt" links to the blue one.
Your plan counts records, so a store with 2,000 products in ten sizes uses 2,000. To keep a record for each item instead, choose One result per variant; each keeps its item_group_id, which you can group results by.
List a product's variants together in the feed, as plugins do. A variant listed more than 20,000 items after its product's others is refused, with that reason.
Runs
- An unchanged feed costs nothing. Findlane asks the server whether the file changed since the last run, and compares the file with the one it last imported, so most hourly runs end there.
- Only what changed is written. Each run replaces the index's products with the feed's, in one step: products that are new or different are written, and products the feed no longer has are deleted. Searches keep using the current products until the step is done.
- Sales start and end on time. When a
sale_price_effective_datebegins or ends, the next run reads the feed even if the file hasn't changed. - Fetch now reads the feed at once, in full.
- Writes between runs through the API, such as a stock change, work as usual, and the next run puts the feed's values back.
The Records page shows the last 20 runs: when, what each did, and why any items were refused.
Refused items
An item is refused, and left out, when it has no id or title, its price can't be read, its id is used by another item, or the index's own checks refuse it, such as a title over the size limit. The rest of the feed is imported. Each run lists how many were refused for each reason, with the first IDs.
When a run is held
A run isn't imported when:
- the feed has no products;
- it would remove more than half the index's products;
- or more than a fifth of its items were refused.
The index keeps its products, and the workspace's owners get an email. If the feed is right, choose Import anyway on the Records page. Otherwise fix the feed: the next run reads it again.
When it fails
When the feed can't be fetched or read, the index keeps its products. After three failed runs in a row, the owners get an email, and Findlane tries every 6 hours, then once a day after a day of failures. After a week of failures the feed pauses, with a second email, until an owner chooses Resume. A run that works puts the feed back on its schedule.
Paged sources
A platform with many stores can answer a JSON endpoint a page at a time instead of writing a file, as the WooCommerce plugin does. Set the feed's source to pages; each page answers:
{ "records": [{ "objectID": "1042", "title": "Rain jacket", "price": 129 }], "next": "https://shop.example/feed?page=2" }recordsare records as the API takes them, up to a few hundred a page;nextis the following page's address on the same site, ornullon the last.- A paged source's password goes in a
findlane-feed-keyheader rather than a Basic sign-in, which WordPress refuses for anyone but its own users. - Up to 5,000 pages; all of them are read on each run.
Let Findlane in
Findlane fetches feeds with the user agent FindlaneFeeds/1.0 (+https://findlane.dev/docs/feeds). If your site blocks unknown visitors, allow it. The address can be https or, without a password, http.
From your own code
Everything above is in the API, the server SDK, and the MCP server, so a platform can connect its stores' feeds for them:
const index = client.index("products")
await index.feed.set({ url: "https://shop.example/feed.xml", schedule: "hourly" })
const feed = await index.feed.get()
console.log(feed?.lastRun?.status, feed?.lastRun?.records)