Skip to main content

Documentation

No results found.
Features

AI Image Generation

WebProCMS lets editors generate images directly inside any image spot on a page — heroes, gallery items, repeater cards, slider slides, even section background images — by typing a prompt or letting the surrounding row content shape the pro...

Bring-your-own API key. Operators configure their own provider key (OpenAI, Stability, Google Gemini, or fal.ai) on the API Keys settings page; per-image generation costs are billed by the provider, not by WebProCMS. No membership required.

WebProCMS lets editors generate images directly inside any image spot on a page — heroes, gallery items, repeater cards, slider slides, even section background images — by typing a prompt or letting the surrounding row content shape the prompt automatically. Four providers are supported behind a single editor UI, and every generation is sized to the slot it's actually about to fill so the saved file matches what the layout needs.


The problem

Off-the-shelf image generation tools sit outside the CMS. The editor has to leave the page they're working on, write a prompt without the context of the surrounding row, generate an image at whatever resolution the tool defaults to, download it, upload it to the media library, fill in alt text by hand, and then attach it to the spot. By the time the image lands on the page, the workflow has crossed three tools and the editor has lost the visual context of what they were trying to make.

The other failure mode is over-generation. A 6 MB hi-res file dropped into a 552-pixel-wide hero slot wastes bandwidth, slows the page, and forces the CMS to downscale at request time. Generic image tools have no way to know the slot's actual dimensions or aspect, so they always return their own default size — usually too big.

The fix

WebProCMS embeds image generation in the page editor. The Generate AI button appears on every image-bearing field card, opens a modal pre-populated with the field context, and the result lands in the media library with auto-generated alt text already filled in. The editor never leaves the page.

Every generation is sized to the slot using the same widths and aspect-* declarations the public-site srcset already reads — so a 552px gallery slot doesn't pay for a 1920px file, and a 16:9 hero never gets returned as a square.


Providers, one workflow

The active provider is configured at Dashboard → Settings → AI (ai.image_provider). The editor UI is identical regardless of which provider is active; only the request-side details change.

Provider Model Notes
OpenAI Configurable (default gpt-image-2.5-flare; also gpt-image-2.5-sunburst, gpt-image-2) Accepts model + size + quality + output_format + compression + background + moderation, all editable from API Keys settings. The model is additionally overridable per call from the generate modal's Advanced section — pick Sunburst for hero/banner work. Supports image-to-image via /v1/images/edits — used by the "Tweak this image" button in the page editor and by AI favicon generation.
Google Gemini Always returns PNG; output extension is reconciled after the response. Supports image-to-image via multimodal generateContent (inline_data alongside the prompt) — used by AI favicon generation.
fal.ai Configurable model (default fal-ai/flux-2) Aspect maps onto a named-bucket enum (landscape_16_9, portrait_4_3, square_hd, etc.). When the configured model is flux/schnell, the modal exposes an inference-steps override.

Provider switching requires no code changes and no migration — the editor reads Setting::get('ai.image_provider') at request time and dispatches to the matching method.

Per-spot sizing

When the editor invokes generation for a field, AiImageSpotResolver reads the owning <x-dl.image> / <x-dl.media> / <x-dl.gallery> / <x-dl.slider> tag's existing attributes and produces a (width, aspect) pair:

  • Width — max($widths) from the same widths="..." attribute the public-site srcset already uses. No new attribute introduced; the source-of-truth stays single.
  • Aspect — parsed from a Tailwind aspect-* utility on the wrapper or image classes. aspect-square → 1:1, aspect-video → 16:9, aspect-[w/h] → w:h, with arbitrary fractions accepted.

AiImageSpotResolver::toProviderOpts($spot, $provider) translates that pair into provider-specific request params:

Provider Translation
OpenAI (width × aspect) on 16-pixel multiples, long edge capped at 3840, long-to-short ratio capped at 3:1, and total pixels clamped into the 655,360–8,294,400 band the gpt-image family accepts (identical across gpt-image-2 and both 2.5 models) — emitted as size: '<w>x<h>'. Arbitrary frames are allowed by the API, so the request matches the slot rather than snapping to a preset; a slot too small to reach the pixel floor is scaled up keeping its aspect.
Stability / Google The aspect string passed through if it's in the provider's allowed set (Stability accepts 9 ratios, Google accepts 8). Anything outside that set is dropped so the provider's default applies.
fal.ai Aspect mapped onto the named-bucket enum. Square goes to square_hd; everything wider than tall lands on a landscape bucket, taller than wide on a portrait bucket.

To add image-generation support to a new component, just declare widths and an aspect-* utility on the wrapper class — the resolver picks them up automatically. No registration step.

Spots that can't be resolved (background images, grid sub-fields without their own component tag, missing widths) return an empty array — the provider's hardcoded default takes over rather than producing a misleading "size: 0x0".

