@ohhwells/bridge
v0.1.84
Published
The OhhWells canvas editor bridge — a standalone npm package that enables inline text and image editing for any site deployed on the OhhWells platform.
Readme
@ohhwells/bridge
The OhhWells canvas editor bridge — a standalone npm package that enables inline text and image editing for any site deployed on the OhhWells platform.
What it does
When a studio owner opens their site in the OhhWells canvas editor, the bridge:
- Connects the iframe (the live site) to the parent canvas editor via
postMessage - Enables click-to-edit for text, images, and background images
- Handles draft saving and content hydration
- Shows the "Add Section" insert line between sections in the canvas editor
- Provides state toggle UI for editing hidden content (hover states, form views)
- Injects scoped styles that never leak into the host template
Installation
npm install @ohhwells/bridgeTemplate setup (required steps)
1. Import styles
In your root layout file, import the bridge stylesheet once:
import "@ohhwells/bridge/styles";2. Add the loader surface
The loader is a full-screen spinner shown while the bridge fetches personalised content. It hides itself once content is ready. Add this before your main content in the <body>:
import { OHW_LOADER_STYLE, OhwLoaderSpinner } from "@ohhwells/bridge";
// Inside <body>:
<div
id="ohw-loader"
suppressHydrationWarning
style={{ ...OHW_LOADER_STYLE, display: "none" }}
>
<OhwLoaderSpinner />
</div>;
{
/* Inline script — shows the loader immediately on the client before React hydrates */
}
<script
dangerouslySetInnerHTML={{
__html: `(function(){try{
var p=location.hostname.split(".");
var fromHost=p.length>=3&&p[0]!=="www"?p[0]:"";
var fromQuery=new URLSearchParams(location.search).get("subdomain")||"";
if(!fromHost&&!fromQuery)return;
var e=document.getElementById("ohw-loader");
if(e)e.style.display="flex"
}catch(e){}})();`,
}}
/>;The inline script detects whether the page is being loaded under a subdomain (personalised content mode) and shows the loader before React has a chance to hydrate, preventing a flash of the default content.
3. Mount OhhwellsBridge
Add <OhhwellsBridge /> inside a <Suspense> boundary in your root layout. It must be in <Suspense> because it calls useSearchParams() internally.
import { Suspense } from "react";
import { OhhwellsBridge } from "@ohhwells/bridge";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{/* loader + inline script here (see step 2) */}
<Suspense>
<OhhwellsBridge />
</Suspense>
{/* rest of your layout */}
{children}
</body>
</html>
);
}The bridge activates automatically when the page is loaded inside the OhhWells canvas editor. It does nothing in production (live site) mode.
4. Mark sections
Every top-level section on each page must have a unique data-ohw-section attribute. This is what the canvas editor uses to:
- Show the "Add Section" insert line between sections
- Persist widget insertions (saving which section a widget was inserted after)
// Good — section element is the direct content root
<section data-ohw-section="hero">
...
</section>
// Also fine — any element type works
<div data-ohw-section="testimonials">
...
</div>Naming rules:
- Use
kebab-case - Must be unique across the entire page
- Must be stable — if a section is renamed, any saved widget insertions referencing that name will break
Example — a page with multiple sections:
export default function HomePage() {
return (
<>
<section data-ohw-section="hero">...</section>
<section data-ohw-section="lagree-intro">...</section>
<section data-ohw-section="classes-strip">...</section>
<section data-ohw-section="testimonials">...</section>
<section data-ohw-section="plan-form">...</section>
</>
);
}5. Mark editable elements
Add data-ohw-editable and data-ohw-key to any element the studio owner should be able to edit:
{
/* Editable rich text (bold, italic, etc.) */
}
<h1 data-ohw-editable="text" data-ohw-key="hero-heading">
Welcome to the studio
</h1>;
{
/* Editable plain text (no formatting) */
}
<p data-ohw-editable="plain" data-ohw-key="hero-subtitle">
Book your first class
</p>;
{
/* Editable image */
}
<img
data-ohw-editable="image"
data-ohw-key="hero-image"
src="/hero.jpg"
alt="Hero"
/>;
{
/* Editable background image */
}
<div
data-ohw-editable="bg-image"
data-ohw-key="hero-bg"
style={{ backgroundImage: "url(/bg.jpg)" }}
/>;Key naming rules:
- Must be globally unique across all pages
- Use
kebab-case - Must be stable — changing a key orphans any saved content for that element
6. Bridge-managed links (nav, footer, CTAs)
For links whose destination URL is edited in the canvas, use a plain <a> with data-ohw-href-key. Put the editable label on an inner span:
<a href="/book" data-ohw-href-key="nav-book-href" data-ohw-role="navbar-button">
<span data-ohw-editable="text" data-ohw-key="nav-book-label">
Book a Class
</span>
</a>Section / body CTAs use the same two-phase select + Edit-link toolbar with data-ohw-role="button" (preferred). navbar-button remains supported as an alias for nav header CTAs:
<a href="/contact" data-ohw-href-key="hero-cta-href" data-ohw-role="button" data-ohw-drag-disabled="true">
<span data-ohw-editable="text" data-ohw-key="hero-cta-label">
Get started
</span>
</a>| Attribute | Purpose |
|-----------|---------|
| data-ohw-href-key | Storage key for the link destination |
| href | Default URL for SSR / first paint |
| data-ohw-key + data-ohw-editable on inner span | Editable label text |
| data-ohw-role="button" | Section CTA — two-phase select + link-action toolbar (no reorder/More) |
| data-ohw-role="navbar-button" | Alias of button for navbar / header CTAs |
The bridge persists and restores href values (including after React re-renders). No template-side wrapper component is required.
SchedulingWidget (vibe-coder placement)
SchedulingWidget lets a template builder embed a scheduling/booking section directly in their template JSX, without going through the "Add Section" flow. The canvas editor treats it exactly like a dynamically-inserted scheduling widget — the studio owner can connect a schedule, switch it, and clear it.
Basic usage
import { SchedulingWidget } from '@ohhwells/bridge'
export default function ClassesPage() {
return (
<>
<PageHeader ... />
<ClassLibrary ... />
<SchedulingWidget />
<WordmarkBand ... />
</>
)
}No props are required. The widget auto-generates a stable identity via React's useId() so the bridge can track which schedule is connected to it across saves.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| insertAfter | string | auto | Stable identifier used as the tracker key. Only set this manually if you need two SchedulingWidgets on the same page. |
| initialScheduleId | string \| null | undefined | Set by the bridge internally after hydration. Do not pass this yourself. |
| notifyOnConnect | boolean | false | Set by the bridge internally. Do not pass this yourself. |
How it works
- The widget renders immediately with a loading skeleton.
- It sends
ow:request-schedule-configto the bridge (running in the same window). - The bridge looks up the tracker for a saved
scheduleIdfor this widget:- Found → responds with
ow:schedule-config { scheduleId }→ widget loads that schedule. - Not found (first time) → bridge adopts the widget (adds it to the tracker, notifies the canvas editor), responds with
scheduleId: null→ widget shows empty state with an "Add Schedule" button.
- Found → responds with
- In the canvas editor, the studio owner clicks "Add Schedule" → selects a schedule → the widget updates and the connection is saved.
- On the live site, the widget fetches the saved schedule by ID and renders it.
Multiple widgets on one page
Each SchedulingWidget must have a unique insertAfter to be tracked independently:
<SchedulingWidget insertAfter="classes-morning" />
<SchedulingWidget insertAfter="classes-evening" />If you omit insertAfter on both, useId() auto-generates different stable IDs for each, so they are tracked separately anyway.
Empty state on live site
If the studio owner has never connected a schedule to this widget, it renders nothing on the live site (no empty placeholder is shown to visitors).
Editable states (advanced)
For elements with multiple display states (e.g. a contact form with default/success/error views), wrap each state in a data-ohw-state-view and mark the container with data-ohw-editable-state:
<div
data-ohw-editable-state="default,success,error"
data-ohw-key="contact-form"
>
<div data-ohw-state-view="default">{/* default form UI */}</div>
<div data-ohw-state-view="success">{/* success message */}</div>
<div data-ohw-state-view="error">{/* error message */}</div>
</div>The bridge shows a state toggle in the canvas editor to switch between states.
Local development workflow
When iterating on the bridge package itself:
# 1. Build the package
cd ohhwells-bridge
npm run build
# 2. Link it globally (one-time setup)
npm link
# 3. In your template repo, use the local build instead of the npm version
cd rebound-template
npm link @ohhwells/bridgeAfter that, every npm run build in ohhwells-bridge is picked up by the template immediately (no re-link needed). To go back to the npm version:
cd rebound-template
npm unlink @ohhwells/bridge
npm installPublishing
The package publishes automatically via GitHub Actions on push to main (production tag) or staging (next tag). To publish manually:
npm run build
npm publish --access publicStaging → rebound-template auto-bump
When staging publishes successfully, the bridge workflow dispatches a bridge-published event to TheFlowOps-Eng/rebound-template. That repo’s bump-bridge.yml workflow checks out staging, runs npm install @ohhwells/bridge@<version> --save-exact, and pushes the lockfile bump.
One-time setup (in ohhwells-bridge GitHub repo → Settings → Secrets):
| Secret | Value |
|--------|--------|
| REBOUND_TEMPLATE_DISPATCH_TOKEN | Fine-grained or classic PAT with repo access to rebound-template (needs permission to trigger repository_dispatch) |
The published staging version looks like 0.1.31-next.42 and is tagged next on npm.
Link dialog (Canvas Editor)
LinkPopover is rendered inside OhhwellsBridge (iframe portal) when editing link destinations. It uses the shadcn Dialog pattern (Radix) — centered modal with overlay — implementing the Figma shadcn kit panel (nodes 8365-7616 / 8382-4161).
// Used internally by OhhwellsBridge — external consumers rarely mount this directly.
import { LinkPopover } from "@ohhwells/bridge";The iframe bridge reports page sections on ow:ready:
{ "type": "ow:ready", "version": "1", "bridgeVersion": "0.1.50", "nodes": [...], "sections": [{ "id": "hero", "label": "Hero" }] }version is the protocol version; bridgeVersion is the npm package version (inlined at build time), letting the editor detect sites running an older bridge and gate version-dependent features (e.g. AI prompting) gracefully.
Templates must mark sections with data-ohw-section (and optional data-ohw-section-label).
Section-scoped content collection (AI prompt context)
The editor can request a single section's editable content (edit mode only):
// editor → iframe
{ "type": "ow:collect-section", "sectionId": "hero" }
// iframe → editor
{ "type": "ow:section-nodes", "sectionId": "hero", "found": true,
"nodes": [{ "key": "hero-headline", "type": "text/rich", "text": "..." }],
"rect": { "top": 0, "left": 0, "width": 1280, "height": 720 } }foundisfalse(with emptynodes,rect: null) when no[data-ohw-section="<id>"]exists on the current page.rectis the section element's document-relative bounding box — the editor uses it to draw the in-flight overlay over just the target section.- The returned set contains pure section content only: structural keys (
__ohw_*), carousel/video-setting/meta nodes, and keyless editables are never included. - Node
typeis normalized to the setimage | bg-image | video | link | plain | text/rich | text(unknown template-declared types becometext).
ow:enter-edit (iframe → editor, sent when the user clicks into an editable) also carries the enclosing section so the editor can offer that section as the AI prompt target:
{ "type": "ow:enter-edit", "key": "hero-headline", "sectionId": "hero", "sectionLabel": "Hero" }sectionId/sectionLabel are null for editables outside any [data-ohw-section] (label also when data-ohw-section-label is absent).
Local link-editor testing (dev fixtures)
Without the canvas editor parent, add ohw-fixtures=1 to the edit URL. This preloads Re:Bound pages and sections into the link popover:
http://localhost:3000 /?mode=edit&ohw-fixtures=1- Click a nav link label (e.g. Pricing) → toolbar → chain icon
- Pick About in Destination → Choose a section → Personal training
- Save — href becomes
/about#personal-training - Open
http://localhost:3001/about#personal-training(withoutmode=edit) to verify scroll
In edit mode link clicks are blocked; check href in DevTools or test on a normal page load.
Design tokens
The package ships the full OhhWells design token set, scoped to [data-ohw-bridge-root] so styles never leak into the host template.
Semantic colors (CSS variables on [data-ohw-bridge-root])
| Token | Light | Dark |
| -------------------- | --------- | --------- |
| primary | #0f172a | #f8fafc |
| primary-foreground | #f8fafc | #0f172a |
| background | #ffffff | #020617 |
| foreground | #020617 | #f8fafc |
| muted | #f1f5f9 | #1e293b |
| muted-foreground | #64748b | #94a3b8 |
| border | #e2e8f0 | #334155 |
| destructive | #dc2626 | #7f1d1d |
| success | #16a34a | #22c55e |
Brand palette
Re:Bound brand colors: bone, ivory, sand, linen, stone, clay, umber, umber-deep, espresso, clove, ink, ash, carbon.
License
MIT
