@metapages/metapage
v1.10.5
Published
Connect web pages together
Maintainers
Readme
@metapages/metapage
Build composable, interconnected web applications using iframes and data pipes
@metapages/metapage is a JavaScript library that lets you create and embed interactive workflows in the browser by connecting independent iframe components together through input/output data pipes.
Quick Links
What is this?
A metapage is a web application made up of connected iframes called metaframes. Each metaframe can:
- Receive data from other metaframes (inputs)
- Send data to other metaframes (outputs)
- Run independently (JavaScript, Docker containers, markdown editors, or any web component)
Think of it like a visual programming environment where each component is a full web application that can communicate with others through simple JSON data pipes.
Installation
npm install @metapages/metapageOr use directly from CDN:
import {
renderMetapage,
Metapage,
Metaframe,
} from "https://cdn.jsdelivr.net/npm/@metapages/[email protected]";Quick Start
Rendering a Metapage
The simplest way to embed a workflow is using renderMetapage:
import { renderMetapage } from "@metapages/metapage";
// Fetch a metapage definition (or define your own JSON)
const response = await fetch(
"https://metapage.io/m/87ae11673508447e883b598bf7da9c5d/metapage.json",
);
const definition = await response.json();
// Render it
const { setInputs, dispose } = await renderMetapage({
definition,
rootDiv: document.getElementById("container"),
});The renderMetapage function the react-grid-layout layout in metapage.json:
{
"meta": {
"layouts": {
"react-grid-layout": {
...
}
}
}
}Creating a Metaframe (Inside an iframe)
If you're building a component to use in a metapage:
import { Metaframe } from "https://cdn.jsdelivr.net/npm/@metapages/[email protected]";
const metaframe = new Metaframe();
// Listen for input data from other metaframes
metaframe.onInput("data", (value) => {
console.log("Received:", value);
// Process the data and send output
metaframe.setOutput("result", value.toUpperCase());
});
// Or listen to all inputs at once
metaframe.onInputs((inputs) => {
console.log("All inputs:", inputs);
});Core Concepts
Metapage Definition
A metapage is defined using JSON that specifies which metaframes to load and how they connect:
{
"metaframes": {
"input": {
"url": "https://editor.mtfm.io/#?hm=disabled"
},
"processor": {
"url": "https://js.mtfm.io/",
"inputs": [
{
"metaframe": "input",
"source": "text",
"target": "code"
}
]
},
"output": {
"url": "https://markdown.mtfm.io/",
"inputs": [
{
"metaframe": "processor",
"source": "output"
}
]
}
}
}This creates a pipeline: input → processor → output
Metaframe Definition
See code
This is provided either by:
https://<your metaframe>/metaframe.jsonhttps://<your metaframe>/#?definition=<json encoded hash param>
The definition describes inputs, outputs, security, and the types of hash parameters (so AI tools can correctly modify)
export interface MetaframeDefinition {
inputs?: {
[key: string]: MetaframePipeDefinition;
}; // <MetaframePipeId, MetaframePipeDefinition>
outputs?: {
[key: string]: MetaframePipeDefinition;
}; // <MetaframePipeId, MetaframePipeDefinition>
metadata: MetaframeMetadataV2;
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Feature_Policy/Using_Feature_Policy#the_iframe_allow_attribute
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy#directives
allow?: string;
// Set or override allowed features for the iframe
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox
sandbox?: string;
// Hash parameters configuration.
// Accepts both legacy array format (string[]) and new object format (HashParamsObject).
// When fetched via helper methods, array format is normalized to object format.
hashParams?: HashParamsRaw;
}Data Pipes
Pipes connect metaframe outputs to other metaframe inputs:
{
"metaframe": "sourceMetaframeId", // Where data comes from
"source": "outputPipeName", // Name of the output pipe
"target": "inputPipeName" // Name of the input pipe (optional, defaults to source)
}Working with Data
The library automatically handles serialization of complex data types:
// In a metaframe - these are automatically serialized when sent between iframes
metaframe.setOutput("file", new File([blob], "data.txt"));
metaframe.setOutput("binary", new Uint8Array([1, 2, 3]));
metaframe.setOutput("buffer", arrayBuffer);
// And automatically deserialized when received
metaframe.onInput("file", (file) => {
console.log(file instanceof File); // true
});Usage Examples
Full HTML Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}
#metapage-container {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="metapage-container"></div>
<script type="module">
import { renderMetapage } from "https://cdn.jsdelivr.net/npm/@metapages/[email protected]";
const definition = await fetch(
"https://metapage.io/m/87ae11673508447e883b598bf7da9c5d/metapage.json",
).then((r) => r.json());
const { setInputs, dispose, metapage } = await renderMetapage({
definition,
rootDiv: document.getElementById("metapage-container"),
onOutputs: (outputs) => {
console.log("Metaframe outputs:", outputs);
},
options: {
hideFrameBorders: true,
hideOptions: true,
},
});
// Send inputs to metaframes
setInputs({
metaframeId: {
inputPipeName: "some value",
},
});
// Clean up when done
// dispose();
</script>
</body>
</html>Building a Metaframe Component
import { Metaframe } from "https://cdn.jsdelivr.net/npm/@metapages/[email protected]";
const metaframe = new Metaframe();
// Handle inputs
metaframe.onInputs((inputs) => {
const { data, config } = inputs;
// Process inputs
const result = processData(data, config);
// Send outputs
metaframe.setOutputs({
result: result,
timestamp: Date.now(),
});
});
// Individual input listener
metaframe.onInput("reset", () => {
metaframe.setOutputs({});
});
// Get a specific input value
const currentValue = metaframe.getInput("data");
// Get all inputs
const allInputs = metaframe.getInputs();
// Clean up
metaframe.dispose();Programmatic Metapage Control
import { Metapage } from "@metapages/metapage";
const metapage = new Metapage({
definition: {
metaframes: {
viewer: {
url: "https://markdown.mtfm.io/",
},
},
},
});
// Listen to metaframe outputs
metapage.on(Metapage.OUTPUTS, (outputs) => {
console.log("Outputs from all metaframes:", outputs);
});
// Set inputs to metaframes
await metapage.setInputs({
viewer: {
text: "# Hello World",
},
});
// Get current outputs
const outputs = metapage.getState().metaframes.outputs;
// Clean up
metapage.dispose();Advanced Features
Hash Parameters in Metaframes
Metaframes can read and write to their URL hash parameters:
import {
getHashParamValueJsonFromWindow,
setHashParamValueJsonInWindow,
} from "https://cdn.jsdelivr.net/npm/@metapages/[email protected]";
// Read from URL hash
const config = getHashParamValueJsonFromWindow("config");
// Write to URL hash
setHashParamValueJsonInWindow("config", { theme: "dark" });Pattern Matching in Pipes
Use glob patterns to match multiple outputs:
{
"inputs": [
{
"metaframe": "source",
"source": "data/*", // Matches data/foo, data/bar, etc.
"target": "inputs/"
}
]
}Binary Data Handling
// Send binary data
const imageData = await fetch("/image.png").then((r) => r.arrayBuffer());
metaframe.setOutput("image", imageData);
// Receive and use
metaframe.onInput("image", async (data) => {
const blob = new Blob([data]);
const url = URL.createObjectURL(blob);
document.getElementById("img").src = url;
});Secrets
Inject sensitive credentials (API keys, tokens, etc.) into metaframe URLs at runtime while ensuring they are never exposed when retrieving the metapage definition.
API
import { Metapage, InjectSecretsPayload } from "@metapages/metapage";
const metapage = new Metapage();
await metapage.setDefinition(definition);
const secrets: InjectSecretsPayload = {
frameSecrets: {
myMetaframe: {
hashParams: {
apiKey: "sk-abc123",
token: "secret-token",
},
queryParams: {
auth: "bearer-token",
},
},
},
};
metapage.injectSecrets(secrets);Type Definition
type InjectSecretsPayload = {
frameSecrets: {
[metaframeName: string]: {
hashParams?: { [name: string]: string };
queryParams?: { [name: string]: string };
};
};
};Behavior
- Injection: Secrets are base64-encoded into metaframe URL hash/query parameters using
setHashParamValueBase64EncodedInUrlfrom@metapages/hash-query - Accumulation: Multiple calls to
injectSecrets()accumulate secrets rather than replacing previous ones - Safe removal:
getDefinition()and definition change events automatically strip secrets, restoring original parameter values - Persistence across updates: Secrets survive
setDefinition()calls — if a metaframe still exists in the new definition, its secrets are re-injected - Cleanup: Secrets are removed when metaframes are removed via
removeMetaframe()orremoveAll()
Example
const metapage = new Metapage();
await metapage.setDefinition(definition);
// Inject a secret
metapage.injectSecrets({
frameSecrets: {
secret1test: {
hashParams: {
secret1: "injected secret",
},
},
},
});
// The metaframe iframe URL now contains the secret (base64-encoded in hash)
// But getDefinition() returns the original URL without secrets:
const def = metapage.getDefinition();
// def.metaframes.secret1test.url has NO secret params
// Definition events also exclude secrets:
metapage.on(Metapage.DEFINITION, (cleanDef) => {
// cleanDef has no secrets
});Security Notes
- Secrets are base64-encoded (not encrypted) in URLs
- Metaframe iframes receive secrets via their URL hash/query params at runtime
- Secrets are stripped from all definition retrieval methods and events
- Secret storage is cleared on
dispose()
updateDefinition
updateDefinition is the preferred way to change the metapage definition at runtime when you need to react to what changed. Unlike setDefinition, it:
- Always emits a
DefinitionUpdateevent — even on the very first call - Includes a structured diff of which metaframes were added and removed
- Automatically emits a
Stateevent when metaframes are added or removed
API
await metapage.updateDefinition(definition, state?);| Parameter | Type | Description |
| ------------ | -------------------------- | ------------------------------------------------------ |
| definition | MetapageDefinition | The new metapage definition |
| state | MetapageState (optional) | Initial state to apply alongside the definition update |
Event Payload
Listen with Metapage.DEFINITION_UPDATE. The event payload has this shape:
interface MetapageEventDefinitionUpdate {
definition: MetapageDefinition; // current definition (secrets stripped)
metaframes: {
current: { [id: string]: MetapageIFrameRpcClient }; // all metaframes after update
added: { [id: string]: MetapageIFrameRpcClient }; // metaframes that were added
removed: { [id: string]: MetapageIFrameRpcClient }; // metaframes that were removed (disposed)
};
}Example
import { Metapage, MetapageEventDefinitionUpdate } from "@metapages/metapage";
const metapage = new Metapage();
metapage.on(
Metapage.DEFINITION_UPDATE,
(event: MetapageEventDefinitionUpdate) => {
const { added, removed, current } = event.metaframes;
console.log("Current metaframes:", Object.keys(current));
console.log("Added:", Object.keys(added));
console.log("Removed:", Object.keys(removed));
},
);
// First call — fires immediately (unlike setDefinition)
await metapage.updateDefinition({
metaframes: {
viewer: { url: "https://markdown.mtfm.io/" },
},
});
// Second call — event reports frame2 in `added`
await metapage.updateDefinition({
metaframes: {
viewer: { url: "https://markdown.mtfm.io/" },
editor: { url: "https://editor.mtfm.io/" },
},
});Comparison with setDefinition
| Behaviour | setDefinition | updateDefinition |
| ----------------------------- | --------------- | ------------------ |
| Emits event on first call | No | Yes |
| Event type | Definition | DefinitionUpdate |
| Diff of added/removed frames | No | Yes |
| Emits State on frame change | No | Yes |
State event
State is automatically emitted (to any listeners) when:
- Metaframes are added or removed, OR
- An explicit
stateargument is passed and is non-empty
API Overview
renderMetapage(options)
Render a metapage into a DOM element.
Parameters:
definition: Metapage definition objectrootDiv: DOM element to render intoonOutputs: Callback for metaframe outputs (optional)options: Rendering options (optional)hideBorder: Hide metapage borderhideFrameBorders: Hide individual metaframe bordershideOptions: Hide options panelhideMetaframeLabels: Hide metaframe labels
Returns: { setInputs, setOutputs, dispose, metapage }
Metapage Class
Methods:
setDefinition(def, state?): Set the metapage definition; emitsDefinitionon subsequent calls onlyupdateDefinition(def, state?): Set the definition and always emitDefinitionUpdatewith added/removed diff (see updateDefinition)setInputs(inputs): Set inputs for metaframesgetState(): Get current state (inputs/outputs)injectSecrets(secrets): Inject secrets into metaframe URLs (see Secrets)dispose(): Clean up and remove all listenerson(event, handler): Listen to events
Events:
Metapage.OUTPUTS: When metaframe outputs changeMetapage.INPUTS: When metapage inputs changeMetapage.DEFINITION: When definition changes (not emitted on firstsetDefinitioncall)Metapage.DEFINITION_UPDATE: WhenupdateDefinitionis called; payload includes added/removed metaframe diff (see updateDefinition)Metapage.STATE: When metapage state changes
Metaframe Class
Methods:
setOutput(name, value): Set a single outputsetOutputs(outputs): Set multiple outputsgetInput(name): Get a single input valuegetInputs(): Get all input valuesonInput(name, callback): Listen to specific inputonInputs(callback): Listen to all inputsdispose(): Clean up
Properties:
id: Metaframe ID assigned by parent metapageisInputOutputBlobSerialization: Enable/disable automatic binary serialization
Creating Your Own Metaframes
Any web application can become a metaframe by:
- Loading the library
- Creating a
Metaframeinstance - Listening for inputs
- Sending outputs
Example minimal metaframe:
<!DOCTYPE html>
<html>
<head>
<title>My Metaframe</title>
</head>
<body>
<script type="module">
import { Metaframe } from "https://cdn.jsdelivr.net/npm/@metapages/[email protected]";
const metaframe = new Metaframe();
metaframe.onInputs((inputs) => {
// Your logic here
metaframe.setOutput("result", "processed: " + JSON.stringify(inputs));
});
</script>
</body>
</html>TypeScript Support
Full TypeScript definitions are included:
import {
Metapage,
Metaframe,
MetapageDefinition,
MetaframeInputMap,
MetapageInstanceInputs,
} from "https://cdn.jsdelivr.net/npm/@metapages/[email protected]";
const definition: MetapageDefinition = {
metaframes: {
example: {
url: "https://example.com",
},
},
};
const metapage = new Metapage({ definition });Browser Support
- Chrome 78+
- Modern browsers with ES2020 support
- ES modules required
License
Apache-2.0
Contributing
Issues and pull requests welcome at https://github.com/metapages/metapage
