Skip to content

Bambushu/lede

Repository files navigation

Lede

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.

Quick start

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 dev

Open http://localhost:3000 and paste any article URL.

Inputs

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.

Required environment variables

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.

Optional environment variables

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).

Pre-bake a canonical showcase (local dev)

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 article

Pre-baked results are committed under data/preloaded/*.json (metadata) and public/preloaded/<slug>/*.png (images), and ship with the build.

Deploy to Vercel

Lede is built for Vercel Pro (cloud bakes use maxDuration: 300, which Hobby caps at 60s).

  1. Push the repo to your GitHub.
  2. Import into Vercel from the GitHub repo.
  3. Set environment variables (see tables above).
  4. 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).
  5. Add your custom domain in Project → Settings → Domains and point the CNAME at Vercel.

What runs where

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

Cost surface

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 3 bakes per 24h (configurable in src/lib/rate-limit.ts)
  • Trigger up to 3 redos 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:

For private demos, leave LEDE_DEMO_PASSWORD set (covered below) so only holders of the password can reach either endpoint.

Security

  • SSRF: all server-side fetches of user-supplied URLs go through src/lib/safe-fetch.ts. Rejects RFC1918, loopback, link-local (incl. cloud-metadata 169.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_PASSWORD is 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).

Stack

  • 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
  • mflux on Apple Silicon for optional local image generation
  • Upstash Redis for rate limiting (optional)
  • linkedom + @mozilla/readability for article extraction

License

MIT — see LICENSE.