How I Show Car Photos Without Storing a Single Image
Source: Dev.to
The Problem
Most car‑data APIs are either expensive, require licensing agreements, or only cover popular markets. I have models like the Renault Clio, SEAT Ibiza, and Fiat Punto – bread‑and‑butter European cars that might not even appear in a US‑centric paid API.
Even if I found a source, hosting thousands of images on R2 or S3 would mean:
- A pipeline to download and store images
- Storage costs that grow with the catalogue
- Maintenance when images go stale
There had to be a better way.
The Solution: Wikimedia Commons
Wikimedia Commons is a free media repository with millions of images — including an enormous collection of car photos, all under Creative Commons licenses. It also offers a public JSON API that requires no authentication, no API key, and has no per‑request cost.
The trick: search Commons for "BMW 3 Series" and grab the first image result. Done.
const url =
"https://commons.wikimedia.org/w/api.php" +
"?action=query" +
"&generator=search" +
"&gsrsearch=" + encodeURIComponent(`${brand} ${model}`) +
"&gsrnamespace=6" + // namespace 6 = File/Image namespace only
"&prop=imageinfo" +
"&iiprop=url" +
"&format=json" +
"&origin=*"; // enables CORS for browser requests
const response = await fetch(url);
const data = await response.json();
const pages = data.query?.pages;
const firstPage = Object.values(pages)[0];
const imageUrl = firstPage?.imageinfo?.[0]?.url;
That’s it. I get a direct URL to a full‑resolution image hosted on Wikimedia’s CDN. My server never touches it.
The Svelte Component
I wrapped this in a VehicleImage component that handles loading, errors, and the fallback gracefully:
<script lang="ts">
import { onMount } from 'svelte';
export let brand: string;
export let model: string;
export let year: number | null = null;
let imageUrl: string | null = null;
let loading = true;
let error = false;
$: searchQuery = year ? `${brand} ${model} ${year}` : `${brand} ${model}`;
async function loadImage() {
loading = true;
error = false;
try {
const url =
'https://commons.wikimedia.org/w/api.php' +
'?action=query&generator=search' +
'&gsrsearch=' + encodeURIComponent(searchQuery) +
'&gsrnamespace=6&prop=imageinfo&iiprop=url&format=json&origin=*';
const res = await fetch(url);
const data = await res.json();
const pages = data.query?.pages;
const first = Object.values(pages ?? {})[0] as any;
imageUrl = first?.imageinfo?.[0]?.url ?? null;
if (!imageUrl) error = true;
} catch {
error = true;
} finally {
loading = false;
}
}
onMount(loadImage);
</script>
{#if loading}
<p>Loading image…</p>
{:else if error || !imageUrl}
<p>Image not available.</p>
{:else}
<img src={imageUrl} alt="{brand} {model}" />
{/if}
Key insight: the fetch happens in the browser, not on the server. This means:
- Zero server load
- Zero storage costs
- Images are served from Wikimedia’s global CDN directly to the user
- If Wikimedia is down, we show a graceful fallback — no error thrown
Why Client‑Side?
I’m running on Cloudflare Workers, which has a 10 ms CPU limit on the free tier for some operations. A server‑side fetch to Wikimedia for every page load would be wasteful and could hit timeouts. By doing it client‑side with onMount, the page loads instantly with SSR content, and the image “pops in” after — a much better perceived performance story.
The Caveat: First Result Isn’t Always Perfect
The first Wikimedia search result isn’t guaranteed to be a perfect photo of the exact trim. Sometimes you get:
- A photo from a slightly different year
- A factory or motor‑show image
- Occasionally an interior shot
That’s why I added a disclaimer across all six languages:
“Images are illustrative and may not exactly represent the described model.”
For a community review platform, this is totally acceptable. Users aren’t buying from us — they’re reading reviews. An approximate image is far better than a blank box.
The Legal Side: Attribution Matters
Here’s something easy to miss: Wikimedia Commons images are not public domain by default. Most are under Creative Commons licenses, which have real requirements:
| License | What it requires |
|---|---|
| CC BY | Credit the author |
| CC BY‑SA | Credit the author + share derivatives under the same license |
| Public Domain | No restrictions — use freely |
If you just grab the URL and display the image with no attribution, you may be violating the license — even though Wikimedia doesn’t technically block hotlinking.
Fetching Attribution Metadata
The good news: the same API call that returns the image URL can also return everything you need to comply. Add extmetadata to the iiprop parameter:
const url =
"https://commons.wikimedia.org/w/api.php" +
"?action=query" +
"&generator=search" +
"&gsrsearch=" + encodeURIComponent(searchQuery) +
"&gsrnamespace=6" +
"&prop=imageinfo" +
"&iiprop=url|extmetadata" + // request URL and metadata
"&format=json" +
"&origin=*";
The response’s extmetadata field contains the license name, author, attribution URL, and more. Extract those values and render a proper attribution line beneath the image, satisfying the Creative Commons requirements.
Image Banner

<div class="attribution">
Via Wikimedia Commons
{#if author} · {author}{/if}
{#if licenseShortName}
· {licenseShortName}
{/if}
</div>
This renders something like:
Via Wikimedia Commons · Vauxford · CC BY‑SA 4.0
Which is exactly what the license requires — and it adds credibility to the page rather than detracting from it.
What About Hotlinking?
Wikimedia explicitly allows external image embedding via their CDN (upload.wikimedia.org). They don’t block it. But their Terms of Use and the individual file licenses still apply. Serving images without attribution is technically a license violation even if it works technically.
The correct mental model: hotlinking is permitted, attribution is required.
The Result
-
0 images stored in my database or on any storage bucket
-
0 API keys to manage or rotate
-
0 monthly costs for image serving
-
Works for every European car brand that has any Wikipedia/Commons presence
-
Graceful fallback for truly obscure models
-
Full legal compliance with a single extra API field and four lines of HTML
The entire solution is ~50 lines of Svelte. Sometimes the best engineering is finding the data that already exists, pointing at it — and reading the license.