How I Built an AI Alt Text Generator for WordPress — and What I Learned

How I Built an AI Alt Text Generator for WordPress — and What I Learned

Accessibility is one of those areas everyone agrees matters, but it often gets pushed down the development queue. Alt text is a good example. Adding descriptive alt text to every image improves accessibility, helps screen-reader users understand a page, and can support SEO. But for sites with hundreds or thousands of images, writing it manually is repetitive and easy to neglect. That problem led me to build an AI alt text generator for WordPress. The problem WordPress makes it easy to upload images, but maintaining accurate alt text across a large media library is another story. Site owners usually face one of three situations: Images have no alt text Existing alt text is vague or duplicated There are too many images to review manually I wanted a tool that could cut that workload while keeping the site owner in control. What the plugin does Generate alt text for individual images Process multiple missing-alt images in bulk Save generated text directly into WordPress Track usage and generation limits Let users review results before relying on them The goal isn't to remove human judgment — it's to make the first draft dramatically faster. The architecture The project grew into more than a simple WordPress plugin. The current stack: WordPress / PHP — plugin interface and media-library integration JavaScript — interactive admin workflows Node.js — backend API Render — backend hosting Supabase — account, entitlement, and usage data Stripe — subscriptions and checkout PostHog — product analytics An AI vision service — image analysis and alt-text generation The WordPress plugin sends an authenticated generation request to the backend. The backend validates the user, checks available quota, processes the image, stores the usage result, and returns the generated description. For billing, Stripe webhooks are the authoritative source for subscription status. The frontend never assumes a payment succeeded just because the user returned from checkout. Challenge 1: Job-level vs image-level analytics One of the most useful lessons came from telemetry. At one point the analytics showed: 4 generation starts 13 generation completions That looked like duplicated completion events. The real issue was that the two metrics measured different things: a generation start could represent one bulk job, while completion events represented each image inside that job. Comparing them directly was like comparing shopping trips with items purchased. I replaced the ambiguous lifecycle with explicit events: generation_job_started generation_item_completed generation_job_completed generation_job_failed Each job now has a generation_run_id, and each image can have its own generation_item_id. That makes it possible to answer useful questions: How many jobs started? How many images were processed? How many jobs fully succeeded? How many items failed inside a bulk job? How long did each operation take? Clear event semantics turned out to be as important as collecting the events. Challenge 2: Preventing duplicate telemetry Analytics code can fire more often than expected. Events may be duplicated by: repeated button clicks component re-renders retries multiple event listeners frontend and backend emitting the same event repeated webhook delivery To make events retry-safe, I added stable PostHog $insert_id values and defined which system owns each event: The frontend records user intent (e.g. clicking an upgrade button) The backend records successful Stripe Checkout Session creation Stripe webhooks determine checkout completion and subscription activation The backend records alt text only after it has been persisted This stops the same business action being counted two or three times by different parts of the app. Challenge 3: Tracing an action across several systems A single checkout can pass through: WordPress → Backend API → Render logs → Stripe Checkout → Stripe webhook → Supabase → PostHog Without a shared identifier, debugging that journey is painful. I introduced: correlation_id checkout_attempt_id generation_run_id signup_attempt_id session_id site_install_id These let me trace one action from the plugin through the backend and into external services — especially useful when a checkout is abandoned or a login fails. Instead of searching disconnected systems by timestamp, the same correlation value appears throughout the journey. Challenge 4: Safe authentication telemetry A generic login_failed event isn't very useful. It doesn't tell you whether the cause was incorrect credentials, a disabled account, rate limiting, a network timeout, an unavailable API, token-creation failure, or session-storage failure. I added a controlled internal error taxonomy while keeping public error messages generic — that distinction matters because detailed public messages can reveal whether an account exists. The system now records safe internal codes such as: invalid_credentials account_disabled rate_limited network_timeout api_unavailable token_creation_failed session_creation_failed unknown_auth_error Passwords, tokens, API keys, and raw server responses are stripped before telemetry is sent. Challenge 5: Privacy by default Product analytics can become a privacy problem surprisingly quickly. The telemetry layer now removes fields such as: email addresses licence keys access tokens API keys alt text content image filenames image URLs prompts raw API responses PostHog session recording is disabled unless explicitly enabled. The analytics should describe what happened without copying the content the user was working with. What I'd do differently If I started again, I'd define the analytics contract before building the dashboards, documenting each event with: the exact trigger whether it's frontend- or backend-owned whether it fires once per user, session, job, item, or transaction required properties deduplication strategy schema version Retrofitting clean telemetry is possible, but it's much more work than designing it properly up front. I'd also add correlation IDs from day one — they're cheap to implement and hugely valuable once an app spans multiple services. What I learned The biggest lesson: building the AI generation feature was only one part of the product. A production-ready plugin also needs: reliable authentication subscription and quota management idempotent webhooks privacy-safe analytics clear event semantics error classification end-to-end observability a migration plan for old telemetry The AI part may be the headline feature, but reliability is what turns it into a usable product. What comes next releasing the updated plugin telemetry deploying the consolidated backend changes validating real production events running a fully correlated test checkout comparing old and new analytics before removing legacy events I'm also continuing to improve how generated alt text is reviewed and applied within WordPress. I'd love to hear from other WordPress, accessibility, or SaaS developers: what's been the hardest part of making your plugin production-ready?

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.