
Google Shopping Feed
- 76 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Generate and optimize a Google Merchant Center product feed so products appear in Shopping ads with correct attributes.
About
Sets up the official Google integration per platform and optimizes feed quality, the primary Shopping ranking factor. A developer uses it to launch Shopping ads, fix Merchant Center disapprovals, or keep price/availability in sync.
- Per-platform native Google integration setup
- Feed-quality optimization and common disapproval fixes
Google Shopping Feed by the numbers
- 76 all-time installs (skills.sh)
- Ranked #1,201 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill google-shopping-feedAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 76 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Generate and optimize a Google Merchant Center product feed so products appear in Shopping ads with correct attributes.
Files
Google Shopping Feed
Overview
Google Merchant Center requires a product data feed with specific attributes to serve Shopping ads. Shopify, WooCommerce, and BigCommerce all have official Google integrations that handle feed generation automatically. This skill covers setting up the integration, optimizing feed quality (the primary Shopping ranking factor), and fixing common disapproval issues.
When to Use This Skill
- When setting up Google Shopping Ads for the first time
- When products are being disapproved in Merchant Center due to feed quality issues
- When price or availability in the feed is lagging behind the live website
- When optimizing feed titles and descriptions to capture more relevant search queries
- When managing feeds for multiple countries or currencies
Core Instructions
Step 1: Set up your Google Merchant Center account
1. Go to merchants.google.com and create an account 2. Verify and claim your store's domain under Business Information → Website (add a meta tag or upload a file to your store) 3. Accept the Merchant Center terms and policies 4. Link your Google Ads account: Settings → Linked Accounts → Google Ads
Step 2: Connect your platform to Merchant Center
---
Shopify
1. Go to Shopify Admin → Sales Channels → + → Google & YouTube 2. Install the Google & YouTube channel 3. Connect your Google account and select your Merchant Center account 4. Under Product Feed, Shopify automatically syncs your entire catalog with all required attributes (title, price, availability, images, GTIN if provided) 5. Go to Google & YouTube → Overview to verify the feed sync status 6. Check Products → Issues in Merchant Center to see any disapproved products
Improving feed quality on Shopify:
- Ensure every product has: a clear title with brand + product name, a description, a main image, and the correct product type
- Add GTINs (barcodes) to products under Products → [Product] → Variants → Barcode — GTINs improve Shopping impression share significantly
- For Google product category: install Google Shopping Feed or Feedonomics app from the Shopify App Store to map Shopify product types to Google Taxonomy IDs
---
WooCommerce
1. Install the Google Listings & Ads plugin (free, official Google plugin) from the WordPress plugin directory 2. Go to WooCommerce → Google Listings & Ads and connect your Google account 3. Complete the setup wizard — it creates the Merchant Center account if you do not have one and submits your feed 4. Products are synced automatically from WooCommerce; check Google Listings & Ads → Product Feed for sync status
For more control: install WooCommerce Product Feed Pro ($70/yr) — it gives you full control over feed attributes, custom labels, and supplemental feeds for promotions.
Add Google product category to WooCommerce products:
- Go to each product or product category in WooCommerce admin
- The Google Listings & Ads plugin adds a "Google Category" field — fill this in using Google's product taxonomy
---
BigCommerce
1. Go to BigCommerce Admin → Channel Manager 2. Click Add a Channel → Google Shopping 3. Connect your Google Merchant Center account — BigCommerce automatically creates the feed at yourstore.com/xmlfeed.xml 4. Check Channel Manager → Google Shopping → Products for product status and disapprovals
For enhanced feed optimization: install GoDataFeed or Feedonomics from the BigCommerce App Marketplace.
---
Custom / Headless
Generate a standards-compliant XML feed endpoint that Google can crawl:
import { create } from 'xmlbuilder2';
export async function generateGoogleShoppingFeed(req: Request, res: Response) {
const products = await db.products.findAll({
where: { status: 'active' },
include: ['variants', 'images'],
});
const root = create({ version: '1.0', encoding: 'UTF-8' })
.ele('rss', { version: '2.0', 'xmlns:g': 'http://base.google.com/ns/1.0' })
.ele('channel')
.ele('title').txt(process.env.STORE_NAME!).up()
.ele('link').txt(process.env.STORE_URL!).up();
for (const product of products) {
for (const variant of product.variants) {
const item = root.ele('item');
item.ele('g:id').txt(variant.sku).up();
// Title format: Brand + Product Name + Key Attribute (Color/Size)
item.ele('g:title').txt(
[product.brand, product.name, variant.color, variant.size].filter(Boolean).join(' ').slice(0, 150)
).up();
item.ele('g:description').txt(product.description.slice(0, 5000)).up();
item.ele('g:link').txt(`${process.env.STORE_URL}/products/${product.slug}?variant=${variant.id}`).up();
item.ele('g:image_link').txt(product.images[0]?.url ?? '').up();
item.ele('g:condition').txt('new').up();
item.ele('g:availability').txt(variant.inventory > 0 ? 'in_stock' : 'out_of_stock').up();
item.ele('g:price').txt(`${(variant.priceInCents / 100).toFixed(2)} USD`).up();
item.ele('g:brand').txt(product.brand ?? process.env.STORE_NAME!).up();
item.ele('g:google_product_category').txt(product.googleProductCategory).up();
item.ele('g:item_group_id').txt(product.id).up(); // groups variants together
if (variant.color) item.ele('g:color').txt(variant.color).up();
if (variant.size) item.ele('g:size').txt(variant.size).up();
if (product.gtin) item.ele('g:gtin').txt(product.gtin).up();
item.ele('g:identifier_exists').txt(product.gtin ? 'yes' : 'no').up();
}
}
res.setHeader('Content-Type', 'application/xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=3600');
res.send(root.end({ prettyPrint: false }));
}Register this feed URL in Merchant Center under Products → Feeds → Add a Primary Feed.
Use the Content API to push real-time price/inventory updates instead of waiting for Google's scheduled crawl:
import { google } from 'googleapis';
const content = google.content({ version: 'v2.1', auth: await new google.auth.GoogleAuth({
keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_KEY_PATH,
scopes: ['https://www.googleapis.com/auth/content'],
}).getClient() });
// Push updated product when inventory or price changes
await content.products.insert({
merchantId: process.env.GOOGLE_MERCHANT_ID,
requestBody: {
offerId: variant.sku,
availability: variant.inventory > 0 ? 'in_stock' : 'out_of_stock',
price: { value: (variant.priceInCents / 100).toFixed(2), currency: 'USD' },
contentLanguage: 'en',
targetCountry: 'US',
channel: 'online',
},
});Step 3: Optimize feed quality
Feed quality is the primary Shopping ranking factor. Fix these in order of impact:
1. Optimize product titles — put brand and key attributes first (Google truncates after ~70 characters):
- Good: "Nike Air Max 90 White Men's Size 10"
- Bad: "Air Max 90 - Various Colors Available"
2. Add GTINs/barcodes — missing GTINs is the most common cause of reduced impression share; add barcodes to every product that has one
3. Set Google product category — use Google's taxonomy (search "Google Product Taxonomy") and map each product type; use numeric IDs for exact matching
4. Ensure landing page price matches feed price — even $0.01 discrepancy causes price-mismatch disapproval; check Merchant Center → Diagnostics weekly
5. Add multiple product images — products with 3+ images get higher quality scores; lifestyle images + white background images both help
Step 4: Fix common disapproval issues
In Merchant Center, go to Products → Diagnostics to see all issues:
| Disapproval Reason | Fix |
|---|---|
| Price mismatch | Ensure feed price exactly matches schema.org price on the product page |
| Missing required attribute | Add missing fields: typically brand, gtin, or google_product_category |
| Image too small | Use images at least 500×500px; 1000×1000px recommended |
| Landing page error | Verify the product URL in the feed returns a 200 status code |
| Policy violation | Read the specific policy — common issues: drug claims, copyright images, counterfeit products |
Best Practices
- Front-load keywords in titles — Google truncates titles at ~70 characters in the UI; put brand and product name first
- Register a supplemental feed for promotions — overlay sale prices without modifying the primary feed; reduces disapproval risk during sales
- Cache the feed response with a 1-hour TTL — Merchant Center crawls frequently; avoid hammering your database on every request
- Monitor disapproval rate daily — a spike often means a recent deploy changed URL patterns or price formatting
- Set `identifier_exists: no` only for truly private-label products without any GTIN/MPN — misusing it causes disapproval for products that do have identifiers
Common Pitfalls
| Problem | Solution |
|---|---|
| Products missing from Shopping ads despite being approved | Check that targetCountry and contentLanguage match the linked Google Ads campaign targeting |
| Feed fetch returns 404 after deployment | The feed URL is registered in Merchant Center — ensure your route still exists after deploys |
| GTIN required errors for private-label products | Set identifier_exists: no and provide a brand + MPN instead |
| Variants showing as separate products | Ensure all variants share the same item_group_id and differ only by color, size, or pattern |
| Shopify feed sync stuck | In Shopify, go to Google & YouTube → disconnect and reconnect the integration; or use a third-party feed app like Simprosys |
Related Skills
- @google-ads-ecommerce
- @ecommerce-seo
- @meta-ads-integration
- @marketing-attribution-dashboard
{
"context": "Tests whether the agent uses the googleapis package with the correct API version and authentication, uses custombatch for bulk updates rather than individual inserts, and correctly generates a supplemental TSV promotion feed with proper format and headers.",
"type": "weighted_checklist",
"checklist": [
{
"name": "googleapis package",
"max_score": 8,
"description": "The implementation imports from `googleapis` (specifically `import { google } from 'googleapis'`) — not from a REST HTTP client, fetch wrapper, or unofficial Google library"
},
{
"name": "Content API v2.1",
"max_score": 8,
"description": "The Content API client is initialised with `version: 'v2.1'` — not v1, v2, or another version string"
},
{
"name": "GoogleAuth with content scope",
"max_score": 9,
"description": "Authentication uses `new google.auth.GoogleAuth(...)` with `keyFile` pointing to `GOOGLE_SERVICE_ACCOUNT_KEY_PATH` environment variable and the scope `https://www.googleapis.com/auth/content`"
},
{
"name": "custombatch for bulk updates",
"max_score": 10,
"description": "Product updates use `content.products.custombatch(...)` — NOT a loop of individual `content.products.insert` or `content.products.update` calls"
},
{
"name": "Batch method insert",
"max_score": 7,
"description": "Each entry in the custombatch request has `method: 'insert'`"
},
{
"name": "batchId per entry",
"max_score": 6,
"description": "Each batch entry includes a `batchId` field (e.g., the array index)"
},
{
"name": "Supplemental TSV format",
"max_score": 9,
"description": "The promotions feed is returned as TSV (tab-separated values or comma-separated with the specified columns), NOT as XML or JSON"
},
{
"name": "TSV columns correct",
"max_score": 8,
"description": "The supplemental feed header row contains exactly `id,sale_price,sale_price_effective_date` (in that order)"
},
{
"name": "sale_price_effective_date format",
"max_score": 9,
"description": "The `sale_price_effective_date` column is formatted as `startISO/endISO` — two ISO 8601 datetime strings separated by a forward slash `/`"
},
{
"name": "TSV Content-Type header",
"max_score": 8,
"description": "The supplemental feed endpoint sets `Content-Type` to `text/tab-separated-values; charset=utf-8`"
},
{
"name": "Price format in batch",
"max_score": 9,
"description": "Price in the batch payload uses a `{ value: '...', currency: '...' }` object where `value` is formatted with two decimal places (toFixed(2))"
},
{
"name": "Batch size respect",
"max_score": 9,
"description": "The code batches or limits requests to a maximum of 1000 products per custombatch call (e.g., chunks the SKU list if over 1000)"
}
]
}
Real-Time Merchant Center Sync Service
Problem Description
Bloom & Branch is a fast-moving home goods retailer with a dynamic catalog: flash sales can start and end within hours, and warehouse inventory fluctuates throughout the day. Their current setup relies on Google's automatic 24-hour feed re-crawl, meaning a product can show as in-stock in Shopping ads long after it has sold out — or worse, customers click on an ad only to find the sale price has already expired.
The engineering team wants to build two TypeScript services:
1. A real-time inventory and price sync — When the warehouse management system triggers a webhook listing SKUs that have changed (price or stock level), the service should push updated product data directly to Google Merchant Center as quickly as possible. The team expects batches of up to several hundred SKUs at once during peak restock or repricing events, so efficiency is critical.
2. A promotions overlay feed — The marketing team runs scheduled promotions configured in a database. Rather than touching the primary product feed (which could introduce crawl lag), they want a separate endpoint that Merchant Center can fetch to get current sale prices with start and end dates.
Build a TypeScript implementation covering both of these services. Since no real database or Merchant Center account is available, define mock data inline (a few product variants with prices and a couple of active promotions) and demonstrate the structure of both services. Write the output to sync-service.ts and include comments explaining key decisions.
Output Specification
sync-service.ts: Contains both the batch sync function and the supplemental feed endpoint handler- The batch sync function should accept an array of SKU strings and push the corresponding product data to Merchant Center
- The supplemental feed handler should return the promotions data in the appropriate format with correct HTTP headers
- Use realistic mock data (at least 3 product variants and 2 active promotions) defined inline
No large files should be left on disk. The implementation does not need to connect to a real API — just demonstrate the correct structure and calls.
{
"context": "Tests whether the agent correctly implements a Merchant Center audit using the productstatuses API, handles multi-country feed expansion with correct offerId format and matching country/language settings, properly groups variants, and follows guidance on private-label product identifiers.",
"type": "weighted_checklist",
"checklist": [
{
"name": "productstatuses.list usage",
"max_score": 9,
"description": "The audit function calls `content.productstatuses.list(...)` to retrieve disapproval data — not productstatuses.get in a loop, or a different API method"
},
{
"name": "maxResults: 250",
"max_score": 8,
"description": "The `productstatuses.list` call includes `maxResults: 250` as a parameter"
},
{
"name": "Multi-country offerId format",
"max_score": 10,
"description": "For multi-country product entries, the `offerId` is formatted as `{sku}-{country}` (e.g., `SKU123-GB`), not just the raw SKU"
},
{
"name": "targetCountry per entry",
"max_score": 8,
"description": "Each country-specific product entry has a distinct `targetCountry` matching that country's code (e.g., `'US'`, `'GB'`, `'CA'`)"
},
{
"name": "contentLanguage per entry",
"max_score": 8,
"description": "Each country-specific product entry has a `contentLanguage` set to the correct language code for that country"
},
{
"name": "Currency per country",
"max_score": 8,
"description": "The price `currency` field varies per country (e.g., USD for US, GBP for GB, CAD for CA) — not a single hardcoded currency for all countries"
},
{
"name": "item_group_id for variant grouping",
"max_score": 8,
"description": "All variants of the same product share the same `item_group_id` (the parent product ID), ensuring they are grouped in Shopping rather than displayed as separate products"
},
{
"name": "Private-label identifier_exists: no",
"max_score": 8,
"description": "Products without a GTIN or UPC have `identifier_exists` set to `'no'` (or equivalent), NOT omitted entirely or set to `'yes'`"
},
{
"name": "Private-label brand + MPN",
"max_score": 9,
"description": "Private-label products with `identifier_exists: no` also include both a `brand` AND an `mpn` field — not just one or neither"
},
{
"name": "Disapproval codes aggregated",
"max_score": 8,
"description": "The audit output aggregates issue codes by frequency (counts per code), not just listing raw issues per product"
},
{
"name": "destinationStatuses checked",
"max_score": 8,
"description": "The audit iterates over `destinationStatuses` (or `itemLevelIssues`) from each product status, not just the top-level status field"
},
{
"name": "Numeric product category",
"max_score": 8,
"description": "The `googleProductCategory` field for products uses a numeric taxonomy ID value, not a human-readable name string"
}
]
}
Merchant Center Health Check and Global Expansion
Problem Description
Meridian Outdoors is a direct-to-consumer gear brand that sells camping and hiking equipment. After a recent backend migration, their product disapproval rate in Google Merchant Center spiked significantly, and they're not sure which issues are most widespread. At the same time, the business is expanding from the US market into the UK and Canada, and needs Shopping ads running in those regions too.
The team has two immediate needs:
1. Disapproval audit tool — A TypeScript script that connects to Merchant Center and produces a ranked summary of the most common disapproval reasons across the entire catalog. The output should make it easy to see which issues to fix first.
2. Multi-country product insertion — A TypeScript function that takes a product SKU and pushes that product to Merchant Center for all three target markets (US, UK, Canada) with region-appropriate pricing. Some products in their catalog are Meridian's own-brand designs with no GTIN, while others are third-party branded items with barcodes. Both cases need to be handled correctly.
The team has encountered two specific recurring problems: variants of the same product appearing as separate unrelated listings in Shopping results, and some of their private-label products getting flagged for missing product identifiers. The solution should handle both of these cases correctly.
Write the implementation in merchant-tools.ts. Since no real API credentials are available, use mock data (at least one GTIN-enabled product and one private-label product, each with at least 2 variants) defined inline. Include comments explaining how each issue is addressed.
Output Specification
merchant-tools.ts: Contains the audit function and multi-country insertion function- The audit function should output a sorted table or JSON showing issue codes by frequency
- The multi-country function should demonstrate correct per-country configuration
- No large files should be left on disk
{
"context": "Tests whether the agent generates a Google-compliant XML product feed using the correct library, RSS structure, required fields, title optimization rules, price format, response headers, and proper handling of images, identifiers, and variant grouping.",
"type": "weighted_checklist",
"checklist": [
{
"name": "xmlbuilder2 library",
"max_score": 8,
"description": "The XML feed is built using the `xmlbuilder2` package (imports `create` from `xmlbuilder2`), not a hand-concatenated string, DOM, or alternative XML library"
},
{
"name": "RSS 2.0 with g namespace",
"max_score": 8,
"description": "The root element is `rss` with `version: '2.0'` and the attribute `xmlns:g` set to `'http://base.google.com/ns/1.0'`"
},
{
"name": "g:id uses SKU",
"max_score": 7,
"description": "The `g:id` field is populated with the variant SKU (not a product ID, database ID, or other identifier)"
},
{
"name": "Description max length",
"max_score": 7,
"description": "The `g:description` value is sliced or truncated to a maximum of 5000 characters"
},
{
"name": "Title format and length",
"max_score": 10,
"description": "Titles are constructed in the order: Brand, Product Name, then variant attributes (color, size, material); the final string is capped at 150 characters"
},
{
"name": "Price format",
"max_score": 8,
"description": "Prices are formatted as `X.XX USD` — two decimal places followed by a space and the currency code (e.g., `29.99 USD`), not as a plain number or object"
},
{
"name": "Cache-Control header",
"max_score": 7,
"description": "The feed endpoint sets a `Cache-Control` header of `public, max-age=3600` (1-hour TTL)"
},
{
"name": "Content-Type header",
"max_score": 7,
"description": "The feed endpoint sets `Content-Type` to `application/xml; charset=utf-8`"
},
{
"name": "Additional images",
"max_score": 8,
"description": "Multiple `g:additional_image_link` entries are included (up to 9 additional images, i.e., images at index 1–9), providing at least 3 total images per product where available"
},
{
"name": "identifier_exists correctness",
"max_score": 8,
"description": "`g:identifier_exists` is set to `'yes'` when a GTIN is present and `'no'` only when it is absent — NOT hardcoded to a single value for all products"
},
{
"name": "item_group_id for variants",
"max_score": 8,
"description": "All variants of the same product share the same `g:item_group_id` value (the parent product's ID), not the variant ID"
},
{
"name": "prettyPrint false",
"max_score": 7,
"description": "The XML is serialized with `prettyPrint: false` (compact output), not with indentation/newlines"
},
{
"name": "Numeric product category",
"max_score": 7,
"description": "The `g:google_product_category` field uses a numeric taxonomy ID (e.g., `187`), not a human-readable category name string"
}
]
}
Google Shopping Feed Generator
Problem Description
Sole Collective is a mid-sized footwear brand selling through their own online store. Their marketing team has decided to launch Google Shopping ads but needs a standards-compliant product data feed that Google Merchant Center can crawl. The catalog contains multiple product lines, each with color and size variants, and products that sometimes have GTINs and sometimes don't (for their private-label styles).
The development team needs a TypeScript/Node.js feed generator that produces a well-structured XML product feed. Past attempts using string concatenation or ad-hoc XML were rejected by Merchant Center's feed validator; the team wants a proper implementation built with a solid XML library that handles all the required fields correctly. The feed must also be performant enough that Google's frequent crawls don't overload the database.
Output Specification
Write a TypeScript file feed.ts that exports a function generateGoogleShoppingFeed(req, res) which, when called, builds and returns the XML product feed.
Your implementation should:
- Query a mock set of products with variants (you can define the data inline or as a helper function — no real database needed)
- Include at least 2 products, each with 2+ variants, at least one product with a GTIN and one without
- Set the appropriate HTTP response headers
- Produce a valid XML feed body
The mock product data should be defined inline in the file so the output is self-contained and runnable. Include a brief README.md explaining any notable design decisions.
Input Files (optional)
The following file provides example product data you should use as the basis for your implementation. Extract it before beginning.
=============== FILE: inputs/products.json =============== [ { "id": "prod-001", "name": "Cloud Runner", "brand": "Sole Collective", "description": "A lightweight daily trainer designed for long-distance comfort. Features a responsive foam midsole, breathable mesh upper, and durable rubber outsole. Suitable for road and light trail running. Built for runners who log 40+ miles per week and need reliable cushioning without excess weight.", "slug": "cloud-runner", "googleProductCategory": 3 , "gtin": "00012345678905", "mpn": "SCR-001", "weightKg": 0.32, "categories": [{ "name": "Athletic" }, { "name": "Running" }], "images": [ { "url": "https://example.com/images/cloud-runner-main.jpg" }, { "url": "https://example.com/images/cloud-runner-side.jpg" }, { "url": "https://example.com/images/cloud-runner-sole.jpg" }, { "url": "https://example.com/images/cloud-runner-back.jpg" } ], "variants": [ { "id": "v-001-wh-9", "sku": "SCR-001-WH-9", "color": "White", "size": "9", "inventory": 12, "priceInCents": 12999, "salePriceInCents": null }, { "id": "v-001-wh-10", "sku": "SCR-001-WH-10", "color": "White", "size": "10", "inventory": 7, "priceInCents": 12999, "salePriceInCents": null }, { "id": "v-001-bk-9", "sku": "SCR-001-BK-9", "color": "Black", "size": "9", "inventory": 0, "priceInCents": 12999, "salePriceInCents": 9999 } ] }, { "id": "prod-002", "name": "Heritage Low", "brand": null, "description": "Our signature canvas sneaker, handcrafted using traditional techniques. The vulcanized rubber sole and unbleached canvas upper give it an authentic retro look. A wardrobe staple for those who appreciate timeless footwear without logos or branding noise.", "slug": "heritage-low", "googleProductCategory": 3, "gtin": null, "mpn": "HL-002", "weightKg": 0.28, "categories": [{ "name": "Casual" }, { "name": "Lifestyle" }], "images": [ { "url": "https://example.com/images/heritage-low-main.jpg" }, { "url": "https://example.com/images/heritage-low-angle.jpg" } ], "variants": [ { "id": "v-002-nt-8", "sku": "HL-002-NT-8", "color": "Natural", "size": "8", "inventory": 20, "priceInCents": 7999, "salePriceInCents": null }, { "id": "v-002-nt-9", "sku": "HL-002-NT-9", "color": "Natural", "size": "9", "inventory": 15, "priceInCents": 7999, "salePriceInCents": null } ] } ]
{
"name": "finsi/google-shopping-feed",
"version": "0.1.0",
"summary": "Product feed generation for Google Merchant Center with optimization rules",
"skills": {
"google-shopping-feed": {
"path": "SKILL.md"
}
}
}