@accelerated-agency/visual-editor
v0.4.5
Published
Conversion visual editor as a reusable React package
Readme
@conversion/visual-editor
Reusable visual editor package for embedding Conversion's editing UI inside a host React app.
Exports
This package exports:
PlatformVisualEditor(recommended host wrapper)EditorShell(low-level editor shell UI)ToastProvider,useToastvisualEditorProxyPlugin(Vite dev-server plugin for/api/proxyand optional AI API route)- Types:
PlatformVisualEditorProps,VisualEditorExperiment,VisualEditorVariation,VisualEditorTab, plus mutation/message types
Install
yarn add @conversion/visual-editorLocal development (package + consumer app)
Use this setup so changes in this package reflect in your consumer app immediately.
1) Consumer app dependency
In consumer app package.json:
"@conversion/visual-editor": "file:../conversion-visual-editor"Run:
yarn install2) Run package in watch mode
In conversion-visual-editor terminal:
npm run watch3) Consumer app Vite config (local-dev only)
import { defineConfig } from "vite";
import path from "path";
export default defineConfig({
resolve: {
preserveSymlinks: true, // local-dev only
},
optimizeDeps: {
exclude: ["@conversion/visual-editor"], // local-dev only
},
server: {
fs: {
allow: [path.resolve(__dirname, "..")], // local-dev only
},
watch: {
ignored: ["!**/node_modules/@conversion/visual-editor/**"], // local-dev only
},
},
});Start consumer app:
yarn dev --forceIf one update is stale, clear cache once:
rm -rf node_modules/.viteRemove these before/after npm publish
When you move from local file: development to npm package usage, remove/revert in consumer app:
file:../conversion-visual-editordependency (replace with npm version)resolve.preserveSymlinks: trueoptimizeDeps.exclude: ["@conversion/visual-editor"](if no longer needed)server.fs.allowoverride used for local package pathserver.watch.ignoredoverride for package path
Required host setup
PlatformVisualEditor is designed to run in embedded mode and expects a proxied page-loading flow.
1) Add the Vite plugin (recommended)
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { visualEditorProxyPlugin } from "@conversion/visual-editor";
export default defineConfig({
plugins: [
react(),
visualEditorProxyPlugin({
// optional, only needed for AI test generation endpoint
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
// optional, defaults to true
enableGenerateTestApi: true,
}),
],
});What this plugin provides:
GET/POST /api/proxyfor loading target storefront pages through your app origin- HTML rewriting for proxied pages (asset/action rewriting, popup suppression, bridge script injection)
POST /api/generate-testendpoint (optional; used by AI panel)
2) Ensure /bridge.js is served by your app
The proxy injects:
<script src="/bridge.js"></script>Your host app must serve this file at /bridge.js for iframe/editor communication to work correctly.
3) Mount PlatformVisualEditor in your route/page
import { PlatformVisualEditor } from "@conversion/visual-editor";
export function VisualEditorPage() {
return (
<PlatformVisualEditor
experiment={{
experimentId: "exp_1",
name: "Homepage Hero Test",
status: "draft",
pageUrl: "https://store.example.com",
editorPassword: "",
variations: [
{
_id: "control",
iid: 0,
name: "Control",
baseline: true,
traffic_allocation: 50,
changesets: "[]",
csscode: "",
jscode: "",
},
],
}}
onRequestSave={async ({ experimentId, variations, hash }) => {
// Save to your backend
// hash is set for save-and-navigate requests (for example "#code-editor")
}}
onNavigateRequested={(hash) => {
// Move to host tab/route after successful save-and-navigate
}}
onEditorUrlChanged={async ({ url, password }) => {
// Optional: persist latest URL/password selection
}}
onClose={() => {
// Close editor page/modal
}}
/>
);
}Environment variables
Required
- None for core visual-editor functionality.
Optional
ANTHROPIC_API_KEY: required only if you use AI test generation (/api/generate-test).- If missing and AI route is enabled, AI generation requests will fail with a server error.
- You can disable the route with
visualEditorProxyPlugin({ enableGenerateTestApi: false }).
Data contracts
VisualEditorExperiment
experimentId?: stringname?: stringstatus?: stringpageUrl?: stringeditorPassword?: stringvariations?: VisualEditorVariation[]
VisualEditorVariation
_id: stringiid?: numbername: stringbaseline?: booleantraffic_allocation?: numbercsscode?: stringjscode?: stringchangesets?: string(JSON string)
Save and navigation flow
When users click save/finalize in the editor:
- Editor emits
save-experimentorsave-and-navigate PlatformVisualEditorcalls youronRequestSave(payload)- On success:
- dirty state resets
onSaveSuccessis called- for
save-and-navigate,onNavigateRequested(hash)is called
- On failure:
onSaveError(error)is called
Main props reference (PlatformVisualEditor)
channel?: stringpostMessage channel name, defaultconversion-platformembeddedGlobalKey?: stringglobal embedded marker, default__CONVERSION_EMBEDDED__className?: stringouter wrapper classeseditorClassName?: stringeditor content wrapper classesshowHeader?: booleanshow default headertitle?: stringheader title overridestatus?: stringheader status badgetabs?: VisualEditorTab[]tabs to render in default headeractiveTab?: stringactive tab label in default headerloading?: booleanloading stateerror?: string | nullerror stateshowCloseButton?: booleantoggle default close buttoncloseLabel?: stringclose button textloadingText?: stringloading textsaveDebounceSkips?: numbermutation-change events to ignore before setting dirtyexperiment?: VisualEditorExperimentinput experiment payloadonClose?: () => voidclose handleronTabChange?: (tab: VisualEditorTab) => voidtab click handleronDirtyChange?: (dirty: boolean) => voiddirty-state callbackonEditorReady?: () => voidcalled after editor bootstrapsonSaveSuccess?: (payload) => voidcalled after successful saveonSaveError?: (error: unknown) => voidcalled on save failureonRequestSave?: (payload) => Promise<void> | voidrequired to persist changesonEditorUrlChanged?: (payload) => Promise<void> | voidURL/password change callbackonNavigateRequested?: (hash: string) => voidpost-save navigation callbackonRestoreVersion?: (payload) => voidrestore callbackonDiscardDirty?: () => boolean | Promise<boolean>custom unsaved-changes guardrenderHeader?: (...) => ReactNodecustom header rendererrenderLoading?: () => ReactNodecustom loading rendererrenderError?: (message) => ReactNodecustom error renderer
Known integration notes
- Keep the editor component mounted while async save is running.
- The editor loads target pages via
/api/proxy; cross-origin requests made by the target page may still need proxying/server-side handling. - If you customize
channel, keep the same value on both host and editor surfaces.
