Last March I zipped up a template pack, uploaded it to Gumroad, and got zero sales for two weeks. When I finally looked at the listing through a buyer's eyes, the problem was obvious. The zip had a name, a price, and a wall of files. No cover image at all. The listing card was a gray placeholder. Nobody buys a gray placeholder.
I fixed it by hand that day: one big image for the listing, one small square for a social post. Then I did the same thing manually for the next product, and the next, and by product five I was resizing JPGs in an image editor at midnight wondering why I kept doing this. That is the whole reason pack_product.py renders two images instead of one. Here is what I learned about why both exist and what each one is actually for.
The cover is 1280×720 because the listing page is wide
Marketplace listing cards and product pages favor a landscape image. 1280×720 is a 16:9 frame, it fills the hero slot on a product page without letterboxing, and it is small enough to render in under a second on the hardware I actually own. That last part matters more than people admit. My whole pipeline runs on a single vCPU with 2 GB RAM and no GPU. I tested rendering covers at 2560×1440 once, out of a vague feeling that bigger is more professional. The peak RSS roughly doubled, the render took three times as long, and the file looked identical at the size anyone actually views it. Render at the resolution you need, not the resolution you wish you needed.
The cover carries the title and the subtitle, drawn with Pillow, nothing fancier. Text on a flat background, readable at card size. I am not a designer and the script does not pretend I am. It just guarantees that every one of the 39 products I have shipped has a real, consistent cover instead of whatever I felt like making that night.
The thumbnail is 600×600 because squares travel
The cover lives on the listing page. The thumbnail lives everywhere else. Square images are what you drop into a social post, a newsletter, a changelog, a marketplace category grid that crops to a square whether you like it or not. If I only render the wide cover, every square crop of it is a bad crop. So the script draws a second image at 600×600 with its own layout, not a resized copy of the first.
600 pixels is deliberately modest. It is big enough to survive a Retina display at typical embed sizes and small enough that the file is a few hundred kilobytes. When you are shipping 856 MB of output across a catalogue, thumbnail weight adds up, and there is no reason to pay it.
Two renders, one command, no decisions left to me
The call looks like this:
python pack_product.py my-product "My Product" "A short subtitle" 9
That writes my-product.zip, my-product-cover.jpg and my-product-thumb.jpg into a _system folder. The zip is the deliverable, the cover is the listing, the thumbnail is the promotion. One command, three artifacts, and the number at the end is the file count that gets printed on the cover so buyers know what they are getting.
The reason this matters is not speed, it is consistency. Before the script, some of my products had covers and some did not, and the ones without covers were the ones that did not sell. Once packaging became automatic, every product got both images by default, and I stopped having products that looked unfinished. The script does not make better art than me. It makes sure there is never a product shipping without its images.
The small-server habits baked into it
Rendering two images per product on a 1-core box taught me a few habits that are now just how the script works:
- One process at a time. Parallel image jobs on one core do not save time, they just spike memory.
-
gc.collect()between items, because Pillow holds references longer than you expect. - Measure peak RSS with
resource.getrusage(RUSAGE_SELF).ru_maxrss, not average usage. The peak is what OOM-kills you. - Render at target resolution. A 600×600 thumbnail should never pass through a 300 dpi intermediate.
None of these are clever. They are the difference between a pipeline that runs overnight unattended and one that dies at product 14.
What I would tell the version of me from last March
The failure was not the pricing or the product. It was that packaging was a manual step, and manual steps get skipped when you are tired. The two images are cheap to automate and expensive to skip, because a listing without a cover converts at basically zero no matter what is in the zip.
pack_product.py is one of four scripts in the pipeline starter kit, alongside make_metadata.py for the Adobe 5-column, Vecteezy 4-column and Dreamstime 15-column CSVs, upload_tracker.py so a re-run never double-posts, and md2pdf.py for turning Markdown into a styled PDF. It is the same tooling that packaged those 39 products, validated on 1 vCPU and 2 GB RAM, with two third-party dependencies total. If you are building your own pipeline, it is the packaging layer I wish I had on day one.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)