top of page
SubscribePopUp

Programmatic content expansion with Python and Velo

Author: Colt Sliva

A screenshot of code behind another screenshot of product inventory in the Wix dashboard, with an image of author Colt Sliva in the bottom left corner

You might’ve heard that content is king when it comes to SEO. While that is absolutely true, it is an open-ended directive. You could create content about anything, in any writing style, for any keyword.


Keyword research can give you an idea of search demand, but you truly don’t know how content will perform until it's created. So, we have a classic chicken and egg problem: You don’t want to invest in content that won’t convert, but you don’t know if traffic will really convert until you’ve created the content.


The brilliant thing about software is that it lets you do things at scale without huge costs. For this demo, we’re going to be using programmatic content to rapidly build out an eCommerce store. By spinning up a framework of content, we can prototype and test whether something is sticky enough to get traffic.


The goal is not to produce a complete website. Rather, the framework should be used to scaffold out a set of products to test their traffic potential. Google has some rules against automatically generated content, so we’ll have to be cognizant about quality through the process.


What’s covered


This is going to be an advanced SEO strategy with some programming walkthroughs. We’re going to rely on Python for some data manipulation and we’re going to build an API endpoint with Wix’s Velo, which uses JavaScript. I would urge you to bookmark this resource because parts of this project can be adapted to many web projects.


Here are some of the core skills we are going to work through:


For those familiar with data engineering, this is a classic ETL process: We will extract product data, transform it into a format that we can use, and then load it into our Wix website with Velo.



What you need to build a programmatic SEO eCommerce store


eCommerce stores don’t need much to get started. You need a content management system (CMS), a brand, and a product.


01. CMS

For this demo, we’ll be working with Wix as our eCommerce CMS. This is the CMS of choice because of its powerful Velo platform—it is a JavaScript IDE that runs NodeJS to interact with the frontend and backend, which will allow us to programmatically add content.


02. Brand

Say hello to Candle Crafty, a boutique homemade candle store. We don’t yet know which candles we should make. Keyword research has been helpful, but doesn’t give us enough direction on which scents or colors will be popular. Instead, let’s programmatically build many variations of our product. Then, we can rely on search engines to send customers to the right products.


03. Product

For our product (supplier) we will be using scents and colors from candlescience.com. They offer a large number of scents with naming ideas and color suggestions to help new candle businesses. We can use this to scaffold out.


Scraping products with Chrome


Getting all product pages

For product extraction, we’ll need to discover all of the available scents. Luckily, the Candle Science product listing page has a complete list of URLs. There are tons of ways to scrape all <a> tags from a given page. One of the fastest ways to prototype scripts is right in the browser.


In Chrome: Right click “Inspect” > “Sources” tab > “New Snippet” button. From there, you can run JavaScript on the page. Here’s our script.


Running javascript within the Google Chrome Inspect feature

let x = document.getElementsByClassName('products')[0]
let links = x.getElementsByTagName("a");
let rows = ['Links'];
for (link of links) {
    rows.push(link.href)
}

let csvContent = "data:text/csv;charset=utf-8," 
    + rows.join("\n");

var encodedUri = encodeURI(csvContent);
window.open(encodedUri);

This script starts by getting the first element with the “products” class. Then, it gets all of the links within that class. Lines 3–5 put the links into an array, and then lines 6+ export that array into a CSV. Now we have a CSV file with all of their products!


Scraping product pages with Puppeteer


For this next section, I’ll be relying on Python. Normally, I would use the python requests library for scraping, but I quickly realized that the supplier’s website was built with Nuxt.js and the description portion of the product pages were not server-side-rendered.


We can work around that with Pyppeteer, though. It’s a wrapper for Google’s headless Chrome product that allows you to render JavaScript websites.


Looking at the page, there are a number of features we can extract that will be helpful to build our products:

  • Product Title

  • Product Description

  • Top Notes

  • Mid Notes

  • Base Notes

  • Blend Ideas

  • Color Ideas


First, let’s create a file called extract.py. At the top of that file, we import our necessary libraries. Pandas to read and write CSVs, BeautifulSoup to parse HTML, asyncio and Pyppeteer to launch our web browser.


