Add images to a Typeroll site
This is the tr-images recipe exactly as the Typeroll MCP server gives it to agents through list_skills and read_skill. See Skills and recipes for how agents load recipes.
The Typeroll API does NOT accept image bytes directly. Uploads go through a signed PUT URL straight to Cloudflare R2, and the API only sees the metadata. Two-step flow:
For public source URLs, prefer upload_media_from_url; for 2–50 files use
upload_media_batch_from_urls (max 25 MiB per file). Both download, upload,
and finalize each asset automatically. Batch results preserve input order and
report failures per item, so retry only the failed entries. Use the manual
signed-URL flow below for local files or when you need direct control over the
PUT.
Recipe
Section titled “Recipe”1. Get an image
Section titled “1. Get an image”Options, in order of preference:
a. Reuse an existing one. list_media returns CDN URLs for every
image already on this site. Search the list before generating
anything — saves bandwidth and keeps the visual catalog tight.
b. Generate locally. The user’s Claude Code installation has access to whatever image-gen tools they’ve configured (DALL-E, Midjourney, Stable Diffusion, Replicate, etc.). Generate and save to a tempfile.
c. Source from the web with appropriate licensing (the user is responsible for clearing rights). Save locally before upload.
2. Mint a signed upload URL
Section titled “2. Mint a signed upload URL”create_upload_url filename="hero-services.png" content_type="image/png" size=<bytes> alt_text="Office worker reviewing documents at a desk"Returns:
{ "upload_url": "https://...r2.cloudflarestorage.com/.../signed-...", "cdn_url": "https://cdn.example.com/orgs/.../images/...png", "key": "orgs/.../images/...png", "media_id": "abc123", "expires_in": 300}The signed URL is valid for 5 minutes. The media doc is already
registered — even before the upload completes — so it’ll show in
list_media immediately.
3. PUT the bytes
Section titled “3. PUT the bytes”Outside the MCP, hit the signed URL directly:
PUT <upload_url>Content-Type: <same content_type as in step 2>Body: <file bytes>In a shell:
curl -sS -X PUT --data-binary @hero-services.png "$UPLOAD_URL"Optionally with -H "Content-Type: image/png" if you need to override what R2 will infer. Nothing else.
The signed URL embeds checksum-related query parameters (
x-amz-checksum-crc32,x-amz-sdk-checksum-algorithm) for legacy SDK compatibility, butX-Amz-SignedHeaders=host— only the host header is part of the signature. Sending thex-amz-*values as request headers yields403 SignatureDoesNotMatch. Don’t. Just--data-binarythe file at the URL.
Or from JS (Claude Code can run a one-line script):
await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': contentType }, body: await fs.readFile(path),});Parallelise N uploads via shell & + wait:
while IFS=$'\t' read -r filename signed_url; do curl -sS -X PUT --data-binary @"$filename" "$signed_url" &done < manifest.tsvwaitA 200 OK from R2 means the image is now live at cdn_url.
4. Patch metadata (alt text, etc.)
Section titled “4. Patch metadata (alt text, etc.)”You set alt_text at create time, but if you generate the image first
and only THEN realize what to caption it as, patch later:
update_media media_id=<id> alt_text="..." filename="hero-services-v2.png"4b. Fill missing alt-text on existing media
Section titled “4b. Fill missing alt-text on existing media”When a customer has uploaded a bunch of images without alt-text (very common after a WP migration), don’t make it up — use vision:
list_media → find items with empty alt_textsuggest_alt_text_context media_id=<id> → returns { image_url, suggested_prompt, language, used_on_pages, current_alt_text }# Pass image_url + the returned suggested_prompt to YOUR OWN vision# capability (you can fetch the URL and pass bytes to vision).update_media media_id=<id> alt_text="<what vision returned>"The prompt is tuned for SEO-grade output: short (5-15 words), no “image of / picture of” filler, written in the site’s content language, decorative images return empty string. Run it sequentially on a list_media batch and you can fix alt-text gaps across a whole site without burning your context on prompt design. The platform does NOT run vision on your behalf — your model does, your usage.
5. Embed in a page
Section titled “5. Embed in a page”read_page the target, insert <img> in the right spot:
<img src="<cdn_url>" alt="<alt_text>" style="width: 100%; height: auto; display: block; margin: 2rem 0;" />Then update_page with the new HTML. Or, if you’re generating a hero
for a brand-new page, include the <img> directly in create_page’s
html_content.
Pitfalls
Section titled “Pitfalls”- Always set
alt_text. Empty alt is bad for SEO + accessibility. Default to a one-sentence description of what’s in the image. <script>etc. in SVGs. The page sanitizer drops<script>inside SVG, so an icon set that includes script-based animations won’t render correctly. Use static SVG or a JPG/PNG export.- CSS background-image references aren’t dedup’d. If you set the
same image as a CSS background on multiple pages, the alt-text +
metadata are page-irrelevant. The sanitizer allows
background-image: url(...)in inline styles, but think about whether an<img>is actually better. - Source URL leakage. If you generated the image from a prompt that contains internal info, don’t bake that prompt into the filename. Use a descriptive but generic filename.
Format choice
Section titled “Format choice”- PNG for logos, icons with hard edges, anything with text.
- JPG for photos. Smaller file, better for big hero images.
- WebP if the target audience runs modern browsers (95%+ in 2026).
- SVG for icons + simple illustrations. Vector scales perfectly.
- PDF is supported by
create_upload_urlfor document downloads; link with<a href>, not<img>.
Check publication resolution (Core 0.2.37+)
Section titled “Check publication resolution (Core 0.2.37+)”Native AVIF/WebP generation includes each smaller preset and the full original
width, without upscaling or duplicate width descriptors. Check currentSrc and
the actual encoded dimensions on the published site; the CMS preview can use the
original and hide an undersized derivative. Do not compensate with custom CSS
or sizes alone. Existing static sites need republication for the versioned v2
recipe; unchanged subsequent builds reuse verified media. Explicit API variant
workflows must regenerate their variant metadata before rebuilding.