BlockDex indexes shadcn-style component registries, and each item page mounts an iframe of the registry's own demo page with a caption underneath: "this is the component running, not a screenshot of it."
On 2026-08-06 I fetched all 22,313 preview URLs the index had stored. 7,382 of them were not component pages.
Two mistakes stacked
The crawler learned a URL shape per registry — try {base}/docs/{name}, {origin}/components/{name}, and last on the list {origin}/r/{name} — against one sample item name. Whichever shape answered got filled in for every item in that registry and written to preview_url unchecked.
The acceptance test was one line:
const res = await get(url, { accept: 'text/html,*/*' });
if (res.status === 200 && (res.text ?? '').length > 500) { /* learned */ }
{origin}/r/{name} is, on a large class of registries, the item JSON route — the same URL the install command reads. A 4KB JSON body clears length > 500 comfortably. The */* in the accept header forces nothing. And then the second mistake compounded it: an API route sends no X-Frame-Options and no CSP, so our framable() helper —
if (xfo.includes('deny') || xfo.includes('sameorigin')) return false;
const m = csp.match(/frame-ancestors([^;]*)/);
if (!m) return true;
— returned true. The JSON endpoint was marked the most embeddable kind of URL there is. Chrome duly framed it and rendered its own JSON viewer inside the panel, under a caption asserting the component was running.
None of this is detectable at render time. The client watches the iframe's onLoad, and a 404 page, a 500 page and a JSON document all fire onLoad perfectly normally. The only failure a browser surfaces is a frame that never loads at all — which is the one that doesn't happen.
What the sweep actually found
| Failure class | Count of 22,313 | Why the old test missed it | What rejects it now |
|---|---|---|---|
| Hard 404 | 4,704 | Never tested per item — the one sample name had a page, the other 7,794 items didn't | res.status !== 200 |
| JSON body | 1,588 | 4KB body passed length > 500; */* accepted any content type |
content-type must contain text/html
|
| Soft 404 | 691 | 200 + HTML + long body — indistinguishable by status. Next.js and Astro answer a missing route with 200 far more often than with 404 |
<title> announces a missing page |
| Server error | 399 | Same as hard 404: nothing per item ever ran | res.status !== 200 |
The guard
export async function verifyPreview(url, { timeout = 15000 } = {}) {
const no = (reason) => ({ ok: false, embeddable: false, reason });
// `text/html` alone, no `*/*` escape hatch. A registry that answers JSON
// to this is answering JSON to everybody.
const res = await get(url, { accept: 'text/html', timeout });
if (res.error) return no(`transport: ${res.error.slice(0, 60)}`);
if (res.status !== 200) return no(`http ${res.status}`);
const type = (res.headers?.get('content-type') ?? '').toLowerCase();
if (!type.includes('text/html')) return no(`serves ${type.split(';')[0]}`);
const body = res.text ?? '';
if (body.length < 500) return no(`body is ${body.length} bytes`);
const title = body.match(/<title[^>]*>([\s\S]{0,200}?)<\/title>/i)?.[1] ?? '';
if (isNotFoundTitle(title)) return no(`not-found page (title: ${title.trim()})`);
return { ok: true, embeddable: framable(res.headers) };
}
The soft-404 check reads the title and nothing else, which was not the first version. Scanning the first 4KB of body condemned two pages that render fine — Apple gallery | 100xUI and Pixel Scroll Transition – unlumen UI — because each carried the string 404 in a nav or an inlined script. Deleting a working preview is worse than letting a broken one through, because the second is visible and the first isn't. So the regex has word boundaries and lives behind unit tests built from real titles: it must catch Component not found - HextaUI and 404: This page could not be found, and must leave Error404 Illustration | someui alone.
Two deliberate asymmetries
docs_url still takes the unverified pattern; preview_url does not. A "Docs on X" chip is a link somebody chooses to follow. The frame is a claim the page makes on their behalf, at 460px, above the fold.
And verification is capped at PREVIEW_CAP=2000 items per registry. Past the cap an item keeps its page, its install command and its access verdict — it just doesn't get a frame nothing checked. After the sweep the index publishes 12,561 verified live previews, 2,858 verified links, and 43,795 items with no preview at all, and the item page's comment block now carries those counted numbers instead of the estimates it used to.
This is how we built BlockDex, a search index for shadcn component registries: https://blockdex.kynth.studio

Top comments (0)