import pandas as pd
from bs4 import BeautifulSoup
import asyncio
from pyppeteer import launch

Our web browser needs a user agent. You can use the Chrome one or identify yourself in other ways.


headers = {
    'User-Agent': 'CandleCrafty 1.0',
}

Then, we need a parse function to look for CSS classes containing the text features we’re interested in. It will take HTML content, turn it into “soup” and then let us grab titles, scent notes, or product descriptions. We return that in list form, so it’s easy to add to a CSV.


 
def parse(content):
    soup = BeautifulSoup(content, 'html.parser')
 
    #Title
    title = soup.find(class_="product-headline").text.strip()
    # Get Notes
    notes = soup.find(class_="fragrance-notes")
    txt = notes.findAll('span')
    res = []
    [res.append(spans.getText().strip()) for spans in txt if spans.getText().strip() not in res]
    res = [i for i in res if i]
    try:
        top_notes = res[1]
    except:
        top_notes = ''
    try:
        mid_notes = res[3]
    except:
        mid_notes = ''
    try:
        base_notes = res[5]
    except:
        base_notes = ''
    notes_fallback = res
 
    # Get Product Description
    txt = soup.find(class_="text")
    description_p = txt.text.split('\n')
    blend_ideas = ''
    brand_ideas = ''
    color_ideas = ''
    note = ''
    complete_list = ''
    paragraphs = ''
    for p in description_p:
        if ':' in p and 'blend' in p.lower():
            blend_ideas = p
        elif ':' in p and 'brand' in p.lower():
            brand_ideas = p
        elif ':' in p and 'color' in p.lower():
            color_ideas = p
        elif ':' in p and 'note' in p.lower():
            note = p
        elif 'complete list' in p.lower():
            complete_list = p
        else:
            paragraphs = paragraphs + '\n' + p
    return [title, top_notes, mid_notes, base_notes, notes_fallback, blend_ideas, brand_ideas, color_ideas, note, complete_list, paragraphs]

Lastly, we run our main function. It reads the CSV of all our target URLs and loops through them, using a launched browser. The results of that are saved.


async def main():
    df = pd.read_csv("download.csv")
    urls = list(df['Links'])
    browser = await launch()
    page = await browser.newPage()
    data = []
    for url in urls:
        await page.goto(url)
        content = await page.content()
        data.append(parse(content))
        df = pd.DataFrame(data, columns=['Title', 'Top notes', 'Mid notes', 'Base notes', 'Notes Fallback', 'Blend ideas', 'Brand ideas', 'Color ideas', 'Note', 'Complete list', 'Paragraphs'])
        df.to_csv('save.csv')
    await browser.close()
 
asyncio.get_event_loop().run_until_complete(main())

And with that, we’re done with our scraping script! Here’s our first row of data (slightly truncated).

​Title

Top notes

Mid notes

Base notes

Blend ideas

Brand ideas

Color ideas

Paragraphs

​Alpine Balsam

Bergamot, Champagne

​Cedar, Balsam

​Patchouli, Moss, Juniper, Pine

Blend recommendations: Saffron Cedarwood, Fireside

Alternative branding ideas: Twinkling Balsam, White Balsam, Noble Fir and Cedar

​Suggested colors: Natural, Dark Green

Imagine a crisp winter night, the crunch of newfallen snow beneath your feet, and the brisk scent of...

Data cleaning


This isn’t the most fun topic, but it tends to be a large part of data or web projects. Taking abstract data off the web and making it orderly is a hugely helpful skill.


Messy text

Our data has started out a little bit messy. We can clean up the Blend ideas, Brand ideas, and Color ideas columns by removing some of the extra text and only returning the comma-separated values that are really helpful. Each of those has a colon separating the values, so we can grab all the text following that colon.


Think something like this: Split at the string of characters at the colon. That will return an array that looks like this:


[“Blend recommendations”, “Saffron Cedarwood, Fireside”]


We can take the 2nd part of that and then strip out any whitespace. Wrap it in a try and except because we might end up with an empty row.


