{"slug": "we-built-an-all-in-one-wordpress-image-toolkit-here-s-the-architecture-behind-it", "title": "We built an all-in-one WordPress image toolkit. Here's the architecture behind it", "summary": "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.", "body_md": "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.\n\nIf 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.\n\nThey fight over hook priority on `wp_get_attachment_image`\n\n. 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.\n\nImageCraft 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.\n\nWe want to walk through the technical decisions, because we think they're more interesting than a feature list.\n\nMost 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.\n\nImageCraft connects directly to the AI provider from your server. No middleman, no credit packs. You pay the provider at their published rates.\n\nThree providers are supported: Anthropic (Claude), OpenAI, and Google Gemini. Each extends a `BaseAIProvider`\n\nclass that handles image fetching, base64 encoding, MIME detection, and prompt construction. Adding a new provider means implementing one method: `generateAltText()`\n\n.\n\nAPI keys are AES-256-CBC encrypted in a custom database table. The REST API never returns the key — only a boolean `has_key`\n\nfield.\n\nA naive implementation would make separate API calls for alt text, title, and caption. Three network round trips, three token charges, three latency waits.\n\nImageCraft makes one call that returns all three in a structured JSON response:\n\n```\n{\n  \"alt_text\": \"Tan leather crossbody bag with brass buckle and adjustable strap\",\n  \"title\": \"Aria Crossbody Bag - Tan Leather\",\n  \"caption\": \"Handcrafted crossbody bag from the Aria Collection, featuring full-grain tan leather and antique brass hardware.\"\n}\n```\n\nThe prompt instructs the model to return this exact shape. A `parseMetaJson()`\n\nhelper 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()`\n\nfunction that truncates at the last word boundary rather than mid-word.\n\nFor 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.\n\nMost 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.\n\nImageCraft compresses directly on the server:\n\nThat 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.\n\nOriginals are backed up to `wp-content/uploads/icais-originals/`\n\nwith the same directory structure. One-click restore copies the backup over the compressed version.\n\n``` php\n// Simplified never-enlarge guard\n$tempPath = $this->compressToTemp($sourcePath, $quality);\nif (filesize($tempPath) >= filesize($sourcePath)) {\n    unlink($tempPath);\n    return; // original is already optimal\n}\n```\n\nThere are broadly three ways to serve WebP on WordPress:\n\n**Rewrite rules** — `.htaccess`\n\nor nginx config that serves a `.webp`\n\nfile when the browser sends `Accept: image/webp`\n\n. Fragile. Breaks on some hosts. Requires server-level config.\n\n**URL replacement** — change the image URL in the HTML from `.jpg`\n\nto `.webp`\n\n. Breaks caching. Confuses CDNs. Hard to reverse.\n\n** <picture> wrapping** — keep the original\n\n`<img>`\n\nintact, wrap it in a `<picture>`\n\nelement with `<source>`\n\nentries 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`\n\n, `photo.jpg.avif`\n\n) next to the originals — for the full-size image and all registered intermediate sizes. A manifest is stored in post meta (`_icais_nextgen`\n\n) with relative paths and byte savings.\n\nOn the frontend, two hooks handle delivery:\n\n`wp_content_img_tag`\n\n(WP 6.0+) — wraps content images`post_thumbnail_html`\n\n— wraps featured imagesBoth hooks bail on `is_admin()`\n\nand are wrapped in `try/catch (\\Throwable)`\n\nso a conversion bug can never break the frontend rendering. The worst case is: the `<picture>`\n\nwrapper fails silently and the original `<img>`\n\nrenders as it always did.\n\n``` php\n<!-- Before -->\n<img src=\"photo.jpg\" srcset=\"photo-300x200.jpg 300w, photo-768x512.jpg 768w\" ...>\n\n<!-- After (automatic, on the frontend only) -->\n<picture>\n  <source type=\"image/webp\"\n          srcset=\"photo.jpg.webp 1024w, photo-300x200.jpg.webp 300w, photo-768x512.jpg.webp 768w\">\n  <img src=\"photo.jpg\" srcset=\"photo-300x200.jpg 300w, photo-768x512.jpg 768w\" ...>\n</picture>\n```\n\nCleanup removes all sidecar files and the post meta. The original images and markup are exactly as they were before.\n\n`get_post_metadata`\n\ntrick\nThe broken image fallback has two parts: a frontend JavaScript listener and a PHP metadata filter.\n\nThe JS part is straightforward — a capture-phase `error`\n\nlistener on `document`\n\nthat catches `<img>`\n\nload failures and swaps in the fallback URL. It also does an initial sweep for images with `naturalWidth === 0`\n\n.\n\nThe PHP part is more interesting. For missing featured images, we needed `has_post_thumbnail()`\n\nto return `true`\n\neven when the real thumbnail is gone. The function checks `get_post_meta($postId, '_thumbnail_id', true)`\n\n, and if that returns a valid attachment ID that happens to be deleted, `has_post_thumbnail()`\n\nreturns `true`\n\nbut the image render fails.\n\nFor posts where `_thumbnail_id`\n\nis completely empty (the meta doesn't exist), we hook `get_post_metadata`\n\nwith a filter:\n\n```\nadd_filter('get_post_metadata', function ($value, $postId, $metaKey) {\n    if ($metaKey !== '_thumbnail_id') return $value;\n    // ... bail checks, recursion guard, settings check ...\n    return $fallbackImageId;\n}, 10, 3);\n```\n\nThis makes `has_post_thumbnail()`\n\nreturn `true`\n\nfor any post type in the allow-list, and `get_the_post_thumbnail()`\n\nrenders the fallback image. The real database is never modified — it's a read-time filter only.\n\nThe recursion guard matters because `get_post_meta`\n\nfor the fallback image's own `_thumbnail_id`\n\nwould trigger the same filter. A static flag prevents the infinite loop.\n\nThe free tier allows 50 actions per day across a rolling 24-hour window. Generation and compression share the same pool.\n\nThe implementation uses a single `wp_options`\n\nkey per user:\n\n``` js\n// Stored as: icais_daily_usage_{userId}\n[\n    'started_at'     => 1691942400,  // window start timestamp\n    'ids'            => [41, 42, 43], // attachment IDs (deduped)\n    'compress_count' => 7,            // compression count (NOT deduped)\n]\n```\n\nThere'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.\n\n`totalCount()`\n\n= `count(ids) + compress_count`\n\n. When it hits 50, the API returns a 429 with a human-readable message about when the window resets.\n\nPro (detected via the `ICAIS_PRO`\n\nconstant defined by the separate Pro add-on plugin) bypasses the check entirely.\n\nA few mistakes from earlier versions, in case they're useful to someone building similar tools:\n\n**Catching \\Exception instead of \\Throwable.** PHP 7+ throws\n\n`\\Error`\n\nfor things like missing classes and type mismatches. `catch (\\Exception)`\n\ndoesn't catch those. Any integration point with an external library or optional dependency now catches `\\Throwable`\n\n. 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_`\n\nto every key, and the keys in the defaults array already start with `icais_`\n\n. So everything is stored as `icais_icais_default_tone`\n\n. We caught this too late to fix without breaking existing installations. It works. It's ugly. It's documented. We moved on.\n\n**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.\n\nImageCraft is free on [WordPress.org](https://wordpress.org/plugins/imagecraft-ai-alt-text-file-renamer-image-seo/). Search \"ImageCraft\" in your plugin installer, or search for \"imagecraft ai alt text\" to find it.\n\nPowered By [Softminal](https://www.softminal.com/)", "url": "https://wpnews.pro/news/we-built-an-all-in-one-wordpress-image-toolkit-here-s-the-architecture-behind-it", "canonical_source": "https://dev.to/softminal/we-built-an-all-in-one-wordpress-image-toolkit-heres-the-architecture-behind-it-cl", "published_at": "2026-08-13 11:03:01+00:00", "updated_at": "2026-08-13 11:16:30.783766+00:00", "lang": "en", "topics": ["ai-products", "developer-tools", "generative-ai", "artificial-intelligence"], "entities": ["ImageCraft", "WordPress", "Anthropic", "OpenAI", "Google Gemini", "ShortPixel", "Imagify", "TinyPNG"], "alternates": {"html": "https://wpnews.pro/news/we-built-an-all-in-one-wordpress-image-toolkit-here-s-the-architecture-behind-it", "markdown": "https://wpnews.pro/news/we-built-an-all-in-one-wordpress-image-toolkit-here-s-the-architecture-behind-it.md", "text": "https://wpnews.pro/news/we-built-an-all-in-one-wordpress-image-toolkit-here-s-the-architecture-behind-it.txt", "jsonld": "https://wpnews.pro/news/we-built-an-all-in-one-wordpress-image-toolkit-here-s-the-architecture-behind-it.jsonld"}}