Skip to content

DocsHosting

Edit real application components visually

Updated Oct 20, 2026

Your application's own components can appear in Site Editor's component palette. Write a counter, product card, form, or layout in your normal application code, register the fields that people can edit, and compose instances by dragging them into a page. Site Editor supports typed property controls, nesting, reordering, and undo/redo, with a preview of the running application.

The implementation stays in your React or JavaScript source. site.json contains a component catalog, page layout, stable instance IDs, and JSON props. Python applications can serve the same browser components while keeping their routes, APIs, and business logic in Python.

Connect the application

  1. Ask the agent to inspect your source and call sites_components_list. Decide which components and fields should be editable.
  2. Implement each component in the app, then register its JSON definition with sites_components_register. A project without site.json gets an empty application layout with a / page. Converting an existing static design requires applicationMode: true and preserves its sections.
  3. Run sites_components_install_runtime to write nextos-components.mjs at the project root. Import it in your app and connect the implementations to its registry. Installation writes source; it does not run or deploy it.
  4. Insert instances with sites_components_insert or Site Editor. Render the layout using createReactPage for React or mountPage for DOM code. Your app must import site.json or deliberately serve it through a route; a root file does not become public automatically.
  5. Set rendering.previewUrl to the running application's HTTP or HTTPS address and connect connectSiteEditor with your trusted NextOS origin. The application must run on a separate origin and permit iframe embedding.
  6. Edit props and move components in Site Editor. Draft changes update the running preview. Save your source/layout and use the application's normal deployment workflow to release them. Updating the preview does not deploy the production application.

Once production and domain setup are ready, the application can use its <name>.sites.jonkum.in address or a verified custom domain. Either can serve as rendering.previewUrl if the app permits embedding and connects the preview bridge. Keep editorOrigin set to the trusted NextOS origin; it identifies the editor, rather than the application's own domain. See application addresses for domain setup and the distinction between a public address and an immutable build URL.

Arrange components on the page

In Select components mode, drag a registered component from the palette, or an existing instance from the outline, onto the live application canvas. Drop near a component's top or bottom edge to place it before or after that component. Drop into the center of a component that accepts children to nest it there; drop on blank page space to append it. The destination must accept that component type.

The outline also provides drop positions and keyboard-accessible movement controls. Use it to navigate or rearrange a nested layout when a canvas target is small. Undo and redo apply to layout changes from either surface. Switch to Use application to operate buttons and forms instead of selecting or arranging components.

If saving is refused because an agent or source editor changed the layout, choose Reload from source in the Builder toolbar, or leave and reopen Builder, to load the current file. Then reapply the intended change. This preserves the other edit. The preview's Reload button reloads the running app; it does not replace a stale layout with the latest source.

A working React counter

This example fits an existing Next.js App Router application with React installed and its root layout already set up. Install the runtime at the project root and place this in app/page.jsx. Replace the example editor origin with the exact origin where you use NextOS, including its port when running locally.

'use client';

import React, { useEffect, useState } from 'react';
import initialSpec from '../site.json';
import {
  createComponentRegistry,
  createReactPage,
  connectSiteEditor,
} from '../nextos-components.mjs';

function Counter({ label = 'Clicks', initial = 0 }) {
  const [count, setCount] = useState(initial);
  useEffect(() => setCount(initial), [initial]);
  return (
    <section>
      <h2>{label}</h2>
      <p aria-live="polite">{count}</p>
      <button type="button" onClick={() => setCount(value => value + 1)}>
        Add one
      </button>
    </section>
  );
}

const registry = createComponentRegistry().register('Counter', Counter, {
  label: 'Counter',
  inputs: [
    { name: 'label', type: 'string', defaultValue: 'Clicks' },
    { name: 'initial', type: 'number', defaultValue: 0, min: 0, step: 1 },
  ],
});

const SitePage = createReactPage(React, registry);

export default function Page() {
  const [view, setView] = useState({ spec: initialSpec, pagePath: '/' });
  useEffect(() => connectSiteEditor({
    editorOrigin: 'https://your-nextos.example',
    onUpdate: ({ spec, pagePath }) => setView({ spec, pagePath }),
  }), []);
  return <SitePage spec={view.spec} pagePath={view.pagePath} />;
}

Keep the catalog in site.json aligned with registry.manifest(), which returns JSON definitions without implementation functions. This complete layout displays one instance; use your real preview URL when connecting the editor:

{
  "version": 1,
  "theme": "ledger",
  "brand": { "name": "Counter demo" },
  "nav": [],
  "footer": {},
  "rendering": {
    "mode": "application",
    "previewUrl": "https://counter-app.example"
  },
  "components": [
    {
      "name": "Counter",
      "label": "Counter",
      "inputs": [
        { "name": "label", "type": "string", "defaultValue": "Clicks" },
        { "name": "initial", "type": "number", "defaultValue": 0, "min": 0, "step": 1 }
      ]
    }
  ],
  "pages": [
    {
      "path": "/",
      "title": "Counter demo",
      "sections": [
        {
          "id": "counter-one",
          "type": "component",
          "component": "Counter",
          "props": { "label": "Clicks", "initial": 0 }
        }
      ]
    }
  ]
}

theme, brand, nav, and footer retain the site format's required structure; the application chooses how to render them. The runtime renders the chosen page's components. It does not add a theme or navigation UI.

Typed inputs and child components

Each registration has a unique name and an inputs array. Optional label and description explain it in the palette. Inputs support these types:

TypeDefinition fields
string, text, color, url, imagelabel, description, required, defaultValue
numberCommon fields plus min, max, and step
booleanCommon fields and a Boolean default
selectCommon fields plus a nonempty options array of strings
objectCommon fields plus nested subFields
listCommon fields plus itemType: string, number, boolean, or object; object items can use subFields

Expose editable values, rather than JavaScript functions or arbitrary executable strings. Event handlers and data access stay in component code. An image or URL input supplies a value to your implementation; the component still decides how to render it.

A parent declares canHaveChildren: true and may restrict child names with allowedChildren. Register allowed children before the parent. For example, register this after Counter and include both definitions in the catalog:

function Stack({ gap = 16, children }) {
  return <div style={{ display: 'grid', gap }}>{children}</div>;
}

registry.register('Stack', Stack, {
  label: 'Stack',
  canHaveChildren: true,
  allowedChildren: ['Counter'],
  inputs: [{ name: 'gap', type: 'number', defaultValue: 16, min: 0 }],
});

A nested instance stores children alongside its props:

{
  "id": "stack-one",
  "type": "component",
  "component": "Stack",
  "props": { "gap": 24 },
  "children": [
    {
      "id": "counter-two",
      "type": "component",
      "component": "Counter",
      "props": { "label": "Inside the stack", "initial": 5 }
    }
  ]
}

A Python app with browser components

Here is the same layout served by Flask without React. Keep the example site.json and installed runtime at the project root. With Flask installed, create app.py:

from pathlib import Path
from flask import Flask, render_template, send_from_directory

app = Flask(__name__)
project_dir = Path(__file__).resolve().parent

@app.get("/")
def index():
    return render_template("index.html")

@app.get("/healthz")
def health():
    return {"status": "ok"}

@app.get("/layout.json")
def layout():
    return send_from_directory(project_dir, "site.json", max_age=0)

@app.get("/nextos-components.mjs")
def runtime():
    return send_from_directory(
        project_dir, "nextos-components.mjs", mimetype="text/javascript"
    )

Create templates/index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Counter demo</title>
  </head>
  <body>
    <main id="app"></main>
    <script type="module" src="/static/app.js"></script>
  </body>
</html>

Create static/app.js. The DOM adapter receives its container and props; it returns cleanup for its event listener:

import {
  createComponentRegistry,
  mountPage,
  connectSiteEditor,
} from '/nextos-components.mjs';

function Counter(container, { props }) {
  const heading = document.createElement('h2');
  heading.textContent = props.label;
  const count = document.createElement('p');
  count.setAttribute('aria-live', 'polite');
  let value = props.initial;
  count.textContent = String(value);
  const button = document.createElement('button');
  button.type = 'button';
  button.textContent = 'Add one';
  const increment = () => { count.textContent = String(++value); };
  button.addEventListener('click', increment);
  container.append(heading, count, button);
  return () => button.removeEventListener('click', increment);
}

const response = await fetch('/layout.json');
if (!response.ok) throw new Error('Could not load the page layout');
const spec = await response.json();
const definition = spec.components.find(item => item.name === 'Counter');
if (!definition) throw new Error('Register Counter in the layout catalog');
const registry = createComponentRegistry().register('Counter', Counter, definition);
const mounted = mountPage(document.getElementById('app'), spec, registry, '/');
const disconnect = connectSiteEditor({
  editorOrigin: 'https://your-nextos.example',
  onUpdate: ({ spec, pagePath }) => mounted.update(spec, pagePath),
});
window.addEventListener('pagehide', event => {
  if (!event.persisted) {
    disconnect();
    mounted.dispose();
  }
});

Run locally with flask --app app run --port 5000, then set the preview URL to that address when the editor can reach it. Deploy using the normal Flask code-project workflow and update the preview URL to the deployed origin. For production embedding, use HTTPS and allow your editor's origin in your app's framing policy.

Python serves the app, layout, and APIs. Browser JavaScript renders and interacts with the editable components. For DOM parents, append the supplied children elements at the intended slot; the runtime manages their updates and calls cleanup when replacing or disposing the page.

Agent tool reference

ToolInputs and result
sites_components_list{ slug } returns registrations, layout summary, and runtime installation status.
sites_components_register{ slug, definition, replace?, applicationMode?, previewUrl? } validates a definition and stores it in the catalog. Replacing a name requires replace: true; existing instances must remain valid.
sites_components_insert{ slug, name, pagePath, index?, parentId?, props? } inserts an instance with defaults and returns its stable section ID. Use parentId for an allowed nested child.
sites_components_install_runtime{ slug, expectedRevision? } creates the runtime module. Read its current source revision before replacing an existing copy.

To adapt an existing static design, opt into application mode explicitly. Its original sections remain in the layout. Provide a renderer for each retained stock section by registering its type name in your application, or deliberately migrate that section to a registered component. An unknown renderer produces an error rather than silently inventing a replacement.

Preview and publishing

Set rendering.mode to application and use an explicit HTTP(S) rendering.previewUrl. That origin must differ from the OS editor's origin. In app code, configure editorOrigin from a trusted setting or literal; do not copy it from an incoming message, URL query, or referrer.

The preview bridge sends JSON layout updates, component selection, and drop intents. It does not run component source in the OS or give the application privileged OS tools. Your normal iframe headers must permit the editor to embed the application. In edit mode, clicking selects a component; switch to Use application or open the application directly to exercise its controls.

The bridge applies updates in order and keeps the latest pending layout. For asynchronous work, onUpdate also receives a second argument containing an abort signal. Pass it to fetches and stop cancelled work before updating your UI. Call the disposer returned by connectSiteEditor when unmounting.

Source, component metadata, and layout must ship together. Registration alone cannot implement a component or launch an application. After editing, follow the regular source → sync → deploy → status workflow, then check the actual application behavior at its returned URL. If your app bundles site.json, the updated production layout requires a rebuild and redeployment.