Hi Velo Community,
We’re trying to programmatically bulk update the SEO Title, Meta Description, and Focus Keyword for approximately 1,850 products in our Wix Store (https://www.minimalisfy.com/). We’ve hit a roadblock specifically with updating the main product’s SEO data and would appreciate any insights or solutions.
Our Process So Far:
Data Generation: We’ve successfully generated unique SEO Titles, Descriptions, and Focus Keywords for each product externally.
Custom Collection: We created a custom CMS collection named SEOMetaData with fields like productNumericId, focusKeyword, metaTitle, metaDescription, and wixProductId.
Populating Custom Collection: We used a Velo backend web module (.jsw file, manually triggered from the editor console) to read our generated data (mapped via numericId) and successfully populated the SEOMetaData collection using wixData.query (to check existence) and wixData.insert/update with { suppressAuth: true }. This part works perfectly. We also store the corresponding Wix Product _id (if found) in the wixProductId field during this step.
The Problem: Updating the Actual Wix Stores Product SEO
Our main goal is to update the actual SEO fields visible in the product’s SEO panel (and used by Google). We are trying to achieve this within the same Velo backend .jsw module (runBatchSeoUpdate function).
Attempt using wix-stores.v2:
Based on the documentation, we are using the wix-stores.v2 API. The package is installed via the Velo Packages Manager.
Here’s the relevant (simplified) part of our batchSeoUpdate.jsw code:
// Imports
import wixData from 'wix-data';
import { products } from 'wix-stores.v2'; // Correct import per Velo v2 docs
// Collection/Field Keys
const SEO_META_COLLECTION_ID = "SEOMetaData";
const PRODUCT_NUMERIC_ID_FIELD_KEY = "productNumericId";
const FOCUS_KEYWORD_FIELD_KEY = "focusKeyword";
const META_TITLE_FIELD_KEY = "metaTitle";
const META_DESC_FIELD_KEY = "metaDescription";
const WIX_PRODUCT_ID_FIELD_KEY = "wixProductId";
const WIX_PRODUCTS_NUMERIC_ID_KEY = "numericId"; // Field key in Stores/Products
export async function runBatchSeoUpdate() {
console.log("Starting batch update...");
// ... (Code to read items from SEOMetaData in batches using wixData.query with suppressAuth:true - This works) ...
for (const seoItem of batchResults.items) {
const numericId = seoItem[PRODUCT_NUMERIC_ID_FIELD_KEY];
const metaTitle = seoItem[META_TITLE_FIELD_KEY];
const metaDescription = seoItem[META_DESC_FIELD_KEY];
const focusKeyword = seoItem[FOCUS_KEYWORD_FIELD_KEY]; // We have this too
const wixProductIdFromMeta = seoItem[WIX_PRODUCT_ID_FIELD_KEY];
const numericIdString = String(numericId);
let targetProductId = wixProductIdFromMeta;
// 1. Find Product ID if not already stored
if (!targetProductId) {
try {
const queryOptions = { filter: { [WIX_PRODUCTS_NUMERIC_ID_KEY]: numericIdString }, paging: { limit: 1 } };
const productQueryResult = await products.queryProducts(queryOptions).find();
const items = productQueryResult.items || productQueryResult.products;
if (items && items.length > 0) {
targetProductId = items[0]._id;
console.log(`Found Stores Product: ${numericIdString} (ID: ${targetProductId})`);
// Optionally update SEOMetaData with the found ID (this part works)
// await wixData.update(SEO_META_COLLECTION_ID, {_id: seoItem._id, ...}, { suppressAuth: true });
} else {
console.error(`ERROR: Stores Product not found for NumericID: ${numericIdString}`);
continue; // Skip to next item
}
} catch (queryError) {
console.error(`ERROR querying Stores Product (NumericID: ${numericIdString}):`, queryError);
continue; // Skip to next item
}
}
// 2. Attempt to Update Wix Stores Product SEO Data
if (targetProductId) {
try {
// Construct the seoData payload based on documentation for updateProduct
const productInfoToUpdate = {
seoData: {
tags: [ // Using the 'tags' structure as shown in createProduct examples
{ type: "title", children: metaTitle, custom: true, disabled: false },
{ type: "meta", props: { name: "description", content: metaDescription }, custom: true, disabled: false }
// We also tried sending just { title: ..., description: ... } - same error
],
settings: { // Attempting to set focus keyword here
keywords: [
{ term: focusKeyword, isMain: true, origin: 'USER' }
]
}
}
};
console.log(`Updating Stores Product (ID: ${targetProductId}) via products.updateProduct...`);
// Call the updateProduct function
await products.updateProduct(targetProductId, { product: productInfoToUpdate } /*, { suppressAuth: false } // Default? */ ); // Added product: {} wrapper based on docs
console.log(`SUCCESS updating Stores Product: ${numericIdString}`);
// ... increment success counter ...
} catch (updateError) {
// !!! THIS IS WHERE WE GET THE ERROR !!!
console.error(`ERROR updating Stores Product (ID: ${targetProductId}, NumericID: ${numericIdString}):`, JSON.stringify(updateError, Object.getOwnPropertyNames(updateError)));
// ... increment fail counter ...
}
}
await new Promise(resolve => setTimeout(resolve, 50)); // Small delay
}
// ... (Looping and summary logic) ...
}
The Error We Consistently Receive:
When the await products.updateProduct(…) line executes, we get the following error in the Site Events logs:
{
"stack": "Error: message: ''\ndetails:\n applicationError:\n description: Forbidden\n code: FORBIDDEN\n data: {}\n ... (stack trace pointing to wix-stores.v2 files) ...",
"message": "message: ''\ndetails:\n applicationError:\n description: Forbidden\n code: FORBIDDEN\n data: {}",
"details": {},
"metadata": {}
}
What We’ve Confirmed/Tried:
- The wix-stores.v2 package is installed.
- We can successfully query products using products.queryProducts() within the same backend function.
- We can successfully read, insert, and update our custom SEOMetaData collection using wixData and { suppressAuth: true }.
- The code is run by manually triggering the exported function from the Editor’s backend console (so it should have Admin privileges).
- We tried simplifying the seoData payload in updateProduct to only include { title: …, description: … } but still received the FORBIDDEN error.
- We tried using the older wixStoresBackend.updateProductFields API (with the default import), but that resulted in … is not a function errors, suggesting it’s deprecated or the import method was incorrect.
Our Questions:
- Is it actually possible to update the product.seoData (title, description, keywords) using the products.updateProduct function from wix-stores.v2 within a backend .jsw file?
- If yes, what is the correct structure for the seoData object within the product payload expected by updateProduct? (Is the tags and settings.keywords structure correct, or should it be simpler?)
- Why are we receiving a FORBIDDEN error specifically on the updateProduct call, even when the code is executed from the backend presumably with sufficient permissions (as querying works)? Does this specific update operation require a different type of authentication or permission elevation beyond the standard backend context?
- Any help or clarification on the correct method to programmatically update Wix Stores product SEO data via Velo backend would be greatly appreciated! We’re stuck at this final hurdle.
Thanks.