Hero Images and the Fallback Chain
A single image API failure should not kill your pipeline — a fallback chain keeps generation running when your primary provider goes down or hits rate limits.
Introduction
Every article that goes live on jeremyknox.ai has a hero image. Not a stock photo pulled from an Unsplash search, not a placeholder, and not an image recycled from a previous post. A generated image, specific to that article, created in the same pipeline run.
Generating images automatically sounds straightforward until you run it in production. Image APIs have rate limits. They go down. They reject prompts that trigger content filters. And when an image API fails mid-run — after SOURCE has selected the topic and DRAFT has written the article — you have to decide whether to fail the whole pipeline or continue without an image.
The answer is neither. The answer is a .
Core Concept
The Fallback Chain
A fallback chain is a prioritized list of image providers. The pipeline tries provider 1. If that call fails for any reason — rate limit, downtime, content rejection, timeout — it tries provider 2. If provider 2 fails, it tries provider 3. Only if all providers fail does the pipeline exit with an error.
The jeremyknox.ai pipeline uses this chain:
- Leonardo AI Phoenix 1.0 — primary. Trained specifically for high-quality editorial images. Produces consistent results with the visual style used across the site.
- Gemini (via mcp-image) — first fallback. Google's image generation, accessible via an MCP tool. Different aesthetic, but reliable.
- OpenAI gpt-image-1 — second fallback. Prompt-adherent, production-grade, higher cost per image but worth it when the first two fail.
The logic in generate_image.py is explicit about this order. If Leonardo returns any non-200 status, the script logs the failure and immediately calls the Gemini endpoint. The output format is normalized — all three providers write to the same file path with the same dimensions — so the PUBLISH stage does not need to know which provider succeeded.
Provider Selection Criteria
When building your own fallback chain, evaluate each provider on three criteria:
Quality consistency. Does the provider produce images that match your site's visual language reliably, or does the output vary wildly across runs? Consistency matters more than peak quality.
Prompt adherence. Does the provider follow specific compositional or stylistic instructions, or does it tend to reinterpret prompts creatively? For a pipeline, you want predictable output, not creative surprises.
Cost and rate limits. Understand each provider's rate limits before you need them. A provider with a strict 10 images per minute limit will throttle a pipeline that generates multiple articles per day. Check limits against your publishing cadence before committing to a provider as your primary.
Startup Validation
Before the cron job runs any real work, validate that your image provider chain is functional. This is a lightweight check — a test API call that confirms authentication is valid and the endpoint is reachable. It does not generate a real image; it just confirms the API key works and the service responds.
Why do this at startup rather than at generation time? Because you want to fail fast and clearly. If your Leonardo API key expired yesterday, you want to know that at 9:00 AM when the cron fires, not at 9:06 AM after SOURCE and DRAFT have already completed their work. A failed validation at startup exits the pipeline with a clear error message and a Discord alert before any compute is spent downstream.
The startup validation pattern:
def validate_providers():
healthy = []
for provider in PROVIDER_CHAIN:
result = provider.health_check()
if result.ok:
log(f"{provider.name}: OK")
healthy.append(provider)
else:
log(f"{provider.name}: FAILED ({result.error})")
if not healthy:
raise RuntimeError("All image providers failed validation. Aborting pipeline.")
If all providers fail validation, the pipeline aborts before spending any time on SOURCE or DRAFT. This is the correct behavior.
Note where this runs: validate_providers() is called from run.py as a pre-flight step, before SOURCE or DRAFT execute — not inside generate_image.py. And note what it returns: nothing. Validation only confirms that at least one provider is reachable. At generation time, generate_image.py still walks the fixed Leonardo → Gemini → OpenAI order — the chain order is configuration, not something validation decides.
Image Prompt Construction
The quality of the image depends on the quality of the prompt you send to the generation API. Do not pass the full article body. Image models work best with concise, visually descriptive prompts — 50 to 150 words.
Construct the image prompt from:
- The article title (the core subject)
- The excerpt or thesis (the visual angle)
- Your site's visual style guide (dark background, cyberpunk aesthetic, no faces, cinematic composition)
- Specific technical elements if relevant (circuit patterns, data visualization metaphors, architectural diagrams)
The jeremyknox.ai image prompt template:
[STYLE] Dark cinematic composition. Deep blue and cyan palette.
Abstract digital aesthetic. No human faces. No text overlays.
Sharp geometric elements. High contrast.
[SUBJECT] {derived from article title}
[MOOD] {derived from article angle}
[TECHNICAL] 1360x768 pixels. Photorealistic render style.
This template is filled in by generate_image.py using the topic record from SOURCE. The same template works across all three providers in the fallback chain.
Practical Application
Here is the IMAGE-stage flow for a single article run (step 0 is the run.py pre-flight; steps 1–5 are generate_image.py):
0. Pre-flight (run.py, before SOURCE or DRAFT): validate_providers()
→ All providers fail: send Discord alert, exit 1 — no downstream work is spent
1. Load topic.json (title, excerpt, category)
2. Construct image prompt from topic + style template
3. Call primary provider (Leonardo)
→ Success: save to /public/images/blog-autopilot/{slug}.png, write image_path.txt, exit 0
→ Fail: log error, continue to step 4
4. Call fallback 1 (Gemini)
→ Success: save to path, write image_path.txt, exit 0
→ Fail: log error, continue to step 5
5. Call fallback 2 (OpenAI gpt-image-1)
→ Success: save to path, write image_path.txt, exit 0
→ Fail: log error, send Discord alert, exit 1
The image_path.txt file is the handoff artifact to the PUBLISH stage. It contains one line: the relative path to the generated image. PUBLISH reads this and injects it into the MDX frontmatter as the coverImage field.
Common Mistakes
Hardcoding a single provider. One API key, one endpoint, no fallback. This works until it does not. When your primary provider goes down on a Monday morning and your cron fires without an image, the PR either stages without a hero or the whole pipeline errors. Neither is acceptable. Build the chain from day one.
Not normalizing output across providers. If provider 1 returns a URL and provider 2 returns base64 data and provider 3 writes a file, your PUBLISH stage needs custom handling for each. Normalize: all providers write to the same local file path at the same dimensions. The downstream stages should not know which provider ran.
Generating images from the full article body. Long text dilutes the visual direction. The model cannot prioritize. Use a concise prompt derived from the title and excerpt plus your visual style constants.
Skipping validation in development. It is tempting to skip the startup validation locally because you know your API key works. Keep it in. It is cheap, and it catches key rotations, quota resets, and provider changes that happen between pipeline runs.
No image size specification. Default sizes differ across providers. Specify dimensions explicitly — jeremyknox.ai uses 1360x768 — so the hero image slot renders correctly without CSS hacks.
Summary
- A fallback chain tries providers in priority order; only if all fail does the pipeline error.
- Startup validation fails fast before SOURCE and DRAFT run, avoiding wasted compute on a broken image layer.
- Image prompts should be concise and style-guided, not the full article body.
- Normalize output format across providers so downstream stages are provider-agnostic.
- The jeremyknox.ai chain is: Leonardo Phoenix 1.0 → Gemini → OpenAI gpt-image-1.
What's Next
The next lesson covers the PUBLISH stage — specifically why opening a pull request rather than publishing directly is the right default, how the human review gate works, and when (and only when) it is safe to move toward fuller automation.