The problem
If you've ever vibe coded a web interface, using an AI editor, a website builder, or just a chatbot, you've probably seen this: beautiful layout, great CSS, and... stock images. Or placeholders. Or worse, nothing at all.
This happens because:
- Not every LLM is good at everything. Claude can't generate images. OpenAI isn't great at web UI. Each has its strengths.
- Most AI coding tools don't focus on images because they don't really know how to handle them.
My usual workflow looked something like this:
- Pick an image model and generate images one by one
- Cry over the keyboard trying to keep a consistent style across them
- Download everything and manually upload to the platform's assets, hoping it even allows that
So I thought: LLMs are already good at writing prompts. Why not teach them how to write prompts that generate great, consistent images? After all, generating text is what they do best.
I started building my first ever library. It's called vibe-img. Here's how it evolved through several prototypes that didn't work before landing on something that does.
Prototype 1: the gateway
My first idea was classic. A backend gateway for image generation:
<vibe-img url="gateway.local?prompt=miami+beach&model=openai">
A tag that contacts a gateway server, which forwards requests to the image generation providers.
Simple, but it had problems: scalability, latency, reliability. Image generation has high I/O, and running everything through a single server wasn't going to work.
So I switched to a decentralized model. Let the browsers do the work.
Prototype 2: the browser does everything
The idea became a JS library that any LLM can include with a single <script> tag. The frontend calls the image generation provider directly, no intermediaries:
Everything happens in the frontend. But this opened a new set of problems:
- I didn't want to go broke regenerating the same images on every page refresh
- The LLM still doesn't know how to generate consistent images. Nobody taught it!
- CORS hell (obviously)
- Every model has a different API
Here's how I solved each one.
The hash algorithm (a.k.a. how I stay not-poor)
The core idea is simple. A <vibe-img> tag has several attributes:
<vibe-img model="recraft" img-style="vector" prompt="a cozy fall cabin in the forest with smoke coming from the chimney"></vibe-img>
I hash all these attributes together with SHA-256 and use that hash as the cache key on a CDN.
On the next page load:
- Check if the image exists in cache (local IndexedDB, then CDN)
- If yes: load it instantly, zero API calls
- If no: generate it, cache it, done
The beauty of this: change your CSS all you want, the images don't regenerate. Only attribute changes produce a new hash and a new image. And you can share the page with anyone, the images load from the CDN, no API key needed.
Consistent images with <vibe-theme>
LLMs are already good at writing prompts that maintain style consistency. You just need to teach them to do it. I did this in two ways:
- llms.txt and skills: instruction files that teach the LLM how to use the library and write good prompts
-
<vibe-theme>: a wrapper tag that appends a shared style prompt to every child image
The theme prompt is simply prepended to each image's prompt at generation time. You can even nest themes for different sections.
First experiment: Parisian bookshop
I asked Claude to build me a landing page for a vintage Parisian bookshop using vibe-img with Recraft. Here's what came out:
And the code Claude wrote:
<vibe-theme prompt="classic vintage hand-drawn book illustration, fine detailed ink lines with soft watercolor washes, warm muted tones, reminiscent of old French editorial illustrations from the 1920s, elegant and refined, cream colored background rgb(245 240 232), no text no watermark no signature">
...
<vibe-img
model="recraft"
img-style="illustration"
quality="hd"
aspect="portrait"
prompt="interior of an old Parisian bookshop, floor-to-ceiling wooden shelves filled with books, a rolling library ladder, a green banker lamp on a desk, Persian rug on parquet floor, cozy intimate atmosphere, viewed from inside"
params='{"controls":{"colors":[{"rgb":[44,62,45]},{"rgb":[184,148,90]},{"rgb":[245,240,232]},{"rgb":[90,70,50]}],"background_color":{"rgb":[245,240,232]}}}'
alt="Intérieur de la librairie avec ses étagères en bois sombre"
></vibe-img>
...
<vibe-img
model="recraft"
img-style="illustration"
quality="hd"
aspect="landscape"
prompt="a curated stack of vintage hardcover books on a wooden reading table, a porcelain cup of coffee beside them, a small vase with a single flower, soft light from a nearby window, bookshelves in the background"
params='{"controls":{"colors":[{"rgb":[44,62,45]},{"rgb":[184,148,90]},{"rgb":[245,240,232]},{"rgb":[90,70,50]}],"background_color":{"rgb":[245,240,232]}}}'
alt="Livres anciens e café sur une table de lecture"
></vibe-img>
...
<vibe-img
model="recraft"
img-style="illustration"
quality="hd"
aspect="landscape"
prompt="charming Parisian bookshop storefront facade, dark painted wood with elegant signage, large display window showing stacked books, cobblestone street, flower boxes with ivy, classic Haussmann building above, evening glow from inside"
params='{"controls":{"colors":[{"rgb":[44,62,45]},{"rgb":[184,148,90]},{"rgb":[245,240,232]},{"rgb":[90,70,50]}],"background_color":{"rgb":[245,240,232]}}}'
alt="La façade de la librairie sur sa rue pavée"
></vibe-img>
</vibe-theme>
Notice how the AI also used Recraft's custom color controls through params. Advanced provider-specific features, accessible through the same universal tag.
Second experiment: Dolomites cabin
Same thing, different provider. I asked Claude to build a Dolomites cabin landing page using OpenAI:
And the code Claude wrote:
<vibe-theme prompt="refined editorial illustration style, muted warm earth tones, soft gradients, elegant and sophisticated alpine atmosphere, clean lines, gentle texture">
...
<vibe-img
model="openai"
img-style="illustration"
aspect="wide"
quality="hd"
prompt="exterior view of a traditional wooden A-frame cabin nestled in the jagged snowy peaks of the Italian Dolomites at golden hour sunset, warm glowing windows, pine trees, soft warm light, cinematic composition"
></vibe-img>
...
<vibe-img
model="openai"
img-style="illustration"
aspect="square"
prompt="cozy cabin interior with a crackling stone fireplace, comfortable leather armchair, thick wool rug, warm ambient lighting, window showing snowy mountains"
></vibe-img>
...
<vibe-img
model="openai"
img-style="illustration"
aspect="square"
prompt="winding hiking trail through lush green alpine meadows with wildflowers, majestic jagged Dolomite peaks towering in background under clear blue sky."
></vibe-img>
...
<vibe-img
model="openai"
img-style="illustration"
aspect="square"
prompt="wooden balcony of an alpine cabin overlooking a breathtaking deep valley in Italian Alps, two steaming mugs of coffee on a small table, morning golden light"
></vibe-img>
...
</vibe-theme>
Not bad for one-shot generation. No cherry-picking.
The CORS situation
Of course, calling AI APIs from the browser means dealing with CORS. Some providers (like Recraft) actually set CORS headers, so requests go straight from the browser to their API. No problem there.
Others (like OpenAI) don't. For those, I built a minimal CORS proxy. It doesn't log anything, the API key only lives in memory for the duration of the request, and you can swap it with your own or turn it off entirely if you only use CORS-friendly providers.
"API keys in the frontend?!"
I know, I know.
But hear me out. The security model has two phases:
During prototyping: you're on localhost or in your dev environment. It goes directly to the AI provider and nowhere else. There's no attack surface because there are no third-party users.
When sharing: you send the URL to your client or deploy the page. The images are already cached on the CDN. No API key is present, needed, or even requested. The key's job is done.
The CDN URLs are unguessable (SHA-256 hash):
cdn.vibe-img.com/vibeimg-4479b58ffa14e53da9ec06e3feedcdb802dbca14b5c4a42fba6c093ba31ef5bc.webp
And keys never leak into LLM system prompts. Someone once said "LLM stands for Leak Language Model." Not here.
Switching providers
One thing that bugged me about image APIs is that they're all different. Different endpoints, different parameter names, different response formats. I wanted to switch providers by changing one word, not rewriting everything.
So the model attribute is the only thing you change. model="recraft" or model="openai", same tag, same attributes. Internally, each provider has an adapter that translates universal attributes to whatever the API expects.
This also means adding a new provider is just writing one adapter file. Which brings me to...
Contributing
There's a lot to do. Two main areas:
Adding providers. Only OpenAI and Recraft are supported right now. If you use another model, adding an adapter is straightforward:
cp src/adapters/_template.ts src/adapters/my-provider.ts
Fill in the mapping tables, implement buildRequest(), write at least 5 test fixtures, register it. Run npm test. Done.
Improving prompts. The llms.txt and skill files can definitely be better. Contributions from people experimenting with different prompt strategies are very welcome.
GitHub: github.com/devhashfortheweb/vibe-img
Site: vibe-img.com




Top comments (0)