Linking to and embedding the playground

Most of the state of a playground lives in the fragment of its URL, so a link can carry code. This guide shows how to build links that open an example in RTCode.io, how to open the editor of a deployment, and how to embed the playground in your own page.

Open in RTCode.io links

The recipe

A link to the playground is https://rtcode.io/x/# followed by a URLSearchParams string. The source goes under the key /, and the title under title. Build the string with URLSearchParams:

html
<a id=play target=_blank>Open in RTCode.io</a>
<script type=module>
  const html = `<style>p { color: oklch(62% .2 30) }</style>
<p>Hello, playground</p>
<script>document.querySelector('p').onclick = e => e.target.textContent += '!'\x3C/script>`
  play.href = `https://rtcode.io/x/#${new URLSearchParams({ '/': html, title: 'Hello' })}`
</script>

The playground decodes the fragment with URLSearchParams, so a + means a space, and a literal +, &, % or # in the source must be percent-encoded. URLSearchParams does that for you, as does encodeURIComponent on each value. The form #/= followed by encodeURIComponent(html) works too.

Inside an inline <script>, write \x3C/script> for any closing script tag that lives in the source string, as above. The example also avoids the id open, which would collide with window.open.

More keys set how the playground looks when it opens. This link shows the output above the input, forces a dark output, and selects 2rem, so that the delta controls appear:

js
const href = `https://rtcode.io/x/#${new URLSearchParams({
  '/': '<style>.card { padding: 2rem; border-radius: 1rem; background: light-dark(oklch(95% .02 250), oklch(30% .05 250)) }</style>\n<div class=card>Select 2rem and press ↑</div>',
  title: 'Nudge me',
  h: 1, se: 'oi', ocs: 'dark',
  '|': JSON.stringify([[1, 25, 1, 29, 0]])
})}`

h=1 with se=oi is the layout o/i. The key | holds the selections as [startLine, startColumn, endLine, endColumn, direction], counted from 1, with the end column exclusive: columns 25 to 28 of line 1 are 2rem. A deployed copy of this page would need its own color-scheme declaration, because only the playground applies one to its output. See Output settings.

These keys are safe in a link that you build. After the playground loads, it drops every key that equals its default, so an address that you copy may be shorter than the one you wrote.

KeySets

Output setting keys

The output keys apply when the playground loads. If you edit ocs in the address bar later, it takes effect at the next full load, F5. The preference keys, orm, opc, ort and ord, need a browser that supports native preference overrides; see Browser support.

KeyValuesDefault
ozThe zoom, at least 0.1, with two decimals1
ocslight or darkthe system
ovpWxH in CSS pixels, such as 390x844the panel
ovfit1 for Auto-fitoff
orm, ort, ordreduce or no-preferencethe system
opcmore, less or no-preferencethe system
odirltr or rtlas authored
olanga language tagas authored
ofsthe root font size in pixels, 0 or moreas authored
oasthe animation rate, 0 or more1

Warning: An invalid value is not ignored. A link with odir=foo, ofs=-1 or ovp=390.5x844 stops the playground from reading its state, and a notice that cannot be closed covers the page: Could not read or save the playground hash. Use only valid values. ovp takes exactly two positive whole numbers.

Note: Build links from these keys. An open playground carries more keys in its address bar than a link needs, among them the routes and editors of its deployment, and copying them would pre-fill them for the next person who deploys.

Long sources: the compressed form

A plain link of any length opens, but very long URLs reach the limits of browsers, and a comment in the edge code notes about 1 MB for Firefox. Limits differ by browser. For a large example, compress the state. When the raw state is 8192 characters or more, the playground gzips it, encodes it as base64url without padding, and writes #v=2&c=<data>. It keeps the compressed form only when that is shorter than the raw one, and it does the same for its own address bar, so a long plain link is rewritten as v=2 right after it opens.

To build the compressed form, compress the raw URLSearchParams string, and copy the layout keys se and h beside the payload, so that the layout applies before the editor loads:

