Cookbook
Recipe 8: Pipeline — fetch → vision AI → alt-text → publish
Full accessibility pipeline. webfetch hands off to a vision model which produces alt-text, and the post publishes with license + alt-text wired up.
Every image on your site should have descriptive alt-text. Every image on your site should have attribution. This pipeline gives you both, automatically.
Try it: a free webfetch API key at app.getwebfetch.com/signup lets you prototype the full pipeline before committing to a vision-model budget.
#The stages
- Search — webfetch
search_imageswithlicensePolicy: "safe-only". - Download — webfetch
download_imageto local bytes. - Describe — vision model (Claude 3.5+ Haiku, GPT-4o, Gemini Pro Vision) generates alt-text from the bytes.
- Publish — your CMS receives
{ url, alt, attribution }and renders the image with both.
#Code (TypeScript, Node)
import { searchImages, downloadImage } from "@webfetch/core";
import Anthropic from "@anthropic-ai/sdk";
import fs from "node:fs/promises";
const client = new Anthropic();
export async function enrichedImage(query: string) {
const { candidates } = await searchImages(query, {
licensePolicy: "safe-only",
minWidth: 1200,
});
const top = candidates[0];
if (!top) throw new Error(`no safe image for ${query}`);
const r = await downloadImage(top.url, { maxBytes: 10_000_000 });
const bytes = await fs.readFile(r.cachedPath);
const base64 = bytes.toString("base64");
const mediaType = r.mime as "image/jpeg" | "image/png" | "image/gif" | "image/webp";
const resp = await client.messages.create({
model: "claude-3-5-haiku-latest",
max_tokens: 200,
messages: [
{
role: "user",
content: [
{ type: "image", source: { type: "base64", media_type: mediaType, data: base64 } },
{
type: "text",
text: "Write one sentence of descriptive alt-text for this image. Describe what's visible, concretely. No 'image of' preamble. Under 125 characters.",
},
],
},
],
});
const alt = resp.content
.filter((c): c is Anthropic.TextBlock => c.type === "text")
.map((c) => c.text)
.join(" ")
.trim();
return {
url: top.url,
sha256: r.sha256,
cachedPath: r.cachedPath,
alt,
attribution: top.attributionLine,
license: top.license,
confidence: top.confidence,
};
}#Usage
const img = await enrichedImage("hummingbird in flight, macro photography");
/*
{
url: "...",
alt: "A ruby-throated hummingbird hovers mid-air with wings blurred,
iridescent feathers catching sunlight against a blurred green background.",
attribution: "'Hummingbird' by Skyler Ewing on Pexels",
license: "CC0",
confidence: 0.85
}
*/#Rendering
<figure>
<img src="/assets/hummingbird.jpg" alt="A ruby-throated hummingbird hovers..." />
<figcaption>'Hummingbird' by Skyler Ewing on Pexels</figcaption>
</figure>#Prompt caching tip
If you're running this at scale (thousands of images/day), use Anthropic prompt caching on the system prompt that describes your alt-text style. You'll cut per-call cost by 80–90%:
system: [{
type: "text",
text: "You write alt-text for a publication... [style guide]",
cache_control: { type: "ephemeral" },
}],#Accessibility notes
- Alt-text under 125 chars plays nicely with most screen readers.
- Purely decorative images: set
alt=""(empty), and don't bother with the AI call. The figure caption still carries attribution. - Complex technical diagrams: don't auto-generate. Have a human write the alt-text.