Check image alt text coverage
Image alt text coverage measures how many images in a supplied list have alt attributes that are both present and meaningfully descriptive.
Run — free
This checker reviews each record without fetching the image or page, calculates the percentage that passes, and returns every offender with its position, source, original alt value when present, and failure reason. It is designed for repeatable SEO and accessibility quality checks in publishing workflows, site crawls, content migrations, and release gates.
What the coverage percentage tells you
A simple count of existing alt attributes can make an image library look healthier than it is. An attribute containing only spaces, a label such as “image,” or a camera filename does not communicate useful subject matter. This checker therefore counts an image as covered only when its alt value is non-empty and does not match a conservative list of generic placeholders. The percentage is the number of passing images divided by the total number supplied, rounded to two decimal places. Alongside that headline result, the covered and total counts make the calculation transparent. The offenders array identifies every record that reduced the score, preserving the zero-based input position and the supplied src value so it can be traced back to a crawler export, content database, or template. This is a coverage signal, not a judgment of writing quality: a specific but inaccurate description can pass because the checker does not inspect image pixels or surrounding page context.
How missing, empty, and generic values are classified
Each offender receives one of three reasons. Missing means the record has no alt property at all, which often indicates that the HTML attribute was absent. Empty means an alt property exists but contains no characters after surrounding whitespace is removed. That distinction matters because an intentionally empty alt attribute can be correct for a decorative image, even though it is still excluded from this coverage metric. Generic means the value is present but matches a deliberately narrow placeholder rule. Examples include “photo,” “picture,” “thumbnail,” numbered labels such as “image 2,” and common camera-style filenames such as IMG_2048.jpg. Matching ignores case, repeated spaces, underscores, hyphens, and trailing punctuation where appropriate. The checker does not reject short text merely for being short and does not perform fuzzy semantic guessing. This conservative behavior keeps results deterministic, reviewable, and suitable for automated release thresholds without unexpectedly condemning legitimate product names or concise icon descriptions.
Use the offender list in an SEO workflow
Supply records from a crawler, CMS export, component inventory, or test fixture. Every record needs a non-empty src identifier; it may be a full URL, a relative path, or an internal asset key. The alt field is optional so that a missing HTML attribute can be represented honestly. After running the check, sort remediation work by the returned reason. Missing attributes usually point to markup or component defaults, generic values often require editorial replacement, and empty values need human review to separate purposeful decorative images from overlooked content images. Run the same list again after corrections and compare covered, total, and percentage values in a build report or audit trail. Because the algorithm uses no network, image analysis, random values, or current time, identical input always produces identical output. Requests with an empty image list are rejected rather than reported as zero percent, since a percentage over no observations would be misleading and could let a broken crawler appear to complete a valid audit.
What you can do with it
Audit a crawler export
Turn extracted image sources and alt attributes into a coverage score and a precise remediation queue.
Gate a website release
Fail a publishing check when meaningful alt text coverage falls below the threshold your team requires.
Review a CMS migration
Compare migrated image records and find attributes that disappeared, became empty, or reverted to placeholders.
FAQ
What counts as covered?
An image is covered when alt is a string containing non-whitespace text that does not match the checker’s conservative generic-value rules.
Does an intentionally empty alt attribute count as covered?
No. It is listed with the empty reason. Empty alt can be correct for decorative images, but this metric measures non-empty descriptive coverage.
Does the checker inspect the images?
No. It evaluates only the supplied src identifiers and alt attributes, so it cannot confirm that a description is accurate for the image.
Why is an empty image list an error?
There is no meaningful coverage percentage without observations. Rejecting the request also exposes failed or misconfigured image extraction.
What does an API request cost?
Each API request costs $0.002. The same deterministic logic can run in the browser for a free interactive check.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/seo/alt-text-coverage \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"images":[{"src":"/team/alex.jpg","alt":"Alex presenting the quarterly roadmap"},{"src":"/products/blue-mug.jpg","alt":"image"},{"src":"/icons/search.svg","alt":""},{"src":"/charts/revenue.png","alt":"Monthly recurring revenue rose from January to June"}]}'const res = await fetch("https://api.kit.forhosting.com/seo/alt-text-coverage", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"images": [
{
"src": "/team/alex.jpg",
"alt": "Alex presenting the quarterly roadmap"
},
{
"src": "/products/blue-mug.jpg",
"alt": "image"
},
{
"src": "/icons/search.svg",
"alt": ""
},
{
"src": "/charts/revenue.png",
"alt": "Monthly recurring revenue rose from January to June"
}
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/seo/alt-text-coverage",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"images": [
{
"src": "/team/alex.jpg",
"alt": "Alex presenting the quarterly roadmap"
},
{
"src": "/products/blue-mug.jpg",
"alt": "image"
},
{
"src": "/icons/search.svg",
"alt": ""
},
{
"src": "/charts/revenue.png",
"alt": "Monthly recurring revenue rose from January to June"
}
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/seo/alt-text-coverage", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"images":[{"src":"/team/alex.jpg","alt":"Alex presenting the quarterly roadmap"},{"src":"/products/blue-mug.jpg","alt":"image"},{"src":"/icons/search.svg","alt":""},{"src":"/charts/revenue.png","alt":"Monthly recurring revenue rose from January to June"}]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"images":[{"src":"/team/alex.jpg","alt":"Alex presenting the quarterly roadmap"},{"src":"/products/blue-mug.jpg","alt":"image"},{"src":"/icons/search.svg","alt":""},{"src":"/charts/revenue.png","alt":"Monthly recurring revenue rose from January to June"}]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/seo/alt-text-coverage", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"images": [
{
"src": "/team/alex.jpg",
"alt": "Alex presenting the quarterly roadmap"
},
{
"src": "/products/blue-mug.jpg",
"alt": "image"
},
{
"src": "/icons/search.svg",
"alt": ""
},
{
"src": "/charts/revenue.png",
"alt": "Monthly recurring revenue rose from January to June"
}
]
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "seo.alt_text_coverage",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |