Stock imagery
A landing page with a grey placeholder where the hero should be is unfinished. This covers sourcing real photography that is free to use commercially, matches the design, and does not break when someone else loads the page.
The endpoint everyone tries first is dead
https://source.unsplash.com/1600x900/?coffee → HTTP 503
source.unsplash.com — the keyless random-image endpoint — was retired. It is still
all over blog posts and older templates, and it now returns 503. If you find it in a
codebase, that is a broken image, not a working one.
What does work, keyless, is the CDN when you already know a photo id:
https://images.unsplash.com/photo-1447933601403-0c6688de566e?auto=format&fit=crop&w=1600&q=80
That returns a real image. But you cannot search without a key, and inventing photo ids does not work. So:
Getting a key (two minutes, free)
- https://unsplash.com/oauth/applications → New Application → accept the terms
- Copy the Access Key
- Export it — never commit it:
export UNSPLASH_ACCESS_KEY="your_access_key"
Demo tier is 50 requests/hour, which is ample for a landing page. If the user has no key, say so and ask — do not silently fall back to grey boxes and do not invent URLs.
curl -s -H "Authorization: Client-ID $UNSPLASH_ACCESS_KEY" \
"https://api.unsplash.com/search/photos?query=coastal+fog&orientation=landscape&per_page=8" \
| python3 -c "
import json,sys
for p in json.load(sys.stdin)['results']:
print(p['id'], p['width'], p['height'], '|', (p.get('alt_description') or '')[:60])
print(' ', p['urls']['raw'])
print(' ', p['user']['name'], p['links']['download_location'])
"
Two API rules that are easy to miss
1. Hotlink the URLs the API returns. The Unsplash API terms require using their CDN URLs rather than re-hosting the file. That conflicts with the usual advice to serve local optimised assets, and the terms win while you are using the API. If a client needs self-hosted files, license the photo through Unsplash+ or buy elsewhere — do not quietly re-host and hope.
2. Trigger the download endpoint when a photo is actually used. Not when it is previewed — when it ships. This is how photographers get credited with usage, and it is a condition of the API, not a courtesy.
curl -s -H "Authorization: Client-ID $UNSPLASH_ACCESS_KEY" \
"$DOWNLOAD_LOCATION" # the links.download_location value from the search result
Licence boundaries
The Unsplash License allows commercial and non-commercial use with no permission and no required attribution. It does not allow selling unaltered copies, and it does not allow building a competing photo service. Photos may contain identifiable people, trademarks or artwork — Unsplash does not clear model or property releases, so for anything implying endorsement (a person appearing to be a customer, a testimonial photo) get a released image instead.
Attribution is not required but costs one line and is the right thing to do:
<!-- Photo: Jane Doe / Unsplash -->
Choosing well
Searching the literal noun is what makes a page look like a template. "Coffee" returns ten thousand identical latte-art overheads.
Search the mood, not the subject. For a coffee roastery: warm industrial workshop,
hands working, morning light interior. For a wealth advisory: still architecture,
quiet minimal interior, long shadows.
Then filter on three things the design actually constrains:
- Orientation and subject placement. A hero that crops with
object-fit: coverneeds the subject off-centre or with headroom, or the crop decapitates it. Check where the subject sits before choosing — the framing arithmetic is in../landing-page/references/traps.md. - Tonal range. Overlaid text needs the region behind it to be consistently light or consistently dark. A busy mid-tone photo makes every text colour wrong.
- One photographer, or one look. Three images from three photographers in one section reads as stock. Prefer several frames from a single shoot, or a consistent grade.
Sizing
Unsplash CDN parameters do the resizing, so request the size you display:
?auto=format&fit=crop&w=1600&q=80 hero
?auto=format&fit=crop&w=800&q=80 card
?auto=format&fit=crop&w=192&h=192&q=80 avatar (square)
auto=format serves WebP/AVIF to browsers that accept it. For responsive heroes, emit a
srcset at 800/1200/1600/2000 rather than shipping one 2000px file to phones.
Alt text
alt describes what the image shows, for someone who cannot see it. It is not a
keyword slot and not the photo's title.
<!-- no -->
<img alt="coffee roastery landing page hero image">
<!-- yes -->
<img alt="Roaster tipping green beans into a drum roaster in a workshop">
If the image is decorative and the surrounding text already says everything, use alt=""
so a screen reader skips it. An empty alt is correct; a missing alt is not.
Checklist
-
UNSPLASH_ACCESS_KEYset, never committed - Searched the mood, not the literal noun
- Subject placement checked against how the slot crops
- Tonal range works under any overlaid text
- Images in one section share a look
- CDN URLs hotlinked as the API requires — not re-hosted
-
download_locationtriggered for every photo that ships - Sizing params match the display size;
srcseton responsive heroes - Real alt text, or
alt=""when decorative - No model/property-release assumption for anything implying endorsement