def clean_pretext(data):
    try:
        return data.split(':')[1].strip()
    except:
        return data
 
clean_pretext( row['Blend ideas'] )

We can also clean up several issues with the paragraphs:

  • Whitespace before or after the content

  • Multiline text

  • Broken encoding that might look like “’”


Cleaning those three action items would look like the example below. It's a lot to take in, but we are essentially breaking apart the content at broken sections and putting it back together with a join() function.


s = row['Paragraphs'].strip()
s = ' '.join(s.splitlines())
s = "".join([xiford(x) < 128else''forxins]) # This is a fancy way to fix any text encoding issues like ’

Transcribing color text to color hex codes

In some cases, we need to turn text to data. Let’s map some color ideas to hex codes. Python has a cool library called Colour, which will help us find the right color for our text description.


For this section of code, I set a default hex value (white). Then I pass in the text that has color ideas. The regex grabs only the words and turns it into an array. Then for each word, we loop through and look for a matching color. If we find one, we set the hex variable to the new results. Otherwise, we continue the loop.


After the loop is complete, the result will either be a new hex code, or fallback to white.


from colour import Color
 
def get_color(data):
    hex = '#ffffff'
    if isinstance(data, str):
        arr = re.findall(r"[\w']+", data) #extract the words
        for item in arr:
            try:
                hex = Color(item).hex
            except:
                pass
    return hex
 
get_color(row['Color ideas'])

Putting it all together

This script loops through every row, cleaning data and generating colors. Then, it saves two files: a new CSV with improved data and a new .txt file with each description on a single row.


import pandas as pd
import re
from colour import Color
 
df = pd.read_csv("../extract/data.csv") # Open csv
df['hexcodes'] = '#ffffff' # Creates a new hexcodes column with the default value of white
df['color_literal'] = 'white'
txt = '' # Create a text variable which will eventually become our text file
 
def clean_pretext(data):
    try:
        return data.split(':')[1].strip()
    except:
        return data
 
def get_color(data):
    hex = '#ffffff'
    color_literal = 'white'
    if isinstance(data, str):
        arr = re.findall(r"[\w']+", data) #extract the words
        for item in arr:
            try:
                color_literal = Color(item)
                hex = color_literal.hex
            except:
                pass
    return {"color": color_literal, "hex": hex}
 
 
for i, row in df.iterrows():
    # Remove pretext
    df.at[i,'Blend ideas'] = clean_pretext( row['Blend ideas'] )
    df.at[i,'Brand ideas'] = clean_pretext( row['Brand ideas'] )
    df.at[i,'Color ideas'] = clean_pretext( row['Color ideas'] )
 
    # Generate hexcodes
    color_data = get_color(row['Color ideas'])
    df.at[i,'hexcodes'] = color_data['hex']
    df.at[i,'color_literal'] = color_data['color']
 
    # Clean paragraph
    s = row['Paragraphs'].strip()
    s = ' '.join(s.splitlines())
    s = "".join([x if ord(x) < 128 else '' for x in s]) # This is a fancy way to fix any text encoding issues like ’
    df.at[i,'Paragraphs'] = s
 
    #Add cleaned paragraph to text file
    txt = txt + '\n' + s
 
# Export new CSV
df.to_csv("cleaned.csv")
 
# Export new content text file
with open('content.txt', 'w') as f:
    f.write(txt)

How to write strong a programmatic product description


You might remember that automatically generated content can result in a manual action. Specifically, Google highlighted “text generated using automated synonymizing or obfuscation techniques.” We also need to be mindful of the Helpful Content update. We’ll need to ensure the descriptions are helpful and useful in describing the products we create.


The flip side of that is this explanation of how Google handles duplicate product descriptions: It picks the most relevant site out of all the pages that have the duplicate content. That means there’s a very good chance a new store cannot compete with the default text from a supplier.


To deal with this, we meet Google half way: It wants to make sure the description is helpful to real humans. Google specifically calls out “human review or curation before publishing.” Basically, if you generate it, make sure it’s readable.


