Clean Room landing page: paste an X post link into a single input, press Clean it, get a clean address
The landing page is one input and one button. Paste an X link, press Clean it, done.

The problem with sharing X links

Sharing an X post used to be as simple as sending the URL. Now the person on the other end hits a sign-in wall if they are logged out, an app interstitial if they are on a phone, and a page carrying trackers either way. If you want someone to read X posts without an account, the raw link no longer does that job.

The usual workarounds are worse. Screenshots lose the link, the date, and any way to verify the post is real. Third-party mirror sites scrape content and tend to disappear. What I wanted was narrow: one post, framed and readable, at a stable address, using only an API that X publishes for exactly this purpose.

Every clean link is deterministic. It is clean.corbet.app/x/ plus the post id, nothing else. The same post always produces the same address, so links stay shareable and cacheable forever.

Animated demo: pasting an X post link into Clean Room, redirecting to the viewer, and the Link copied confirmation appearing
The whole flow. Paste a link, land on the viewer, and the clean address is already on your clipboard.

How it works: one Worker and the X oEmbed API

The service is a single-file Worker in vanilla JS with no build step. It exposes three routes per post: the viewer page at /x/:id, a raw cached passthrough at /x/:id/oembed.json, and a generated Open Graph card at /x/:id/card.png.

  • X's public oEmbed API (publish.x.com/oembed) is the only upstream. It is keyless, official, and returns the post text as sanitized blockquote HTML. Clean Room requests it with omit_script=true and dnt=true, so X's script is never injected server-side and Do Not Track is on for every embed.
  • A KV namespace caches the oEmbed JSON for seven days. Failures are cached too, for thirty minutes, so a deleted post does not trigger an upstream request on every visit.
  • Static assets serve the landing page imagery from the edge before the Worker runs. The viewer page itself ships no images at all and weighs under 15KB before the optional widget loads.

The cache logic also coalesces concurrent requests for the same post into one upstream fetch, which matters when a link gets posted somewhere busy:

// oEmbed fetch with a KV cache in front.
// Success caches for 7 days. Failure caches for 30 minutes,
// so a deleted post does not hammer X on every visit.
async function getOembed(env, id, ctx) {
  const key = 'oembed:' + id;
  const cached = await env.OEMBED_CACHE.get(key, { type: 'json' });
  if (cached) return cached;

  // Coalesce concurrent requests for the same id into one fetch
  if (inflight.has(id)) return inflight.get(id);
  const p = fetchOembed(id)
    .then((result) => {
      const ttl = result.ok ? SEVEN_DAYS : HALF_HOUR;
      ctx.waitUntil(
        env.OEMBED_CACHE.put(key, JSON.stringify(result), { expirationTtl: ttl })
      );
      return result;
    })
    .finally(() => inflight.delete(id));
  inflight.set(id, p);
  return p;
}

Fallback first

The design decision I care most about is that the unhydrated state is the primary product, not a loading state. The Worker parses the post text, author, and date out of the oEmbed blockquote HTML and renders its own quote card server-side, as a semantic figure and blockquote with all CSS inlined. That card is what you get if X's widget script is blocked, slow, or gone entirely. There is no spinner and no visual state that reads as failure.

The Clean Room fallback quote card: post text, author, and date rendered as a styled blockquote with no external scripts
The fallback card, rendered entirely by the Worker. This is the page with widgets.js blocked. It reads as finished because it is.

If widgets.js does load, it hydrates the blockquote into the full interactive embed with media, like counts, and replies. That is progressive enhancement in the original sense. The page works, then it gets better.

The same post hydrated by widgets.js into the full X embed with photo, timestamp, and reply count
The same page a few seconds later, hydrated into the full embed. Photos, counts, and replies arrive as an upgrade, not a requirement.

One deliberate constraint: the quote card is Clean Room's own identity, never a pixel replica of the native post. Mimicking X's UI would invite confusion about what this page is. The card looks like Clean Room, credits the author, and links to the original.

Three ways to get a clean link

A viewer is only useful if getting a link into it takes no thought, so there are three paths in.

  • The paste form. The landing page input posts to a GET /clean route that extracts the post id and issues a 302 to the viewer. It works with JavaScript disabled. With JS available, the parse happens client-side and the clean link is copied before you land.
  • The iOS Shortcut. One tap from the share sheet on any post. The Shortcut is a single Replace Text regex action, .*status/([0-9]+).* rewritten to the clean URL, then copy and notify. It makes zero network calls. The transformation is pure text.
  • The bookmarklet. A dashed, draggable chip on the landing page. Click it while viewing any X post and it copies the clean link and opens the viewer with ?copied=1, which announces the confirmation on arrival. Clipboard writes need a user gesture, so the copy happens inside the click before navigation.
Landing page sections for the iOS Shortcut and the draggable bookmarklet chip, plus the clean link format
The two installable paths: an iOS Shortcut for the share sheet and a bookmarklet chip for the desktop bookmarks bar.

Every copy confirmation on the site announces through an aria-live="polite" region, so screen reader users hear the same confirmation sighted users see.

One generated OG card, one dependency

A clean link is usually shared in a chat, and chats unfurl links. Text-only unfurls looked thin, so each post gets a generated 1200 by 630 PNG quote card at /x/:id/card.png, rendered on the Worker with workers-og, which wraps Satori and resvg compiled to WASM.

The generated Open Graph card PNG: dark quote card with the post text, author, and Clean Room mark at 1200 by 630
The generated card that unfurls in iMessage and chat apps. Rendered as a PNG on the Worker, cached at the edge.

This is the one exception to the zero-dependency rule, and it earned its place. Everything else on the service is hand-written vanilla JS in one file. The card renders in the dark scheme with the same teal accent as the site, so an unfurled Clean Room link is recognizable before anyone taps it.

What this does not do

No analytics, no cookies, no logs beyond Cloudflare's defaults, and Do Not Track on every embed request. This is stated on the landing page because it is the point of the service, not a settings checkbox. Rate limiting lives in Cloudflare's WAF rather than in Worker state, scoped to the viewer routes so the landing page is never throttled.

Failure is handled as carefully as success. Protected and deleted posts return the same result from X, so the error page does not guess which one happened. Each upstream status code maps to its own contextual message, and the failed lookup is cached for thirty minutes to stay polite to X.

Clean Room's friendly failure page: This post is not available, with an explanation and an Open on X button
The failure page for a deleted or protected post. Honest about what it does not know, with an exit to X if you want to check.

The footer rotates a small set of one-liners in the same spirit. "Served without cookies, because you did not order any." "Your visit here was not recorded. There was nothing to record it with." The privacy stance is the product's voice, so the product gets to be a little wry about it.

Clean Room is at clean.corbet.app. Paste any X post link, or grab the iOS Shortcut or bookmarklet from the landing page. It is free, it has no account, and it never will.