@orbiocloud/site-widgets
v1.2.0
Published
Drop-in OrbioSite page widget for a customer's own website
Readme
@orbiocloud/site-widgets
Drop a published OrbioSite page into a customer's own website.
Phase 3.2 shipped the headless read API (GET /api/public/website/page/[slug]); this
package is the drop-in half. The page is fetched as JSON and rendered with the same
SitePageRenderer the editor live-preview and the server read path use, so the
read-path isSafeHref gate travels with it.
Usage
Served from npm via unpkg, pinned to an exact version — never a floating tag like
@latest, so a republish cannot change what already-embedded customer pages execute.
integrity is required, not decorative. Without it, anyone able to serve a different
body at that URL — a CDN compromise, a cache poisoning, a hijacked npm account republishing
the version — executes arbitrary JavaScript on every customer site that embeds this. SRI
makes the browser refuse a body that does not match the hash.
Generate the real hashes from the published artifact and paste them in (they can only be produced after publish, which is why they are not committed here):
V=1.2.0
for f in orbio-site-widgets.umd.js orbio-site-widgets.css; do
printf '%s sha384-%s\n' "$f" \
"$(curl -sL "https://unpkg.com/@orbiocloud/site-widgets@$V/dist/$f" \
| openssl dgst -sha384 -binary | openssl base64 -A)"
done<link rel="stylesheet"
href="https://unpkg.com/@orbiocloud/[email protected]/dist/orbio-site-widgets.css"
integrity="sha384-QVUTJmMFOz60YqOhH7IidcdQBOkJ5sWRcRBN5vr5p7j4o5OnyrKVIG6H4hRcga9P" crossorigin="anonymous">
<script src="https://unpkg.com/@orbiocloud/[email protected]/dist/orbio-site-widgets.umd.js"
integrity="sha384-D575txNzxfeofUOdyznJEZctrz8wVbK6HJP3VpjDlVaBCBmb/mc0KlgZlKENtHNz" crossorigin="anonymous"></script>
<script>
OrbioSiteWidgets.init({ apiKey: 'ws_live_...', subdomain: 'my-tenant' });
</script>
<div data-orbio-site-page data-slug="about"></div>
<div data-orbio-site-schedule data-slug="staff" data-days="7"></div>Every [data-orbio-site-page] and [data-orbio-site-schedule] element with a data-slug
is rendered. init() may be called before DOM ready. For markup injected later (SPA route
change, lazy CMS section), call OrbioSiteWidgets.mountAll() — already-mounted containers
are skipped.
Scheduled collections
[data-orbio-site-schedule] renders a published scheduled collection. data-days is
optional and sets the window length (the server clamps it to 1–62, default 7); the window
always begins on the collection's own current business day.
A mounted schedule keeps itself on the right day. Collections have their own timezone and rollover minute — a venue open until 2am still shows Friday's roster at 1am on Saturday — and a tab left open overnight would otherwise display the previous day indefinitely, because the fetch already happened and no cache header can reach it. The widget re-checks when the tab becomes visible and periodically while it is, and refetches when the business day has moved.
root.unmount() disposes everything the mount created, including that watcher.
Options
| Option | Required | Description |
|---|---|---|
| apiKey | yes | Publishable website key (ws_live_...) — read-only. |
| subdomain | yes | The tenant's OrbioCloud subdomain. |
| baseUrl | no | API origin. Defaults to https://app.orbiocloud.com. |
The key is publishable by design: it is read-only and reaches only published, tenant-scoped content, so it is safe in page source. Each key carries its own hourly rate limit, configured in the OrbioCloud website hub.
It is NOT origin-locked today. An earlier version of this document said it was, which was wrong and amounted to advice to embed a credential in public page source on a protection that did not exist.
The mechanism is now wired —
validateWebsiteKeyselectsallowed_originsand the public-API wrapper enforces it, matching the showroom and shop validators — but every website key currently has an empty allowlist, and an empty allowlist means the check is skipped. So origin is not protecting anything for this key type until someone populates it per key, which is a deliberate per-consumer decision: setting it on a key whose site serves from an unexpected origin breaks that site.Until then, treat a publishable website key as public: anyone who views source can read it and call the same read-only endpoints. That is the intended threat model — the protection is that the key grants nothing but published, tenant-scoped content — not that the origin is checked.
Distribution
npm → unpkg, version-pinned. We do not host. That is the operator decision on record
(docs/ACTIVE-TODO-orbiosite.md: "headless JSON API + drop-in WIDGETS (we do NOT host)",
and "Host npm→unpkg PINNED to an exact version (no CDN wired today)").
An earlier version of this section said "serve from our own origin" and documented
app.orbiocloud.com/widgets/v1.0.0/…. That was wrong in two ways at once: it described a
hosting model the plan excludes, and the URL it gave 404s — no such route was ever wired.
Anyone following it reached a dead link and had no way to tell whether the package was
broken or simply unreleased.
Self-hosting would buy control of cache headers and rollback, which matters more here than
for @orbiocloud/shop-widgets (vanilla, dependency-free) because this bundle ships React +
react-dom + @portabletext/react and runs a full render pipeline on the customer's page.
SRI is what substitutes for that control: a pinned version plus an integrity hash means
a mutated artifact fails closed in the browser rather than executing. That is why the
integrity attribute above is mandatory rather than advisory.
Never document a floating @latest. Emit the hashes per release with:
openssl dgst -sha384 -binary dist/orbio-site-widgets.umd.js | openssl base64 -A
openssl dgst -sha384 -binary dist/orbio-site-widgets.css | openssl base64 -AStyling
The renderer emits Tailwind utility classes, which resolve to nothing on a non-Tailwind
host page — so the widget ships its own precompiled stylesheet. Two properties make it
safe to inject into a site we do not own, and both are enforced by
src/scoped-stylesheet.test.ts:
- Every rule is scoped to the
.orbio-site-pagecontainer class. - No Preflight — Tailwind's base reset would restyle the host's own document.
@tailwind componentsis omitted for the same reason: itscontainercomponent is emitted unscoped, and.containeris a common class on real sites.
The stylesheet's Tailwind content globs point at the renderer's sources, so editing a
className in SitePageRenderer is picked up on the next build rather than silently
drifting.
Global blocks (ref)
ref blocks resolve server-side in the public API. The payload reaching this bundle
contains fully-resolved, already-sanitized blocks, so the widget never passes resolveRef
and never fetches global-block content itself — a security control must not live in a
bundle running on a host page we do not control. When M7 ships, this package is unchanged.
Development
pnpm --filter @orbiocloud/site-widgets build # vite lib build -> dist/
pnpm --filter @orbiocloud/site-widgets test # stylesheet + mount tests
pnpm --filter @orbiocloud/site-widgets type-check