Bring-your-own API key. Operators configure their own provider key (Anthropic Claude, OpenAI, Google Gemini, or DeepSeek) on the API Keys settings page; per-call generation costs are billed by the provider, not by WebProCMS. No membership required.
WebProCMS lets editors generate, translate, rewrite, and proofread text directly inside any text or rich-text field on a page — using whichever LLM provider they've configured. The same editor entry point handles single-field generation, whole-section translation into 20+ languages, tone rewriting across an entire row, and SEO title + meta description generation pulled from the page's actual content.
The problem
Every CMS that adds AI text generation has to choose one of two compromises. Either it ships its own opinionated wrapper (locking the customer into the vendor's chosen provider, model, and pricing), or it shells out to an external tool the editor has to copy-paste into. Both fail the everyday case: an editor who wants to rewrite a paragraph in a friendlier tone, or translate a hero into Spanish, without leaving the page they're working on.
The other failure mode is context blindness. AI-generated copy that doesn't know what's already on the page produces generic boilerplate that contradicts the surrounding rows. SEO metadata especially — if the AI hasn't seen the actual content of the page, it reverts to keyword-stuffing the page name and the result is worse than no help at all.
The fix
WebProCMS embeds AI text generation in the page editor with provider-agnostic plumbing. The editor brings the API key for whichever provider the customer prefers; WebProCMS provides the prompts, the field-aware wiring, the batch orchestration, and the per-row context plumbing.
Every prompt is field-type-aware (rich-text fields get HTML-preserving instructions; class fields get Tailwind-expert instructions; SEO fields get strict-JSON instructions) and surrounding row content is folded into the prompt so suggestions fit the page rather than fighting it.
Four providers, one workflow
The active text provider is configured at Dashboard → Settings → API Keys (ai.text_provider). The editor UI is identical across providers; only the request shape changes per call.
| Provider | API | Notes |
|---|---|---|
| Claude | https://api.anthropic.com/v1/messages |
Default for high-quality copy. Vision-capable, so it can also generate alt text from saved images. |
| OpenAI | https://api.openai.com/v1/chat/completions |
Vision-capable. Same key powers image generation when OpenAI is also the image provider. |
| Google Gemini | https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent |
Vision-capable. Configurable model, default gemini-3.7-flash. |
| DeepSeek | https://api.deepseek.com/chat/completions |
Text-only — does not support vision input. The vision-derived alt text flow treats DeepSeek as "not configured" and falls back to the typed prompt. |
Provider switching requires no code changes and no migration — every method in EditorAiActions reads Setting::get('ai.text_provider') at call time and dispatches via match.
Field-aware single-field generation
generateAiContent($fieldKey, $prompt, $fieldType, $currentClasses, $useHtml) is the entry point for the per-field AI button. The system prompt varies by field type so the model produces output that's safe to drop straight into the field:
| Field type | System prompt shape |
|---|---|
text |
"Generate concise, well-written text… Return only the text, no HTML tags, no markdown, no quotes, no explanation." |
richtext (with $useHtml=true) |
"Generate clean, concise HTML… Use only <p>, <strong>, <em>, <a>, <ul>, <ol>, <li>. Do not use heading tags (h1–h6). Do not produce empty tags. Return only the HTML, no markdown code fences." |
richtext (with $useHtml=false) |
Same as plain text. |
classes |
"You are a Tailwind CSS expert… Return only the updated class string — space-separated Tailwind classes — with no explanation, no quotes, no backticks, and no markdown." The current class string is included in the user message. |
seo |
"Generate an optimized page title and meta description. Title 50–60 chars, description 150–160 chars. Respond with ONLY valid JSON {"title":"...","description":"..."} — no markdown, no code fences, no explanation, no refusals." |
For SEO generation, the user message includes the page name plus up to 30 existing content snippets pulled from ContentOverride rows on the page (filtered to skip class/image/url/toggle/grid/translation-variant keys). That way the title and description reflect what the page actually says, not just its slug.
The result dispatches one of two events: ai-content-generated for normal fields (the editor's Alpine layer applies it to the right input), or seo-ai-complete for SEO with separate title and description payloads. Errors dispatch ai-generate-error with the provider's message so the editor can surface a real error rather than a silent failure.
Translate any section to 20+ languages
translateAllSectionFields($rowIndex, $targetLang) is invoked from the row's AI batch tools menu. It walks every translatable field in the row's content schema and calls the provider once per field, stashing the translation in the matching __{lang} variant key.
The selection logic:
- Translatable types —
text,richtext,headingtext. Class fields, URLs, htag dropdowns, alt text, IDs, and custom-attrs are all skipped. - Excluded suffixes —
_url,_htag,_alt,_new_tab,_classes,_id,_attrsplus any key starting withtoggle_. - Variant matching — only fields whose key ends with
__{targetLang}get processed (the row's translation infrastructure registers these variant keys ahead of time). - Source content — pulled from the corresponding non-variant key. Fields with empty source content are skipped silently.
Each translation gets a system prompt tailored to the field type — rich-text fields get "Preserve all HTML tags exactly as they appear", plain text fields get "Do not add HTML tags". The result is written to contentValues and to the session's editor_draft_overrides so the row preview reflects it immediately and the next save persists it.
A two-pass orchestrator (prepareTranslation → translateNextField) is also exposed for incremental UIs that want to show progress and handle one field at a time, including grid item sub-fields. The single-call translateAllSectionFields covers the common "translate this whole row" button.
Tone rewriting
prepareToneRewrite($rowIndex) returns the list of rewritable field keys for a row, and rewriteToneNextField($fieldKey, $tone) rewrites them one at a time.
| Tone preset | Instruction |
|---|---|
proof |
"Proofread and lightly edit the following text to fix grammar, spelling, punctuation, and clarity issues. Preserve the original meaning, voice, and length as closely as possible." |
professional |
"Rewrite the following text in a professional, polished tone suitable for a business audience. Keep the core message." |
casual |
"Rewrite the following text in a casual, conversational tone. Make it feel approachable and natural." |
playful |
"Rewrite the following text in a playful, fun, and energetic tone. Add personality while keeping the core message." |
The instruction sentence is shared between the per-field menu (rewriteAiContent) and the bulk batch tool (rewriteToneNextField) via toneInstructionFor($tone) so tone presets stay in sync between the two surfaces.
Field selection mirrors the translate flow: only text / richtext / headingtext, skipping class/url/htag/alt/toggle keys and any field whose key contains __ (translation variants — those belong to the translate flow). Fields with no current value fall back to the schema's default so default copy also benefits from the tone pass.
For rich-text content, the system prompt includes "The content is HTML — preserve the HTML structure and tags, only modify the text nodes" so the rewrite doesn't mangle inline links, lists, or emphasis.
Per-row context
Single-field generation can opt into surrounding-row context via the includeContext flag (default true on the AI generate modal). When enabled, the prompt is augmented with snippets describing:
- The row's section preset (background colour family, text colour, link colour) — described in human terms with hex values, not raw class strings.
- The row's image aspect ratio (when the field is image-adjacent), so generated copy and generated images are sized for the same slot.
- Existing headings paired with their
_htagvalue (e.g.h2 headline: Our Mission), so the model knows what the row already says. - Other text fields on the row, with class/url/toggle/translation-variant keys filtered out.
- Grid item context when the field lives inside a grid (the matching item's headlines and text are surfaced).
The result is one cohesive prompt that fits the page rather than a generic suggestion that contradicts it.
SEO title and meta description
The SEO field type triggers a separate code path that pulls the entire page's content as context (up to 30 snippets across all rows), wraps it with the page name, and asks for a strict JSON response. The response is parsed defensively:
- Markdown code fences are stripped if the model wrapped the JSON.
- A non-JSON conversational response is surfaced to the editor with a "the AI returned this instead of JSON" message — better than a silent failure.
- A valid response sets
seoTitleandseoDescriptionon the editor and dispatchesseo-ai-completeso the form fields update.
Vision-derived alt text
When an image is saved (whether from AI generation, an upload, or a media-library pick that triggers a refresh), the configured text provider's vision API runs against the saved bytes to produce alt text optimized for screen readers (max 10 words). Claude, OpenAI, and Google all support vision; DeepSeek doesn't, so a DeepSeek install falls back to the typed prompt.
The vision call (generateAltTextFromPath) is wrapped to return null on any failure — saving never fails because of alt-gen. See ai-image-generation.md for the full flow.
Per-field error handling
Every batch operation (translate, tone, generate-all-rows) catches per-field exceptions and continues to the next field rather than aborting the whole batch. Individual failures are aggregated by the JS orchestrator so a long-running batch can surface a list of which fields failed without losing the ones that succeeded.
Single-field generation surfaces errors via ai-generate-error events with the provider's actual message, so authentication failures, rate-limit responses, and content-policy refusals all reach the editor with the real reason rather than a generic "something went wrong".
Page editor chat assistant
Alongside the per-field button and the batch (translate / tone) tools, the editor includes a conversational assistant you can talk to in plain language — "rewrite this section to be more persuasive", "add a pricing section below the hero", "make the heading bigger and the background darker". It can be opened scoped to a single section or for the whole page, and its reply streams in token-by-token over a Server-Sent Events endpoint.
Custom tools — it edits the page the right way
The assistant does not paste raw markup into your pages or guess at the site's internals. It's given a small set of purpose-built tools that drive the editor exactly the way a person would. Every change therefore flows through the same staging pipeline as a manual edit — it lands in content_overrides on save, updates the live preview immediately, and stays fully reviewable and discardable until you save. There's no path for it to produce malformed markup or bypass the design system.
On every turn the assistant is handed a structured snapshot of the current page — page_name, every section (slug + name), the editable text/richtext field keys and values on each section, and the catalogue of available design-library row templates — so it always acts on the real, current state of the page rather than stale blade defaults.
The tools it can call (defined in assistantToolDefinitions, Anthropic Messages tool-call format, normalised across providers by Prism):
| Tool | What it does |
|---|---|
update_content_field |
Rewrites a top-level text or rich-text field on a given row (rowSlug + fieldKey + new value). HTML for rich-text, plain text otherwise. Self-corrects common field-name guesses (e.g. primary_label → primary_button, stripping _text/_copy suffixes) and returns the list of valid keys when it can't match, so the model can retry. |
set_design_field |
Restyles a row by updating a design field — typically the Tailwind class strings that control padding, colour, and layout (section_classes, section_container_classes, headline_classes, etc.). |
insert_row |
Inserts a new section from the design library at a position on the page, choosing the closest-matching templateId from the snapshot's available_templates. |
remove_row |
Removes a section from the page by its index. |
Two ways to apply changes
- Auto-apply — tool calls run immediately and the results come back in the same turn.
- Review first — the proposed tool calls are returned as a checklist; you approve the ones you want before any are applied.
Deliberate limits
A few guardrails keep the assistant safe and predictable:
- It edits top-level text and design fields. For repeating content inside a section (grid cards, list items, gallery entries) its system prompt tells it to guide you to the editor UI rather than editing those items blindly.
- For ambiguous or potentially destructive requests ("remove everything"), it asks a brief clarifying question before acting.
- When opened scoped to a single section, it writes only to that section by default (while reading the whole page for context), and only broadens scope when you explicitly ask it to.
The assistant follows the same ai.text_provider switch as every other text tool — it works on Claude, OpenAI, Gemini, or DeepSeek, not Claude-only.
What lives where
| Path | Purpose |
|---|---|
app/Concerns/EditorAiActions.php |
The trait the page editor mixes in. Single-field (generateAiContent, rewriteAiContent), bulk (translateAllSectionFields, prepareToneRewrite, rewriteToneNextField, generateAiSectionContent, generateAiAllRowText), and per-field utilities (generateAltTextFromPath, generateAltForField). |
resources/views/partials/ai-generate-modal.blade.php |
The Alpine modal that hosts the prompt input, mode (generate / rewrite), useHtml toggle, and includeContext toggle for single-field calls. |
resources/views/pages/dashboard/pages/⚡editor.blade.php |
Hosts the per-row AI batch tools menu (Translate / Tone tabs) and the SEO generate button. |
app/Concerns/EditorAssistantActions.php |
The chat assistant trait. Builds the per-turn system prompt + page-context snapshot (assistantSystemPrompt, assistantPageContext), declares the tool catalogue (assistantToolDefinitions), and executes approved tool calls (executeAssistantToolCalls → update_content_field / set_design_field / insert_row / remove_row). |
app/Http/Controllers/EditorAssistantStreamController.php |
Server-Sent Events endpoint that streams the assistant's reply token-by-token and relays tool calls back to the editor for execution. |
Settings reference
| Key | Default | Notes |
|---|---|---|
ai.text_provider |
'' |
One of claude, openai, google, deepseek. Empty means "not configured" — the editor surfaces a clear error instead of silently failing. |
ai.claude_key |
'' |
Anthropic API key. |
ai.claude_model |
'' |
Claude model id (e.g. claude-sonnet-5). |
ai.openai_key |
'' |
OpenAI API key. Same key powers image generation when OpenAI is also the image provider. |
ai.google_key |
'' |
Google AI Studio key. |
ai.google_model |
gemini-3.7-flash |
Gemini model id. |
ai.deepseek_key |
'' |
DeepSeek API key. Note: DeepSeek does not support vision input, so vision-derived alt text falls back to the typed prompt on a DeepSeek install. |
site.languages |
[] |
Languages enabled for the site, used by the translate flow to map a target language code to a human-readable label for the prompt. |