Skip to content

metapages/metaframe-js

Repository files navigation

Javascript embedded in the URL: run and edit with AI tools

Official Docs

Github

Edit and run javascript code in the browser. Share and connect the self-contained websites with other chunks of code and visualization.

Copy and paste into AI such as Claude Code or ChatGPT and create shareable code that will always run.

Edit with AI

  1. Copy the AI prompt, paste into e.g. Claude Code or ChatGPT, the ask what you want inputs

  2. Copy the result back to the Javascript window. Now you have something to share or embed

View the result!

Examples

Javascript high level

  • code is an es6 module
  • top-level await
  • export a function onInputs to listen to inputs
  • send outputs with setOutput/setOutputs (predefined functions available in your module)
  • export a function onResize to listen to window/div resizes
  • use es6 module imports, or add any css / npm modules to the page, they are embedded in the URL

Useful code snippets

Handling Inputs and outputs in code

Simply export a function (arrow function also good 👍) called onInputs:

// regular js function
export function onInputs(inputs) {
  // do something here
  // inputs is a plain object (key and values)
}
//  OR arrow function
export const onInputs = (inputs) => {
  // do something here
  // inputs is a plain object (key and values)
};

To send outputs, there are two functions in the scope setOutput and setOutputs:

// send a single JSON output
setOutput("outputname", 42);

// send an output object of keys+values
setOutputs({
  outputname: true,
  someOtherOutputName: "bar",
});

Output values can be strings, JSON, objects, arrays, numbers, ArrayBuffers, typed arrays such as Uint8Array;

Define Inputs and Outputs

In Settings you can define inputs and outputs. This doesn't change how the code runs, but it allows much easier connecting upstream and downstream metaframes when editing metapages.

In this example, we defined an input: input.json and an output data.csv:

inputs

You will see these inputs and outputs automatically in the metapage editor.

The root display div element is exposed in the scope

The root display div is exposed in the script scope: the name is root and the id is also root:

console.log(root.id);
// logs "root"
// Add any custom dome elements into "root".

You can also just get it with:

document.getElementById("root");

Height / width / window resize

To get the root element width/height:

const width = root.getBoundingClientRect().width;
const height = root.getBoundingClientRect().height;

For automatically resizing: export a function (arrow function also good 👍) called onResize. This will be called when either the window resizes event and/or the local div element resizes:

// regular js function
export function onResize(width, height) {
  // Your own code here, handling the resize of the root div
}
//  OR arrow function
export const onResize = (width, height) => {
  // Your own code here, handling the resize of the root div
};

Prevent scroll events from propagating to the parent window

Often if you use (wheel) scroll events to interact with content, the event is also propagated to the parent window, scrolling the entire metapage, which is almost always undesired.

To prevent this, on the dom element you intercept wheel scroll events, add this code to prevent the event from propagating up. Replace myContainer with your dom element:

// prevent parent from scrolling when zooming
function maybeScroll(event) {
  if (myContainer.contains(event.target)) {
    event.preventDefault();
  }
}
window.addEventListener("wheel", maybeScroll, { passive: false });

Save state in the URL

State is stored in the URL, you can get and set values using the @metapages/hash-query module:

import {
  getHashParamsFromWindow,
  getHashParamFromWindow,
  getHashParamValueJsonFromWindow,
  setHashParamValueJsonInWindow,
  setHashParamValueBase64EncodedInWindow,
  getHashParamValueBase64DecodedFromWindow,
} from "https://cdn.jsdelivr.net/npm/@metapages/hash-query@0.9.12/+esm";

// Get JSON stored in URL
const myJsonBlob = getHashParamValueJsonFromWindow("someKey") || {};
// update the JSON blob
myJsonBlob["someKey"] = "foobar";
// set it back in the URL
setHashParamValueJsonInWindow("someKey", myJsonBlob);
// delete it if needed
deleteHashParamFromWindow("someKey");

