@relyio/toolbar
v0.3.0
Published
The RelyIO preview toolbar: a development-only, shadow-DOM panel showing the dataset, branch, key and sandbox a page is reading from, and badging each bound field published or draft with a Studio link, over @relyio/sdk.
Readme
@relyio/toolbar
The RelyIO preview toolbar: a small floating panel, for development only, that shows which
dataset, branch, key and relyio dev sandbox the page is reading from, and when that sandbox
expires — and badges every field you bound with relyioField as published or draft in its locale,
with a link into the Studio.
The preview key must never reach a production bundle. The toolbar holds a preview key in the browser, and a preview key reads drafts. Render the toolbar only in development, and read the key from an environment variable your production build does not set. The toolbar refuses to start under
NODE_ENV=production, but that is a second line of defence, not the first: a key in your production JavaScript is readable by anyone whether or not the toolbar mounts.
npm install @relyio/toolbar @relyio/sdkNext.js (App Router)
Render <RelyIOToolbar> from your root layout. The layout is a server component and a client cannot
cross into a client component, so pass plain values: the preview key, the dataset, and the server
client's sandbox — and baseUrl if your client sets one.
// app/layout.tsx
import { RelyIOToolbar } from '@relyio/toolbar/react'
import { relyio } from '@/lib/relyio'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<body>
{children}
{process.env.NODE_ENV !== 'production' && (
<RelyIOToolbar
apiKey={process.env.RELYIO_PREVIEW_KEY!}
dataset='main'
sandbox={relyio.sandbox}
/>
)}
</body>
</html>
)
}React single-page app
Pass the client you already created.
import { RelyIOToolbar } from '@relyio/toolbar/react'
{
import.meta.env.DEV && <RelyIOToolbar client={relyio} />
}Without React
mountToolbar from the package root is plain DOM. Import it dynamically so it stays out of your
main bundle:
if (import.meta.env.DEV) {
const { mountToolbar } = await import('@relyio/toolbar')
const toolbar = mountToolbar({ client: relyio })
// later: if (toolbar.state === 'mounted') toolbar.unmount()
}What it shows
| Row | Shows |
| ------- | ------------------------------------------------------------------------------------------------- |
| Dataset | The sandbox's dataset when a relyio dev sandbox is active, otherwise the one you configured |
| Branch | The sandbox's branch, or says the handshake named none, or the dataset's default |
| Key | The key's scope prefix only — rly_prev_…. Never the key |
| Sandbox | Active (and from where) with the relyio promote command, expired, unreadable, none, and more |
| Expires | A countdown to the sandbox's deadline, or "no expiry" for a sandbox without one |
| Studio | Whether the badges link to the Studio, or why not — "Studio URL not configured" among them |
| Live | How many entries it follows for changes, or that it is reconnecting, resynced or stopped, and why |
Bound fields
The toolbar finds every element your page stamped with relyioField — on a server render as much as
in the browser, because it reads the attributes off the page — and puts a badge on it:
| Badge | Meaning |
| --------------------------------------- | ------------------------------------------------------------------------- |
| published | The field's locale is published |
| published, draft changes | Published, with unpublished edits in that locale — what a preview shows |
| draft | The locale has never been published |
| archived | The locale is archived |
| no content in this locale | The locale has no content of its own; what you see is its fallback |
| not found with this key | The key cannot read that entry |
| reading…, unreadable (…), and a few | The read is in flight or failed, or the API reported something it doesn't |
Publication is per locale, not per field, so every field of one entry in one locale carries the same
badge. Hovering a bound element outlines it. Each badge links to that entry, field, locale and dataset
in the Studio; when the API has no Studio URL configured, the badges are plain text and the Studio row
says so. An element missing any of the relyioField attributes is ignored.
A block is bound too. @relyio/react's <Blocks> and <RichText>, handed the field's binding
(field={relyioField(page, 'sections')}), stamp each block they render with its key path —
sections[_key=="f2k6ub9s"], and a callout inside it
sections[_key=="f2k6ub9s"].body[_key=="c1"] — so each gets a badge, and its link takes you to
that block rather than to the whole field.
What it reads
Only on a page with bound fields, and only reads, through @relyio/sdk:
- each bound entry when it first appears, with
GET /v1/{contentType}/{id}, and again whenever the changes feed names it; - the project once, with
GET /v1/project, for the Studio URL; - the changes feed's current position, with one
GET /v1/changesbefore the first entry read, so a change that lands while the page is being read is not missed; - the changes feed, with
GET /v1/changes?ids=…for the entries on the page, from that position, once the first entry read has settled. A new bound entry reopens it with the wider set;unmount()aborts it. A page binding more entries than the API filters by id follows the whole dataset's feed instead and ignores changes to entries it does not show.
It never writes. The reads are made from the browser with the preview key, so the RelyIO API must admit your development origin.
Live updates
Each change the feed reports for an entry on the page is read again, its badges are redrawn, and then
the toolbar dispatches a relyio:change event on window whose detail is the change — its entry id,
content type, locale, kind and cursor, never the content. The name is RELYIO_CHANGE_EVENT from
@relyio/sdk. @relyio/next's <RelyIORefresh /> listens for it and refreshes the route; any other
page can do the same:
import { RELYIO_CHANGE_EVENT, type RelyIOChangeDetail } from '@relyio/sdk'
window.addEventListener(RELYIO_CHANGE_EVENT, event => {
const change = (event as CustomEvent<RelyIOChangeDetail>).detail
})Because the toolbar holds a preview key, draft saves reach it as well as publishes. When the feed's
position has expired it reads every entry again, says so in the Live row, and then dispatches a
relyio:resync event (RELYIO_RESYNC_EVENT) instead of a change, because the feed named none.
Anything on the page may be stale then; <RelyIORefresh /> refreshes on it too:
import { RELYIO_RESYNC_EVENT, type RelyIOResyncDetail } from '@relyio/sdk'
window.addEventListener(RELYIO_RESYNC_EVENT, event => {
const { cause } = (event as CustomEvent<RelyIOResyncDetail>).detail
})When it does not start
mountToolbar returns { state: 'mounted', unmount() }, or { state: 'disabled', reason }:
| reason | When |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| production | Your build's NODE_ENV is production and you did not pass dev: true |
| environment-unknown | Your build does not say what it is — no bundler replaced process.env.NODE_ENV, or it is unset — and you did not pass dev: true |
| opted-out | You passed dev: false |
| no-document | There is no DOM, as in a server render |
| already-mounted | A toolbar is already on the page |
Next.js, webpack, Vite and esbuild all replace process.env.NODE_ENV in your bundle, so the
production check works without configuration. A page served without a bundler has no NODE_ENV, so
pass dev: true there — only in development.
Nothing mounts when you import the package.
Isolation
The panel is rendered inside a shadow root on one <relyio-toolbar> element appended to <body>.
Your page's styles cannot reach it, its styles cannot reach your page, and it adds nothing to
document.head or window (it dispatches relyio:change and relyio:resync there, and sets nothing). Badges and outlines are drawn inside that shadow root, over your page;
nothing is written to your elements.
Licence
Apache-2.0
