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

FieldTypeRequiredDescriptionConstraints
idstringyesThe card's stable id in its site (live-now, new-worlds, trending). Two cards of one site never share it.
  • pattern ^[a-z][a-z0-9-]{1,39}$
sitestringyesThe Frame site id that publishes the card (live, games, media-hub). Network checks it against the host the file came from.
  • pattern ^[a-z][a-z0-9-]{1,39}$
titlestringyesThe card's heading, in the owner's words.
  • minLength 1
  • maxLength 60
kindenumyesThe block the Frame draws: a rail of live channels, a ranked list of rows, or a short row of chips.
  • one of "rail", "rows", "chips"
itemsarray of objectyesThe 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.
  • maxItems 6
  • items: no other fields
items[].titlestringyesThe entry's name, plain words.
  • minLength 1
  • maxLength 80
items[].metastringOne short line under the name: "12 watching", "7,412 views this week", "3 players".
  • maxLength 120
items[].urlstringyesWhere the entry opens: https on the publishing site's own origin or an openvibe.* host.
  • pattern ^https://
  • format uri
  • maxLength 400
items[].imageobjectA 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.
  • no other fields
items[].image.srcstringyesThe picture's address: https on the publishing site's own origin or an openvibe.* host. Network drops an image from another host.
  • pattern ^https://
  • format uri
  • maxLength 400
items[].image.wintegerThe picture's natural width in pixels, when the site knows it.
  • minimum 16
  • maximum 1920
items[].image.hintegerThe picture's natural height in pixels, when the site knows it.
  • minimum 16
  • maximum 1920
items[].image.altstringA short description of the picture for anyone who cannot see it. May be empty when the entry's title already says it.
  • maxLength 120
moreobject
  • no other fields
more.labelstringyesThe link's words ("See all", "More worlds").
  • minLength 1
  • maxLength 24
more.urlstringyeshttps on the publishing site's own origin or an openvibe.* host.
  • pattern ^https://
  • format uri
  • maxLength 400
updated_atstringyesWhen the site last built the card.
  • format date-time
ttl_sintegeryesHow long the site considers the card fresh, in seconds. Live uses 30; Network itself refetches every five minutes.
  • minimum 30
  • maximum 86400

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 }] }