Note: this is to store relatively small values. Huge multi-megabyte JSON blobs are not yet supported, but we have a plan wtoill support large blobs.

Unload/cleanup

When iterating with the code editor, the script is re-run. In some cases, this can cause problems as multiple listeners maybe responding to the same event.

This is not an issue when simply running the page once with code, only when develping iteratively.

To have your script cleaned up because of new script (when editing), declare a function cleanup, this will be called prior to the updated script re-running:

// regular js function
export function cleanup() {
  console.log("internal scriptUnload call");
  // do your cleanup here
}
// OR arrow function
export const cleanup = () => {
  // do your cleanup here
};

Wait until page load

You don't need to wait for the load event: your script will not run until load event fires.

Logging to the display (instead of console)

Some globally available functions for logging:

log("something here");
logStdout("something here");
logStderr("an error");

These will be added to the root div (see below) so if your own code manipulates the root div, it could be overwritten. This is mostly useful for headless code.

Misc

  • "use strict" is automatically added to the top of the module code.

Jupyter Notebook Widget

Use any metaframe as an interactive Jupyter notebook widget. Install the metaframe-widget package:

pip install metaframe-widget

Basic usage

from metaframe_widget import MetaframeWidget

# From a URL — paste any metaframe URL
w = MetaframeWidget(url="https://js.mtfm.io/#?js=...")
w  # renders the iframe in the notebook

Create from inline code

w = MetaframeWidget.from_code("""
export const onInputs = (inputs) => {
    document.getElementById("root").textContent = JSON.stringify(inputs);
    setOutput("echo", inputs);
};
""")
w

Push inputs from Python

w.set_inputs({"data": [1, 2, 3], "message": "hello from Python"})
w.set_input("count", 42)

Read and react to outputs

# Read current outputs
print(w.outputs)

# React to output changes
w.on_outputs_change(lambda change: print("Got:", change["new"]))

Pipe widgets together

Connect the output of one widget to the input of another:

source = MetaframeWidget.from_code("...")
sink = MetaframeWidget.from_code("...")

# When source emits "doubled", push it to sink's "data" input
source.pipe_to(sink, output_key="doubled", input_key="data")

Works in Jupyter, JupyterLab, VS Code, Colab, and marimo.

marimo

In marimo, wrap the widget with mo.ui.anywidget() to get reactive bindings:

import marimo as mo
from metaframe_widget import MetaframeWidget

w = mo.ui.anywidget(MetaframeWidget(url="https://js.mtfm.io/"))
w

Then in a separate cell, w.outputs will reactively update when the metaframe emits output — any cell referencing it re-runs automatically.

w.set_inputs({"data": [1, 2, 3]})

For piping, access the underlying widget via .widget:

source.widget.pipe_to(sink.widget, output_key="result", input_key="data")

See examples/marimo/demo.py in the repo for a complete example.

Short URLs

Full URLs with embedded code can get long. Use the shorten button in the editor toolbar to generate a compact short URL.

Full URL (code and all config embedded in the hash):

https://js.mtfm.io/#?js=ZXhwb3J0IGNvbnN0IG9uSW5wdXRzID0gKGlucHV0cykgPT4gew0KICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJyb290IikudGV4dENvbnRlbnQgPSBKU09OLnN0cmluZ2lmeShpbnB1dHMpOwp9&inputs=%7B%22data.json%22%3A%7B%22type%22%3A%22url%22%2C%22value%22%3A%22https%3A%2F%2Fjs.mtfm.io%2Ff%2Fabc123%22%7D%7D

Short URL (same content, shareable):

https://js.mtfm.io/j/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Short URLs are content-addressed: the path is /j/{sha256} where the SHA-256 is computed from the hash parameters. Identical content always produces the same short URL.

Programmatic shortening

From raw hash params:

curl -X POST https://js.mtfm.io/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"hashParams": "?js=ZXhwb3J0IGNvbnN0IG9uSW5wdXRzID0gKGlucHV0cykgPT4ge30%3D"}'

