very case study on this site has a slot for real product screenshots — and for work that's still in progress, some of those slots are genuinely empty, because the screen doesn't exist to photograph yet. The easy fix is a stock photo: someone smiling at a laptop, a vague dashboard mockup that looks plausible from six feet away. It fills the space. It also isn't the product, and pretending otherwise for the sake of a tidier-looking page is exactly the kind of small dishonesty that compounds.
So for a while we did the obvious honest thing instead: a dashed frame, the shot's intended caption in mono type, and a small note reading "add src to lib/case-studies.ts" — a reminder to ourselves about where the file goes. It was truthful. It was also, we eventually admitted, a developer's TODO list rendered at full width on a page meant for clients, three times per case study. Truthful and worth showing are not the same test.
Astockphototellsasmallliethesizeoftheboxitfills.Alabeledplaceholdertellsthetruth,atthesamesize—andthesizewastheproblem.
All three, side by side
the lie
generic stock photo of “people looking at a laptop, smiling”
what we used to do
trail map with elevation profile
add src to lib/case-studies.ts
what we render now
nothing at all
What ships today is the third card. CaseStudyPage filters the shot list down to entries that have a src before it renders anything, and the whole "Screens" section is behind a check on what's left — so a study with no photographed screens yet doesn't show an empty gallery, a placeholder, or a heading promising screens that aren't there. It shows the writing, and stops. ShotFrame itself no longer has a fallback branch at all; by the time a shot reaches it, the image is guaranteed to exist.
The data stays, the UI goes
The filtering happens at render, not in the content file. Every shot still carries its alt text and caption in lib/case-studies.ts, written before the screenshot exists — that's the note-to-self the dashed frame used to be, kept where notes to ourselves belong. Adding one src brings that shot back, and the section reappears with it. Nothing needs rewriting to publish; the page is already waiting for the file.
What we actually learned
The first version optimised for not lying, which is the right thing to optimise for and not the only thing. An unfinished page is allowed to be shorter than a finished one — that's what unfinished looks like, and padding it with an honest label is still padding. The discipline that survived isn't the component, which is now a few lines shorter than when it had a fallback. It's the rule underneath: when there's nothing to show, show nothing, and don't reach for something to put in the gap.