This leads us to our two strategies for text generation:

  • Text generated through machine learning

  • Ad lib style text generation


Machine learning has the power to be incredibly helpful for content writing, but it is as much an art as a science. For the purpose of this demo, the results from machine learning text generation were too poor and needed too much editorial work. It was the kind of content that Google doesn’t want.


With the second strategy, we can make unique enough descriptions that are helpful to the reader and detail the product, so we’re going to use that.


“Fill in the blank” strategy

This strategy is very similar to Ad Lib-type games. We provide a basic template, add in a little variety, and then get back a description.


A good product description can do a couple of things:

  • Allows the consumer to picture themselves using the product

  • Describe the product’s benefits

  • Use sensory words

  • Provide social proof

  • Use numbers


With that, we can prebuild some arrays of helpful text.


verb = ['drifting', 'wafting', 'floating', 'whirling']
noun = ['treat', 'delight', 'joy']
adj = ['charming', 'delightful']
feeling = ['elated', 'happy', 'gratified', 'blissful', 'delighted']
craftsmanship = ['well-crafted', 'homemade', 'handpoured', 'designer', 'architected', 'tailored', 'curated']
benefits = ['Find you zen with', 'Design your happy place with', 'Entice your guests with', 'Relax and thrive with']
socialproof = ['One of our most popular scents, ', 'A fan favorite, ', 'Always getting rave reactions, ']
cta = ['Get yours now!', 'Order today!', 'Come join the candle club and order now!', 'Order your ideal scent today!' 'Take this scent home today!']
numbers = ['These 8.5 ounce candles will last for an average of 60 hours.', 'Our 60 hour candles will fill your home time and time.', 'These long-lasting candles with provide scents for up to 60 hours.']
product = ['soy wax candle', 'soy candle', 'all-natural candle', 'scent']

For the description, we want to use some of the candle details. We have quite a few options to pull from:

  • Color

  • Title

  • Parts of the supplier description

  • Scent notes


We’re going to loop through each row of the CSV and pull those details to build some randomized sentences.


def getNotes(data):
    try:
        return re.findall(r"[\w']+", data)
    except:
        return []
 
for i, row in df.iterrows():
    color = row['color_literal']
    # Borrow the first sentence from the supplier
    sentence = tokenize.sent_tokenize(row['Paragraphs'])[0]
    title = row['Title']
    # Grab all our scent notes
    top_notes = getNotes( row['Top notes'] )
    mid_notes = getNotes( row['Mid notes'] )
    base_notes = getNotes( row['Base notes'] )
    # Merge them into a single array
    all_notes = [*top_notes, *mid_notes, *base_notes]
    # Format them into comma separated text. Add 'and' before the last note.
    notes_txt = ''
    for x in all_notes[:-1]:
        notes_txt = notes_txt + x + ', '
    notes_txt = notes_txt + 'and ' + all_notes[-1]

Next, we can take those variables and merge them into a list of sentences.


    #Build some sentences
    sentences = [
        f'{title} is a {random.choice(craftsmanship)} {random.choice(product)} sure to be a {random.choice(noun)} in your home.',
        f'{random.choice(socialproof)} this {color} {random.choice(product)} will leave you feeling {random.choice(feeling)}.',
        f'{random.choice(benefits)} {title} - {random.choice(craftsmanship)} with notes of {notes_txt}.',
        sentence
    ]

We can add another layer of randomization by shuffling those sentences into a random order. We wrote these so they would still make sense in any order.


random.shuffle(sentences) #Shuffle the order to add some variety

The last sentence should be a call to action. Let’s add that in.


sentences.append( random.choice(cta) ) #Append a random Call to action to the end of our content.

Finally, we can merge the array of sentences into a final description.


description = ' '.join(sentences)

Putting it all together

And, here’s what the script looks like once it’s all put together.


import pandas as pd
from nltk import tokenize
import random
import re
 