From structured JSON (preferred):

curl -X POST https://js.mtfm.io/api/shorten/json \
  -H "Content-Type: application/json" \
  -d '{
    "js": "export const onInputs = (inputs) => { root.textContent = JSON.stringify(inputs); }",
    "inputs": {
      "data.json": { "type": "url", "value": "https://js.mtfm.io/f/abc123def456..." }
    }
  }'

Response:

{
  "id": "e3b0c44298fc1c14...",
  "shortUrl": "https://js.mtfm.io/j/e3b0c44298fc1c14...",
  "fullUrl": "https://js.mtfm.io/#?js=...&inputs=...",
  "hashParams": "?js=...&inputs=..."
}

Supported fields in /api/shorten/json: js, inputs, definition, modules, options.

File and blob handling

You can upload files (images, data, etc.) by dragging them onto the editor or by adding file-type inputs in Settings. Uploaded files are stored in S3-compatible storage and referenced by content hash.

How uploads work

  1. The client computes a SHA-256 hash of the file content
  2. A presigned upload URL is requested from POST /api/upload/presign
  3. The file is uploaded directly to S3 via the presigned URL
  4. The file becomes accessible at https://js.mtfm.io/f/{sha256}

File references in inputs

Uploaded files are added to the inputs hash parameter as DataRef objects with type: "url":

{
  "inputs": {
    "photo.jpg": {
      "type": "url",
      "value": "https://js.mtfm.io/f/a1b2c3d4e5f6..."
    },
    "data.csv": {
      "type": "url",
      "value": "https://js.mtfm.io/f/f6e5d4c3b2a1..."
    }
  }
}

When the code runs, the runtime fetches each URL and delivers the content to your onInputs function. JSON files are automatically parsed into objects; other types arrive as Blobs.

DataRef types

Inputs support several reference types:

Type Value Resolved to
url A URL string Fetched content (JSON object, string, or Blob depending on content-type)
utf8 Plain text String
base64 Base64-encoded binary Blob
json A JSON value The value as-is
(none) Any value Treated as native JSON

Example: short URL with file inputs

# 1. Upload a file
SHA=$(shasum -a 256 mydata.json | cut -d' ' -f1)
PRESIGN=$(curl -s -X POST https://js.mtfm.io/api/upload/presign \
  -H "Content-Type: application/json" \
  -d "{\"contentType\": \"application/json\", \"fileSize\": $(stat -f%z mydata.json), \"sha256\": \"$SHA\"}")
curl -X PUT "$(echo $PRESIGN | jq -r .presignedUrl)" \
  -H "Content-Type: application/json" \
  --data-binary @mydata.json

# 2. Create a short URL that references the uploaded file
curl -X POST https://js.mtfm.io/api/shorten/json \
  -H "Content-Type: application/json" \
  -d "{
    \"js\": \"export const onInputs = (inputs) => { root.textContent = JSON.stringify(inputs); }\",
    \"inputs\": {
      \"mydata.json\": { \"type\": \"url\", \"value\": \"https://js.mtfm.io/f/$SHA\" }
    }
  }"

Data persistence and storage lifetime

Code (stored forever)

Code and configuration stored via URL shortening (/j/{sha256}) are persisted indefinitely. Short URLs will continue to resolve for as long as the service is running. Since the storage key is a content hash, identical content is deduplicated automatically.

Uploaded files (planned: 1 month expiry)

Files uploaded via /f/{sha256} are currently stored without expiration, but file expiry of approximately 1 month is planned. Once enabled, files that have not been re-uploaded will be removed after roughly 30 days.

This gives you plenty of time to transfer blobs to your own storage if you are building on top of this platform. The recommended workflow:

  1. Upload files and create short URLs as needed
  2. If you want permanent file hosting, copy the blobs from https://js.mtfm.io/f/{sha256} to your own S3/CDN/storage
  3. Update the inputs in your short URL (or your own stored URL) to point to your permanent file URLs instead

