DEV Community

Cover image for The Metadata Mistake That Makes Great Art Invisible in Search

The Metadata Mistake That Makes Great Art Invisible in Search

I once spent a weekend making 40 vector illustrations I was genuinely proud of, ran them through my pipeline, uploaded the batch to Adobe Stock, and got search traffic of exactly zero over the following two weeks. The art was fine. The metadata was the problem, and it took me embarrassingly long to figure that out, because nothing failed. The uploads were accepted. They just sat there, invisible.

Here is what I learned fixing it, and the rules I now enforce in code so it never happens again.

The header is a hard gate, not a suggestion

All three of the stock platforms I sell through validate the CSV header before they look at a single row. Adobe Stock wants a 5-column header. Vecteezy wants 4 columns. Dreamstime wants 15. If your header does not match exactly, the upload is rejected instantly, before any human or quality model sees your work.

That part is annoying but obvious. The invisible part is subtler: a header that is close enough can pass a manual glance but still fail or, worse, silently drop a field. I had a filename column mapped one position off on a Dreamstime sheet once. Everything uploaded. The titles were all wrong. Nobody searching for what I actually made could find it, because the title field was full of neighboring filenames.

The fix in my pipeline is boring and total: make_metadata.py writes the exact header each platform demands and nothing else. No custom columns, no "helpful" extras. Adobe gets 5 columns, Vecteezy gets 4, Dreamstime gets 15, and the script refuses to emit anything different. When the platform changes its spec, I change one constant in one file.

The filename is your first draft of the title

If you do not supply real titles, the default behavior in my script is to title-case the filename and use it as the title. That is fine for a first pass, and I want to be honest that it is a first pass. "asset-0041-final-v2.jpg" becomes "Asset 0041 Final V2", which is a string of noise that matches zero search queries.

What actually works: a titles.json file keyed by filename, with a real descriptive title and a keyword list per asset. I write these while the batch is fresh, maybe 30 seconds per asset, and it is the highest-value 30 seconds in the whole pipeline. Search on stock platforms matches on title and keywords far more than on anything visual. A great image with a lazy title is a great image nobody sees.

Double-posting is the fastest way to get flagged

This one bit me early. I re-ran a batch script that had partially failed, forgot which items had already gone through, and posted duplicates. Duplicated content is one of the quickest ways to draw a review flag on your account, and account-level flags affect how your future uploads get reviewed. Your clean work pays for your sloppy bookkeeping.

The fix is a dumb little ledger, and dumb is the point. upload_tracker.py keeps a plain record of what has been posted:

python upload_tracker.py pending items.txt   # what is NOT done yet
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Enter fullscreen mode Exit fullscreen mode

Before any batch runs, it checks the ledger. A re-run can never double-post because the ledger is the source of truth, not my memory at 1 a.m. It uses the Python standard library alone. No database, no install.

Review checks happen after the metadata gate

It helps to know the order of operations. Your CSV header is checked first, mechanically. Then your files go through review, where quality and relevance are judged. If the metadata layer is wrong, you never even reach the review that your art could pass. So when a batch "fails", check the boring layer first. Nine times out of ten on my own projects, the art was fine and the plumbing was not.

Folder structure is metadata too

Once you have more than a couple hundred assets, a flat folder becomes a junk drawer and you start re-uploading things you already posted, which loops back to the flagging problem. The layout I use now:

project/
  assets/            raw output, one file per asset
  output/            ready-to-submit files + metadata.csv
  packs/             finished product zips + covers
  research/          notes and sources
  scripts/           the tools
Enter fullscreen mode Exit fullscreen mode

One thing I got wrong once and will not repeat: I deleted research/ during a cleanup pass. Those notes were the raw material for two later products. Keep it forever. Storage is cheaper than re-research.

Why all of this runs on a tiny box

None of this needs a real server. The whole pipeline ran on 1 vCPU, 2 GB RAM, no GPU. The habits that make that work: one process at a time, gc.collect() after each asset, measuring peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss instead of guessing, and inspecting previews at 60 dpi instead of rendering at 300. Metadata work in particular is just text processing, it runs in kilobytes.

The tool this became

After packaging 39 products (856 MB of output, from about 19,000 lines of pipeline code backed by 300+ pages of research), I pulled the four scripts that solve exactly these problems into a standalone kit: make_metadata.py for the Adobe 5-column, Vecteezy 4-column and Dreamstime 15-column CSVs, upload_tracker.py for the ledger, plus md2pdf.py and pack_product.py for turning documents into sellable PDFs and zips with covers. Two third-party packages total, Pillow and reportlab, and the metadata and tracking scripts need nothing but Python. It is the tool I built because I lost a weekend to invisible art and did not want to lose another one.

See the kit


Building digital products? Free guides & kits for digital product sellers.

Top comments (0)