Per-call advanced overrides

The editor's AI generate modal (resources/views/partials/ai-generate-modal.blade.php) has a collapsed "Advanced" section whose contents are provider-aware. Defaults come from the spot's resolved params; the override applies to that one generation only.

Provider Modal overrides
OpenAI size, quality
Stability / Google aspect_ratio
fal.ai image_size, plus num_inference_steps when the configured model is flux/schnell

AiImageGenerator::normalizeOverrides($overrides, $provider) filters the user-supplied dict down to a per-provider whitelist, drops empty strings, and casts inference steps to int. Anything not whitelisted for the active provider is silently dropped, so adding new modal knobs is safe — they only take effect once the whitelist has them.

The accordion's open/closed state persists in localStorage['aiGenAdvancedOpen'] so editors who like working with overrides don't have to reopen the section every time.

The full resolution order on every call is: spot defaults → user overrides → provider's hardcoded fallback (only for keys neither layer supplied).

Image-to-image: tweak this image

After a generation, OpenAI users see two buttons instead of a single Regenerate:

  • Tweak this image — passes the current preview's tempPath as the reference, so the next call iterates on the visible result rather than starting fresh. The modal calls $wire.generateAiImage with the referencePath argument set, which routes the request from POST /v1/images/generations (text-only JSON body) to POST /v1/images/edits (multipart form with the file as an image part) via generateImageEditViaOpenAi.
  • Generate fresh — drops any reference and hits /generations for an unrelated new image.

Tweak (and the "Attach reference image" control) is hidden on providers that can't take a reference. That capability has ONE definition — AiImageGenerator::supportsReferenceImages(), currently OpenAI, Managed, and Google — which the blades read via a server-resolved advancedConfig.supportsReference flag rather than re-spelling the provider list; hand-copied lists here had drifted to openai-only. AiImageGenerator::generate() routes reference-image calls: OpenAI and Managed → /v1/images/edits, Google → multimodal generateContent with inline_data. fal.ai throws a clear error ("Reference images aren't supported yet for the {provider} provider") since its img2img endpoint is not yet integrated.

Reference images from the media library

Editors can also attach an existing media-library image as a reference before the first generation. The modal's "Attach reference image" link opens the standard media picker with mediaPickerKey = 'ai-reference'. Alpine listens for media-image-picked window-scoped, filters by key, and stages the path locally — same picker flow used everywhere else, so the editor learns it once.

Both the picker path and the previous-preview path live on the same public disk, so generateAiImage accepts either source as a storage-relative referencePath without any translation step.

Auto-generated alt text via vision API

When the editor saves a generated image to the library, the MediaItem.alt field is filled by running the configured text provider's vision API against the saved file with the prompt "Generate a concise, descriptive alt text for this image suitable for screen readers. Maximum 10 words."

This matters because the directive prompt the user typed ("make me a hero image") is rarely a good description of the rendered result. Running vision against the actual bytes produces alt text that matches what the screen reader will read.

Text provider Vision support
Claude Yes
OpenAI Yes
Google Gemini Yes
DeepSeek No — DeepSeek doesn't support vision input, treated as "not configured" for this path

The vision call is wrapped in EditorAiActions::generateAltTextFromPath($storagePath): ?string. Returns null on any failure (no provider, missing key, API error, missing file) so the save flow falls back to the typed prompt rather than failing — saving never fails because of alt-gen.

When a matching <image_key>_alt page-level field exists for the spot, the generated alt is also written to that override and dispatched to the Alpine store via content-text-reset so the sidebar input reflects it immediately. Same shape as the regular media-picker's onMediaImagePicked flow.

The vision call adds roughly 1–3 seconds to the save round-trip; preview generation is unaffected because vision only runs at save time.

Save flow

When the editor clicks Save in the modal:

  1. The temp file is moved from tmp/ into the configured media category folder (e.g. uncategorized/ai-20260505-abc123.webp for foreground images, backgrounds/... for section background images).
  2. Dimensions are read with getimagesize; mime is derived via mimeFromImageInfo so the saved MediaItem.mime_type matches the actual format on disk.
  3. The vision API runs against the saved file and produces alt text (max 10 words, screen-reader optimized).
  4. A MediaItem row is inserted with the path, filename, alt, size, mime, width, and height.
  5. If the field is a row's foreground image, contentValues[$fieldKey] is updated and the matching <key>_alt override (if the row declares one) is written too. If the field is a row background image (row-design-bg:{slug}), the slug's section_bg_image is updated instead.
  6. The editor's isDirty flag is set, the preview iframe is refreshed, and the sidebar field card receives a content-image-reset (and optionally content-text-reset) event so its thumbnail updates without waiting for a re-drill-in.

Memory-safe streaming