js
const v2 = async raw => {
  const P = new URLSearchParams(raw)
  const bytes = new Uint8Array(await new Response(new Blob([raw]).stream().pipeThrough(new CompressionStream('gzip'))).arrayBuffer())
  const outer = new URLSearchParams({ v: 2, c: bytes.toBase64({ alphabet: 'base64url', omitPadding: true }) })
  for (const key of ['se', 'h']) if (P.has(key)) outer.set(key, P.get(key))
  return `https://rtcode.io/x/#${outer}`
}
const href = await v2(new URLSearchParams({ '/': bigHTML, title: 'Big example', h: 1 }).toString())

The playground reads a v=2 link only if it has exactly one v and one c, c is base64url, and the payload decodes as UTF-8. Otherwise it shows Invalid compressed playground URL. Plain links of any length are always accepted.

Note: The compressed form needs Uint8Array.prototype.toBase64 and fromBase64: Chrome and Edge 140, Firefox 133, Safari 18.2 and Samsung Internet 30. In an older browser a v=2 link, a large deployment's source, or saving a state of 8192 characters or more, fails with Could not read or save the playground hash. See Browser support.

Share what is on your screen

The address bar of the playground is a link to it. The playground writes it as you edit, with no reload and no new history entry. You can also ask for it from code, in io.eval.js or the browser's developer console:

js
await H.flush(); (await HashState.url()).href   // the current link, plain, or v=2 from 8192 characters if shorter
Code.v                                           // always the uncompressed form

The split position is not part of a link. While a folder is shared with Agent Folder Share in a Chromium browser, the playground stops writing the address bar, because Chromium records replaced URLs in history, and a toast says Folder sharing is active. The address bar is paused; reloads and deployments stay up to date. Do not copy the address bar then: use HashState.url() or Code.v, or unshare, which writes the current state back. In an embedded playground, Open ▸ Playground ▸ Tab opens the current state in a new top-level tab.

https://rtcode.io/x/#act=code.new opens the New Code chooser, and https://go.rt.ht/ leads there. https://rtcode.io/x/#act=code.import.url opens the Import URL prompt, and https://xcodepen.io/ leads there. See The playground interface.

Forward links from your own site

A tiny deployment can forward links to the playground, such as try.example.com. A top-level visit that carries a hash goes to the playground with that hash, and a bare visit appends ?, which opens the redirector's own source. Then https://try.example.com/#/=… is a link to code in RTCode.io.

html
<script>if (self === top) location.hash.length > 2 ? location.replace(`//rtcode.io/x/${location.hash}`) : location.search = '?'</script>
<p>Open this page with #/=… to try code in RTCode.io.</p>

Open a deployment's editor

Append a question mark

A deployment stores the playground state it was deployed from. Append ? right after a slash, and the edge answers with a page that opens the editor on that state: https://<route>/?. The query ?io or ?sync does the same on any path. The editor opens exactly as deployed, with its routes, editors, title, layout, selections and folds.

plain
https://new.rt.ht/?             the editor of new.rt.ht
https://md.rt.ht/worker/?       a path route: keep the slash before the ?
https://md.rt.ht/worker?io      ?io and ?sync work on any path
https://in.rt.ht/new/?          the gateway form
https://new.rt.ht/??            the versions of the deployment

The page that opens the editor carries social tags, so a shared ? link previews with the screenshot of the deployment, and the title of the playground or the host name as its description. Deployments with a large state are compacted to the v=2 form when the stored link is 8192 characters or more.

Write the slash

A bare ? must follow a slash. A request whose path does not end in /, and contains no ., * or :, gets a redirect to the same path with a slash before the query: md.rt.ht/worker? goes to md.rt.ht/worker/?. That holds on rt.ht hosts and on custom domains. rtcode.io itself is excluded, and rtcode.io/? returns 404. A path with a dot cannot be fixed that way, so write the slash yourself: example.com/docs/?.

You writeThe edge answers
::i, ::e or ::c after a pathA redirect to the same path followed by ?, so new.rt.ht/::i gives new.rt.ht/?.
::v after a pathA redirect to the same path followed by ??.

The in.rt.ht gateway

https://in.rt.ht/<route> forwards to that route. A host without a dot gets .rt.ht, so in.rt.ht/new/? is new.rt.ht/?. The gateway applies the same slash rule to short routes, but a route with a dotted host needs the slash written out: in.rt.ht/example.com/docs/?. See in.rt.ht and short links.

Who can open the source