df = pd.read_csv("../../transform/cleaned.csv")
df['adlib'] = df['Paragraphs']
 
 
verb = ['drifting', 'wafting', 'floating', 'whirling']
noun = ['treat', 'delight', 'joy']
adj = ['charming', 'delightful']
feeling = ['elated', 'happy', 'gratified', 'blissful', 'delighted']
craftsmanship = ['well-crafted', 'homemade', 'handpoured', 'designed', 'architected', 'tailored', 'curated']
benefits = ['Find your zen with', 'Design your happy place with', 'Entice your guests with', 'Relax and thrive with']
socialproof = ['One of our most popular, ', 'A fan favorite, ', 'Always getting rave reactions, ']
cta = ['Get yours now!', 'Order today!', 'Come join the candle club and order now!', 'Order your ideal scent today!', 'Take this scent home today!']
numbers = ['These 8.5 ounce candles will last for an average of 60 hours.', 'Our 60 hour candles will fill your home time and time.', 'These long-lasting candles with provide scents for up to 60 hours.']
product = ['soy wax candle', 'soy candle', 'all-natural candle', 'scent']
 
 
def getNotes(data):
    try:
        return re.findall(r"[\w']+", data)
    except:
        return []
 
for i, row in df.iterrows():
    color = row['color_literal']
    # Borrow the first sentence from the supplier
    sentence = tokenize.sent_tokenize(row['Paragraphs'])[0]
    title = row['Title']
    # Grab all our scent notes
    top_notes = getNotes( row['Top notes'] )
    mid_notes = getNotes( row['Mid notes'] )
    base_notes = getNotes( row['Base notes'] )
    # Merge them into a single array
    all_notes = [*top_notes, *mid_notes, *base_notes]
    # Format them into comma separated text. Add 'and' before the last note.
    notes_txt = ''
    for x in all_notes[:-1]:
        notes_txt = notes_txt + x + ', '
    notes_txt = notes_txt + 'and ' + all_notes[-1]
 
 
    #Build some sentences
    sentences = [
        f'{title} is a {random.choice(craftsmanship)} {random.choice(product)} sure to be a {random.choice(noun)} in your home.',
        f'{random.choice(socialproof)} this {color} {random.choice(product)} will leave you feeling {random.choice(feeling)}.',
        f'{random.choice(benefits)} {title} - {random.choice(craftsmanship)} with notes of {notes_txt}.',
        sentence
    ]
 
    random.shuffle(sentences) #Shuffle the order to add some variety
    sentences.append( random.choice(cta) ) #Append a random Call to action to the end of our content.
 
    description = ' '.join(sentences)
    df.at[i,'adlib'] =  description
 
df.to_csv('adlib.csv')

Generating product images on the fly


With some base data, we can look to generate images. There are a few really incredible machine learning image-generation tool kits now available, like Google’s Imagen or OpenAI’s DALL·E 2, but we don’t need to be that fancy.


I originally tried to use line art and Scalable Vector Graphics (SVGs) to programmatically generate the images. It’s a good strategy because it’s very similar to HTML and you can even use CSS. All the elements of the image are programmable.


Vector graphic candle example

However, the quality just wasn’t there. Maybe if you’re a better designer, this is a viable strategy, which is why it's still worth mentioning.


The static base layer of a template candle image, showing a plain white candle.

Instead, let’s try using regular bitmap pixel images. The idea here will be to have two layers of PNGs. One will be our base layer (shown above), which will be a static background. The other image (shown below) we will tint with various RGB settings. Then, we can dynamically color our images.

The layer of the candle image to be tinted with RGB settings and dynamically added.

On the left, we have a base image that is fairly nice looking by itself. There’s room to change wax color or add text onto the jar. We can color/tint the layer on the right, and overlay it back onto the base layer.


Let’s power up the Python Imaging Library (PIL) for this demo to dynamically generate the images. Start a new Python file and begin it with the following imports:


from PIL import Image, ImageOps, ImageDraw, ImageFont

We need to load in some preliminary assets, like our PNGs and some fonts. I’ve added the same font twice, just once at 28 pixels and once at 10 pixels.


foreground = Image.open("image/Candle01.png")
background = Image.open("image/Candle02.png")
fnt = ImageFont.truetype("Shrikhand-Regular.ttf", 28)
fnt_sml = ImageFont.truetype("Shrikhand-Regular.ttf", 10)

