Exporting files with o:

An element with an o: attribute becomes a file. Its content is served at its own URL: /name.ext in the playground output, and https://<route>/name.ext on RTEdge.net once you deploy. This guide covers names, bodies, content types and headers, and says where the playground and RTEdge.net differ.

Exporting an element

Write the file name as the value of o:. In o:=nav.js the attribute name is o: and the value is nav.js. The colon belongs to the attribute syntax, so the URL is /nav.js, not /:nav.js.

A stylesheet, a module and a page

This source exports three files and shows one of them in a frame:

html
<style o:=.css>
  :root { color-scheme: light dark; font: 1rem/1.5 system-ui; }
  h1    { color: oklch(0.6 0.15 250); }
</style>
<script type=module o:=greet.js h:access-control-allow-origin=*>
  export const greet = name => `Hello, ${name}!`
</script>
<template o:=page.html>
  <meta charset=utf-8>
  <link href=.css rel=stylesheet>
  <h1 id=out></h1>
  <script type=module>
    import { greet } from './greet.js'
    out.textContent = greet('exports')
  </script>
</template>
<iframe src=page.html></iframe>

In the playground output the files are served beside the page: /.css, /greet.js and /page.html. Deployed to the route new, they are https://new.rt.ht/.css, https://new.rt.ht/greet.js and https://new.rt.ht/page.html. The module carries h:access-control-allow-origin=*, so other sites can import it.

Note: The playground rebuilds exports only when the output reloads in full. Editing the module reloads the output; after editing the <style> or the <template>, press Load (F5). See Keeping exports fresh in the playground.

Where a file is served

EnvironmentWhere o:=nav.js is served
Playground/nav.js on the output origin. Only that playground session's output and worker can request it, plus sibling playgrounds on the same host.
RTEdge.nethttps://<route>/nav.js for every route in Code.D. Each route receives the same deploy; see Deploying.

With Code.D = ['new'], <script o:=nav.js> is served at https://new.rt.ht/nav.js as text/javascript.

Files answer any HTTP method, so curl -I on a file returns its headers. A query string is ignored: nav.js?v=2 serves nav.js. On RTEdge.net the root document answers only GET.

File names

A name must contain a . and must not contain a /. Names that start with a dot, such as .js, .css, .md and .svg, are valid and common: they are served at <route>/.js and so on.

NamePlaygroundRTEdge.net
nav.js, .cssServed.Served.
dir/x.jsServed at /dir/x.js.Never served. A file exists only directly under its route path.
a b.txtServed at /a%20b.txt.Does not match: the request path is percent-encoded, the stored name is not.
notesNever served as a file.Unreachable in practice: paths without a dot are redirected to the slash form.

Stick to letters, digits, _ and - plus one ., or to a leading-dot name. Matching is case-sensitive on RTEdge.net. Names that contain :, ? or #, names that end in *, and a few names the platform reserves are not reachable; see Reserved paths. On a host-root route, the platform also answers favicon.ico and robots.txt itself.

Where a file lives on RTEdge.net

On RTEdge.net every route is its own deployment, and a file exists only directly under its route path: /<route path>/<name>. A request for /route/sub/x.js belongs to the route route/sub, not to route.

The platform also normalizes the request path before routing, so //nav.js and /x/../nav.js serve /nav.js.

Use relative URLs

On RTEdge.net a page of a path route such as name.rt.ht/docs/ resolves x.js to /docs/x.js, while an absolute /x.js addresses the files of the host-root route. In the playground the output document lives at /, so both x.js and /x.js reach your own export. Relative URLs work in both, and so do absolute paths that include the route path, such as /docs/x.js.

CodeLens

Each o:= start tag shows a CodeLens in the editor. A valid name reads 📄 ./nav.js, followed by → 🌐 //new.rt.ht/nav.js once the playground has a route; Ctrl+click (⌘+click on macOS) opens the URL. An invalid name shows ⚠️ and a hint: must contain . or must not contain /.

Choosing the body

Every export has a body, a content type and optional headers. The body comes from the element itself.

Default bodies

The body of the file depends on the element:

ElementDefault body
<script>, <style>textContent
<template>innerHTML
<img> without o:content whose src starts with data:The decoded bytes (a binary file)
Every other element, including <img> with another srcouterHTML

The playground, RTEdge.net and File Sync use the same table.

Bodies are taken verbatim. The textContent of a script or style includes the line break right after the start tag and the indentation before the end tag; nothing is trimmed. The textContent of other elements is entity-decoded. Start the content right after > when the file must not begin with a line break.

Another property with o:content

