An embed, and the frame policy the app never had

/embed is the converter with no chrome, for a page that wants to host it, and it talks to its host with postMessage. Adding it meant writing the rule that was missing: every other route now refuses to be framed at all, which nothing had said before.

A dropzone, the document it produced, a copy button and a download. That is the whole of /embed: no header, no footer, no blog, no account, no history — nothing that would look like our navigation turning up inside somebody else's page.

What the host controls

?conversion= decides which conversion the frame opens on, and ?theme=dark or ?theme=light decides how it looks. The host chooses the theme rather than the visitor's operating system, because a widget that follows the OS lands as a dark rectangle in a light page for half the audience.

Results come back by postMessage to the parent window, and every message carries source: 'TransformPipe' — a host listening on window hears from every frame it has and from its own scripts, so without a name to check, the first handler anybody writes fires on somebody else's message. One message is sent on load, so a host can wait for the frame instead of guessing at a timeout. Check event.origin at your end.

Why frame it rather than proxy it

The conversion runs in the visitor's browser, exactly as it does here. Your reader drops a file and it reaches neither your server nor ours. A host that wants server-side conversion should call the API; what the embed offers is the one thing an API cannot, which is the file never leaving the machine it is on.

It is also anonymous on purpose. The embed is rendered instead of the application rather than as the application with its chrome hidden, and it sits above the part that asks the server who is signed in — so a page on any domain framing it never causes a credentialed request to us. There is no cookie banner in it either: asking for consent inside somebody else's page is asking on their behalf.

Everything else now refuses to be framed

The embed needed a frame-ancestors rule, and writing one exposed that no route had ever had one. Every other address now says no — frame-ancestors 'none' and X-Frame-Options: DENY — so the converter that has an account behind it cannot be put in a frame at all. /embed and its translations (/de/embed and the rest) allow any ancestor and carry noindex, because a chromeless converter is not a page anybody should arrive at from a search.

Keeping a document, sharing it and the history stay on transformpipe.com. The embed has no session to reach them with, and that is the design rather than a limitation.

Related: converting Markdown to HTML in JavaScript, and whether an online converter is safe.