Website Image Extractor — Alt Text, Size & SEO Audit
Pricing
$0.80 / 1,000 image reads
Website Image Extractor — Alt Text, Size & SEO Audit
List every image on a web page or site with its real pixel size and file size, alt text and SEO/accessibility flags. Reads only the first bytes of each image: nothing is downloaded or stored. Pay per image, blocked pages are free.
Pricing
$0.80 / 1,000 image reads
Rating
0.0
(0)
Developer
Francesco Antonio Russo
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Give it a web page (or a whole site) and get one record per image: the real pixel size and file size, the alt text, where the image sits on the page, and a list of SEO, accessibility and performance problems. It reads only the first bytes of each image to learn its size: no image is downloaded, stored or returned, so there is nothing to unzip, nothing to pay storage for, and no copy of anyone's pictures.
Use it to audit alt text before a launch, find the 3 MB hero image that slows a page, list every image of a competitor's product page with its real dimensions, feed an image inventory into a spreadsheet or a CMS migration, or check that a site declares width/height (layout shift) on its images.
- Plain HTTP, no browser. Fast, cheap, 256 MB of memory. The flip side: images that a page only draws with JavaScript are not seen (see Limits).
- Pay per image read: $0.0008 per image ($0.80 per 1,000). Pages that block us, images that cannot be read and pages that need JavaScript are free.
- Polite and honest. It identifies itself as
ImageMetadataBot, obeysrobots.txt(for the page and for the host of every image), spaces its requests and never tries to get past a bot wall.
What you get
| Field | Meaning |
|---|---|
pageUrl, imageUrl, finalUrl | The page, the image as the page declares it (absolute URL), and the image URL after redirects |
source | img · picture · srcset (no src, the largest candidate) · lazy-attr:data-src (also data-lazy-src, data-original, data-lazy, data-url) · css-background · og:image · twitter:image |
alt | The alt text as written. null = no alt attribute at all, "" = a decorative image (the difference matters for accessibility) |
declaredWidth, declaredHeight | The width / height attributes of the tag, in pixels |
naturalWidth, naturalHeight | The real size, read from the file header (JPEG: EXIF rotation applied, as browsers do) |
format, contentType | Format from the file's magic bytes (jpeg, png, gif, webp, avif, heic, svg, ico, bmp) and the type the server sent |
fileSizeBytes, sizeSource | File size, and how it was learned: range (Content-Range) · content-length · body (a short body counted) · unknown |
srcset | Every srcset and <picture><source> candidate with its descriptor (800w, 2x) and MIME type |
loading, fetchpriority, region, domIndex, occurrences | loading and fetchpriority attributes; nearest header/nav/main/aside/footer; position on the page; how many times the same URL appears on it |
status | ok (read) · broken (404/410) · unmeasured (could not be read: free) |
unmeasuredReason, httpStatus | Why an image was not measured (robots_txt, blocked, rate_limited, timeout, http_error, private_address, no_dimensions…) and the HTTP status |
issues | The flags below |
charged, checkedAt | Whether this record is a billed event; when it was checked |
Example record (real run, https://wordpress.org/news/)
{"pageUrl": "https://wordpress.org/news/","imageUrl": "https://wordpress.org/news/files/2024/01/sunita-rai-featured-img.jpg","finalUrl": "https://wordpress.org/news/files/2024/01/sunita-rai-featured-img.jpg","source": "img","alt": "Sunita Rai at WordCamp Asia","title": null,"declaredWidth": 1920,"declaredHeight": 1200,"naturalWidth": 1920,"naturalHeight": 1200,"format": "jpeg","contentType": "image/jpeg","fileSizeBytes": 1457137,"sizeSource": "range","srcset": [{ "url": "https://i0.wp.com/wordpress.org/news/files/2024/01/sunita-rai-featured-img.jpg?w=1920&ssl=1", "descriptor": "1920w" },{ "url": "https://i0.wp.com/wordpress.org/news/files/2024/01/sunita-rai-featured-img.jpg?resize=300%2C188&ssl=1", "descriptor": "300w" }],"loading": null,"fetchpriority": "high","region": "main","domIndex": 0,"occurrences": 1,"status": "ok","unmeasuredReason": null,"httpStatus": 206,"issues": ["TOO_HEAVY", "LEGACY_FORMAT"],"charged": true,"checkedAt": "2026-10-15T10:37:41.796Z"}
(The real record lists five srcset candidates; two are shown. 1.4 MB is the real size of that file: the check read 32 KB of it.)
The flags in issues
| Flag | When it is raised | Why you care |
|---|---|---|
ALT_MISSING | An <img>-based image with no alt attribute, not marked decorative (role="presentation", aria-hidden="true") | Accessibility and image SEO |
ALT_EMPTY | alt="" | Correct for decorative images; listed so you can check each one really is decorative |
ALT_TOO_LONG | Alt text over 125 characters | Screen readers cut or skip it |
ALT_IS_FILENAME | Alt is a file name: with an extension (hero.jpg), a camera name (IMG_1234, DSC0042) or a technical name with digits, hyphens or underscores (hero-banner-2). A plain word that is also a fair description ("Atalanta" for atalanta.png, "PayPal" for paypal.svg) is not flagged | It describes nothing |
ALT_EMPTY_IN_LINK | alt="" on an image that is the only content of a link | The link has no accessible name |
TOO_HEAVY | File larger than heavyThresholdKb (default 300 KB) | Page weight |
DIMENSIONS_NOT_DECLARED | No width and height (nor inline width+height or aspect-ratio) | Layout shift (CLS) |
DECLARED_MISMATCH | (not for SVG) The declared width/height ratio differs from the real one by more than 5 % | Distorted or shifting image (unless a stylesheet crops it on purpose, e.g. object-fit: cover: stylesheets are not read) |
OVERSIZED_FOR_DISPLAY | (not for SVG) Real width more than 2x the declared width (declared sides under 10 px, like width="3" height="2", are read as an aspect ratio, not a size) | Bytes the visitor never sees |
LEGACY_FORMAT | JPEG or PNG over 100 KB, with no WebP/AVIF alternative in the page's srcset or <picture> | Smaller formats are available |
BROKEN_IMAGE | The image answers 404 or 410 | Broken image on the page |
MIXED_CONTENT | An http:// image loaded by an https:// page | Browsers block or warn |
NOT_AN_IMAGE | The URL answers 200 but the bytes are not an image (usually an HTML error page) | A "soft 404" |
Size flags (TOO_HEAVY, DECLARED_MISMATCH, OVERSIZED_FOR_DISPLAY, LEGACY_FORMAT) need a measured image (an SVG is drawn at any size, so it has no DECLARED_MISMATCH or OVERSIZED_FOR_DISPLAY); alt and dimension flags work from the HTML alone. CSS backgrounds and og:image/twitter:image get size flags but no alt flags (they have no alt attribute to check).
Run summary
Next to the dataset, the key-value store holds a SUMMARY record (free): every page read with its status, the number of images found, delivered and charged, the issues per page, PAGE_LIKELY_CSR flags and warnings, plus totals and a count per issue type. Use it to see at a glance why a page gave nothing.
Input
| Field | Default | What it does |
|---|---|---|
startUrls | Wikipedia "Lighthouse" | Pages to read. A bare domain (example.com) gets https:// |
crawlSameDomain | off | Also read pages linked from yours, on the same host (www. and the bare domain count as one). At most 30 new links per page. Never leaves the site |
maxCrawlDepth | 1 | Link levels to follow (1-3) |
maxPages | 20 | Pages read in total, start pages included (1-1,000). The start pages you list are always read first (in your order, up to maxPages; any beyond it are listed in SUMMARY.pages as stopped, with a warning). The pages the crawl finds share what is left: sites take turns, equal parts, and a part a site does not use goes to the others. Every page left out for lack of allowance is counted in SUMMARY.warnings |
maxImagesPerPage | 200 | Images processed per page, in document order (1-500). The rest are neither read nor charged |
measureImages | on | Read each image's header. Off = HTML facts only (alt, declared size, source) and no size flags |
includeCssBackgrounds | on | background / background-image in style attributes and <style> blocks |
includeMetaImages | on | og:image and twitter:image. A tag whose value is not a URL (content="https:") is skipped with a page warning, free |
heavyThresholdKb | 300 | TOO_HEAVY threshold (1 KB = 1,024 bytes) |
minSidePx | 0 | Skip icons and tracking pixels: images whose longest side is below this are not delivered and not charged |
dedupeAcrossPages | on | An image used on several pages (logo, icons) is delivered once, with the first page that uses it; the later pages say how many images they left out (imagesFound still counts them) |
Repeats of one URL on the same page always collapse into one record (occurrences says how many; the alt/dimension problems of each occurrence are kept).
What you pay, and what you don't
$0.0008 per image record that was read (event image): $0.80 per 1,000 images. A record is billed when its status is ok or broken.
| Run | Images billed | Cost |
|---|---|---|
| One article page (Wikipedia "Lighthouse", real run) | 39 | $0.031 |
| A shop product page (Allbirds, real run) | 34 | $0.027 |
| 5 pages of a site, crawling (wordpress.org, real run) | 76 | $0.061 |
| 1,000 images | 1,000 | $0.80 |
| 50 pages x 40 images | 2,000 | $1.60 |
Free, always:
- a page that answers 401 / 403 / 429 or shows a challenge page ("blocked"), 404/410, a server error, a timeout, a file that is not HTML;
- a page that
robots.txtforbids; - a page that only draws its pictures with JavaScript (
PAGE_LIKELY_CSR): there is nothing in the HTML to report, so nothing is billed; - an image that could not be measured (
status: "unmeasured"): forbidden byrobots.txt, hot-link protected (403), rate limited (429), server error, timeout, unreachable host, or an image whose pixel size could not be read (no_dimensions: an unusual format, an SVG with no size, a header the Actor could not reach). Such an image is still delivered, with the alt text and every HTML-based flag, at no charge. (A measured image is never billed without its dimensions; the one exception is abrokenimage, a 404/410, which is billed as a verified finding and has no dimensions to give); - images skipped by
minSidePx, repeats removed by deduplication, images beyondmaxImagesPerPage, theSUMMARYrecord.
Your spending limit is exact. If you set a maximum cost per run, the Actor checks the budget before every record and stops when it is spent. It never charges past it; SUMMARY.stopReason says spending limit reached and heldByLimit counts what was found but not delivered. maxPages x maxImagesPerPage is the other natural cap.
FAQ
Does it download the images? No. For each image it asks the server for the first 32 KB (Range: bytes=0-32767) and reads the size from the header. If the server ignores Range it reads at most 64 KB (256 KB for a JPEG whose header sits behind a large EXIF block, or an AVIF/HEIC whose size box comes late) and hangs up. The bytes are examined in memory and thrown away; no file is stored or returned.
Can I use the images I find? The Actor reports facts about images. You are responsible for the right to use any image it lists: most images are protected by copyright and licence terms, whatever the technical possibility of fetching them.
Why is alt sometimes null and sometimes ""? null means the tag has no alt attribute (a problem for screen readers and SEO); "" means the author deliberately marked the image as decorative (usually correct). Images from CSS backgrounds and meta tags have no alt attribute at all and are null by nature.
Which image does a srcset record show? The one in src. With no src, the largest srcset candidate. All candidates, including <picture> sources, are listed in srcset.
A page returned nothing. Why? Open SUMMARY: each page has a status and a reason. blocked = the site answered 401/403/429 or a challenge (this Actor does not try to get past bot protection: no proxies, no CAPTCHA solving, one honest User-Agent). robots_disallowed = the page's robots.txt forbids the bot. PAGE_LIKELY_CSR = the HTML has no images and looks like a JavaScript app. All of these are free.
Why is a crawl slow on some sites? If a host's robots.txt has a Crawl-delay, requests to that host are spaced accordingly (up to 10 seconds). Image CDNs that ask for 3 seconds make 40 images take two minutes. That is the polite behaviour; it costs you almost nothing in compute.
Why did og:image show a different size than I expected? The record reports the real file, not the og:image:width the page claims.
Does it follow links to other sites? No. With crawlSameDomain it stays on the host of each start URL (www. and the bare domain are one host). Subdomains are different hosts.
Can it read internal addresses? No. Only http/https on ports 80 and 443; private, loopback, link-local and cloud-metadata addresses are refused, including after redirects and for image URLs.
Limits, stated plainly
- JavaScript-drawn images are not seen. Plain HTTP reads the HTML the server sends. On four of five sites we compared, a real browser added only 0-13 % more images, mostly trackers; on a single-page app the HTML may have none. Such pages are flagged
PAGE_LIKELY_CSR, free. - Content inside
<noscript>is ignored (it is a fallback for images already present, and often a tracking pixel). External stylesheets and scripts are not fetched, so CSS backgrounds are only those instyleattributes and<style>blocks of the page. - HTML is read up to 5 MB per page. At most 5 redirects are followed.
- A page is read once per run. Pages behind a login, or that need cookies, are not supported.
- The first measurement of an image is what is reported; CDNs that choose a format by browser (WebP/AVIF) are asked with a browser-like
Acceptheader, so you see what a modern browser receives. - Concurrency is 4 requests per host and 10 in total.
Run it
Locally: put your input in <storage>/key_value_stores/default/INPUT.json, then CRAWLEE_STORAGE_DIR=$PWD/<storage> node src/main.js. Tests: npm test.
Fetch only the images with problems through the dataset API, for example the heavy ones with no alt text:
.../datasets/<id>/items?format=csv&fields=pageUrl,imageUrl,naturalWidth,naturalHeight,fileSizeBytes,alt,issues
and filter on the issues column in your spreadsheet, n8n or Make flow.