> ## Documentation Index
> Fetch the complete documentation index at: https://juo.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Building a custom portal

> Build a host application that renders blocks in production and integrates with the Juo Editor.

A **portal** is the host application that mounts `<juo-context-root>`, provides services, registers blocks, and decides what to render where. Themes ship one. So can extensions and standalone storefront surfaces. This guide walks through building a portal from scratch, then layering on the editor integration.

<Note>
  Prefer to learn from a working example? The [sample-beauty-portal](https://github.com/juo/sample-beauty-portal) repository is a complete reference portal — registered blocks, services wired up, editor mode enabled — to clone and adapt.
</Note>

## Install

`@juo/blocks` ships as a single package:

<CodeGroup>
  ```sh npm theme={null}
  npm install @juo/blocks
  ```

  ```sh pnpm theme={null}
  pnpm add @juo/blocks
  ```

  ```sh yarn theme={null}
  yarn add @juo/blocks
  ```

  ```sh bun theme={null}
  bun add @juo/blocks
  ```
</CodeGroup>

`vue`, `react`, and `preact` are **optional peer dependencies** — install only the one the portal runs on, or none for plain DOM rendering.

## Entry points

`@juo/blocks` exposes a handful of subpath entry points. Import from the ones that match the stack — bundlers tree-shake the rest:

| Entry point                          | Purpose                                                                  |
| ------------------------------------ | ------------------------------------------------------------------------ |
| `@juo/blocks`                        | Core: block system, signals, contexts, services                          |
| `@juo/blocks/vue`                    | Vue 3 renderers and composables                                          |
| `@juo/blocks/react`                  | React renderers and hooks                                                |
| `@juo/blocks/preact`                 | Preact renderers and hooks                                               |
| `@juo/blocks/editor`                 | Bridge for integrating with the Juo Editor                               |
| `@juo/blocks/web-components/editor`  | Custom elements with Editor affordances (selection, inline text editing) |
| `@juo/blocks/web-components/runtime` | Lean custom elements for storefront rendering                            |

## Anatomy

A portal does four things:

1. **Loads the web components.** Pick the runtime or editor build.
2. **Mounts a `<juo-context-root>`** in the HTML shell.
3. **Provides services** on the root.
4. **Registers blocks** so the editor and runtime can instantiate them by name.

When the portal runs inside the editor iframe, it also calls `setupEditorMode` to advertise its blocks and routes. See [Editor](/docs/blocks/concepts/editor) for what that protocol carries.

## Runtime: the bare minimum

```html theme={null}
<!-- index.html -->
<!doctype html>
<html>
  <body>
    <juo-context-root>
      <juo-page route="/"></juo-page>
    </juo-context-root>

    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```

```ts theme={null}
// src/main.ts — runtime build
import "@juo/blocks/web-components/runtime";
import {
  provideContext,
  registerBlock,
  createThemeState,
  ThemeStateContext,
  createSubscriptionService,
  createApiSubscriptionAdapter,
  createCustomerService,
  createApiCustomerAdapter,
  SubscriptionServiceContext,
  CustomerServiceContext,
} from "@juo/blocks";

import { HomePageBlock } from "./blocks/HomePage";
import { ButtonBlock } from "./blocks/Button";

// 1. Register blocks
registerBlock(HomePageBlock);
registerBlock(ButtonBlock);

// 2. Provide services on the root
const root = document.querySelector("juo-context-root") as HTMLElement;

provideContext(
  root,
  SubscriptionServiceContext,
  createSubscriptionService(createApiSubscriptionAdapter({ baseUrl: "/api/v1" })),
);

provideContext(
  root,
  CustomerServiceContext,
  createCustomerService(createApiCustomerAdapter({ baseUrl: "/api/v1" })),
);

// 3. Provide a ThemeState so <juo-page> knows what to render for each route
provideContext(
  root,
  ThemeStateContext,
  createThemeState({
    resolve: async (_surface, path) =>
      path === "/" ? { blocks: [{ name: "HomePage", props: {}, slots: {} }] } : null,
  }),
);
```

The `<juo-page route="/">` element subscribes to `ThemeStateContext`, resolves the `/` route, and renders the resulting root block. Nested blocks resolve services from the surrounding context tree.

### Setup: providing services and rendering the page

The app shell registers blocks and calls `provideContext` for each service on the `<juo-context-root>`. `<juo-page>` then injects `ThemeStateContext` to resolve the current route and render the resulting root block.

<img src="https://mintcdn.com/juo/DmI4hy9S0Wn-6Yem/docs/blocks/images/portal-architecture-setup.svg?fit=max&auto=format&n=DmI4hy9S0Wn-6Yem&q=85&s=3771578389cc0267b7072a8c8245640f" alt="A green juo-context-root containing a gray app shell. Inside the app shell, two rose 'provideContext' rectangles sit in a row — one for ThemeStateContext and one for SubscriptionServiceContext. Below them, a cyan juo-page route='/' frame contains a single rose 'injectContext ThemeStateContext' rectangle. A dashed rose arrow labelled 'resolves' connects the provideContext ThemeStateContext to the injectContext inside juo-page." width="800" height="412" data-path="docs/blocks/images/portal-architecture-setup.svg" />

### Composition: rendering a block from theme state

The resolved root block emits one or more `<juo-extension-root>` slots; the children placed in each slot are themselves blocks. Each block calls `injectContext` to pull services out of the surrounding context tree, and the renderer turns its props plus those services into DOM.

<img src="https://mintcdn.com/juo/DmI4hy9S0Wn-6Yem/docs/blocks/images/portal-architecture-render.svg?fit=max&auto=format&n=DmI4hy9S0Wn-6Yem&q=85&s=66f5b02de645433933b96646a955ab67" alt="An amber juo-extension-root name='main' frame containing a purple juo-block SubscriptionList. Inside the block, a rose 'injectContext SubscriptionServiceContext' rectangle sits above a dashed 'renderer output' panel showing a stylized subscription list — a 'Your subscriptions' header followed by three rows, each with a thumbnail, two text lines, a colored 'Active' or 'Paused' status pill, and a 'Manage' button." width="800" height="440" data-path="docs/blocks/images/portal-architecture-render.svg" />

See [Web components](/docs/blocks/concepts/web-components) for the full list of custom elements, [Services](/docs/blocks/concepts/services) for the providers, and [Contexts](/docs/blocks/concepts/contexts) for how the lookup works.

## Adding editor support

When the merchant opens the portal inside the editor, the iframe loads the **editor build** of the web components and the portal needs to advertise its blocks and routes. Detect the editor (e.g. by checking `window.parent !== window` or a URL flag), load the right web component bundle, and call `setupEditorMode`:

```ts theme={null}
// src/main.ts — editor-aware
import {
  provideContext,
  registerBlock,
  createThemeState,
  createRouterService,
  ThemeStateContext,
  RouterServiceContext,
} from "@juo/blocks";
import { setupEditorMode } from "@juo/blocks/editor";

const isEditor = window.parent !== window;

if (isEditor) {
  await import("@juo/blocks/web-components/editor");
} else {
  await import("@juo/blocks/web-components/runtime");
}

const root = document.querySelector("juo-context-root") as HTMLElement;

// ... register blocks and provide services ...

const themeState = createThemeState({
  resolve: (surface, path) => editor.requestThemeState(surface, path),
  upsertState: (surface, path, blocks) => editor.upsertThemeState(surface, path, blocks),
});
provideContext(root, ThemeStateContext, themeState);

const routerService = createRouterService();
provideContext(root, RouterServiceContext, routerService);

if (isEditor) {
  const cleanup = await setupEditorMode({ routerService, themeState });
  window.addEventListener("beforeunload", cleanup);
}
```

`setupEditorMode`:

* Serializes every registered block (name, group, schema, initial value, presets, locales) and sends it to the editor.
* Reports the portal's routes from `routerService.getPages()`.
* Subscribes to incoming editor messages (selection, prop updates, theme state changes, translation overrides) and applies them to the local `ThemeState`.

It returns a cleanup function — call it on teardown.

## Preset previews with `useBridge`

The editor's block picker shows **presets** for each block (see [Blocks → Presets](/docs/blocks/concepts/blocks)). When the merchant hovers a preset, the editor sends a `SET_BLOCK_PRESET` message and the matching block can render a preview without committing.

A block opts into preset previews using `useBridge` from its framework adapter:

```vue theme={null}
<!-- Vue -->
<script setup lang="ts">
import { useBridge } from "@juo/blocks/vue";
import { ref } from "vue";

const previewProps = ref<MyProps | null>(null);

useBridge<typeof MyBlock["presets"]>({
  onPresetChange(preset) {
    previewProps.value = preset?.state.props ?? null;
  },
});
</script>
```

```tsx theme={null}
// React
import { useState } from "react";
import { useBridge } from "@juo/blocks/react";

function MyBlock() {
  const [previewProps, setPreviewProps] = useState<MyProps | null>(null);

  useBridge<typeof MyBlock["presets"]>({
    onPresetChange(preset) {
      setPreviewProps(preset?.state.props ?? null);
    },
  });
}
```

```tsx theme={null}
// Preact
import { useState } from "preact/hooks";
import { useBridge } from "@juo/blocks/preact";

function MyBlock() {
  const [previewProps, setPreviewProps] = useState<MyProps | null>(null);

  useBridge<typeof MyBlock["presets"]>({
    onPresetChange(preset) {
      setPreviewProps(preset?.state.props ?? null);
    },
  });
}
```

`useBridge` is a no-op outside the editor — `isEditorMode` returns `false` and `onPresetChange` never fires. You can ship the same block to both modes without branching.

## Folder layout

A typical portal package looks like:

```
src/
  main.ts                  ← entry: register blocks, provide services, setup editor
  blocks/
    HomePage/index.ts      ← defineBlock + renderer
    Button/index.ts
    SubscriptionCard/index.ts
  services/
    router.ts              ← the concrete RouterService
  index.html
```

Keep `defineBlock` calls in their own folder per block — each block is self-contained, and `registerBlock` is the only thing that needs to touch them all.