o:content=NAME uses element[NAME] as the body, so any property works: value, innerHTML, textContent, innerText, id. Numbers and booleans are served as text. See o:content.

html
<textarea hidden o:=notes.txt o:content=value>Remember the milk.</textarea>

Text, JSON and SVG data blocks

Script types that are not JavaScript never run, which makes <script> a good container for data. These blocks export a text file, a JSON file and an SVG image, and the frames and the image show them:

html
<script type=text/plain o:=llms.txt o:type="text/plain;charset=utf-8"># Example
> One-line summary for LLM tools.
</script>
<script type=application/json o:=data.json>{"version":1}</script>
<script type=image/svg+xml o:=.svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="tomato"/></svg>
</script>
<iframe src=llms.txt></iframe> <iframe src=data.json></iframe> <img src=.svg alt width=48 height=48>

The text starts right after the > of the first script, so llms.txt has no leading line break. Quote an o:type value that contains =, such as ;charset=utf-8. SVG needs xmlns="http://www.w3.org/2000/svg".

Note: Do not export an <svg o:=…> element through its outerHTML. The playground keeps the o: attribute in that body, and an attribute with a colon is not well-formed in an SVG image, so the playground's copy can fail to load; RTEdge.net strips the attribute, so the deployed file works. A raw-text block such as <script type=image/svg+xml o:=.svg> avoids the problem in both.

Binary files from data: URLs

An <img> with a data: URL and no o:content exports the decoded bytes. Hide the carrier image with hidden and an empty alt:

html
<img hidden alt o:=dot.png src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
<img src=dot.png alt=dot width=64 height=64>
  • The text between data: and the first , is split on ;. The payload is base64 when a parameter equals base64; otherwise it is percent-decoded. The first parameter without = is the MIME type of the data.
  • Only data: URLs export bytes. <img o:=logo.png src=//example.com/logo.png> exports the text of the <img> element, as image/png.
  • The playground serves the bytes as a Blob. RTEdge.net stores them and serves the decoded bytes.
  • A malformed data: URL stops the extraction before the output reloads in the playground, so the previous output stays.
  • Never reuse the name of a binary export: any duplicate that involves a binary part is an error.

Several elements, one file

Elements that share a name are joined in document order, with one line break between the parts and none at the end. The merged file keeps the type and the h:* headers of the first element; the o:type and h:* of later elements are silently ignored.

A second part that never runs can extend an export. Here /.js runs inline as a classic script and is also importable as a module:

html
<script o:=.js>
  class Api { static ping = () => 'pong' }
</script>
<script type=none o:=.js>export { Api }</script>
<output id=pong></output>
<script type=module>
  import { Api } from './.js'
  pong.value = Api.ping()
</script>

Edit the first script and the output reloads, which rebuilds /.js. The type=none part is not a JavaScript type, so edits to it wait for Load (F5).

Content types

Both environments use the same extension table of 78 entries; RTEdge.net adds wasm. The extension is the text after the last ., lowercased, so x.JS is text/javascript. The full table is on o:type.

PlaygroundRTEdge.net
Ordero:type, the extension table, the MIME type of a data: URL (binary images), application/octet-stream (binary), text/plain.h:content-type (used verbatim, and nothing else is added), then o:type, the extension table, the data: MIME type, application/octet-stream (binary), text/plain;charset=utf-8.
Charset;charset=utf-8 is appended unless the type already has charset=, even to binary types: image/png;charset=utf-8.Nothing is appended to o:type or table values.
h:content-typeHas no effect.Wins when present.

The table has no entry for webmanifest, map, yaml, yml, toml, vtt, jsx, tsx or glb, nor for wasm in the playground. Those fall back to the default text type, so set o:type for them. .ts maps to video/mp2t.

For non-ASCII text that visitors open directly (.txt, .md, .csv), give the charset yourself, with o:type="text/plain;charset=utf-8" or h:content-type. A page exported from a <template> should start with <meta charset=utf-8>, because RTEdge.net serves table types without a charset.

Note: Responses on RTEdge.net hosts carry x-content-type-options: nosniff, so a script or stylesheet with the wrong type does not run. A module exported as .ts is served as video/mp2t and will not execute.

Headers

h:<name>=<value> adds a response header to the file. The HTML parser lowercases attribute names, so header names arrive in lowercase. An element can carry any number of h:* attributes; they are kept in order. On an element with neither o: nor worker:fs they have no effect.

RTEdge.net

Sends the h:* headers verbatim, then the computed content type (only when there is no h:content-type), edge and deploy. It adds no cache-control unless you set one, and no CORS headers for other sites unless you set h:access-control-allow-origin. Platform headers such as server-actual and, on *.rt.ht, strict-transport-security take priority over yours.

