Skip to content

CDN delivery ​

Every @gomagaming/* widget can be loaded straight from the Goma CDN: no npm install, no bundler, no build step. Drop a script tag on a page and the custom elements are defined.

This is the alternative to Installation. Both deliver the same widget bytes; they differ in who resolves the dependencies.

Which one do I want? ​

npm installCDN
Build stepRequiredNone
Host appAny bundlerAny page, any framework, or none
UpgradesBump package.json, rebuild, redeployNothing: follows the release
Dependency resolutionYour bundlerThe browser, via an import map
NeedsAn npm tokenYour origin registered with Goma Gaming. No token goes in the page: a browser cannot send one. You still hold a token for CI and builds, and to mint signed URLs if you want a hard gate

Use npm when the widgets sit inside an application you already build and deploy. Use the CDN when you want them on a page you don't compile (a CMS template, a landing page, a server-rendered site) or when you want upgrades to land without a redeploy on your side.

Before you start ​

Ask your Goma Gaming contact to register the origins you'll serve from: for example https://acme.bet and https://staging.acme.dev. Browser delivery is gated on the Origin header, so an unregistered origin gets a 403 and nothing renders.

Registration is per-origin, and origins are exact. https://acme.bet does not cover https://www.acme.bet, and a local dev origin such as http://localhost:5173 has to be registered too if you want to develop against the CDN.

Never put an npm token in a page

A read token for @gomagaming lets anyone reading your page source install every private package in the scope. A browser also cannot send an Authorization header on an import() (there is no API for it) so a token in your HTML would not authenticate anything anyway. If you need a credential rather than an origin, use signed URLs.

One classic script tag installs everything and imports the widgets you name:

html
<script
  src="https://cdn.gomagaming.com/-/loader.js?w=events-horizontal,betslip-floating"
  crossorigin
></script>

<goma-events-horizontal></goma-events-horizontal>
<goma-betslip-floating></goma-betslip-floating>

That's the whole integration. No version appears anywhere: the CDN resolves the widget set from the current release, so upgrading is something Goma Gaming does on their side rather than an edit on yours.

Parameter
?w=events-horizontal,hero-cardWhich widgets this page needs. Names only: a version here is refused; pinning is ?release=
(no ?w=)Everything in the release you are licensed for
?release=1.0.0Pin the whole set. Also accepts a dist-tag, e.g. ?release=stable

Two rules that will bite you ​

It must be a classic script. Not type="module". An import map cannot be installed once a module script has started, so the loader has to run first and synchronously.

It must come before any module script, and you must not add your own import-map tag to the page. If the loader finds one it logs a warning and installs its map anyway: it does not stop. Most browsers ignore the second map, so the widgets fail to resolve and the only clue is that warning. Use the loader or a manual map, never both.

The loader finishes asynchronously ​

This is the single most useful thing to understand about the loader, because it explains nearly every "it works locally but not on the page" report.

The loader does two things, and only the first is synchronous:

  1. It installs the import map: immediately, while the classic script runs.
  2. It imports the widget modules: asynchronously, over the network.

So by the time your own <script type="module"> runs, the map exists but the <goma-*> elements are usually not defined yet. They are still unknown elements, and they stay that way for as long as the module downloads take.

That is fine for markup and attributes, which the browser applies whenever the element is upgraded:

html
<!-- Safe at any time. Attributes survive upgrade. -->
<goma-region-filter sport-id="8" locale="en"></goma-region-filter>

It also now applies to properties. Anything you assign before the definition arrives is replayed through the widget once it upgrades:

js
// Safe. Applied when the element upgrades, even if that is seconds later.
document.getElementById('rf').config = { socketUrl: '…', socketRealm: '…' }

Widgets published at 0.34.0 and earlier

That property rescue is recent. Against an older release set, a property assigned before the widget module finished loading was silently discarded: it became a plain own property that permanently shadowed the widget's setter. The widget mounted, emitted ready, and sat there with no config, no socket connection and nothing in the console. If you are pinned to an older release, await ready before assigning anything (below), or set configuration through attributes, which were never affected.

Waiting for the widgets ​

The loader exposes window.__gomaCdn:

js
const { release, imports, widgets, ready } = window.__gomaCdn

await ready // resolves once every widget module has evaluated

You do not need it just to configure a widget. You do need it when your code has to talk to an element rather than only feed it: reading a property back, calling a method, or mounting an app that renders <goma-*> tags itself and expects them defined at first render:

js
const ready = globalThis.__gomaCdn?.ready ?? Promise.resolve()

ready
  .catch((error) => console.error('CDN widgets failed to load:', error))
  .then(() => createApp(App).mount('#app'))

Mount regardless of the outcome, as above. A custom element that never got defined is an inert empty tag, so a CDN problem costs you those widgets rather than your whole page.

To check whether a specific widget has arrived without waiting for all of them:

js
await customElements.whenDefined('goma-region-filter')

2. The manual import map ​

If you need to pin per-package versions, or assemble the map into one you already maintain, ask the CDN for it at build time and inline the result:

GET https://cdn.gomagaming.com/-/importmap?p=events-horizontal@0.26.0,hero-card@0.26.0
json
{
  "imports": {
    "vue": "https://cdn.gomagaming.com/npm/vue@3.5.42/dist/vue.esm-browser.prod.js",
    "@gomagaming/core": "https://cdn.gomagaming.com/@gomagaming/core@0.26.0/dist/core.js"
  },
  "widgets": [
    {
      "pkg": "@gomagaming/events-horizontal",
      "version": "0.26.0",
      "url": "https://cdn.gomagaming.com/@gomagaming/events-horizontal@0.26.0/dist/events-horizontal.js"
    }
  ]
}

imports is the import map. widgets is not part of it: those are the URLs to import once the map is installed:

html
<script type="importmap">
  { "imports": { "...": "..." } }
</script>

<script type="module">
  await import('https://cdn.gomagaming.com/@gomagaming/events-horizontal@0.26.0/dist/events-horizontal.js')
</script>

Inline the map: <script type="importmap" src> has patchy browser support. Generate it at build time; fetching it at runtime delays every widget by a round trip.

Reaching the transport layer from your own code ​

The map covers every bare @gomagaming/* specifier the release set carries. A release set that includes the shared socket also carries @gomagaming/network, and that is what lets a page on this lane join the widgets' socket for its own data:

html
<script type="module">
  const { acquireSharedConnection } = await import('@gomagaming/network')
</script>

An older release set has no @gomagaming/network entry, so this import fails to resolve. Pin a release set that includes the shared socket before you use it. See The shared socket.

Why an import map is needed at all ​

Published widgets externalise their dependencies so a bundler can resolve them once. Open one and it starts:

js
import { ref } from 'vue'
import { createWidgetElement } from '@gomagaming/core'

A browser has no node_modules and no rule for resolving a bare name like 'vue', so it throws Failed to resolve module specifier "vue" and nothing renders. The import map supplies the missing answer. It is browser-native: no library involved.

This is also why all widgets on a page share one version. A map holds one entry per specifier, so there is exactly one @gomagaming/core, and therefore one set of Pinia stores that every widget coordinates through. Two different core versions on a page would mean two store instances and two silently diverging betslips. Pin a set and move it together; mixing widget versions on one page is not supported.

@gomagaming/sports-domain also needs exactly one import map entry. The <goma-missions-hub> and <goma-mission-details> widgets share the challenge override store through that one module instance. With a second copy, a claim or a quit in one widget does not show in the other.

Authorisation ​

Three tiers, most specific first. All fail closed.

TierCredentialUsed by
Origin allowlistOrigin headerBrowsers. The default
Signed URL?exp=&sig=Browsers needing a hard gate
Bearer tokenAuthorization: Bearer <npm token>CI, SSR, build tooling: never a browser

Origin allowlist ​

The default, and all most integrations need. It works because module scripts are always fetched in CORS mode, so the browser always sends Origin and the response must carry a matching Access-Control-Allow-Origin or the load fails. A URL lifted from your page and pasted on another site will not run.

It is spoofable outside a browser (anyone can send an Origin header by hand), so treat it as preventing embedding rather than preventing download.

Signed URLs ​

When you need a hard gate, your backend mints a short-lived signed URL from your npm token and hands it to the page:

POST https://cdn.gomagaming.com/-/sign
Authorization: Bearer <your npm token>

{ "path": "/@gomagaming/events-horizontal@0.26.0/dist/events-horizontal.js",
  "origin": "https://acme.bet",
  "ttl": 3600 }

The signature covers path, expiry and origin, so a signed URL cannot be re-aimed at another path or another site. Maximum TTL is 24 hours, so this suits a backend that mints on request, not URLs baked into a static build, which would expire.

The token stays on your server. It never reaches the browser.

Troubleshooting ​

Nothing renders and the console says Failed to resolve module specifier. The import map is missing, or it loaded after a module script had already started. Make sure the loader is a classic script in <head>, before everything else.

Nothing renders and the console says Failed to fetch dynamically imported module. That message is all a browser gives you for a failed module fetch: it hides the real status. Fetch the same URL directly to see it:

js
await fetch('https://cdn.gomagaming.com/@gomagaming/core@0.26.0/dist/core.js')
  .then((r) => r.text())
  .then(console.log)

origin_not_allowed. The origin you're serving from isn't registered. This is the one most integrations hit first, on the day they deploy a new domain. Send the exact origin (scheme, host and port, no trailing slash) to your Goma Gaming contact.

not_entitled. Your licence doesn't cover that package.

invalid_token. Only applies to the bearer and signed-URL paths. Your npm token has expired, which means your builds are failing too.

[goma-cdn] This page already has an import map. You have a hand-written import-map tag as well as the loader. The loader installs its map regardless, but most browsers honour only the first, so the widgets never resolve. Drop one of the two: the loader, or the manual map.

[goma-cdn] The loader is already on this page. Two loader script tags. The second one stops immediately. Name every widget you need in a single ?w= list instead.

A widget renders as an empty tag, with nothing in the console. Check whether it is defined at all:

js
customElements.get('goma-region-filter')   // undefined → it never loaded

undefined means the module did not arrive: look for a failed request or one of the authorisation errors above. If it is defined and the widget still shows nothing, it most likely never received its configuration. On release sets at 0.34.0 and earlier, a property assigned before the widget module finished loading was silently dropped (see The loader finishes asynchronously); confirm by checking for a leftover own property that should not be there:

js
Object.getOwnPropertyDescriptor(el, 'config')   // present → the value was shadowed

Fix it by awaiting window.__gomaCdn.ready before assigning, by configuring through attributes, or by moving to a release that carries the rescue.

Notes ​

  • Widget URLs are served immutable for a year, so a pinned URL is safe to cache forever. Only release aliases and dist-tags are short-lived.
  • Everything the widgets need is served by the CDN, including third-party dependencies like Vue. You do not install anything.