Skip to content

A front on hub-core

@filudi/hub-core is public on npmjs.com under the MIT licence. It is the package filudi.com is built on: the mechanics of a site front, with no appearance. You draw every screen.

Install

Terminal window
pnpm add -E @filudi/hub-core preact

-E pins the exact version. The package is ESM with four entry points: @filudi/hub-core, @filudi/hub-core/preact (hooks for islands), @filudi/hub-core/cmp (a TCF 2.2 consent manager) and @filudi/hub-core/tokens.css. Import tokens.css once in your stylesheet: it defines the colours, type scale, spacing and radii as CSS variables, with a light theme under [data-theme='light'].

fetchCatalog(apiUrl, site, { locale }) is a typed GET /v1/catalog/{site}. It answers the site row, its categories and tags, and the games the site lists and shows, in site order, with texts per locale, media and rating. The helpers read it: textsFor(game, locale), navCategories(catalog), tagsOf(catalog), filterGames(...) for category pages, search(...) for accent-insensitive search.

Catalog

import { fetchCatalog, textsFor } from '@filudi/hub-core';
const API = 'https://api.filudi.com';
const SITE = 'string'; // your site's slug
const catalog = await fetchCatalog(API, SITE, { locale: 'en' });
for (const game of catalog.games) {
const texts = textsFor(game, 'en');
console.log(game.slug, texts.title, game.bundleUrl);
}

A game with bundleUrl: null has no live version: show it without a play button, or leave it out.

PlayerClient.for(apiUrl, site) is one instance per page. ensure() creates a guest on the first visit and keeps the token in localStorage under filudi:player:<site>. The same object signs the player in by magic link, by the code of that email or by password, sets the nickname, lists the saves, rates, comments and sends feedback. A 401 forgets the token and the next call creates a new guest.

Player

import { PlayerClient } from '@filudi/hub-core';
const player = PlayerClient.for(API, SITE);
await player.ensure();
await player.setNickname('Pebble');
await player.requestMagicLink(email, '/account/');
// On /account/verify?t=<token>:
const { merged, redirect } = await player.consumeMagicLink(token);

The sign-in email links to https://<your first domain>/account/verify?t=<token>, and to /<locale>/account/verify for a locale other than your first. You build those pages; they call consumeMagicLink(t). Serve them with Cache-Control: private, no-store. The email also shows a 6-digit code, for a link that opens in another browser: offer a code field on the page that asked for the link, and call consumeCode(email, code).

GameFrame creates the sandboxed iframe and runs the parent side of the handshake: it answers filudi:ready with filudi:init, exchanges the player token for a game token through gameSession, refreshes that token every 12 hours, forwards consent and locale, and reports the frame state. It draws nothing: you draw the loading, unavailable and offline screens from frame.state.

GameFrame

import { fetchCatalog, framePolicy, GameFrame, PlayerClient, textsFor } from '@filudi/hub-core';
const NO_CONSENT = { ads: false, analytics: false };
const catalog = await fetchCatalog(API, SITE, { locale: 'en' });
const game = catalog.games.find((entry) => entry.slug === 'gloop');
const player = PlayerClient.for(API, SITE);
await player.ensure();
const frame = new GameFrame(document.querySelector('#game'), {
game: game.slug,
bundleUrl: game.bundleUrl,
apiUrl: API,
site: SITE,
locale: 'en',
policy: framePolicy(catalog.site.policy, NO_CONSENT),
consent: NO_CONSENT,
gameVersion: game.version ?? '',
gameSession: (slug) => player.gameSession(slug),
renewPlayer: async () => {
player.signOut();
await player.ensure();
},
title: textsFor(game, 'en').title,
orientation: game.orientation,
});
frame.on('state', ({ state }) => {
// loading, ready, playing, unavailable, offline
});

What to know:

  • Call player.ensure() before mounting, so the token exists when the frame asks for the game token.
  • loading turns unavailable after 20 s without filudi:ready. Draw an indeterminate bar from loadStartedAt; progress is not visible from the parent. Call frame.retry() on a “Try again” button.
  • frame.fullscreen() needs a user gesture. A game’s own request is honoured only when the player just clicked inside it.
  • save, score and track events are claims made by the game. Read the real values from the API with the player’s token; never grant anything on an event.
  • ads defaults to NoAds: every ad request answers unavailable. Implement AdBridge to run a network.
  • Pass the visitor’s real consent once your consent manager has an answer, then frame.setConsent() on change (useGameFrame does it). @filudi/hub-core/cmp is a TCF 2.2 CMP if you want one.
  • GameFrame sends frame_loaded to the API when the bundle posts filudi:loaded, which is the first step of the funnel in your statistics.
  • frame.destroy() when the page goes away.

Your pages send a CSP that lets the frame load and the page call the API, and nothing lets a bundle onto your origin:

frame-src https://play.filudi.com https://*.<play domain>
connect-src 'self' https://api.filudi.com
img-src 'self' data: https://play.filudi.com
media-src https://play.filudi.com

GameFrame throws on a bundle URL from the page’s own origin: with allow-same-origin, such a bundle could read your localStorage.

The SDK playground of the monorepo (pnpm --filter @filudi/sdk-playground dev, http://localhost:5173) plays the host around a bundle and logs every SDK call and every host message. Load Gloop! or your own bundle on the filudi host and read what filudi:init carries and what the game posts back; that is what your front exchanges with the frame. Its “Mock API” answers /v1/sdk/* in memory, so a front can be built before a key or an approved domain exists.

seo helpers give you VideoGame, WebSite, CollectionPage and BreadcrumbList JSON-LD, OpenGraph tags, hreflang links, sitemap.xml, robots.txt and canonical URLs. Analytics sends page-level events (share, search, fullscreen, feedback sent) to a CausalIT domain through your own proxy, gated by consent when you pass it. The API counts game_start, account_linked and the other server events itself, so a front never sends them twice.