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

# Immersive mode

> An experimental widget that puts editing directly on the site page. What it offers, how a developer mounts it, and why the editor no longer links to it.

## Overview

<Warning>
  **Experimental, and no longer linked from the editor.** The editor's "Open in
  immersive mode" button was removed with the floating preview controls, so there
  is no way to reach immersive mode from the editor's screens today. It still
  works where a site mounts the widget itself, which is what the rest of this page
  describes. It is for developers; nothing here is something an editor can switch
  on.
</Warning>

Immersive mode renders the editing UI **directly on the site page** — no
iframe, no chrome around the content. The widget (chat FAB, add-block pill,
text-selection "Ask AI", inline field prompts, undo/redo) is portalled into
the site DOM. It is the intended mode for copy-heavy and single-page edits
where you want to see the real page at full width.

The main editor (`apps/editor`) and immersive mode share the same draft via
the orchestrator's `(session, siteId, slug)` — switching between them does
not move or duplicate state.

```mermaid theme={null}
flowchart LR
    editor["Main editor<br/>(apps/editor)"]
    immersive["Immersive view<br/>(apps/site)"]
    orchestrator[("Orchestrator<br/>session + siteId draft")]

    editor <--> orchestrator
    immersive <--> orchestrator

    immersive -- "← Back (same tab)" --> editor
```

## Entering immersive mode

On a site that renders the widget on its draft route, as this repository's
demo site does, immersive mode is a URL:

```
{siteOrigin}/{slug}?__editor=1&immersive=1&session=...&siteId=...&editorOrigin=...
```

A site that guards its draft route with a draft secret also needs that secret
on the URL, in plain view, so treat such a link as a credential.

The `editorOrigin` query param tells the widget where the main editor lives
so it can render the "Back" pill. The site's `__editor=1` middleware rewrites
the request to the `preview-draft` route internally; you do not link to
`/preview-draft/...` directly. Navigation inside the page keeps the immersive
parameters.

## Returning to the main editor

A small glass **"← Back" pill** sits in the top-left corner of the immersive
page. It:

* only renders when `config.editorOrigin` is present (so embedded / public
  widget uses stay clean),
* auto-hides when the chat panel is open (to stop it colliding with
  panel chrome),
* auto-hides on scroll-down and reappears on scroll-up or near the top
  (browser-chrome style),
* navigates **same-tab** to `{editorOrigin}?session=...&siteId=...&slug=...`.

The editor reads `?slug=` on bootstrap, so you land on the exact page you
were editing — no flash to the home route before the iframe catches up.

## What is available inside immersive mode

* **Chat FAB** (bottom right) with the full chat pipeline — text, structural,
  and image ops all route through the same orchestrator as the main editor.
* **Add-block pill** (bottom right, next to the chat FAB) opens the inline
  block picker above itself and inserts after the last block on the page.
* **Inline field prompt** — click any editable text field to get a tight
  prompt box anchored to the field.
* **Text selection "Ask AI"** — select a range of copy to get an inline
  toolbar. In text-only mode, selections route into the field prompt with
  the excerpt pre-filled.
* **Undo / redo** — `Cmd+Z` and `Cmd+Shift+Z` / `Cmd+Y`. Buttons also appear
  in the chat panel header. Proxies to the orchestrator's
  `/history/{undo,redo}` so draft history is shared with the main editor.

## Feature flags

| Flag | Default | Effect |
| - | - | - |
| `NEXT_PUBLIC_IMMERSIVE_TEXT_ONLY` | `0` | When `1`, restricts the block picker to `Hero`, `FeatureGrid`, `Testimonials`, `FAQAccordion`, `CTA`, `RichText` and routes text selections into the inline field prompt with the excerpt pre-filled. Used for the text-blocks MVP. |

## Key files

| Layer | File |
| - | - |
| Widget root | `packages/immersive-widget/src/ImmersiveWidget.tsx` |
| Back-to-editor pill | `packages/immersive-widget/src/components/BackToEditorPill.tsx` |
| Add-block pill | `packages/immersive-widget/src/components/AddBlockFab.tsx` |
| Undo/redo hook | `packages/immersive-widget/src/hooks/useUndoHistory.ts` |
| Widget config type | `packages/immersive-widget/src/lib/widget-state.ts` |
| Site page rendering the widget | `apps/site/app/preview-draft/[[...slug]]/page.tsx` |
| Site wrapper | `apps/site/components/immersive-wrapper.tsx` |
| Editor slug bootstrap | `apps/editor/src/store/editor-store.ts` — `readInitialSlug()` |

***

## Embedding on an external site

The widget lives in `packages/immersive-widget` and can be mounted on any Next.js 15 or 16 (App Router) site already integrated with the orchestrator via the site-sdk. The monorepo's `apps/site` is its first consumer.

<Warning>
  `@ai-site-editor/immersive-widget` is **not published to npm** — it is `private: true`, has no build step, and its entry points are raw `.ts`/`.tsx` source. `pnpm add` will 404. To use it today, vendor the package into your repo or consume it from a checkout of this monorepo, and add it to `transpilePackages` in your `next.config` (`apps/site` does the same). The snippets below assume that wiring.
</Warning>

### Prerequisites

