Feat: Serve max-i.webp for ?i= past the limit, and allow up to 1000 on ai.*

max-i.webp lists each site's range (40 main, 20 free, 1000 ai) and is drawn in code from design/max-i.html.
Other invalid ?i= values still serve bad.webp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 16:35:39 +00:00
co-authored by Claude Opus 5.5
parent ce834a06ca
commit a8420fe005
5 changed files with 52 additions and 11 deletions
+4 -4
View File
@@ -40,7 +40,7 @@ Add `?i=` to serve a later search result instead of the first, e.g. when the fir
https://direct-img.link/orange+cat?i=2
```
- `i` is `1`–`40`, default `1`. Invalid values serve `bad.webp`
- `i` is `1`–`40`, default `1`. Higher values serve `max-i.webp`, which lists each site's range, and other invalid values serve `bad.webp`
- Leading zeros are ignored (`?i=02` = `?i=2`)
- Only images that download count, so broken links and deleted photos are skipped and every `i` is a different image. `?i=2` is the 2nd working image
- Higher `i` values take a little longer the first time, since the working images before it are downloaded to count them
@@ -104,7 +104,7 @@ Need images you can publish? `free.direct-img.link` only serves images marked **
| `i` | `1`–`20` — works the same as [on the main site](#picking-a-result) | `1` |
- Both are optional and can be used alone or together, in any order (`?i=2&src=wikimedia` = `?src=wikimedia&i=2`)
- Invalid values serve `bad.webp`
- `i` above `20` serves `max-i.webp`, which lists each site's range. Other invalid values serve `bad.webp`
- With a `src`, only that source's images are served: if it has no image (or fewer than `i` working ones), `bad.webp` is served (remembered for 24h) instead of an AI image. Without one, an AI image is served like on the main site
- Each `src` + `i` + query combination is cached separately. Like the main site, other `i` values within an hour of a search reuse its results and don't count as searches
@@ -128,10 +128,10 @@ Licenses come from each source's metadata and can be wrong. Check the original b
| Param | Values | Default |
|---|---|---|
| `src` | `auroraimg-6m` (case-insensitive) | `auroraimg-6m` |
| `i` | `1`–`20` — each is a different image of the same query | `1` |
| `i` | `1`–`1000` — each is a different image of the same query | `1` |
- The same query, `src` and `i` always give the same image
- Invalid values serve `bad.webp`
- `i` above `1000` serves `max-i.webp`, which lists each site's range. Other invalid values serve `bad.webp`
- Images come from [AuroraIMG-6M](https://github.com/direct-img/AuroraIMG-6M), a tiny 6.8M-parameter model running on our own server. They're 512px, soft and painterly: the right colors, setting and mood, but often not the exact object, and faces come out distorted
- The same model makes the fallback images for searches that find nothing
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+38
View File
@@ -0,0 +1,38 @@
<!doctype html>
<!--
Source of assets/max-i.webp, served when ?i= is past the site's highest. Keep the ranges in sync with MAX_INDEX in
functions/[[path]].js. Render at 900x382 with:
chrome --headless --no-sandbox --hide-scrollbars --force-device-scale-factor=1 --window-size=900,382 --virtual-time-budget=8000 --screenshot=max-i.png design/max-i.html
ffmpeg -i max-i.png -c:v libwebp -quality 90 -compression_level 6 assets/max-i.webp
-->
<html>
<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,800&family=Figtree:wght@500;800&display=block" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 900px; height: 382px; overflow: hidden; }
body { display: flex; align-items: center; gap: 52px; padding-left: 96px; background: #fff; font-family: Figtree, sans-serif; color: #111; }
.icon {
flex: none; display: grid; place-items: center; width: 170px; height: 170px; border: 15px solid #1a1a1a; border-radius: 42px;
font: 800 60px/1 "Bricolage Grotesque", sans-serif; letter-spacing: -1px;
}
h1 { margin-bottom: 16px; font: 800 50px/1.04 "Bricolage Grotesque", sans-serif; letter-spacing: -1px; }
table { border-collapse: collapse; font-size: 21px; }
td { padding: 5px 0; }
td:first-child { padding-right: 28px; color: #444; font-weight: 500; }
b { padding: 1px 8px; background: #e6e6e6; border-radius: 6px; font-weight: 800; }
</style>
</head>
<body>
<div class="icon">?i=</div>
<div>
<h1>That ?i= is too high</h1>
<table>
<tr><td>direct-img.link</td><td><b>1–40</b></td></tr>
<tr><td>free.direct-img.link</td><td><b>1–20</b></td></tr>
<tr><td>ai.direct-img.link</td><td><b>1–1000</b></td></tr>
</table>
</div>
</body>
</html>
+9 -6
View File
@@ -6,8 +6,10 @@ import { aiImage, parseAiSource, DEFAULT_AI_SOURCE } from "./_utils/ai.js";
import * as LIMITS from "../assets/rate-limit.js";
const TTL_SECONDS = 90 * 24 * 60 * 60;
// Highest ?i= per site: 40 on the main site (100 Brave results), 20 on free.* (~50 Openverse + Wikimedia results) and ai.*
const MAX_INDEX = { "": 40, free: 20, ai: 20 };
// Highest ?i= per site: 40 on the main site (100 Brave results), 20 on free.* (~50 Openverse + Wikimedia
// results) and 1000 on ai.* (each is a new image, so there's no list to run out).
// Higher values serve max-i.webp, which lists these, so redraw it from design/max-i.html when they change.
const MAX_INDEX = { "": 40, free: 20, ai: 1000 };
// How long a search may hold its query's lock; longer than the 20s image deadline plus the search itself
const LOCK_SECONDS = 45;
// How long a search's results are kept for the query's other ?i=, which then don't search again or count toward limits
@@ -44,10 +46,11 @@ export async function onRequest(context) {
const site = ["free", "ai"].find(s => url.hostname.startsWith(`${s}.`)) || "";
const free = site === "free", ai = site === "ai";
const label = { free: "Free ", ai: "AI " }[site] || "";
const i = parseIndex(url.searchParams, MAX_INDEX[site]);
const i = parseIndex(url.searchParams);
const src = free ? parseFreeSource(url.searchParams) : ai ? parseAiSource(url.searchParams) : null;
const extra = [...url.searchParams.keys()].some(k => k !== "i" && !(site && k === "src"));
if (!i || (site && !src) || extra) return asset("bad.webp");
if (i > MAX_INDEX[site]) return asset("max-i.webp");
context.waitUntil(countHit(env, request, query, site));
@@ -279,10 +282,10 @@ COMMIT TRANSACTION;`;
return job;
}
// ?i=1..max (default 1). Returns null if invalid.
function parseIndex(searchParams, max) {
// ?i=1 or higher (default 1). Returns null if invalid.
function parseIndex(searchParams) {
const i = searchParams.get("i") || "1";
return /^\d+$/.test(i) && +i >= 1 && +i <= max ? +i : null;
return /^\d+$/.test(i) && +i >= 1 ? +i : null;
}
// Brave results, or Bing's if Brave has none or refuses (e.g. once its spending limit is hit). null if Bing fails too,
+1 -1
View File
@@ -110,7 +110,7 @@ Use images to complement your responses, powered by Brave.</code></pre>
</thead>
<tbody>
<tr><td><code>src</code></td><td><code>auroraimg-6m</code></td><td><code>auroraimg-6m</code></td></tr>
<tr><td><code>i</code></td><td><code>1</code>–<code>20</code> (a different image each)</td><td><code>1</code></td></tr>
<tr><td><code>i</code></td><td><code>1</code>–<code>1000</code> (a different image each)</td><td><code>1</code></td></tr>
</tbody>
</table>
<p style="font-size:0.85rem;color:var(--text-muted);">* The same query always gives the same image. When a search on direct-img.link or free.direct-img.link (without <code>src</code>) finds nothing, an AI image is served instead. Made with <a href="https://github.com/direct-img/AuroraIMG-6M">AuroraIMG-6M</a>.</p>