cd /news/ai-products/we-built-an-all-in-one-wordpress-ima… Β· home β€Ί topics β€Ί ai-products β€Ί article
[ARTICLE Β· art-95125] src=dev.to β†— pub= topic=ai-products verified=true sentiment=↑ positive

We built an all-in-one WordPress image toolkit. Here's the architecture behind it

The team behind ImageCraft, an open-source WordPress plugin, has detailed the architecture of their all-in-one image toolkit, which combines AI alt text generation, compression, and WebP conversion into a single codebase. The plugin connects directly to AI providers like Anthropic, OpenAI, and Google Gemini, avoiding credit-based middlemen, and performs on-server compression using Imagick to reduce costs and improve efficiency.

read7 min views2 publishedAug 13, 2026

We want to talk about why most WordPress image plugins exist as separate, single-purpose tools β€” and why we think that's the wrong approach.

If you manage WordPress sites, you've probably installed some combination of these: an AI alt text generator, a compression plugin, a WebP converter, a media file renamer, and maybe something to catch broken images. Each one solves a real problem. But together, they create new ones.

They fight over hook priority on wp_get_attachment_image

. They store overlapping metadata without knowing about each other. They enqueue separate admin scripts on the same pages. And when one of them breaks on a WordPress update, you're debugging a five-plugin interaction.

ImageCraft is our answer to this. One plugin, one pipeline, every image optimization task in a single codebase. It's free and open source on WordPress.org.

We want to walk through the technical decisions, because we think they're more interesting than a feature list.

Most AI alt text plugins work on a credit model. You buy credits from the developer, they proxy your request to OpenAI or Claude, and pocket the margin. The markup is significant β€” GPT-4o-mini costs about $0.15 per 100 images at retail, but credit-based plugins charge $3–6 for the same volume.

ImageCraft connects directly to the AI provider from your server. No middleman, no credit packs. You pay the provider at their published rates.

Three providers are supported: Anthropic (Claude), OpenAI, and Google Gemini. Each extends a BaseAIProvider

class that handles image fetching, base64 encoding, MIME detection, and prompt construction. Adding a new provider means implementing one method: generateAltText()

.

API keys are AES-256-CBC encrypted in a custom database table. The REST API never returns the key β€” only a boolean has_key

field.

A naive implementation would make separate API calls for alt text, title, and caption. Three network round trips, three token charges, three latency waits.

ImageCraft makes one call that returns all three in a structured JSON response:

{
  "alt_text": "Tan leather crossbody bag with brass buckle and adjustable strap",
  "title": "Aria Crossbody Bag - Tan Leather",
  "caption": "Handcrafted crossbody bag from the Aria Collection, featuring full-grain tan leather and antique brass hardware."
}

The prompt instructs the model to return this exact shape. A parseMetaJson()

helper handles the response β€” including edge cases like markdown-wrapped JSON, extra fields, and truncated responses. Each field has a configurable max length with a capText()

function that truncates at the last word boundary rather than mid-word.

For WooCommerce images, the prompt includes product metadata (name, SKU, categories, price) before the image, so the model writes with product context rather than describing the image generically.

Most compression plugins β€” ShortPixel, Imagify, TinyPNG β€” upload your images to an external API, compress them there, and download the result. This makes sense if your server doesn't have good image libraries. But most modern hosts have Imagick installed, and it's plenty capable.

ImageCraft compresses directly on the server:

That last step is important. Some already-optimized images (especially PNGs with good compression) get larger after re-encoding. The never-enlarge guard prevents this silently.

Originals are backed up to wp-content/uploads/icais-originals/

with the same directory structure. One-click restore copies the backup over the compressed version.

// Simplified never-enlarge guard
$tempPath = $this->compressToTemp($sourcePath, $quality);
if (filesize($tempPath) >= filesize($sourcePath)) {
    unlink($tempPath);
    return; // original is already optimal
}

There are broadly three ways to serve WebP on WordPress:

Rewrite rules β€” .htaccess

or nginx config that serves a .webp

file when the browser sends Accept: image/webp

. Fragile. Breaks on some hosts. Requires server-level config.

URL replacement β€” change the image URL in the HTML from .jpg

to .webp

. Breaks caching. Confuses CDNs. Hard to reverse.

** <picture> wrapping** β€” keep the original

<img>

intact, wrap it in a <picture>

element with <source>

entries for WebP/AVIF. The browser picks the best format. The original URL stays as the fallback.We went with approach 3. The converter generates sidecar files (photo.jpg.webp

, photo.jpg.avif

) next to the originals β€” for the full-size image and all registered intermediate sizes. A manifest is stored in post meta (_icais_nextgen

) with relative paths and byte savings.