Next, we need to define a magic function called tint_image. It’s going to take the image, copy its opacity, make it grayscale, pop in some color, and then put the opacity back. The result is a tinted image! Using that on the foreground dynamically generates our colored candle. We’ll make our demo orange.


def tint_image(src, color="#FFFFFF"):
   src.load()
   r, g, b, alpha = src.split()
   gray = ImageOps.grayscale(src)
   result = ImageOps.colorize(gray, (0, 0, 0, 0), color)
   result.putalpha(alpha)
   return result
 
color = "orange"
foreground = tint_image(foreground, color)

We can go ahead and merge the colored foreground into the background.


background.paste(foreground, (0, 0), foreground)

As a next step, we can add some text. Here I’m writing the name as “Apricot Grove.” The text is centered 330 pixels to the right and 400 pixels down (it was just trial and error to find the right location to place the text).


We use the small text to fill out some white space.


d = ImageDraw.Draw(background)
d.multiline_text((330, 400), "Apricot\nGrove", font=fnt, fill=(100, 100, 100, 10), align='center', spacing=28, anchor="mm")
d.multiline_text((330, 650), "Homemade soy wax candles", font=fnt_sml, fill=(80, 80, 80, 10), align='center', spacing=14, anchor="mm")
background.show()

Lastly, we save the complete image as a PNG.


background.save("save.png")

The result is radically better than the line drawing and creates a very predictable result.


An example of the automatically generated candle image, with the label “apricot grove” and an apricot-colored wax

Loading the products


Using all of the scripts we’ve written so far, we can connect our new product information to Velo.


Building an API endpoint

Velo is an extremely powerful platform that allows direct interaction of both the frontend and backend of a Wix website. Velo offers fine-tuned control across a spectrum of CMS features. Today, we are just expanding the functionality of our backend with a few Velo functions. This guide is just the first step into Velo and what could be built in the future.


At the top of the Wix Editor, make sure you’ve enabled Dev mode.


The Velo Dev Mode menu within the Wix Editor

Next, we’ll want to hop into the Public & Backend section. Under Backend, you can select Expose site API, which will auto generate a file called http-functions.js. This opens up your Wix website as an API, enabling you to write custom functions or services as endpoints. Think of it like a backend with NodeJS, but with direct integrations into Wix.


How to expose the site API within the public & backend section of the Wix editor

You can make GET or POST requests against these and access all of the Velo tooling. Functions just need to be prepended with get_funcName() or post_funcName() to define their purpose.


With our script, here are the Velo libraries we’ll need: Import Wix Media Backend, Wix Stores Backend, and Wix HTTP Functions.


import { mediaManager } from'wix-media-backend';
import wixStoresBackend from'wix-stores-backend';
import { ok, notFound, serverError } from'wix-http-functions';

The mediaManager library from wix-media-backend allows us to manipulate the product images we upload. The wix-store-backend is where we will upload products. Lastly, the wix-http-functions allows us to build our API responses.


Here are the steps that we need to take in our Velo endpoint:


01. Accept a POST request with a payload containing product and image data.

02. Create a product with our product data.

03. Upload an image from our local PC to our Wix site.

04. Apply that image to the previously repeated product.


To start, let’s create a new endpoint:


export async function post_echo(request) {
   let response = {
       "headers": {
           "Content-Type": "application/json"
       }
   }
   response.body = 'This endpoint works!'
   ok(response)
}

After saving the file, this example function can be accessed at https://{my-username}.wixsite.com/{my-store-name}/_functions-dev/echo. Hitting it with a POST request should send back the message!


Next, let’s write some code to create a product. So, we create a new promise to allow our function to run asynchronously. Modern JavaScript just makes life easier. Then, we use wixStoresBackend, which we imported earlier, and call createProduct with our product data that we pass in. Then we get back information from Velo, like the product ID or extra details about what was just created. That’s all we need for this step.


functioncreateProduct(product){
returnnewPromise(resolve => {
   wixStoresBackend.createProduct(product).then(res => {
     resolve(res)
   }).catch(err => {console.error(err)})
 })
}

The next step is to upload an image from our local PC to the Wix site. We will need a base64 encoded image, a folder name, image filename, and a mimetype (like png). The Base64 encoded image gets turned into a buffer and streamed over to Wix. Then, we just give the Wix mediaManager all the info it needs and it will upload the image for us!


function uploadImage(image_base64, image_folder, image_filename, image_mimeype) {
   return new Promise(resolve => {
       let buf = Buffer.from(image_base64, 'base64')
       mediaManager.upload(
           image_folder,
           buf,
           image_filename, {
               "mediaOptions": {
                   "mimeType": image_mimeype,
                   "mediaType": "image"
               },
               "metadataOptions": {
                   "isPrivate": false,
                   "isVisitorUpload": false,
               }
           }
       ).then(res => {
           mediaManager.getDownloadUrl(res.fileUrl).then(url => {
               resolve(url)
           })
       });
   })
}

For the last step, we need a function to put everything together. That looks something like this:

  • Get the data from the post request.

  • Upload the image.

  • Create the product.

  • Use the product ID we get back from the upload to map the image url to the product.

  • Profit!


export async function post_upload(request) {
   let response = {
"headers": {
"Content-Type": "application/json"
       }
   }
   let body = await request.body.text()
   let data = JSON.parse(body)
   let img_url = await uploadImage(data.image.base64, data.image.folder, data.image.filename, data.image.mimeype)
   let product = await createProduct(data.product)
   response.body = product.productPageUrl
   ok(response)
   wixStoresBackend.addProductMedia(product._id, [{'url':img_url}])
}

In summary, here is that code all put together:


import { mediaManager } from'wix-media-backend';
import wixStoresBackend from'wix-stores-backend';
import { ok, notFound, serverError } from'wix-http-functions';

exportasyncfunctionpost_upload(request) {
let response = {
"headers": {
"Content-Type": "application/json"
       }
   }
let body = await request.body.text()
let data = JSON.parse(body)
let img_url = await uploadImage(data.image.base64, data.image.folder, data.image.filename, data.image.mimeype)
let product = await createProduct(data.product)
   response.body = product.productPageUrl
   ok(response)
   wixStoresBackend.addProductMedia(product._id, [{'url':img_url}])

}

// Upload Image
// Returns a URL which can be assigned to a product
functionuploadImage(image_base64, image_folder, image_filename, image_mimeype) {
returnnewPromise(resolve => {
let buf = Buffer.from(image_base64, 'base64')
       mediaManager.upload(
           image_folder,
           buf,
           image_filename, {
"mediaOptions": {
"mimeType": image_mimeype,
"mediaType": "image"
               },
"metadataOptions": {
"isPrivate": false,
"isVisitorUpload": false,
               }
           }
       ).then(res => {
           mediaManager.getDownloadUrl(res.fileUrl).then(url => {
               resolve(url)
           })
       });
   })
}

functioncreateProduct(product){
returnnewPromise(resolve => {
let productID = ''
   wixStoresBackend.createProduct(product).then(res => {
     resolve(res)
   }).catch(err => {console.error(err)})
 })
}

Using the API endpoint

Now that we have somewhere to send out data, let’s create some products!


First, create a new Python script called “load.py.”


We’ll have a number of steps to build all of the information that makes up a product, including:

  • A filename for the image

  • A SKU for the product

  • A high CTR meta description

  • A rich, search-optimized title

  • Something to open the image and convert it to base64


Here’s what all of those look like:


def get_filename(name):
   return name.lower().replace(" ", "_") + '.png'
 
def get_sku(name):
   return name.lower().replace(" ", "_") + '_g'
 
def get_metadescription(color, name):
   year = date.today().year
   return f'{year} edition{color} soy wax candles - hand poured and hand crafted. CandleCraftys {name} best candles for living spaces and ambience. Buy now!'
 
def get_image(name):
   filename = f'image/saves/save-{name}.png'
   with open(filename, "rb") as f:
       im_bytes = f.read()       
   im_b64 = base64.b64encode(im_bytes).decode("utf8")
   return im_b64
 
