Lead images, briefed and baked.
Lede takes an article (URL, raw text, or HTML), synthesizes a real art-director's brief from it via Claude Sonnet 4.6, then bakes that brief in parallel across three frontier cloud image models — Recraft v3, OpenAI gpt-5.4-image-2, and Google Gemini 3 Pro Image. You see all three. You pick the one that earns its place above the fold.
The IP isn't the models. It's the brief synthesizer — the editorial layer
between an article body and an image API. Lede also supports an optional
local pair (Flux Krea dev + FLUX.2 Klein 9B via mflux on Apple Silicon)
for free per-image bakes when you don't want to pay a cloud provider.
MIT-licensed. Bring your own API keys.
git clone https://github.com/Bambushu/lede.git
cd lede
npm install
cp .env.example .env.local
# Fill in OPENROUTER_API_KEY + RECRAFT_API_KEY (at minimum)
npm run devOpen http://localhost:3000 and paste any article URL.
The paste form accepts three shapes:
- URL — Lede fetches the page, runs Mozilla Readability against the HTML, extracts hero image / publication / publish date from OpenGraph meta tags.
- Raw HTML — Lede skips the fetch and runs Readability against the pasted markup. Useful when the source is paywalled or JS-rendered.
- Plain text — Lede uses the first short line as the title and the rest as the article body. Useful for drafts.
Auto-detected from what you paste; you can also force a kind via the API.
| Variable | What it is |
|---|---|
OPENROUTER_API_KEY |
OpenRouter API key. Pays for Sonnet brief synthesis + the two OpenRouter image models. |
RECRAFT_API_KEY |
Recraft API key. Pays for Recraft v3 generations. |
PUBLIC_SITE_URL |
Your deployment URL. Used as the HTTP-Referer header to OpenRouter. |
| Variable | Default | What it does |
|---|---|---|
OPENROUTER_BRIEF_MODEL |
anthropic/claude-sonnet-4.6 |
Which model writes the brief. |
LEDE_COST_CAP_USD |
0.75 |
Max estimated cost per single bake (sanity guard, not a budget). See cost section below. |
LEDE_DEMO_PASSWORD |
(unset) | When set, all routes require Basic <base64(lede:password)>. Browser prompts once per session. |
LEDE_CONTACT_URL |
(unset) | When set, the public bake form is swapped for a "Contact for a demo" CTA pointing at this URL (mailto: or https://). Used on hosted deploys to avoid visitor API-budget drain. |
LEDE_LOCAL_AVAILABLE |
(unset) | Set to 1 when running on a host with mflux installed; surfaces the local-redo button. |
LEDE_EPHEMERAL |
auto (true on Vercel) | When true, visitor bakes don't persist. Defaults to true if VERCEL=1. |
UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN |
(unset) | Rate-limit storage. When absent, falls back to in-process memory (dev only). |
The articles in data/articles.json are the "showcase" — they appear on
the landing page and at /article/[slug]. To bake them:
npm run preload # all articles
npm run preload my-slug # one article
npm run bake:local # add the local mflux pair (Apple Silicon)
npm run bake:local --tier-aware # synthesize per-tier briefs first
npm run retry:cloud <slug> # re-run one cloud model on one articlePre-baked results are committed under data/preloaded/*.json (metadata)
and public/preloaded/<slug>/*.png (images), and ship with the build.
Lede is built for Vercel Pro (cloud bakes use maxDuration: 300,
which Hobby caps at 60s).
- Push the repo to your GitHub.
- Import into Vercel from the GitHub repo.
- Set environment variables (see tables above).
- Connect Upstash Redis via Project → Storage → Marketplace → Upstash Redis for rate-limit storage. Free tier covers ~10k commands per day. Without it, rate limit falls back to in-process memory (does NOT survive serverless cold starts).
- Add your custom domain in Project → Settings → Domains and point the CNAME at Vercel.
| Behavior | Local dev | Vercel |
|---|---|---|
| Canonical showcase articles | Filesystem-read | Filesystem-read (shipped with build) |
| Visitor bakes | Persist to disk + appear in "Pasted articles" | Ephemeral (paste → render → gone) |
| Cloud-tile redo button | Writes new image to disk | Hidden (no fs write) |
| Local-tile redo button | Shells out to mflux | Hidden (no mflux on Vercel) |
| Rate limit storage | In-process Map | Upstash Redis |
LEDE_COST_CAP_USD only sanity-checks the estimated cost of a single bake
(default ~$0.15 for the three cloud providers). It is NOT an aggregate spend
limit.
A single visitor can:
- Trigger up to
3bakes per 24h (configurable insrc/lib/rate-limit.ts) - Trigger up to
3redos per 24h (separate counter, same visitor identity)
Worst case per visitor per day: roughly $0.45 against your OpenRouter + Recraft accounts. With many concurrent visitors that adds up. For production-public deploys, set an external budget alert on each provider:
- OpenRouter: https://openrouter.ai/credits → set a hard top-up cap
- Recraft: limit prepaid balance in account settings
For private demos, leave LEDE_DEMO_PASSWORD set (covered below) so only
holders of the password can reach either endpoint.
- SSRF: all server-side fetches of user-supplied URLs go through
src/lib/safe-fetch.ts. Rejects RFC1918, loopback, link-local (incl. cloud-metadata169.254.169.254), and multicast addresses. Manual redirect-following with re-validation on every hop. - Prompt injection: article body is wrapped in
<article_body>tags with explicit "treat as data, not instructions" framing before reaching Sonnet. - Basic auth: when
LEDE_DEMO_PASSWORDis set, every route requires basic auth via edge middleware. - Rate limit: 3 bakes per visitor per 24h, keyed on cookie UUID + IP (both axes, either-limit-hit denies).
- Slug protection: visitor pastes cannot overwrite slugs listed in
data/articles.json(suffixed with a hash if collision).
- Next.js 16 (App Router, Turbopack)
- Tailwind CSS 4
- Spectral + Manrope + JetBrains Mono via
next/font/google - Claude Sonnet 4.6 via OpenRouter for brief synthesis
- Recraft + OpenRouter for cloud image generation
mfluxon Apple Silicon for optional local image generation- Upstash Redis for rate limiting (optional)
- linkedom + @mozilla/readability for article extraction
MIT — see LICENSE.