Last spring I opened my Gumroad dashboard next to a folder of 40 finished products and felt embarrassed. The products were fine. The catalogue looked like forty strangers had made them. One cover was dark blue with serif type, one was neon green with clip art, one was just a screenshot of a zip file. A buyer emailed me asking if all of it was really from the same shop. That was the moment I stopped treating packaging as an afterthought and started treating the catalogue as one product.
Here is what I learned fixing it, running the whole thing on a 1 vCPU, 2 GB RAM server with no GPU.
A catalogue is one product with many SKUs
The mental shift that fixed everything: buyers do not evaluate individual zips, they evaluate the shop. If your covers, file names, and folder structure are inconsistent, every new product makes the shop look less trustworthy instead of more.
So I picked one template for covers and never touched it again. My pack_product.py renders every cover at 1280x720 and every thumbnail at 600x600 from the same Pillow code path, same font, same layout, only the title and subtitle change. It takes a product name, a subtitle, and a file count, and it writes the zip, the cover, and the thumb in one command:
python pack_product.py my-product "My Product" "A short subtitle" 9
Nine products in, the covers are interchangeable. That is the goal. A buyer who liked one product can recognize the next one at a glance.
Naming and folders before anything else
A catalogue becomes a junk drawer without a fixed structure. This is the layout the whole pipeline assumes, and I stopped improvising it:
project/
assets/ raw output (one file per asset)
output/ ready-to-submit files + metadata.csv
packs/ finished product zips + covers
research/ your notes and sources
scripts/ the tools
Two rules that sound boring and matter a lot. First, filenames become titles. My metadata script title-cases the filename when no title file is supplied, which means botanical-border-04.jpg becomes a usable title on the first pass. If your filenames are final_final_v3.jpg, your CSVs inherit that. Second, keep research/ forever. I deleted my scrape logs once to "clean up" and lost the raw material for three follow-up products. Never again.
Metadata is where stock platforms judge you first
If you also sell on stock channels, the catalogue problem doubles, because each platform demands a different CSV shape and rejects a wrong header instantly. Adobe Stock wants a 5-column header. Vecteezy wants 4 columns. Dreamstime wants 15. Get one column wrong and the whole upload bounces before a human ever sees your art.
I wrote make_metadata.py so it writes exactly the header each platform demands and nothing else. Same folder of assets, three commands:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
python make_metadata.py ./my-assets --platform vecteezy --out vecteezy.csv --ext .jpg
python make_metadata.py ./my-assets --platform dreamstime --out dreamstime.csv --ext .jpg
The consistency benefit is the same as with covers: every asset across every platform gets titles and keywords from one source of truth, so the brand reads the same whether the buyer finds you on Adobe or on your own checkout.
The one mistake that gets accounts flagged
This one is not about looks, it is about survival. The fastest way to get a stock account flagged is running your upload batch twice. I did it once early on, re-ran a script that had partially failed, and posted 30 duplicates. Cleaning that up took an evening and I got lucky.
Now upload_tracker.py is a plain ledger that remembers what has been posted:
python upload_tracker.py pending items.txt # shows what is NOT done yet
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
A re-run can never double-post, because the ledger is checked before anything is queued. It uses only the Python standard library, no install at all.
Turning documents into products that look like products
Half my catalogue is not art files, it is documents: guides, checklists, prompt playbooks. A raw Markdown file is not a product. md2pdf.py takes one Markdown file and renders a clean PDF with styled headings, tables, lists, and links:
python md2pdf.py content.md product.pdf "My Product Title"
It uses reportlab, that is the whole dependency. Same typography every time, so a PDF in the catalogue matches the covers next to it.
Small server habits that kept this alive
None of this needed a bigger machine, but it needed discipline. One process at a time, never fan out image jobs in parallel on a single core. Call gc.collect() after each asset to free memory between items. Measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss, not average usage, because the peak is what kills a 2 GB box. Run heavy render steps one file per subprocess so memory returns to the OS. And inspect previews at 60 dpi instead of rendering at 300 when you only need to check a layout.
The end result: 39 finished products, 856 MB of packaged output, sitting under about 19,000 lines of pipeline code, all validated on that same 1 vCPU, 2 GB RAM server.
The tool I built from this
Everything above is not advice I wrote down after the fact. It is four standalone Python scripts, no framework, no cloud account: make_metadata.py, upload_tracker.py, md2pdf.py, and pack_product.py. Two third-party packages total, Pillow and reportlab, and half the scripts need nothing but Python. They are the packaging layer of a bigger pipeline, and they are what turned my junk drawer into a catalogue that looks like one brand.
If you want the same scripts instead of rebuilding them from this post, they are packaged as the Pipeline Starter Kit.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)