What this means for your URLs

What Path format Persistence
Short URL (code + config) /j/{sha256} Forever
Uploaded file /f/{sha256} ~1 month (planned), currently no expiry

If a short URL references uploaded files via /f/... URLs and those files expire, the short URL itself will still resolve but the file fetches will fail. To avoid this, either re-upload files periodically or migrate them to your own storage.

URL format reference

The full URL format is:

https://js.mtfm.io/#?js={base64}&inputs={json}&modules={json}&definition={json}&options={json}&edit={bool}
Parameter Encoding Description
js btoa(encodeURIComponent(code)) JavaScript ES6 module source code
inputs encodeURIComponent(JSON.stringify(obj)) Input DataRef objects (see above)
modules encodeURIComponent(JSON.stringify(arr)) Array of CSS/JS URLs or import maps to load
definition encodeURIComponent(JSON.stringify(obj)) Metaframe definition (input/output names)
options encodeURIComponent(JSON.stringify(obj)) Runtime options (debug, disableCache, disableDatarefs, etc.)
edit true or absent Show the editor panel
editorWidth CSS value (e.g. 80ch) Width of the editor panel
bgColor CSS color Background color
hm disabled, invisible, visible Menu button visibility

Security and iframe permissions

When embedding a metaframe as an iframe in your own page, browsers restrict certain APIs by default. You need to explicitly grant permissions via the allow attribute on the <iframe> tag.

Clipboard access

If your embedded code uses the Clipboard API (e.g. copying a URL or text to the clipboard), you must grant clipboard permissions:

<iframe
  src="https://js.mtfm.io/#?js=..."
  allow="clipboard-read *; clipboard-write *"
></iframe>

Without this, calls to navigator.clipboard.writeText() or navigator.clipboard.readText() inside the iframe will be blocked by the browser.

The metaframe definition already declares clipboard-write in its allow field, which is used when metaframes are loaded by the metapage runtime. But if you embed the iframe directly in your own HTML, you must set the allow attribute yourself.

Other permissions

Depending on what your code does, you may need additional permissions:

<iframe
  src="https://js.mtfm.io/j/abc123..."
  allow="clipboard-read *; clipboard-write *; camera; microphone; geolocation"
></iframe>

Common permissions:

Permission When needed
clipboard-read * Reading from the clipboard
clipboard-write * Writing to the clipboard
camera Accessing the user's camera
microphone Accessing the user's microphone
geolocation Using location APIs
fullscreen Requesting fullscreen mode

Sandbox attribute

If you use the sandbox attribute on your iframe, you must also include allow-scripts and allow-same-origin for the metaframe to function:

<iframe
  src="https://js.mtfm.io/#?js=..."
  sandbox="allow-scripts allow-same-origin allow-popups"
  allow="clipboard-read *; clipboard-write *"
></iframe>

Longer description and architecture

Run arbitrary user javascript modules embedded in the URL. Designed for metapages so you can connect inputs + outputs to other metaframe URLs. Similar to Codepen, JSFiddle, but completely self-contained and does not require an active server, these is a simple tiny static website.

Connect upstream/downstream metaframes

graph LR
    subgraph metapage
        direction LR
        left1(upstream metaframe) -->|inputs| M[This Metaframe]
        M --> |outputs| right1(downstream metaframe)
    end
Loading

Connecting with other chunks of code and visualization

This website is also a metaframe: connect metaframes together into apps/workflows/dashboards: metapages

Architecture

  • Code and configuration are embedded in the URL hash or stored via short URLs
  • Short URLs (/j/{sha256}) store hash params in S3, persisted indefinitely
  • Uploaded files (/f/{sha256}) are stored in S3 with planned ~1 month expiry
  • The client runs the embedded javascript directly (code is not sent to the server for execution)

The server runs on https://deno.com/deploy which is

  • simple
  • fast
  • very performant
  • deploys immediately with a simply push to the repository

About

Embed any JS module and custom code

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors