mirror of
https://github.com/direct-img/direct-img.link.git
synced 2026-10-13 08:25:21 +00:00
A search's result URLs are kept in SurrealDB (results table) for 1 hour, fetched in the same transaction as the rate limit. Other ?i= of the query then skip the Brave/Bing search and don't count toward the daily limit. The record also tracks which results worked (pos) and how far it got (next), so an already-known ?i= downloads only its own image and a new one carries on from where the last left off. free.* searches Openverse and Wikimedia together so its whole list can be cached. Empty results aren't cached, since they can mean Brave and Bing failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
13 KiB
HTML
193 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>direct-img.link | Live images in markdown</title>
|
||
<meta name="description" content="Embed live images in markdown powered by search. No APIs, no tokens, just search.">
|
||
<link rel="icon" href="/assets/favicon.ico" sizes="16x16 32x32 48x48"><link rel="icon" href="/assets/logo.svg" type="image/svg+xml">
|
||
<script data-goatcounter="https://direct-img.goatcounter.com/count"
|
||
async src="//gc.zgo.at/count.js"></script>
|
||
<style>
|
||
:root{--bg:#fff;--text:#1a1a1a;--text-muted:#666;--accent:#0070f3;--warning:#eb5757;--code-bg:#f5f5f5;--border:#eaeaea;--radius:8px;}
|
||
*{box-sizing:border-box;}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.6;color:var(--text);background-color:var(--bg);margin:0;display:flex;flex-direction:column;align-items:center;}
|
||
main{max-width:680px;width:100%;padding:80px 24px;}
|
||
h1{font-size:2.5rem;font-weight:800;letter-spacing:-0.02em;margin-bottom:8px;}
|
||
p.tagline{font-size:1.25rem;color:var(--text-muted);margin-bottom:32px;}
|
||
.status-notice{padding:20px;background:#fff5f5;border:1px solid #ffe0e0;border-radius:var(--radius);margin-bottom:48px;}
|
||
.status-notice p{margin:0;font-size:1rem;}
|
||
.status-notice strong{color:var(--warning);}
|
||
.donate{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-top:12px;}
|
||
.btc-address{font-family:monospace;background:rgba(0,0,0,0.05);padding:4px 8px;border-radius:4px;font-size:0.85rem;word-break:break-all;display:inline-block;}
|
||
section{margin-bottom:48px;}
|
||
h2{font-size:1.5rem;font-weight:700;margin-bottom:16px;border-bottom:1px solid var(--border);padding-bottom:8px;}
|
||
pre{background:var(--code-bg);padding:16px;border-radius:var(--radius);overflow-x:auto;font-size:0.9rem;border:1px solid var(--border);}
|
||
.copy-wrap{position:relative;}
|
||
.copy-wrap pre{white-space:pre-wrap;padding-right:72px;}
|
||
.copy{position:absolute;top:8px;right:8px;font:inherit;font-size:0.75rem;font-weight:600;padding:4px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text-muted);cursor:pointer;}
|
||
.copy:hover{color:var(--accent);border-color:var(--accent);}
|
||
code{font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;}
|
||
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:0.9rem;}
|
||
th,td{text-align:left;padding:12px;border-bottom:1px solid var(--border);}
|
||
th{color:var(--text-muted);font-weight:600;}
|
||
.card{border:1px solid var(--border);padding:16px;border-radius:var(--radius);}
|
||
.card h3{margin:0 0 8px 0;font-size:1rem;}
|
||
.card p{margin:0;font-size:0.9rem;color:var(--text-muted);}
|
||
.sub{display:flex;flex-wrap:wrap;gap:8px;}
|
||
.sub input{flex:1;min-width:200px;font:inherit;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius);color:var(--text);}
|
||
.sub input:focus{outline:none;border-color:var(--accent);}
|
||
.sub button{font:inherit;font-weight:600;padding:10px 20px;border:0;border-radius:var(--radius);background:var(--accent);color:#fff;cursor:pointer;}
|
||
.sub button:disabled{opacity:.6;cursor:default;}
|
||
.cf-turnstile{margin-top:8px;}
|
||
#sub-msg{margin:8px 0 0;font-size:0.9rem;}
|
||
footer{width:100%;max-width:680px;padding:40px 24px;border-top:1px solid var(--border);color:var(--text-muted);font-size:0.9rem;display:flex;justify-content:space-between;margin-top:auto;}
|
||
a{color:var(--accent);text-decoration:none;} a:hover{text-decoration:underline;}
|
||
@media(max-width:600px){h1{font-size:2rem;}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>direct-img.link</h1>
|
||
<p class="tagline">Live images in markdown, powered by search.</p>
|
||
|
||
<section class="status-notice">
|
||
<p>This service is <strong>free</strong> to use. However, search API and server costs are high. Without community donations, <strong>this project will die quickly.</strong></p>
|
||
<p style="margin-top:10px;">If we receive significant donations, we will <strong>increase the daily search limits</strong> for everyone.</p>
|
||
<div class="donate">
|
||
<iframe src="https://github.com/sponsors/multipleof4/button" title="Sponsor multipleof4" height="32" width="114" style="border:0;border-radius:6px;"></iframe>
|
||
<div class="btc-address">BTC: bc1q3d975cd57205dx6mz05s2g27xujxsc3q0nlv59</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>For AI System Prompts</h2>
|
||
<p>Add this to your system instructions:</p>
|
||
<div class="copy-wrap">
|
||
<button class="copy" onclick="copyPrompt(this)">Copy</button>
|
||
<pre><code id="prompt">When writing markdown, embed relevant images/gifs using direct-img.link — a free image search proxy that returns images directly from a URL. But keep it entertaining, for example use it to show reaction faces, girl, or fun, not technical.
|
||
Example structure:
|
||

|
||
Use images to complement your responses, powered by Brave.</code></pre>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Usage</h2>
|
||
<p>Embed directly in your markdown files:</p>
|
||
<pre><code></code></pre>
|
||
<p>Not the image you wanted? Add <code>?i=2</code> (up to <code>20</code>) to serve the next one:</p>
|
||
<pre><code></code></pre>
|
||
<p style="font-size:0.85rem;margin-top:12px;color:var(--text-muted);">* Note: Literal dots (.) and slashes (/) are rejected to prevent bot abuse. Use <code>%2E</code> or <code>%2F</code> if your query requires them.</p>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Free Images</h2>
|
||
<p>Need images you can publish? <code>free.direct-img.link</code> only serves images marked <strong>free of restrictions</strong> (public domain or CC0), so no credit is needed.</p>
|
||
<pre><code>
|
||

|
||
</code></pre>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Param</th><th>Values</th><th>Default</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td><code>src</code></td><td><code>openverse</code>, <code>wikimedia</code></td><td>Openverse, then Wikimedia</td></tr>
|
||
<tr><td><code>i</code></td><td><code>1</code>–<code>20</code> (which result)</td><td><code>1</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p style="font-size:0.85rem;color:var(--text-muted);">* Both params are optional and work in any order. Licenses come from each source and can be wrong, so check the original before publishing. Images from <a href="https://openverse.org">Openverse</a> (made using the Openverse API, not endorsed or certified by Openverse) and <a href="https://commons.wikimedia.org">Wikimedia Commons</a>.</p>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>AI Images</h2>
|
||
<p><code>ai.direct-img.link</code> generates an image of anything instead of searching, so it never comes up empty. Expect soft, painterly, abstract results: the right colors and mood, not always the exact thing.</p>
|
||
<pre><code>
|
||

|
||
</code></pre>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Param</th><th>Values</th><th>Default</th></tr>
|
||
</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>
|
||
</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 finds nothing, an AI image is served instead. Made with <a href="https://github.com/direct-img/AuroraIMG-6M">AuroraIMG-6M</a>.</p>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Rate Limits</h2>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Scope</th><th>Limit</th><th>Action</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td>Burst</td><td>~10 req/10s per IP</td><td>HTTP 429 (~10s)</td></tr>
|
||
<tr><td>New Searches</td><td><span data-limit="DAILY_LIMIT">35</span> per day per IP</td><td>Limit image served</td></tr>
|
||
<tr><td>New Free Searches</td><td><span data-limit="FREE_DAILY_LIMIT">100</span> per day per IP</td><td>Limit image served</td></tr>
|
||
<tr><td>New AI Images</td><td><span data-limit="AI_DAILY_LIMIT">100</span> per day per IP</td><td>Limit image served</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p style="font-size:0.85rem;color:var(--text-muted);">* free.direct-img.link and ai.direct-img.link each have their own daily limit, separate from the main site's. Cached images don't count toward it and keep loading after you hit it. Neither do other <code>?i=</code> values of a query searched in the last hour. The burst limit applies to all image requests and is enforced approximately. The daily limit resets at 00:00 UTC.</p>
|
||
</section>
|
||
|
||
<section>
|
||
<div class="card">
|
||
<h3>Caching</h3>
|
||
<p>Images are cached for 90 days. Subsequent requests for the same query are served instantly. A search's results are kept for an hour, so trying other <code>?i=</code> values right after is free and fast. Searches that find nothing serve an AI image instead, cached the same way.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Get Updates</h2>
|
||
<p>Get an email when direct-img.link gets new features, sources, or higher limits. No spam.</p>
|
||
<form onsubmit="event.preventDefault();sub(this)">
|
||
<div class="sub">
|
||
<input type="email" name="email" required placeholder="you@example.com" autocomplete="email" aria-label="Email address">
|
||
<button type="submit">Sign up</button>
|
||
</div>
|
||
<div class="cf-turnstile" data-sitekey="0x4AAAAAAFG4HWETOcDDQ5vg" data-appearance="interaction-only" data-size="flexible"></div>
|
||
<p id="sub-msg" role="status"></p>
|
||
</form>
|
||
<p style="font-size:0.85rem;color:var(--text-muted);">* Unsubscribe anytime with the link in any email. We never share your email.</p>
|
||
</section>
|
||
</main>
|
||
|
||
<footer>
|
||
<div>Built for the AI era.</div>
|
||
<div><a href="https://github.com/direct-img/direct-img.link/blob/main/TERMS.md">Terms</a> · <a href="https://github.com/direct-img/direct-img.link/blob/main/PRIVACY.md">Privacy</a> · <a href="https://github.com/direct-img/direct-img.link">GitHub</a></div>
|
||
</footer>
|
||
<script>
|
||
const copyPrompt=b=>{
|
||
const p=document.getElementById('prompt');
|
||
(navigator.clipboard?.writeText(p.textContent)??Promise.reject()).then(()=>{
|
||
b.textContent='Copied!';
|
||
setTimeout(()=>b.textContent='Copy',1500);
|
||
},()=>getSelection().selectAllChildren(p));
|
||
};
|
||
const sub=async f=>{
|
||
const b=f.querySelector('button'),m=document.getElementById('sub-msg'),tok=()=>f.querySelector('[name=cf-turnstile-response]')?.value;
|
||
b.disabled=true;b.textContent='Signing up…';m.textContent='';
|
||
for(let i=0;i<50&&!tok();i++)await new Promise(r=>setTimeout(r,100));
|
||
let ok=false,e='Network error. Please try again.';
|
||
try{
|
||
const r=await fetch('https://newsletter.planetrenox.com/api/sub',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({pool:'direct-img',email:f.email.value.trim(),token:tok()||''})}),d=await r.json();
|
||
ok=r.ok&&d.ok;
|
||
e={'invalid email':"That email doesn't look right.",'captcha failed':"Couldn't verify you're human. Please try again."}[d.error]??'Something went wrong. Please try again.';
|
||
}catch{}
|
||
m.textContent=ok?"You're signed up. Thanks!":e;
|
||
m.style.color=ok?'#1a7f37':'var(--warning)';
|
||
if(ok)f.reset();
|
||
window.turnstile?.reset();
|
||
b.disabled=false;b.textContent='Sign up';
|
||
};
|
||
</script>
|
||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||
<script type="module">
|
||
import * as L from '/assets/rate-limit.js';
|
||
for(const e of document.querySelectorAll('[data-limit]'))e.textContent=L[e.dataset.limit]??e.textContent;
|
||
</script>
|
||
</body>
|
||
</html>
|