npm package: @metapages/metapage
Description: Connect web pages together
Author: Dion Whitehead
Latest version: 1.10.5

# @metapages/metapage

> Build composable, interconnected web applications using iframes and data pipes

[![npm version](https://badge.fury.io/js/%40metapages%2Fmetapage.svg)](https://www.npmjs.com/package/@metapages/metapage)

**@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

- 📚 [Full Documentation](https://docs.metapage.io/docs)
- 🎨 [Live Examples](https://metapage.io)
- 🛠️ [Interactive Tools](https://module.metapage.io)
- 💻 [Example Component](https://js.mtfm.io)

## 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

```bash
npm install @metapages/metapage
```

Or use directly from CDN:

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

## Quick Start

### Rendering a Metapage

The simplest way to embed a workflow is using `renderMetapage`:

```javascript
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`:

```json
{
  "meta": {
    "layouts": {
      "react-grid-layout": {
        ...
      }
    }
  }
}
```

[Implentation in source code](https://github.com/metapages/metapage/blob/2ef8bd7bfb151ad1616da46aa9797bcf2b1c3d78/app/libs/src/metapage/metapageRenderer.ts#L204)

### Creating a Metaframe (Inside an iframe)

If you're building a component to use in a metapage:

```javascript
import { Metaframe } from "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.5";

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:

```javascript
{
  "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](https://github.com/metapages/metapage/blob/73549f9f3e27da7a2aef5a935ec112c5bdb130db/app/libs/src/metapage/v2/metaframe.ts#L40)

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)

```typescript
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:

```javascript
{
  "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:

```javascript
// 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

```html
<!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.5";

      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

```javascript
import { Metaframe } from "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.5";

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

```javascript
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:

```javascript
import {
  getHashParamValueJsonFromWindow,
  setHashParamValueJsonInWindow,
} from "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.5";

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

```javascript
{
  "inputs": [
    {
      "metaframe": "source",
      "source": "data/*",     // Matches data/foo, data/bar, etc.
      "target": "inputs/"
    }
  ]
}
```

### Binary Data Handling

```javascript
// 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

```typescript
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

```typescript
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

```typescript
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

```typescript
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:

```typescript
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

```typescript
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](#updatedefinition))
- `setInputs(inputs)`: Set inputs for metaframes
- `getState()`: Get current state (inputs/outputs)
- `injectSecrets(secrets)`: Inject secrets into metaframe URLs (see [Secrets](#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](#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:

```html
<!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.5";

      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:

```typescript
import {
  Metapage,
  Metaframe,
  MetapageDefinition,
  MetaframeInputMap,
  MetapageInstanceInputs,
} from "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.5";

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](https://github.com/metapages/metapage)

## More Resources

- [Documentation](https://docs.metapage.io)
- [Examples Gallery](https://metapage.io)
- [Create Metaframes](https://js.mtfm.io)
- [Community](https://github.com/metapages/metapage/discussions)