The source of a deployment without a worker or an access rule is public. If a deployment has an edge worker that is not marked io:guest, or an element with io:user or edge:user that is not marked guest, then ? is open only to its owner, its editors and the permits of io:user. An anonymous visitor is asked to sign in and is returned to the source, and a signed-in account that is not allowed sees You are not authorized with a way to sign in as someone else. See Restricting access and Keeping secrets out of deployments.

Versions and reverting

?? after a route URL opens the versions of the deployment, for example https://md.rt.ht/worker/??. In the playground, File ▸ Edge ▸ Load, also called Code: Revert Changes, asks Revert all changes since the last deployment? and reopens the deployed source in the tab. See Versions and Smart Undo and ?.

Embed the playground

The iframe

Any site can frame https://rtcode.io/x/. The playground sends no X-Frame-Options and no frame-ancestors. Put the link of your example in src, and lazy-load the frame:

html
<iframe src="https://rtcode.io/x/#/=%3Ch1%3EHello%3C%2Fh1%3E&title=Hello" allow="clipboard-read; clipboard-write; fullscreen" loading=lazy></iframe>
<a href="https://rtcode.io/x/#/=%3Ch1%3EHello%3C%2Fh1%3E&title=Hello" target=_blank>Open in RTCode.io</a>

The playground uses the clipboard for the Import URL prefill, copy and paste, and fullscreen for the Fullscreen commands, so clipboard-read; clipboard-write; fullscreen is the allow list that we suggest. A frame inside another frame gets only the features that its embedder delegates, so add the ones your own code needs, such as the camera or the microphone, to your allow attribute.

The link beside the frame opens the top-level playground, which has features that an embedded one lacks: Agent 📂 Share™, Capture and Deploy.

A live embed

Select the button to load the playground in a frame. Click the counter in the output, and the count appears in the source too, because the page writes its changes back.

html
<button onclick=this.textContent++>0</button>

What changes when it is embedded

PartIn an embedded playground
The outputRuns on out.rtcode.net.
A link with no / keyLoads a starter page, https://rtcode.io/c/1.html, with a clock, logos and a counter button. A top-level playground stays empty and shows its hint.
The act keyIgnored.
FocusThe frame blurs after it loads instead of taking the focus.
Sign-inThe session of the signed-in user is not restored.
MenusThe Code and Edge sections of the File menu, Open ▸ Edge, the tooltip of the User menu and the Capture section are hidden. Open ▸ Playground ▸ Tab and Playground ▸ Reload appear, and the title button opens the File menu.
Agent 📂 Share™Absent. It needs a top-level playground.
FullscreenThe Fullscreen commands exist only if your frame allows fullscreen.
CaptureThe commands stay in the palette, and capture works only if your frame delegates display-capture.

Inside the output, F4 to F8 and F12, and combinations of Ctrl, Alt and ⌘ except those with A, C, X, V, Z, Y, an arrow key or Tab, are forwarded to the playground, so its shortcuts work while the output has focus. See Keyboard shortcuts.

Messages to your page

The embedded playground posts string messages to its parent window. Their origin is https://rtcode.io:

ready

The initialization has finished.

content-change

The input changed, after each change once ready.

selection-change

The selection changed, after each change once ready.

oui

The first pointer press inside the output, sent once.

html
<iframe id=demo loading=lazy allow="clipboard-read; clipboard-write; fullscreen" style="inline-size: 100%; block-size: 30rem; border: 0"></iframe>
<output id=log></output>
<script type=module>
  demo.src = `https://rtcode.io/x/#${new URLSearchParams({ '/': '<button onclick=this.textContent++>0</button>', title: 'Counter', h: 1 })}`
  addEventListener('message', e => e.origin === 'https://rtcode.io' && e.source === demo.contentWindow && (log.textContent = e.data))
</script>

Check event.origin and event.source, as above. The log output shows ready, then content-change, selection-change and oui as the reader interacts.

Storage access

The output needs a service worker. In a browser that has the Storage Access API, except Safari, an embedded output whose service worker cannot register shows a button, Enable output, followed by while visiting and the address of your site. A click requests storage access and reloads the output. Other playgrounds embedded in the same site reload with it. Safari gets neither the button nor a message. Embedding in Firefox is untested. See Browser support and Troubleshooting.

See also