> From the [Strife developer docs](https://strife.app/docs/reference/live-preview/custom-live-preview-implementation). Every page is listed in [llms.txt](https://strife.app/docs/llms.txt).

# Custom Live Preview implementation

No matter what front-end framework you are using, you can build your own hook using the same underlying tooling that Strife provides.

First, install the `@strifeapp/strife` base package:

```bash
npm install @strifeapp/strife
```

This packages provides you with the following functions:

| Path        | Description                                                                                                 |
| ----------- | ----------------------------------------------------------------------------------------------------------- |
| `subscribe` | Subscribes to the the Strife studio's `window.postMessage` events and calls the provided callback function. |

The `subscribe` function takes the following arg:

| Path       | Description                                                                                 |
| ---------- | ------------------------------------------------------------------------------------------- |
| `callback` | A callback function that is called with `data` every time a change is made to the document. |

With this function, you can build your own hook using your front-end framework of choice:

```javascript
import { subscribe } from '@strifeapp/strife'

const onChange = (data) => { ... }
const unsubscribe = subscribe(onChange);
// To unsubscribe, call `unsubscribe();`

// To build your own hook, subscribe to Live Preview events using the`subscribe` function
// It handles everything from:
// 1. Listening to `window.postMessage` events
// 2. Calling the `onChange` callback with the result
// Your hook should also:
// 1. Handle the results of the `onChange` callback to update the UI
// 2. Unsubscribe from the `window.postMessage` events when it unmounts
```

#### Callback data shape

The `data` argument passed to your callback is a plain object representing the current document state, keyed by each editor's `propertyName`. For a template defined as:

```typescript
const Homes: Template = {
  displayName: 'Home',
  collection: 'Homes',
  editors: [
    { label: 'Heading',     editor: { name: 'str-input',    type: 'text', propertyName: 'heading' } },
    { label: 'Description', editor: { name: 'str-textarea', type: 'text', propertyName: 'description' } },
  ],
};
```

the callback receives:

```typescript
subscribe((data) => {
  // data.heading: string        — current value of the Heading editor
  // data.description: string    — current value of the Description editor
});
```

Every editor's `propertyName` becomes a top-level key on `data`. Nested types (e.g. objects from `str-image-group` or arrays from `str-multi-select`) follow the same shape as they appear in the stored document.

#### Hover highlighting and `stegaClean`

While a document is open in the editor's live preview, moving the pointer over your page outlines the element under it on the page, and a click on it opens the matching field in the editor: the tab it sits on, the chapter it belongs to, an image's editing card. To make that work for a site that renders the state itself, the editor hides each text field's path **inside the text value** as zero-width characters (the same technique as Vercel's Content Link and Sanity's Visual Editing). They carry no glyph and no width, HTML escaping leaves them alone, and a published site never receives them: only the editor's preview sends them.

They are still characters, so a comparison against a preview value can fail:

```javascript
import { subscribe, stegaClean } from '@strifeapp/strife'

subscribe((data) => {
  data.heading === 'Welcome';             // false in the preview: the value starts with the hidden path
  stegaClean(data.heading) === 'Welcome'; // true
  const clean = stegaClean(data);         // a deep copy with every hidden path removed
});
```

Use `stegaClean` on any preview value you compare, match a route against, or measure (`length`). Values a site is likely to compare are never encoded to begin with: slugs, dates, numbers, option codes of pickers and radio groups, labels, keywords and SEO fields. Only text, textarea and rich text values carry a path, plus an image's `alt` text (its URL is never touched).

Rendering needs no change: print the values as they come, and the pointer resolves the field from the text under it, or from an image's `alt` attribute. A click keeps its effect on your page — nothing is prevented. If your components already carry `data-field` attributes (the SDK's own web components do), those win over the hidden path.

| Path          | Description                                                                                          |
| ------------- | ---------------------------------------------------------------------------------------------------- |
| `stegaClean`  | Removes the hidden field paths from a string, or recursively from an array or plain object.          |
| `stegaDecode` | Returns the field path hidden in a string, or `null`. Useful for building your own hover affordance. |

#### Initialization timing

Strife Studio performs a handshake with the preview iframe at page load. For `subscribe` to receive events, the SDK must be present in the page **before** that handshake completes — meaning before your framework has finished hydrating client components.

Load the SDK eagerly at the top of your application, via either of these patterns:

**npm install + import in your entry-point** (recommended for build-tool projects):

```js
// src/main.js (or main.ts, index.js, wherever your app boots)
import "@strifeapp/strife";
```

**CDN tag in your HTML `<head>`** (simplest when you don't have a build step):

```html
<script type="module" src="https://unpkg.com/@strifeapp/strife"></script>
```

Components can then import `subscribe` as normal — the ESM module is cached and resolves to the already-initialized singleton.
