FrameMenuCard frame.menu-card@1
Generated at from
openvibe-contracts v0.419.0 and
openvibe-sdk v0.161.0.
- Version
- 1.0.0
- Owner
- network
- Visibility
- public
- Status
- active
- Compatibility
- backward
- Schema
https://openvibe.network/contracts/frame/menu-card.v1.json
frame.menu-card@1: one card a site publishes for the network's Frame menu. Each open site serves an array of at most three cards at /.well-known/openvibe-menu.json (frame.menu-cards@1); Network fetches that public file every five minutes, validates every card here, strips markup from the text and ranks the cards in GET /api/frame. A card is one kind of menu block: a rail (the live rail), rows (a ranked list) or chips (a short row of words). Every item links to https on the site's own origin or an openvibe.* host; Network refuses another origin. An item may also carry a small picture (image) of the same shape: https on the site's own origin or an openvibe.* host, with an optional 16–1920 pixel width and height and a short alt text. The text is plain words: markup is stripped, unknown fields are dropped and a file that fails this schema leaves the site's last good card in place for 30 minutes.
Fields
| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
id | string | yes | The card's stable id in its site (live-now, new-worlds, trending). Two cards of one site never share it. |
|
site | string | yes | The Frame site id that publishes the card (live, games, media-hub). Network checks it against the host the file came from. |
|
title | string | yes | The card's heading, in the owner's words. |
|
kind | enum | yes | The block the Frame draws: a rail of live channels, a ranked list of rows, or a short row of chips. |
|
items | array of object | yes | The card's entries, in the site's own order. Empty means the site has a card but nothing to show (nobody is live); Network then skips it. |
|
items[].title | string | yes | The entry's name, plain words. |
|
items[].meta | string | One short line under the name: "12 watching", "7,412 views this week", "3 players". |
| |
items[].url | string | yes | Where the entry opens: https on the publishing site's own origin or an openvibe.* host. |
|
items[].image | object | A small picture of the entry: a video's thumbnail, a world's cover or a stream's preview. Optional; a site with no real picture leaves it out rather than sending a placeholder. |
| |
items[].image.src | string | yes | The picture's address: https on the publishing site's own origin or an openvibe.* host. Network drops an image from another host. |
|
items[].image.w | integer | The picture's natural width in pixels, when the site knows it. |
| |
items[].image.h | integer | The picture's natural height in pixels, when the site knows it. |
| |
items[].image.alt | string | A short description of the picture for anyone who cannot see it. May be empty when the entry's title already says it. |
| |
more | object |
| ||
more.label | string | yes | The link's words ("See all", "More worlds"). |
|
more.url | string | yes | https on the publishing site's own origin or an openvibe.* host. |
|
updated_at | string | yes | When the site last built the card. |
|
ttl_s | integer | yes | How long the site considers the card fresh, in seconds. Live uses 30; Network itself refetches every five minutes. |
|
Examples
From the contract's own test fixtures: valid ones validate, rejected ones must fail.
Valid: empty-rail
{
"id": "live-now",
"site": "live",
"title": "Live now",
"kind": "rail",
"items": [],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 30
}Valid: live-now
{
"id": "live-now",
"site": "live",
"title": "Live now",
"kind": "rail",
"items": [
{
"title": "Nova",
"meta": "12 watching",
"url": "https://openvibe.live/@nova",
"image": {
"src": "https://openvibe.live/t/nova.jpg?w=640",
"w": 640,
"h": 360,
"alt": "Nova at the desk"
}
},
{
"title": "Kitchen radio",
"meta": "3 watching",
"url": "https://openvibe.live/@kitchen"
}
],
"more": {
"label": "See all",
"url": "https://openvibe.live/live"
},
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 30
}Valid: new-worlds
{
"id": "new-worlds",
"site": "games",
"title": "New worlds",
"kind": "rows",
"items": [
{
"title": "Harbour Town",
"meta": "Built by 4 people",
"url": "https://openvibe.games/w/harbour-town",
"image": {
"src": "https://openvibe.games/worlds/harbour-town/cover.jpg",
"w": 1200,
"h": 675
}
},
{
"title": "Night Market",
"meta": "18 visitors this week",
"url": "https://openvibe.games/w/night-market"
}
],
"updated_at": "2026-11-07T07:58:00Z",
"ttl_s": 300
}Rejected: bad-kind
{
"id": "trending",
"site": "media-hub",
"title": "Trending",
"kind": "grid",
"items": [],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 300
}Rejected: http-image
{
"id": "trending",
"site": "media-hub",
"title": "Trending",
"kind": "rows",
"items": [
{
"title": "A walk",
"url": "https://openvibe.video/v/1",
"image": {
"src": "http://openvibe.media/t/1.jpg",
"w": 640,
"h": 360
}
}
],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 300
}Rejected: http-url
{
"id": "trending",
"site": "media-hub",
"title": "Trending",
"kind": "rows",
"items": [
{
"title": "A clip",
"meta": "2,100 views",
"url": "http://openvibe.video/watch/abc"
}
],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 300
}Rejected: image-alt-too-long
{
"id": "live-now",
"site": "live",
"title": "Live now",
"kind": "rail",
"items": [
{
"title": "Nova",
"url": "https://openvibe.live/@nova",
"image": {
"src": "https://openvibe.live/t/nova.jpg",
"alt": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 30
}Rejected: image-no-src
{
"id": "live-now",
"site": "live",
"title": "Live now",
"kind": "rail",
"items": [
{
"title": "Nova",
"url": "https://openvibe.live/@nova",
"image": {
"w": 640,
"h": 360
}
}
],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 30
}Rejected: image-unknown-field
{
"id": "live-now",
"site": "live",
"title": "Live now",
"kind": "rail",
"items": [
{
"title": "Nova",
"url": "https://openvibe.live/@nova",
"image": {
"src": "https://openvibe.live/t/nova.jpg",
"credit": "Nova"
}
}
],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 30
}Rejected: image-width-too-small
{
"id": "live-now",
"site": "live",
"title": "Live now",
"kind": "rail",
"items": [
{
"title": "Nova",
"url": "https://openvibe.live/@nova",
"image": {
"src": "https://openvibe.live/t/nova.jpg",
"w": 8,
"h": 360
}
}
],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 30
}Rejected: seven-items
{
"id": "trending",
"site": "media-hub",
"title": "Trending",
"kind": "rows",
"items": [
{
"title": "One",
"url": "https://openvibe.video/one"
},
{
"title": "Two",
"url": "https://openvibe.video/two"
},
{
"title": "Three",
"url": "https://openvibe.video/three"
},
{
"title": "Four",
"url": "https://openvibe.video/four"
},
{
"title": "Five",
"url": "https://openvibe.video/five"
},
{
"title": "Six",
"url": "https://openvibe.video/six"
},
{
"title": "Seven",
"url": "https://openvibe.video/seven"
}
],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 300
}Rejected: title-too-long
{
"id": "live-now",
"site": "live",
"title": "Live now, and everything happening across the whole network tonight",
"kind": "rail",
"items": [],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 30
}Rejected: ttl-too-short
{
"id": "live-now",
"site": "live",
"title": "Live now",
"kind": "rail",
"items": [],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 5
}Rejected: unknown-field
{
"id": "trending",
"site": "media-hub",
"title": "Trending",
"kind": "rows",
"items": [],
"updated_at": "2026-11-07T08:00:00Z",
"ttl_s": 300,
"html": "<script>alert(1)</script>"
}Validate
const contracts = require('openvibe-contracts');
contracts.validate('frame.menu-card@1', value); // { valid, errors: [{ path, message }] }