* Next.js 15 or 16, App Router (React 19 peer dep)
* Site already registered with the orchestrator (`npx avocado-register`) — you need a valid `session` and `siteId`
* The orchestrator reachable at a known URL from the browser (it's called directly by the widget over SSE, not proxied through Next.js)

### Install

```ts theme={null}
// next.config.ts — the package ships as source, so Next must transpile it.
export default {
  transpilePackages: ["@ai-site-editor/immersive-widget"],
}
```

### Import the CSS

Add the widget stylesheet once, in your root layout or a global CSS file:

```ts theme={null}
// app/layout.tsx  (or globals.css @import)
import "@ai-site-editor/immersive-widget/styles.css"
```

The styles use `.iw-*` class names with a `--iw-*` CSS custom property API so they don't conflict with your site's own styles.

### Mount the widget

Create a `"use client"` wrapper — the widget needs browser APIs and the Next.js router:

```tsx theme={null}
// components/ImmersiveEditingWidget.tsx
"use client"

import { useRouter, usePathname } from "next/navigation"
import { ImmersiveWidget } from "@ai-site-editor/immersive-widget"
import type { WidgetConfig } from "@ai-site-editor/immersive-widget"

const config: WidgetConfig = {
  orchestratorUrl: process.env.NEXT_PUBLIC_ORCHESTRATOR_URL ?? "http://localhost:4200",
  session: "my-session",   // replace with your session logic — see below
  siteId: process.env.NEXT_PUBLIC_DEFAULT_SITE_ID ?? "my-site",
  // Optional: if you want the "← Back" pill to appear
  editorOrigin: process.env.NEXT_PUBLIC_EDITOR_ORIGIN,
}

export function ImmersiveEditingWidget({ slug }: { slug: string }) {
  const router = useRouter()
  const pathname = usePathname()

  return (
    <ImmersiveWidget
      config={config}
      slug={slug}
      pathname={pathname}
      refresh={() => router.refresh()}
      navigate={(href) => router.push(href)}
    />
  )
}
```

Then render it from your page component (must be a Server Component that passes the slug down):

```tsx theme={null}
// app/[[...slug]]/page.tsx
import { ImmersiveEditingWidget } from "@/components/ImmersiveEditingWidget"

export default async function Page({ params }: { params: { slug?: string[] } }) {
  const slug = params.slug?.join("/") ?? ""
  // ... your existing page rendering ...
  return (
    <>
      {/* your page content */}
      <ImmersiveEditingWidget slug={slug} />
    </>
  )
}
```

The widget portals itself to `document.body`, so its position in the tree doesn't matter for z-index or layout.

### Passing the block manifest (recommended)

Without a manifest the widget's block picker falls back to the SDK's built-in block list. To show only your site's actual blocks, fetch the manifest from your existing `/api/editor/blocks` route and pass it in:

```tsx theme={null}
// Fetch once at page load (Server Component)
const manifestRes = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/editor/blocks`)
const manifest = manifestRes.ok ? await manifestRes.json() : null

// Pass to the client wrapper
<ImmersiveEditingWidget slug={slug} manifest={manifest} />
```

Update the client wrapper's props signature to accept and forward `manifest`.

### Session management

`WidgetConfig.session` is the orchestrator session key that scopes the draft. You have a few options:

| Approach | When to use |
| - | - |
| Hard-coded string (e.g. `"dev"`) | Local dev / single editor |
| `NEXT_PUBLIC_*` env var | Staging deploys where one session serves all visitors |
| URL query param (`?session=...`) | Entering from the main editor — read it from `useSearchParams()` |
| Short-lived JWT / cookie | Production multi-user deploys (not yet shipped) |

The main editor passes `?session=...` in the URL when opening immersive mode — read it via `useSearchParams()` if you want the immersive view to share the editor's session automatically.

### Props reference

| Prop | Type | Required | Description |
| - | - | - | - |
| `config` | `WidgetConfig` | yes | `orchestratorUrl`, `session`, `siteId`, optional `editorOrigin` |
| `slug` | `string` | yes | Current page slug (without leading `/`) |
| `pathname` | `string` | yes | Full pathname — used by the block selection overlay |
| `refresh` | `() => void` | yes | Called after ops apply; use `router.refresh()` |
| `navigate` | `(href: string) => void` | yes | Called on undo/redo when slug changes; use `router.push()` |
| `manifest` | `BlockManifest \| null` | no | Block list for the picker and AI context |
| `siteContext` | `SiteContext` | no | `{ siteName?, purpose?, tone?, constraints? }` — enriches AI prompts |
| `accessToken` | `string` | no | Bearer token forwarded to the orchestrator on every request |
| `textOnly` | `boolean` | no | Restricts picker to `Hero`, `FeatureGrid`, `Testimonials`, `FAQAccordion`, `CTA`, `RichText` and routes text selections to the inline field prompt |

### Restricting access

The widget renders unconditionally if mounted. Gate it behind your existing auth check before rendering the component — for example, only mount it when a `?__editor=1` query param is present or when a Draft Mode cookie is set:

```tsx theme={null}
import { draftMode } from "next/headers"

const { isEnabled } = await draftMode()
// Only mount the widget in draft/edit mode
{isEnabled && <ImmersiveEditingWidget slug={slug} />}
```

## What to read next

<CardGroup cols={2}>
  <Card title="Visual editing with Puck" icon="hand-pointer" href="/integration/puck-mode">
    The other direct-manipulation surface — drag and drop on the same blocks, built on Puck (puckeditor.com).
  </Card>

  <Card title="A tour of the editor" icon="pen-to-square" href="/editing">
    The chat editor these modes sit alongside.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.