I shipped a free tool that scans any Shopify storefront and tells the owner how many product images are missing alt text. No install, no OAuth, no signup — you type a domain and get a number back in a few seconds.
The scanning logic took an afternoon. The compliance work took longer, and it changed the design three times.
Here's what actually happened.
Why build a scanner at all
I run a small Shopify app that writes alt text for product images. It's been live for months. Installs: close to zero.
The App Store doesn't send traffic to new apps. No reviews, no installs, low search rank — the listing just sits there. Waiting is not a strategy.
The other option was paid ads, which I'm not doing. So the funnel has to start somewhere free, and the only thing a stranger will do for you before installing anything is type their domain into a box.
That's the whole thesis: turn "my images might not have alt text" from an abstract worry into a number. 612 out of 847. That number creates the next question — how do I fix it — and that's where the app comes in.
Discovery 1: the public product feed lies about alt text
The obvious data source is /products.json, which every Shopify store exposes by default. It returns products, variants, and images with an alt field. One request, structured JSON, done.
I tested it against my own dev store before writing any UI:
GET /products/test-product → an img tag with alt="White wireless earbuds product photo" GET /products.json → "images": [{ "alt": null }] Same image. The rendered page has alt text. The JSON says null.
If I had built the report on products.json, that image would have been counted as missing. The store owner would have checked the product page, seen alt text right there, and concluded the tool was broken. One false positive kills trust in every other number on the page.
So the architecture changed:
-
products.jsongives me counts — how many products, how many images. It's accurate for that. - Rendered product pages give me the ratio — what share of images actually have descriptive alt text.
The report labels the ratio as sampled, not exact, and says so in the caveats. Being approximate and honest beats being precise and wrong.
Discovery 2: robots.txt has to be read before anything else
The scanner reads a store I don't own. That's the kind of thing that gets tools blocked, complained about, or sued depending on how it's done.
The line I settled on: read robots.txt first, and if it says no, stop.
Not "read it and see if we can work around it." Stop. Return a message saying the store asked automated tools not to scan it, and show nothing else.
Implementation detail that bit me: a single-pass robots.txt parser is wrong for any file with more than one group. Real files look like this:
User-agent: * Disallow: /cart User-agent: Googlebot Disallow: / A naive parser sets a wildcard = true flag when it sees User-agent: *, then keeps applying every Disallow it encounters — including the one meant for Googlebot. My first version would have refused to scan stores that were perfectly happy to be scanned.
The fix is to split the file into groups on blank lines first, then evaluate only the group whose agent list contains *. Twenty lines of code, and it's the difference between respecting a store's wishes and quietly misreading them.
Discovery 3: HTTP 429 is the normal state, not an error
I burned through my own IP's rate limit during testing and started seeing this:
HTTP/2 429 retry-after: 60 server: cloudflare Shopify rate-limits /products.json per IP. On Vercel, the egress IP is shared with every other function invocation in the region, so a busy neighbour can throttle you without you doing anything wrong.
The first version treated 429 like any other failure — if (!res.ok) return null — and fell through to "this doesn't look like a Shopify store." Which is exactly wrong. The store is fine. We're the ones being told to slow down.
Now 429 is a first-class outcome with its own message: "Shopify is temporarily rate-limiting automated requests to this store. This isn't a problem with your store — wait a minute and try again."
That distinction matters more than it sounds. A user who's told their store is broken will go hunting for a problem that doesn't exist.
The compliance checklist I ended up with
Every one of these is enforced in code, not in a policy document:
Read robots.txt before touching store data. Disallow means stop, with a message explaining why.
Touch only public endpoints. /robots.txt, /products.json, /products/:handle. No Admin API, no authentication, no customer data, no order data.
Store nothing. Scan results live in memory for one request, plus a short-lived cache keyed by domain so a refresh doesn't re-crawl. Nothing is written to a database.
Identify yourself. The User-Agent is AltOptimizerScanner/1.0 (+https://alt-optimizer.vercel.app/scanner; read-only alt-text audit). A store owner reading their access log can see who we are and why we're there.
Block SSRF. Without a hostname guard, "scan this domain" is a gadget for hitting 169.254.169.254 or an internal service. Localhost, 10.x, 192.168.x, 172.16-31.x, 169.254.x are all rejected before any request goes out.
Cap everything. 250 products enumerated, 12 product pages sampled, 5.5 seconds total. A scan is roughly 14 requests. The goal is "roughly how bad is it," not mirroring the catalogue.
Say what the numbers are. The coverage score is defined in the report footer — share of sampled images whose alt actually describes the image. Title-copied alt and sub-15-character alt don't count. No black box.
Make no promises. The report states plainly that it's a sample of public pages, that figures are indicative, and that nothing here is a guarantee of ranking or traffic.
What it looks like now
Type a domain, wait a few seconds, get:
- A coverage score out of 100
- A breakdown: no alt / copied from title / too short / actually descriptive
- Two or three real examples from the store, with the actual alt text shown
- Caveats, in plain text, not fine print
That's it. No email gate, no "create an account to see your score," no modal.
The email gate is tempting — capturing addresses would be the real byproduct of a free tool. I left it out of v1 for two reasons: it's another compliance surface (consent, storage, actually sending the report, CAN-SPAM), and I'd rather find out whether anyone uses the thing before building a list nobody wants.
What I'd tell someone building the same thing
The scanning is the easy part. fetch and a regex over img tags will get you 90% of the way.
The parts that took real thought:
- Verify your data source against reality, not against its documentation.
products.jsonhas analtfield. It's also wrong sometimes. One afternoon of testing saved a report full of false positives. - Treat robots.txt as a hard gate, not a hint. It's both the ethical line and the legal one, and it costs you twenty lines.
- Distinguish "their problem" from "your problem." 429 is your problem. Reporting it as theirs makes the tool useless at exactly the moment it matters.
- Put the caveats where people will read them. A sampled estimate presented as exact is a lie with extra steps.
The scanner is live at alt-optimizer.vercel.app/scanner if you want to point it at a store and see what comes back. It's free, it stores nothing, and it will tell you plainly if it can't scan.