Product Feed
A product feed for Magento 2 in the Google Shopping format (RSS 2.0 XML) — read as it is by Google Merchant Center and Meta (Facebook and Instagram), and accepted by TikTok and Pinterest catalogues — that is built to finish on any catalogue size, sends prices exactly as the storefront shows them, exports configurable products as proper variants, and gives you custom labels you can actually build campaigns on.
What it does
Section titled “What it does”- Finishes on any catalogue. Products are read and written in batches of 500 and streamed to disk, so memory stays flat however many products you have: measured on 75,879 items (50,000 simple products and 1,000 configurables with 24 variants each), memory in use stayed at about 115 MB from the first batch to the last, and the run took under three minutes. Feeds that load the whole catalogue at once need gigabytes at that size and fail on a standard
memory_limit. - Never stalls the store. The feed runs in its own cron group, in a separate process, so a long run does not hold up emails or indexers. One run per store view at a time (lock). Generate Now in the admin queues the run for cron instead of running it inside the web request, where large feeds hit the server’s time limit.
- Fails safe. A product that cannot be built is logged and left out; the rest of the feed is still published. A run that produces no item keeps the previous file — platforms delete every product missing from a fetched feed. The file is replaced in one atomic rename, so a platform never fetches a half-written feed.
- Prices as on the product page. Regular and sale price from Magento’s price index — special prices, catalog price rules — with the sale window when the special price has an end date. Tax is added or removed with Magento’s own tax calculation when the catalogue is entered differently from what the feed must show (Europe: including tax; United States and Canada: excluding), and amounts are converted to the store view’s display currency.
- Stock that matches checkout. Salability comes from Magento Inventory (MSI), reservations of unshipped orders included — the last unit of a product already ordered is not advertised as in stock.
- Configurable products as variants. One item per size and colour, grouped under the parent SKU with
item_group_id,item_group_titleandvariant_option, a distinguishing title (“Hoodie - M, Blue”),colorandsize. Each variant links to the parent page with its options preselected, so the price and stock a shopper lands on are the variant’s — on Hyvä and Luma alike. - Bundle products, optionally. One item per bundle, priced by Magento’s own price model (the price index keeps 0 for bundles), out of stock as soon as a required item is, described by its items’ categories when it only sits in a “Bundles” category, and sent with
is_bundlewhen your bundles are a main product with add-ons. - Square images. Images are padded onto a white 1:1 canvas, so square ad placements show the whole product instead of a crop. The storefront images are not touched; squares are created within a time budget per run and by a background job.
- Categories you can target. Every category branch as
product_type; the product’s main branch as category level labels; merchandising branches (Sale, New In, Brands) left out; a Feed Category attribute on the product to choose the main branch when a product sits in several. - Custom labels from the admin. Choose what fills each of the five custom labels: category level 1–3, all categories, any product attribute, on sale, or a discount band read from the feed’s own prices.
- Identifiers done right. GTIN only with a valid check digit (EAN, UPC, ISBN, GTIN-14); MPN; brand from an attribute or a fixed default;
identifier_exists=noonly when the product truly has none. - One feed per store view, in its language and currency; one element per line (Meta rejects XML lines over 5 MB); optional gzip.
- Any theme. No storefront code: works the same on Hyvä and Luma.
- Extensible. Every element of an item comes from a modifier in a pool; add or override elements with one class and a few lines of
di.xml.
Requirements
Section titled “Requirements”| Magento Open Source / Adobe Commerce | 2.4.8 (tested on 2.4.8-p5) |
| Magento Inventory (MSI) | enabled (Magento_InventorySalesApi) |
| PHP | 8.3 – 8.4, with the gd, xmlwriter and zlib extensions |
| Cron | Magento cron running (bin/magento cron:run every minute) |
Installation
Section titled “Installation”Add the repository you received with your license to composer.json (once), then:
composer require slova/module-product-feed:^1.0bin/magento module:enable Slova_ProductFeedbin/magento setup:upgradebin/magento setup:di:compile # production modebin/magento cache:flushThen enable the feed for your store views (Stores > Configuration > Slova > Product Feed > General > Enable Feed) and generate it once:
bin/magento slova:feed:images # large catalogue: square every image nowbin/magento slova:feed:generateUpgrading
Section titled “Upgrading”composer update slova/module-product-feedbin/magento setup:upgradebin/magento setup:di:compile # production modebin/magento cache:flushRead CHANGELOG.md first; settings are kept across versions.
Uninstalling
Section titled “Uninstalling”bin/magento module:uninstall Slova_ProductFeed --remove-data--remove-data deletes the module’s settings, run reports and the Feed Category product attribute with its values. The module creates no tables. Feed files (pub/media/feed/) and square images (pub/media/slova-productfeed/) are left in place, so platforms still fetching them do not drop your catalogue at once; delete them by hand when the platforms no longer use them.
Configuration
Section titled “Configuration”Stores > Configuration > Slova > Product Feed. Settings marked global apply to every store view; the others can be set per store view.
A table with one row per store view: the feed URL, the last run (time, duration, items, products left out, errors, file size) and a Generate Now button. Generate Now queues the feed; cron starts it within a minute.
General
Section titled “General”| Setting | Default | |
|---|---|---|
| Enable Feed | No | Each enabled store view gets its own file. |
| Schedule (global) | 40 */4 * * * |
Cron expression: every 4 hours at minute 40. |
Products
Section titled “Products”| Setting | Default | |
|---|---|---|
| Simple Products | Yes | Enabled simple products visible in the catalog or search. |
| Configurable Products | Yes | One item per enabled variant (see Configurable products). |
| Variant Links Preselect Options | Yes | Variant links open the parent page with the variant’s options chosen. |
| Bundle Products | No | One item per visible bundle (see Bundle products). |
| Send Bundles as g:is_bundle | Yes | Yes for a main product with add-ons; No for sets of equal products. |
| Leave Out Out-of-Stock Products | No | No is recommended: out-of-stock items are sent as out_of_stock, so platforms stop advertising them but keep them in the catalogue, and tracking events from their pages still match. |
Attributes
Section titled “Attributes”| Setting | Default | |
|---|---|---|
| GTIN | — | Attribute holding the EAN/UPC/ISBN, or SKU when your SKUs are GTINs. |
| MPN | — | Manufacturer part number, for products without a GTIN. |
| Brand | — | |
| Default Brand | — | Sent when a product has no brand, e.g. your own brand. |
| Color / Size | color / size |
|
| Google Product Category | — | ID or full path from the Google taxonomy, sent for every item. |
Categories
Section titled “Categories”| Setting | Default | |
|---|---|---|
| Send Category Paths | Yes | Every category branch as g:product_type, the main branch first. |
| Category Root ID | store root | Only categories below it count; levels are numbered from it. |
| Leave Out These Branches | — | Merchandising branches (Sale, New In, Brands) kept out of category paths and category labels. |
| Primary Category Attribute | Feed Category | Product attribute holding the main category id (see Category labels). |
Custom Labels
Section titled “Custom Labels”Each of g:custom_label_0 to 4 takes one source:
| Source | Value |
|---|---|
| Category, Level 1 / 2 / 3 | The main branch’s category name at that level. |
| All Categories | Every category name of the product, deepest first, joined with “ | “. |
| Product Attribute | The attribute’s value(s), joined with “ | “. |
| On Sale | sale when the item has a sale price, else regular. |
| Discount Band | sale 1-19%, sale 20-39%, sale 40-59%, sale 60%+, or regular. |
Defaults: labels 0–2 category levels 1–3, label 3 all categories, label 4 On Sale. Google Ads splits campaigns by custom labels (it cannot split by price), and Meta product sets filter on them. When every product carries a standard discount, use Discount Band: On Sale would mark the whole catalogue.
Prices
Section titled “Prices”| Setting | Default | |
|---|---|---|
| Tax in Feed Prices | As Displayed on the Storefront | Or force Including Tax / Excluding Tax. |
Images (global)
Section titled “Images (global)”| Setting | Default | |
|---|---|---|
| Square Images | Yes | |
| Square Size (px) | 1080 | Longest side; smaller images are not enlarged. Changing it gives every image a new URL. |
| Time for New Images per Feed Run (s) | 60 | Missing squares are created during the run for this long; then the original image is linked until the square exists. |
| Background Image Job Time (s) | 240 | Every 10 minutes, a cron job creates missing squares for this long. 0 turns it off. |
| Setting | Default | |
|---|---|---|
| File Name | google-shopping-{store}.xml |
In pub/media/feed/; {store} is replaced by the store view code. |
| Compress (gzip) | No | Writes .xml.gz: 3 to 10 times smaller, depending on how repetitive the text is (3× measured on a bookshop with long, unique descriptions). Google Merchant Center and TikTok accept compressed feeds; Meta does not list them among its formats, so leave this off for a feed Meta reads. The URL changes. |
Registering the feed
Section titled “Registering the feed”Copy the feed URL from the Feeds table and add it to each platform as a product data source fetched from a URL on a schedule — in Google Merchant Center as a product source from a file link, in Meta Commerce Manager as a scheduled data feed of the catalogue, and likewise in TikTok and Pinterest catalogues. The menus differ between platforms and change over time; their help pages name the current path.
Schedule the fetch shortly after a feed run (with the default schedule: at minute 50, every 4 hours).
Running the feed
Section titled “Running the feed”-
Cron runs the scheduled feed, the Generate Now queue (every minute) and the background image job (every 10 minutes), in the cron group
slova_productfeed. -
Command line:
Terminal window bin/magento slova:feed:generate # every enabled store viewbin/magento slova:feed:generate --store=en # one store view, even when disabledbin/magento slova:feed:generate --all-images # no time limit for square images in this runbin/magento slova:feed:images # create every missing square image -
Log:
var/log/slova_productfeed.log— one report per run, and the SKU and reason of every product left out after an error (the first 20 per run).
Configurable products
Section titled “Configurable products”Each enabled variant of a visible configurable product is an item of its own, as Google requires:
<g:id>MH01-M-Orange</g:id><g:title>Chaz Kangeroo Hoodie - M, Orange</g:title><g:link>https://example.com/chaz-kangeroo-hoodie.html?size=168&color=56#142=168&93=56</g:link><g:item_group_id>MH01</g:item_group_id><g:item_group_title>Chaz Kangeroo Hoodie</g:item_group_title><g:variant_option><g:name>Size</g:name><g:value>M</g:value></g:variant_option><g:variant_option><g:name>Color</g:name><g:value>Orange</g:value></g:variant_option><g:color>Orange</g:color><g:size>M</g:size>The link carries the options twice: as a query (attribute codes, read by Hyvä and by Luma swatches) and as a fragment (attribute ids, read by Hyvä and by both Luma renderers). A variant without an image of its own uses its parent’s; description and categories come from the parent. A simple product that is also visible on its own appears once, as a variant.
Bundle products
Section titled “Bundle products”With Bundle Products on, each visible, enabled bundle is an item of its own, with its own title, description, image and link.
- Price. Magento’s price index keeps 0 as the regular and final price of most bundles, so the price comes from Magento’s price model, the one the product page renders: the price of the bundle’s lowest configuration — for a bundle whose options are all required with a single choice, simply its price — taxed the way the page taxes it (item by item, each at its own tax class, for a dynamic bundle). By default a struck-through price is sent only when the bundle itself is discounted (special price, catalog price rule), not the sum of its items’ separate prices: Google shows its sale annotation only when the regular price was the product’s price for 30 of the past 200 days, which a bundle’s item total never was. A store whose bundle pages show that total struck through can send it too, with a preference (below); the feed then matches the page, and the ad shows the bundle’s price, with or without Google’s annotation.
- Stock. From Magento Inventory, which marks a bundle unsalable as soon as one of its required items is.
- Categories. A bundle that only sits in a merchandising category (“Bundles”, “Sets”) outside the category root is described by the categories of its items.
is_bundle. Google asks foris_bundle=yeson a merchant-made bundle of a main product with add-ons (a camera with a bag and a card), and treats a set of equal products (a series of books, a hat and scarf) as an ordinary product. Choose with Send Bundles as g:is_bundle.- Identifiers. A bundle without a GTIN of its own is sent with
identifier_exists=no.
To price bundles another way, add a preference for Slova\ProductFeed\Api\BundlePriceInterface; it returns the regular and final price in the base currency, already including or excluding tax as the feed asks ($includingTax). The feed only converts the currency.
Feed prices were checked against the prices rendered on product pages — a simple product, one reduced by a catalog price rule and a dynamic bundle — with prices entered with and without tax, shown with and without tax, and shown in another currency than the base one.
Category labels
Section titled “Category labels”A category label holds one value, so a product in several branches is filed under one main branch, and all its level labels come from that same branch — never level 1 from one branch and level 2 from another.
- If the product’s Feed Category (in the Product Feed group of the product form) is set, the main branch is the one through that category. Pick a domain (“Fiction”) or a precise category (“Fiction > Historical”); it counts even when the product is not assigned to it.
- Otherwise, the first branch alphabetically by its full path. No branch is more important than another in general, so this rule is deliberately arbitrary; what matters is that it is stable from run to run.
The other branches stay in g:product_type and in the All Categories label. A product assigned both to a category and to its parent is described by the deeper one.
Extending
Section titled “Extending”Every element of an item is written by a modifier. Implement Slova\ProductFeed\Api\ItemModifierInterface and add it to the pool; modifiers run in ascending sortOrder, and a later one can add elements or overwrite earlier ones:
class PreOrder implements \Slova\ProductFeed\Api\ItemModifierInterface{ public function getAttributeCodes(\Magento\Store\Api\Data\StoreInterface $store): array { return ['is_preorder', 'release_date']; // added to the product query }
public function modify(array $item, \Magento\Catalog\Model\Product $product, \Slova\ProductFeed\Model\Feed\ItemContext $context): ?array { if ($product->getData('is_preorder')) { $item['availability'] = 'preorder'; $item['availability_date'] = (new \DateTime($product->getData('release_date')))->format('c'); } return $item; // null leaves the product out }}<type name="Slova\ProductFeed\Model\Feed\ItemBuilder"> <arguments> <argument name="modifiers" xsi:type="array"> <item name="preorder" xsi:type="array"> <item name="class" xsi:type="object">Vendor\Module\Model\Feed\PreOrder</item> <item name="sortOrder" xsi:type="number">35</item> </item> </argument> </arguments></type>Built-in modifiers: basic (10), image (20), availability (30), price (40), identifiers (50), variant (60), taxonomy (70), custom_labels (80). An item is an ordered map of element names (without g:) to a string, a list of strings (repeated element) or a list of maps (group element such as variant_option). Leave Out Out-of-Stock Products is applied after every modifier, so a modifier that turns an item into a pre-order keeps it in the feed.
Defaults in your own module’s etc/config.xml: an empty element does not clear a value set by this module (Magento keeps the earlier value when merging configuration files). Use none to switch off an attribute or a custom label there, e.g. <color_attribute>none</color_attribute>.
Image directory: squares are written to pub/media/slova-productfeed/. To keep the URLs of squares made by another tool with the same layout (<dir>/jpg/square-<side>-v1/<path>.jpg), set the cacheDirectory argument of Slova\ProductFeed\Model\Image\SquareImage.
Troubleshooting
Section titled “Troubleshooting”- The feed is not generated, or Generate Now stays queued. Magento cron is not running, or not for the
slova_productfeedgroup. Checkcron_schedulefor theslova_productfeed_*jobs. - A product is missing. It must be enabled, assigned to the store view’s website, visible (simple products) and have a price in the price index — after mass imports, reindex
catalog_product_price. Products left out after an error are listed in the log. - Prices differ from the product page. Check Tax in Feed Prices and, for a store view in another currency, the currency rates.
- Images are the originals, not squares. The run’s image budget was used up; the background job squares the rest, or run
bin/magento slova:feed:images. - Production mode, after installing or upgrading. Run
setup:di:compile; without it, new classes are not wired.
Known limitations
Section titled “Known limitations”- Grouped, virtual and downloadable products are not exported.
- A bundle with optional or alternative items is sent at the price of its cheapest configuration (the “From” price of its page).
- Requires Magento Inventory (MSI).
- One price per item: the NOT LOGGED IN customer group’s price; tier and customer group prices are not sent.
- Only the base image is sent (no
additional_image_link). - Shipping and returns are not part of the feed; set them in each platform’s account settings.
Support and license
Section titled “Support and license”Proprietary software, licensed per the agreement accepted at purchase — see LICENSE. Support: through the channel listed in your license.