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