Skip to content

metapages/metapage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

274 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@metapages/metapage

Build composable, interconnected web applications using iframes and data pipes

npm version

@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/metapage

Or use directly from CDN:

import {
  renderMetapage,
  Metapage,
  Metaframe,
} from "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.1";

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": {
        ...
      }
    }
  }
}

Implentation in source code

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/metapage@1.10.1";

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: inputprocessoroutput

Metaframe Definition

See code

This is provided either by:

  • https://<your metaframe>/metaframe.json
  • https://<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/metapage@1.10.1";

      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/metapage@1.10.1";

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/metapage@1.10.1";

// 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 setHashParamValueBase64EncodedInUrl from @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() or removeAll()

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 DefinitionUpdate event — even on the very first call
  • Includes a structured diff of which metaframes were added and removed
  • Automatically emits a State event 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 state argument is passed and is non-empty

API Overview

renderMetapage(options)

Render a metapage into a DOM element.

Parameters:

  • definition: Metapage definition object
  • rootDiv: DOM element to render into
  • onOutputs: Callback for metaframe outputs (optional)
  • options: Rendering options (optional)
    • hideBorder: Hide metapage border
    • hideFrameBorders: Hide individual metaframe borders
    • hideOptions: Hide options panel
    • hideMetaframeLabels: Hide metaframe labels

Returns: { setInputs, setOutputs, dispose, metapage }

Metapage Class

Methods:

  • setDefinition(def, state?): Set the metapage definition; emits Definition on subsequent calls only
  • updateDefinition(def, state?): Set the definition and always emit DefinitionUpdate with added/removed diff (see updateDefinition)
  • setInputs(inputs): Set inputs for metaframes
  • getState(): Get current state (inputs/outputs)
  • injectSecrets(secrets): Inject secrets into metaframe URLs (see Secrets)
  • dispose(): Clean up and remove all listeners
  • on(event, handler): Listen to events

Events:

  • Metapage.OUTPUTS: When metaframe outputs change
  • Metapage.INPUTS: When metapage inputs change
  • Metapage.DEFINITION: When definition changes (not emitted on first setDefinition call)
  • Metapage.DEFINITION_UPDATE: When updateDefinition is called; payload includes added/removed metaframe diff (see updateDefinition)
  • Metapage.STATE: When metapage state changes

Metaframe Class

Methods:

  • setOutput(name, value): Set a single output
  • setOutputs(outputs): Set multiple outputs
  • getInput(name): Get a single input value
  • getInputs(): Get all input values
  • onInput(name, callback): Listen to specific input
  • onInputs(callback): Listen to all inputs
  • dispose(): Clean up

Properties:

  • id: Metaframe ID assigned by parent metapage
  • isInputOutputBlobSerialization: Enable/disable automatic binary serialization

Creating Your Own Metaframes

Any web application can become a metaframe by:

  1. Loading the library
  2. Creating a Metaframe instance
  3. Listening for inputs
  4. 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/metapage@1.10.1";

      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/metapage@1.10.1";

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

More Resources