{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/embed-catalog",
  "slug": "packages/embed-catalog",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/embed-catalog — the built-in embeddable pages",
  "name": "@leumas/embed-catalog",
  "eyebrow": "the built-in embeddable pages",
  "chip": null,
  "summary": "The built-in embeddable-page catalogue — games and tools, generated from shared/knowledge/iframe-urls, verified live and frameable at generate time, and hydrated into the iframes collection. Taxonomy...",
  "keywords": [
    "embed-catalog",
    "iframe-urls",
    "frameable",
    "embed-catalog arcade",
    "taxonomy",
    "embed catalog api",
    "leumas embed catalog",
    "how to use embed catalog"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# @leumas/embed-catalog — the built-in embeddable pages\n\nThe games and tools Studio's **Dynamic → Iframes** page opens out of the box, plus the schema and the\nboot-time hydration that put them there. The embed POLICY itself is not here — that is\n`@leumas/arcade` (`buildEmbedAttrs`, `sanitizeSandbox`, `isSafeUrl`), and it stays in one place.\n\n## What is in the catalogue, and what is not\n\n`src/catalog.json` is **generated** from `shared/knowledge/iframe-urls/{games,tools}.txt` by\n`ops/tools/embed-catalog/generate.mjs`. Do not hand-edit it — add a URL to the `.txt` and regenerate.\n\nThe generator does not trust the source lists. It **contacts every URL once** and keeps an entry only\nif it answers 2xx, permits cross-origin framing, **and serves the thing the line claimed**. As of the\nlast run that was **500 of 1,233** — 360 games and 140 tools:\n\n| dropped | why |\n|---:|---|\n| 509 | `http-404` — the URL does not exist. Mostly guessed slugs: 164 of 225 `retrogames.cc` entries, 245 of 508 `miniplay` |\n| 180 | `x-frame-options: sameorigin` — loads in a browser, can never load in a frame. **Every Coolmath Games entry** |\n| 114 | `duplicate-url` — the same embed listed twice under two names |\n| 36 | `serves something else` — 200, frameable, and **the wrong game** (see below) |\n| 8 | `unreachable` — `retrogamesonline.io` does not resolve |\n\n### [critical] Why a 200 is not enough\n\n`miniplay.com/embed/2048` answers 200, frames happily, and plays **Get Set Boogie**. Twenty-seven\n`retrogames.cc` slugs do the same with a different ROM each. These portals substitute another game\nfor a delisted slug instead of 404'ing, so a catalogue built on status codes alone ships tiles that\nlie about what they open — and nothing catches it except loading one and looking at it.\n\nSo the generator reads the served `<title>` and compares. Two rules keep that from over-firing:\n\n- **a generic title is not evidence.** GameFlare prints `Free Online Games | Gameflare.com` on every\n  embed, which identifies nothing; 30 good entries would otherwise have been dropped.\n- **the check runs on games only.** Substitution is a games-portal behaviour, while a tool's title is\n  marketing copy rather than a name — `Draw.io Diagram Editor` serves \"Flowchart Maker & Online\n  Diagram Software\", `Minesweeper` serves \"Buscaminas\". Holding tools to a name match dropped six\n  good ones on the first run.\n\n## Pictures\n\nEvery row carries one `thumb` field with two possible sources, and a consumer renders it the same\neither way:\n\n- **a game's cover art**, hotlinked from the provider's own CDN. The portals publish it in\n  `og:image`, so it costs nothing to collect — the same fetch that reads `<title>` reads this.\n  GameFlare keeps it only on `/online-game/<slug>/` rather than on the embed page, so that one\n  provider gets a second request. **353 of 360 games** get real artwork this way.\n- **a screenshot of the tool**, at `/embeds/<id>.webp`, taken by `pnpm embeds:thumbs` and served out\n  of Studio's `public/`. This is what the tools half gets, and it is the better preview anyway: you\n  see the actual tool rather than a logo.\n\n[critical] **A generic card is not a cover.** utilitydocker serves `og-default.png` for all 78 of its pages\nand quill.tools renders one \"138 Free Online Tools\" banner for all 48 of theirs. Honouring those\nwould put the identical picture on 126 cards, which reads as a rendering bug rather than as a missing\nthumbnail — so `coverFrom` rejects them and they fall through to a screenshot.\n\n**Hotlinked art can rot**, and the consumer must expect it: a provider can move, expire or start\nrefusing the file. Studio's card stacks the image over a glyph and hides it on `error`, so a dead\ncover degrades to an icon with no reflow rather than to a torn-image box.\n\nEvery one is kept, with its reason, in `src/rejects.json`. A 404 today can be a live URL next month;\n`pnpm embeds:verify` re-probes both files and reports the drift without writing.\n\n**Why this is done at build time and cannot be done at runtime:** Chrome fires an iframe's `load`\nevent even on an X-Frame-Options-blocked frame, so a page watching `onLoad` cannot tell \"rendered\"\nfrom \"refused\". The probe result is the only reliable answer, and it has to be taken from outside a\nbrowser.\n\n## Exports\n\n- `EMBED_CATALOG` / `EMBED_REJECTS` — the two generated lists, frozen.\n- `findEmbed(id)` — one entry by its `<provider-slug>-<title-slug>` id.\n- `EMBEDS_COLLECTION` (`'iframes'`) and `embedSchema()` — the record contract. `validate: false` on\n  purpose: this collection held hand-added rows before it had a schema, and enforcing a guessed field\n  list would 422 edits to rows nobody here wrote.\n- `hydrateEmbedCatalog({ adapter, upsert })` — registers the schema if absent, then upserts the\n  catalogue with `hydrateCollection`. Called once at API boot.\n- `@leumas/embed-catalog/taxonomy` — `providerFor`, `categoryFor`, `tagsFor`, `PROVIDERS`, `CATEGORIES`.\n\nThere is deliberately **no facet-counting helper**: `useCollection` in `@leumas/ui` already counts a\nfacet's options against the rows surviving every *other* facet, and a copy here would be a worse one.\n\n## How far to trust a field\n\n`kind` and `provider` are facts — which list the line came from, which host serves it. `httpStatus`\nand `verifiedAt` are a measurement. **`category` is a keyword guess**, and the two halves of the list\nare not equally guessable: tool slugs are descriptive (`/embed/pdf-watermark`) so the pass is close to\nexact, while game titles carry almost no signal — `OvO`, `Dotless`, `Paint Race`. About a third of the\ngames are therefore `Uncategorised`, which is a real answer. Filing them under a confident wrong genre\nwould make the facet wrong a third of the time with no way for a reader to tell which third.\n\n## Ownership, and who may change a built-in\n\n`hydrateCollection` writes through the adapter, not the CRUD router, so seeded rows carry **no\n`owner`** and are marked `public: true` (without which nobody could read them — reads are\nowner-or-public filtered). Since `canWrite === isOwner`:\n\n- an **admin** may edit or delete a built-in;\n- a **member** may not — one member deleting a shared row would delete it for the whole install;\n- **anyone may add their own**, as a normal owner-stamped row in the same collection. It shows up in\n  the same grid, and the hydration's prune cannot reach it: it carries no `seed` flag.\n\nStudio's Duplicate action is what turns a built-in into something you own — it creates a fresh row\nrather than trying to edit one you cannot.\n\n**Run `embeds:thumbs` AFTER `embeds:generate`** — the generator rewrites `catalog.json` from scratch,\nso the local `/embeds/...` paths are stamped back on by the thumbs pass, which reads the directory\nrather than trusting what it captured this run. Files are skipped when they already exist, so a\nre-run only shoots what is new.\n\nBump `SEED_VERSION` in `src/hydrate.js` when a regenerated catalogue should overwrite untouched rows\nand prune the ones that dropped out of it. Rows a person edited (`userModified`) are never clobbered.\n[critical] **Adding a FIELD needs that bump too** — `hydrateCollection` skips a row whose `seedVersion`\nalready matches, so a new column simply never lands on an install that has already seeded. That is\nhow the cards first came back with no pictures despite a catalogue full of them.\n\n## Regenerating\n\n```\npnpm embeds:generate          # probe every URL, rewrite catalog.json + rejects.json (~3 min)\npnpm embeds:thumbs            # screenshot every row the providers gave no cover for (~15 min)\npnpm embeds:verify            # re-probe, report drift, write nothing\nnode ops/tools/embed-catalog/generate.mjs --offline   # taxonomy only, no network\n```\n\n## Caveat — license review pending\n\nThese are third-party embeds, and `@leumas/arcade`'s README already carries the same warning for its\nown registry: rights and attribution are unconfirmed. This surface is Studio-admin-only, which is why\nthat is not blocking today. Settle it before any of this reaches a public page.\n",
  "source": {
    "path": "shared/packages/embed-catalog/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 8408,
    "hash": "2599985c0eb023b6aab2785c64dd41132e75bfda"
  },
  "urls": {
    "html": "/p/packages/embed-catalog",
    "json": "/docs/packages/embed-catalog.json",
    "md": "/docs/packages/embed-catalog.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  },
  "exports": {
    "total": 15,
    "component": 7,
    "hook": 0,
    "helper": 8,
    "names": [
      {
        "n": "CATEGORIES",
        "k": "component"
      },
      {
        "n": "EMBED_CATALOG",
        "k": "component"
      },
      {
        "n": "EMBED_REJECTS",
        "k": "component"
      },
      {
        "n": "EMBEDS_COLLECTION",
        "k": "component"
      },
      {
        "n": "EMBEDS_COLLECTION",
        "k": "component"
      },
      {
        "n": "PROVIDERS",
        "k": "component"
      },
      {
        "n": "SEED_VERSION",
        "k": "component"
      },
      {
        "n": "categoryFor",
        "k": "helper"
      },
      {
        "n": "embedSchema",
        "k": "helper"
      },
      {
        "n": "embedSchema",
        "k": "helper"
      },
      {
        "n": "findEmbed",
        "k": "helper"
      },
      {
        "n": "hydrateEmbedCatalog",
        "k": "helper"
      },
      {
        "n": "hydrateEmbedCatalog",
        "k": "helper"
      },
      {
        "n": "providerFor",
        "k": "helper"
      },
      {
        "n": "tagsFor",
        "k": "helper"
      }
    ]
  }
}
