DocsHosting
Edit real application components visually
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
- Ask the agent to inspect your source and call
sites_components_list. Decide which components and fields should be editable. - Implement each component in the app, then register its JSON definition
with
sites_components_register. A project withoutsite.jsongets an empty application layout with a/page. Converting an existing static design requiresapplicationMode: trueand preserves its sections. - Run
sites_components_install_runtimeto writenextos-components.mjsat 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. - Insert instances with
sites_components_insertor Site Editor. Render the layout usingcreateReactPagefor React ormountPagefor DOM code. Your app must importsite.jsonor deliberately serve it through a route; a root file does not become public automatically. - Set
rendering.previewUrlto the running application's HTTP or HTTPS address and connectconnectSiteEditorwith your trusted NextOS origin. The application must run on a separate origin and permit iframe embedding. - 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:
| Type | Definition fields |
|---|---|
string, text, color, url, image | label, description, required, defaultValue |
number | Common fields plus min, max, and step |
boolean | Common fields and a Boolean default |
select | Common fields plus a nonempty options array of strings |
object | Common fields plus nested subFields |
list | Common 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
| Tool | Inputs 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.