Playground

Builds the response from your h:* headers, then forces origin-agent-cluster: ?1, cache-control: no-store, Referrer-Policy: unsafe-url and the computed content-type; later keys win. h:content-type, h:cache-control, h:origin-agent-cluster and h:referrer-policy are therefore overridden, and h:set-cookie has no effect. Other headers, such as CORS, CSP and content-disposition, apply.

Headers belong to files only; they do not apply to the root document. When several elements share a name, the first element's headers apply. See h:* and Response headers.

Caching and CORS headers

html
<script type=module o:=code.js h:access-control-allow-origin=* h:cache-control="public, max-age=300">
  export const answer = 42
</script>
<script type=module>
  import { answer } from './code.js'
  console.log(answer)
</script>

Quote a value that contains spaces. After a deploy, curl -I https://<route>/code.js shows both headers. In the playground, cache-control is always no-store.

Exporting HTML pages

The usual way to export a page is a <template>: its default body is innerHTML, the template is inert, and it can hold a complete page with <meta>, <title> and <link>. Keep the default innerHTML: o:content=textContent is empty in the playground but not on RTEdge.net, and o:content=outerHTML on a template drops the template's attributes on RTEdge.net.

To publish many pages from one playground, see Building a multi-page site.

Keeping exports fresh in the playground

The playground rebuilds exports and env.FS only when the output initializes in full: on the first load, on Load (F5), and on any edit that reloads the output. Other edits are hot patches. They morph the page but leave /name serving the previous content.

See Live sync and reloads for the complete list of triggers.

A Markdown file that stays fresh

html
<script reload type=text/markdown o:=.md>
# Notes
</script>
<iframe src=.md></iframe>

Playground and RTEdge.net

The two environments share the same tables and defaults; Playground and edge differences covers the rest of the platform. They differ in these places for exports. Because bodies read from outerHTML, innerHTML or another property are cleaned and re-serialized on deploy, they are not byte-identical between the two; script and style textContent and binary exports are, for sources with LF line endings.

PlaygroundRTEdge.net
Where files are served/name on the output origin.https://<route>/name for every route.
File namesResolved as URLs: names with /, spaces or non-ASCII characters work.Only names directly under the route path; no /; percent-encoded names do not match.
Bodies from outerHTML and innerHTMLRead from the parsed element before any cleanup: RTCode attributes and nested edge:remove and edge:redact content stay.Read from a cleaned copy, then re-serialized: RTCode attributes are stripped, nested edge:remove becomes blank lines and edge:redact becomes a comment.
<template> contentso: on elements inside a template is ignored; the template itself can be exported.o: inside templates is exported too.
o:= on <script type=worker>Exported.Not exported: workers are removed before extraction.
Line endingsCR and CRLF become LF.CR and CRLF in the source are kept.
Type and charset;charset=utf-8 appended; h:content-type ignored.h:content-type wins; table types have no charset.
h:* headersSome are overridden; cache-control: no-store always.Sent verbatim.
HTML pagesNo doctype added.<!doctype html> added on navigations.
FreshnessRebuilt on full reloads only.Rebuilt on every deploy.
.wasmNo table entry.application/wasm
Relative fetch() in a workerReaches your exports.Fails; use absolute URLs or worker:fs.

Exports and workers

A file wins over the worker: a request for a name that is also an export is answered by the export. A path with a : prefix is never a file lookup and reaches the worker, so <iframe src=:data> calls the worker with the path /data.

In the playground, a worker's fetch() resolves same-origin URLs whose last segment contains . to your exports first, so fetch("x.js") returns the live export. On RTEdge.net, relative URLs in a worker do not resolve: use absolute URLs, or give the worker its assets with worker:fs. See Using workers.

Exports and agents

With File Sync on, which is the default, Agent Folder Share writes every element that has o: to the shared folder as a file named by its o: value.

See File Sync and Agent files.

Who can read an export

A deployed export is a public URL. The edge:user gate protects exports as well as the page. edge:redact and edge:remove on the exporting element keep the element out of the served page, but the file is still served at its URL.

Warning: Do not export anything you do not want visitors to download. See Keeping secrets out and Restricting access.

Availability

FeaturePlaygroundRTEdge.netNotes
o: exportsYesYesSame extension table and defaults. Bodies, types and headers differ as described above.
<template> pagesYesYesThe playground ignores o: on elements nested in a template.
Names with /, spaces or non-ASCIIYesNoServed only by the playground.
h:* headersPartialYesOverridden in part in the playground.
File Sync (Agent Folder Share)YesNoChrome or Edge on desktop.

See also