def get_seotitle(row):
   name = row['Title']
   notes = (row['Top notes'].split(', ') + row['Mid notes'].split(', ') + row['Base notes'].split(', '))
   note = random.choice(notes)
   color = row['color_literal']
   return f'{name} - {note} scented {color} soy candles'

Next, we need to build the product and image object for every row in our CSV of product data. First, we open the CSV in pandas and loop through it. We can grab some common things we’ll need from that row like name and color.


Then, I check to see if the product has “Discontinued” in the name as a final quality check. After that, we map our functions or variables to relevant fields in the big data object. This is the magic sauce that this whole guide has been building up to.


df = pd.read_csv('data.csv')
for i, row in df.iterrows():
   name = row['Title']
   color = row['color_literal']
   if 'Discontinued' not in name:
       print(name)
       data = {
           'image': {
               'base64': get_image(name),
               'folder': 'programmatic',
               'filename': get_filename(name),
               'mimetype': 'image/png'
           },
           'product': {
               'name': name,
       'description': row['adlib'],
       'price': 20,
       'sku': get_sku(name),
       'visible': True,
       'productType': 'physical',
       'product_weight': 1,
       'product_ribbon': '',
       "seoData": {
          "tags": [{
                  "type": "title",
                  "children": get_seotitle(row),
                  "custom": False,
                  "disabled": False
              },
              {
                  "type": "meta",
                  "props": {
                      "name": "description",
                      "content": get_metadescription(color, name)
                  },
                  "custom": False,
                  "disabled": False
              }
          ]
       }
       }
   }
   upload(data)

The last line on that section of code is an upload() function, which we don’t have yet. Let’s go through that now.


Get the product data and convert it to JSON that the Wix API can read. Then, send the payload off.


def upload(data):
   url = 'https://username.wixsite.com/candle-crafty/_functions-dev/upload'
   headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
  
   payload = json.dumps(data) ## Pass in all product details at once
   response = requests.post(url, data=payload, headers=headers)
   try:
       data = response.json()    
       print(data)               
   except requests.exceptions.RequestException:
print(response.text)

Let the automated store run


If the stars align and all our code works, we’ll have a fully functioning storefront piled high with dynamically built products. Occasionally refresh the page and watch as products appear. Enjoy the numerous products which you can now A/B test, optimize, and look for top-selling variations.


A screenshot of the python code on the left side of the image, with product listings in the Wix dashboard on the right side.

Why content expansion works


This entire strategy is centered around the idea of casting a wide net: SEO is already top-of-funnel-marketing. At the very top of the SEO funnel itself, is keywords. By creating many new diverse and keyword-driven pages, we are increasing impressions. Then through optimization strategies, we can increase clickthrough rate.


The real secret to content expansion, though, is to collect data. If you don’t have data, you won’t be empowered to make informed decisions. By generating a broad range of keyword-relevant pages, you can begin to iterate. Build in high impact areas, and reduce or redirect low impact pages. Remember, content is king, but not all kingdoms are prosperous.


Customize to fit your needs with Velo


Velo is a powerful CMS IDE—this article is just the beginning of what you can accomplish with it. I would urge anyone to review the API Overview to see just how much can be achieved.


Adding features to most content management systems feels hacky and leaves you wondering if the software might break at any time. Integrating with Velo was the opposite. It felt like the Wix website wanted me to customize it to fit any custom request I had. If you’ve ever needed more from your CMS, I would consider Wix and enabling Velo. It’s the best spectrum between no-code, low-code, and full-code sites.


 

Colt Sliva

Colt Sliva is a technical SEO who has experience working with SaaS, eCommerce, UGC Platforms, and News Publishers across the Fortune 500. His main area of study is SEO at scale, automations, and breaking things to see how they really work.

Get the Searchlight newsletter to your inbox

* By submitting this form, you agree to the Wix Terms of Use

and acknowledge that Wix will treat your data in accordance

with Wix's Privacy Policy

Thank you for subscribing

bottom of page