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
我是如何为 WordPress 构建 AI 替代文本生成器的——以及我的心得体会
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.
无障碍访问是每个人都认同其重要性,但往往在开发优先级中被排到末尾的领域。替代文本(Alt text)就是一个很好的例子。为每张图片添加描述性的替代文本可以改善无障碍体验,帮助屏幕阅读器用户理解页面,并有助于 SEO。但对于拥有成百上千张图片的网站来说,手动编写这些文本既重复又容易被忽略。正是这个问题促使我为 WordPress 构建了一个 AI 替代文本生成器。
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
WordPress 让上传图片变得简单,但在庞大的媒体库中维护准确的替代文本却是另一回事。网站所有者通常会面临以下三种情况之一:
- 图片没有替代文本
- 现有的替代文本模糊不清或重复
- 图片数量太多,无法手动逐一检查
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
-
为单张图片生成替代文本
-
批量处理多张缺失替代文本的图片
-
将生成的文本直接保存到 WordPress 中
-
跟踪使用情况和生成限额
-
让用户在采用结果前进行审核
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
这个项目已经不仅仅是一个简单的 WordPress 插件。目前的架构栈包括:
- WordPress / PHP — 插件界面和媒体库集成
- JavaScript — 交互式管理工作流
- Node.js — 后端 API
- Render — 后端托管
- Supabase — 账户、权限和使用数据
- Stripe — 订阅和结账
- PostHog — 产品分析
- AI 视觉服务 — 图像分析和替代文本生成
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.
WordPress 插件向后端发送经过身份验证的生成请求。后端验证用户、检查可用配额、处理图像、存储使用结果并返回生成的描述。在计费方面,Stripe Webhook 是订阅状态的权威来源。前端绝不会仅仅因为用户从结账页面返回,就假设支付已成功。
Challenge 1: Job-level vs image-level analytics
挑战 1:作业级与图像级分析
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.
最宝贵的经验之一来自遥测数据。有一次分析显示:4 次生成开始,13 次生成完成。这看起来像是重复的完成事件。真正的问题在于这两个指标衡量的是不同的事物:生成开始可能代表一次批量作业,而完成事件则代表该作业内的每一张图片。直接比较它们就像是在比较“购物次数”与“购买商品数量”。
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 themselves.
我用明确的事件取代了模糊的生命周期:generation_job_started(作业开始)、generation_item_completed(单项完成)、generation_job_completed(作业完成)、generation_job_failed(作业失败)。现在每个作业都有一个 generation_run_id,每张图片都有自己的 generation_item_id。这使得回答有价值的问题成为可能:有多少作业开始了?处理了多少图片?有多少作业完全成功?批量作业中有多少项失败了?每次操作耗时多久?事实证明,清晰的事件语义与收集事件本身同样重要。
Challenge 2: Preventing duplicate telemetry
挑战 2:防止重复的遥测数据
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, or repeated webhook delivery.
分析代码的触发频率可能超出预期。事件可能会因以下原因重复:重复点击按钮、组件重新渲染、重试、多个事件监听器、前端和后端同时触发同一事件,或重复的 Webhook 投递。
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
为了使事件具备重试安全性,我添加了稳定的 PostHog $insert_id 值,并定义了每个事件的归属系统:
- 前端记录用户意图(例如点击升级按钮)
- 后端记录 Stripe 结账会话的成功创建
- Stripe Webhook 确定结账完成和订阅激活
- 后端仅在替代文本持久化后才进行记录
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
挑战 3:跨多个系统追踪操作
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.
一次结账流程可能会经过:WordPress → 后端 API → Render 日志 → Stripe 结账 → Stripe Webhook → Supabase → PostHog。如果没有共享标识符,调试这个过程会非常痛苦。我引入了:correlation_id、checkout_attempt_id、generation_run_id、signup_attempt_id、session_id 和 site_install_id。这些标识符让我能够追踪从插件到后端再到外部服务的整个操作过程——这在结账被放弃或登录失败时特别有用。无需再通过时间戳在各个孤立的系统中搜索,同一个关联值会贯穿整个旅程。
Challenge 4: Safe authentication telemetry
挑战 4:安全的身份验证遥测
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.
通用的 login_failed 事件用处不大。它无法告诉你失败的原因是凭据错误、账户被禁用、速率限制、网络超时、API 不可用、令牌创建失败还是会话存储失败。我添加了一套受控的内部错误分类,同时保持公共错误消息的通用性——这种区分很重要,因为详细的公共错误消息可能会泄露账户是否存在。
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.
系统现在记录安全的内部代码,例如:invalid_credentials、account_disabled、rate_limited、network_timeout、api_unavailable、token_creation_failed、session_creation_failed、unknown_auth_error。在发送遥测数据之前,密码、令牌、API 密钥和原始服务器响应都会被剔除。
Challenge 5: Privacy by default
挑战 5:默认隐私保护
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.
产品分析可能会迅速演变成隐私问题。遥测层现在会移除以下字段:电子邮件地址、许可证密钥、访问令牌、API 密钥、替代文本内容、图片文件名、图片 URL、提示词、原始 API 响应。除非明确开启,否则 PostHog 会话录制功能默认禁用。分析应当描述发生了什么,而不是复制用户正在处理的内容。
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. 事后补救并建立清晰的遥测系统是可能的,但这比在初期进行合理设计要费力得多。