Generation is designed to handle large images without crashing the server: provider responses are sinked directly to disk via Guzzle's sink option and base64 fields are stream-extracted with php://filter/convert.base64-decode, so peak in-PHP memory stays in the few-KB range regardless of image size. A 6 MB hi-res file never gets buffered as a 30+ MB JSON-decoded string in memory.

Reusable across entities

The provider methods, streaming response handlers, and reconcile / WebP-convert logic all live on App\Support\Ai\AiImageGenerator — a single shared service. The page editor uses it via the EditorAiActions trait's generateAiImage method. Other entity flows (events, locations, content items) can adopt the same pattern by:

  1. Adding three wire methods to the Volt component: generate{Entity}AiImage, save{Entity}AiImage, discard{Entity}AiImage.
  2. Building an entity-specific context-snippets array (title + excerpt + content, or whatever fields make sense for that entity).
  3. Including a modal partial that dispatches its own scoped events ({entity}-ai-image-preview, {entity}-ai-image-saved, etc.) so multiple modals on one page never collide.

The image-generation memory-safety guarantees, provider switching, advanced overrides, reference-image support, and vision-derived alt text are all owned by the service — entity flows inherit them automatically.

What lives where

Path Purpose
app/Support/Ai/AiImageGenerator.php The shared service. generate() dispatches to the active provider (OpenAI / Stability / Google / Fal), reconciles the file extension, and runs the OpenAI → WebP normalization. generateViaOpenAi() is the OpenAI-only variant used by the "generate for every row image" batch entry. normalizeOverrides() filters per-call user overrides per provider; mimeFromImageInfo() derives mime from getimagesize results.
app/Concerns/EditorAiActions.php The trait the page editor mixes in. Orchestrates the page-editor flow: generateAiImage, saveAiImagePreview, saveAiGridItemImagePreview, discardAiImagePreview, generateAltForField. Delegates the provider call to AiImageGenerator.
app/Support/Ai/AiImageSpotResolver.php Reads widths and aspect-* from the owning <x-dl.*> tag and produces per-provider request params.
app/Support/Rows/RowBladeSurgery.php findFieldOwnerAttrs — locates the <x-dl.*> tag that registered a given field key inside the row blade.
resources/views/partials/ai-generate-modal.blade.php The Alpine modal hosted by the page editor — prompt input, advanced overrides, reference-image picker, preview, Tweak/Fresh/Save/Discard buttons.

When the buttons show

Every Generate with AI control gates on AiImageGenerator::available() — the one answer shared by the page editor's image fields and row backgrounds, the media library, the featured-image sparkles, the branding generators, the AI site generator, the blog agent and the editor assistant's tool list. A selected provider is not enough: on a managed install the image lane is a per-member switch on the mothership that defaults off (see Managed AI credits), and while it is off the controls are hidden rather than leading to a refused call. AiImageGenerator::unavailableMessage() is the matching explanation, so a managed install that is off reads "purchase AI credit or add your own key" instead of the self-keyed fallback's complaint about a missing OpenAI key. Self-keyed providers read as available on selection alone; a missing key is the admin's own configuration and the generator names it.

Settings reference

Key Default Notes
ai.image_provider '' One of openai, stability, google, fal. Empty means "not configured" — the editor surfaces a clear error instead of silently failing.
ai.openai_key '' OpenAI API key. Same key powers both image and text/vision calls when OpenAI is the text provider.
ai.openai_image_size 1536x1024 Default OpenAI size when the spot can't be resolved.
ai.openai_image_model gpt-image-2.5-flare Default OpenAI image model. Overridable per call from the generate modal. On a managed install this setting is not used — the model comes from the mothership's signed block and is bounded by its allowlist.
ai.openai_image_quality medium Default rendering tier, and the biggest single cost lever (effort drives the output token count each image bills for). Tiers offered depend on the model: the 2.5 models take low/medium/high/xhigh/max, gpt-image-2 tops out at high. auto is offered on every model and sends no quality parameter at all, letting the provider pick the effort. A tier the resolved model can't serve is clamped DOWN at request time rather than dropped, so a managed install whose mothership model is older still generates.
ai.openai_image_output_format webp Preferred format. webp triggers server-side GD conversion because gpt-image-2 ignores the webp request and ships PNG. The conversion no-ops on bytes that are already WebP — which is what happens on the 2.5 models, verified against the live API 2026-09-08, so the GD step costs nothing there.
ai.openai_image_output_compression 85 WebP quality used by the server-side conversion.
ai.openai_image_background auto OpenAI background param.
ai.openai_image_moderation auto OpenAI moderation level.
ai.google_key / ai.fal_key '' Provider-specific keys, configured per active provider.
ai.fal_image_model fal-ai/flux-2 Active fal.ai model. flux/schnell enables the inference-steps override in the modal.