On the frontend, two hooks handle delivery:

wp_content_img_tag

(WP 6.0+) β€” wraps content imagespost_thumbnail_html

β€” wraps featured imagesBoth hooks bail on is_admin()

and are wrapped in try/catch (\Throwable)

so a conversion bug can never break the frontend rendering. The worst case is: the <picture>

wrapper fails silently and the original <img>

renders as it always did.

<!-- Before -->
<img src="photo.jpg" srcset="photo-300x200.jpg 300w, photo-768x512.jpg 768w" ...>

<!-- After (automatic, on the frontend only) -->
<picture>
  <source type="image/webp"
          srcset="photo.jpg.webp 1024w, photo-300x200.jpg.webp 300w, photo-768x512.jpg.webp 768w">
  <img src="photo.jpg" srcset="photo-300x200.jpg 300w, photo-768x512.jpg 768w" ...>
</picture>

Cleanup removes all sidecar files and the post meta. The original images and markup are exactly as they were before.

get_post_metadata

trick The broken image fallback has two parts: a frontend JavaScript listener and a PHP metadata filter.

The JS part is straightforward β€” a capture-phase error

listener on document

that catches <img>

load failures and swaps in the fallback URL. It also does an initial sweep for images with naturalWidth === 0

.

The PHP part is more interesting. For missing featured images, we needed has_post_thumbnail()

to return true

even when the real thumbnail is gone. The function checks get_post_meta($postId, '_thumbnail_id', true)

, and if that returns a valid attachment ID that happens to be deleted, has_post_thumbnail()

returns true

but the image render fails.

For posts where _thumbnail_id

is completely empty (the meta doesn't exist), we hook get_post_metadata

with a filter:

add_filter('get_post_metadata', function ($value, $postId, $metaKey) {
    if ($metaKey !== '_thumbnail_id') return $value;
    // ... bail checks, recursion guard, settings check ...
    return $fallbackImageId;
}, 10, 3);

This makes has_post_thumbnail()

return true

for any post type in the allow-list, and get_the_post_thumbnail()

renders the fallback image. The real database is never modified β€” it's a read-time filter only.

The recursion guard matters because get_post_meta

for the fallback image's own _thumbnail_id

would trigger the same filter. A static flag prevents the infinite loop.

The free tier allows 50 actions per day across a rolling 24-hour window. Generation and compression share the same pool.

The implementation uses a single wp_options

key per user:

// Stored as: icais_daily_usage_{userId}
[
    'started_at'     => 1691942400,  // window start timestamp
    'ids'            => [41, 42, 43], // attachment IDs (deduped)
    'compress_count' => 7,            // compression count (NOT deduped)
]

There's an intentional asymmetry here: generation is deduped per attachment ID (re-generating alt text for the same image within the window is free), while every compression counts. The reasoning is that regenerating alt text is a common review workflow (generate, reject, regenerate with a different tone), but re-compressing the same image is an unusual action that probably indicates a settings change.

totalCount()

= count(ids) + compress_count

. When it hits 50, the API returns a 429 with a human-readable message about when the window resets.

Pro (detected via the ICAIS_PRO

constant defined by the separate Pro add-on plugin) bypasses the check entirely.

A few mistakes from earlier versions, in case they're useful to someone building similar tools:

Catching \Exception instead of \Throwable. PHP 7+ throws

\Error

for things like missing classes and type mismatches. catch (\Exception)

doesn't catch those. Any integration point with an external library or optional dependency now catches \Throwable

. This one nearly caused a white-screen on a site that had an older PHP version with a missing Imagick extension.Settings double-prefix. Our settings system prepends icais_

to every key, and the keys in the defaults array already start with icais_

. So everything is stored as icais_icais_default_tone

. We caught this too late to fix without breaking existing installations. It works. It's ugly. It's documented. We moved on.

The wizard gate. Early versions required an API key to get past the setup wizard. Then we added compression, which doesn't need an API key at all. Users who only wanted compression were blocked by a screen asking for an AI provider key. The wizard is now skippable, and the plugin works fine without any API key configured β€” you just don't get the AI features.

ImageCraft is free on WordPress.org. Search "ImageCraft" in your plugin installer, or search for "imagecraft ai alt text" to find it.

Powered By Softminal

── more in #ai-products 4 stories Β· sorted by recency
── more on @imagecraft 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/we-built-an-all-in-o…] indexed:0 read:7min 2026-08-13 Β· β€”