# Runtime reference
One script tag. `window.nsiteclay` (short alias `window.nc`) exists as soon as the script runs,
carrying a `ready` promise. Everything else is safe once that resolves.
```html
```
## Configuration: attributes on ``
```html
```
| Attribute | Required | Meaning |
|---|---|---|
| `nc:owner` | yes | npub or 64-hex of the key allowed to save this document |
| `nc:relays` | no | comma list. Default `wss://nos.lol,wss://relay.damus.io,wss://relay.primal.net` |
| `nc:servers` | no | comma list of Blossom servers. Default `https://cdn.hzrd149.com,https://nostr.download` |
| `nc:site` | no | nsite `d` tag. Absent means the root site (kind 15128); set means a named site (kind 35128), 1 to 13 characters of `[a-z0-9-]` |
| `nc:path` | no | this document's path inside the manifest. Default `/index.html` |
| `autosave` | no | save once edits settle (2.5 s debounce, 15 s throttle). `⌘S` / `Ctrl+S` works either way |
| `nc:autoreload` | no | `"false"` stops the document watching its manifest for newer versions |
| `nc:runtime-owner` | no | whose runtime this page will offer to upgrade to. Default is the project's own key; `"off"` never looks |
| `nc:watch-dom` | no | count any DOM change as an edit, not only typing. Off by default: a page that redraws itself would be permanently unsaved |
Unknown `nc:*` attributes are ignored, so a document written for a later version still renders.
An image can declare `nc:crop="1:1"`, `"16:9"`, `"4:3"` or `"free"` to select
the initial crop mode in its image picker. This affects new file uploads only.
`await nc.media.crop(file, { aspect: 1 })` returns quickcrop's `{ blob, width,
height, dataURL }`, or `null` on cancellation. Crops are capped at 2560 pixels
on either axis. The original file stays untouched.
An image can also carry `nc:prompt`, a description of the picture for an image
generator. The publisher's AI builder asks the model for one on every frame it
leaves empty when pictures are on, and the image picker offers "Make it with AI"
prefilled with it (falling back to `alt`) when an AI key and a picture model are
set. `await nc.ai.picture(prompt, crop)` makes one picture cut to `crop` and
returns `{ name, blob }`, where `name` is `images/.webp` (or `.jpg` on a
browser that cannot write WebP).
A rewrite asked to change pictures marks each frame to redraw with
`nc:redraw` and a new `nc:prompt`. Only marked frames, and empty ones, are
drawn again, within the credit; the marker never stays in the page. The redraw
instruction is sent only when the request mentions pictures, so a change to
the words is sent exactly as before.
The runtime writes these back onto `` for the document's own CSS and logic, and strips
every one of them from anything it saves:
`nc:ready`, `nc:status` (`idle` / `saving` / `saved` / `error`), `nc:pubkey`,
`nc:owner-here` (`true` / `false`), `nc:editmode`, `nc:editable`, `nc:outdated`.
```css
html[nc\:status="saving"] .dot { animation: pulse 1s infinite; }
html[nc\:owner-here="true"] .owner-only { display: block; }
```
## Members
### Content fields and data
The existing `nc:cms` rules accept `@innerHTML` for rich text. The content form
shows an editable rich-text field and sanitises its output. Other fields can
declare `nc:cms-type="select|number|date|time|url|email|color|textarea"` on the
target element. A select uses `nc:cms-options='["draft","published"]'` or a
list of `{ "value": "draft", "label": "Draft" }` objects. Native constraints
use `nc:cms-min`, `nc:cms-max`, `nc:cms-step`, `nc:cms-required`,
`nc:cms-pattern` and `nc:cms-maxlength`; invalid input leaves the page unchanged.
`nc.cms.getData(name = "cms")` reads scalar fields, nested groups and lists.
`nc.cms.setData(partialValues, name = "cms")` validates all targets before
changing them, requires the owner, and records one undo step. Lists must match
their current length; their existing Add and Remove controls change structure.
Template seeds are excluded from data. No JSON sidecar is published automatically.
### Source-preserving saves
`nc.source.ready` settles after fetching and pairing the source HTML. A save
before it settles uses the full serializer. `nc.source.text()` returns the
source string, and `nc.source.locate(element)` returns `{ from, to, line,
column }` or null for an unpaired element. Offsets are UTF-16 code units, not
UTF-8 bytes. Pairings describe the loaded or last accepted source, not an
unsaved replacement node.
The renderer verifies every output against the cleaned save document. On a
mismatch it uses the full serializer and emits `nsiteclay:save-reprinted` with a
reason. `nc.source.reprints` counts those fallbacks. Blossom still stores the
whole document after a change; this preserves author formatting, not storage quota.
### Undo
`nc.undo.undo()` and `nc.undo.redo()` navigate the current editing session.
`nc.undo.commit(label, fn)` groups a synchronous DOM change into one step;
`nc.undo.flush()` ends a typing batch. `canUndo` and `canRedo` report availability.
Recording runs only while the owner is editing. Runtime chrome, fetched content
and no-save regions are excluded. Plain form fields keep native typing undo;
property-only changes made by scripts can call `nc.undo.recordValue(element,
{ prop: "value", oldValue, newValue })`. History clears on sign-out or leaving edit mode.
### Common operations
`await nc.agentInstructions()` returns the text that **Settings → Copy instructions
for your own agent** puts on the clipboard: how an outside coding agent pulls this
site, edits it and deploys it back without breaking the editor, filled in with the
page's owner, site name, relays, Blossom servers, the pages in the live manifest
and the engine version. It is generated from a template in the runtime, with no AI
call, and holds no key. The same template with no facts is published as
`/llms-edit.txt` on the project site.
`nc.ai.settings()` opens the AI settings hub, with provider/model configuration
under Advanced and backup/recovery in its own section. `nc.ai.addCredit()` opens
the focused Lightning/Cashu flow; `nc.ai.withdraw()` opens withdrawal. `nc.ai.client`
is the OpenAI-compatible client; its default base is
`https://routstr.cypherpunk.today/v1` and its default model is
`deepseek-v4-1-flash-1017`. Settings and endpoint-bound credentials are kept locally
and backed up by `nc.ai.keepOnRelays()` to the owner's encrypted vault. The vault
keeps the legacy `ai[endpoint]` key map plus `aiProfiles[endpoint]` with `mode`,
`base` and `model`, and `aiSelected` for fresh-device restoration. `nc.ai.adopt()`
restores the complete profile; `{ force: true }` requests an explicit restore.
Backup failure returns false and the dialogs show recovery instructions.
`models()`, `balance()`, `cashu(token)`, `invoice(sats)`, `invoiceStatus(invoice)`
and `refund()` use that explicit endpoint. No node discovery or automatic retry
of paid requests occurs. `complete(messages, { signal, onProgress, maxTokens })`
streams text but only resolves for a complete response with `finish_reason: stop`.
Raw stream fragments must never be applied to the live document.
`await nc.ai.client.check()` returns readiness, available Routstr sats after
reservations, and current model pricing when available. States are `noKey`,
`ready`, `insufficient`, `invalidKey`, `modelMissing`, `unavailable`, and
`configured` for BYOK. BYOK has no standard balance API. Unknown balance is not
reported as zero. Cost ranges are estimates, not provider quotes or spending caps.
`await nc.ai.edit(element)` opens the prompt and preview flow, including proposed
version refinement, comparison, and locally recovered drafts. Omitting the element
selects the whole static page. Live feeds go to the model as numbered
`nc:keep` places and come back exactly as they were; a rewrite that leaves one
out is refused. Pages with forms or custom scripts offer element editing
instead. For a custom UI,
`await nc.ai.propose(element, prompt, { signal })` returns a proposal and
`nc.ai.accept(proposal)` applies it as one undo step. Both require the owner.
Acceptance refuses a target changed since generation began. Only the selected
element goes to the model. Scripts, live feeds and whole-document elements are
excluded from element proposals. `nc.ai.proposePage(prompt, options)` returns
`{ page, before }`; `nc.ai.applyPage(page, { before })` checks ownership and the
original snapshot before replacing the page as one undo step.
`nc.ai.drafts.read(kind)`, `.write(kind, draft)`, `.clear(kind)` and
`.import(kind, jsonText)` manage local drafts. Kinds are `edit`, `builder` and `newpage`.
Storage is scoped to the signed-in pubkey, document path and workflow, keeps up
to five proposed versions, and reports write failure without discarding memory.
Draft files are sanitised again before preview or acceptance. They contain page
content and instructions, not provider credentials, and never enter the published
document or the relay vault.
`await nc.ai.buildPage(description, { template, lang, signal, onProgress })`
generates a static document. Omit `template` to start from scratch.
`nc.ai.preparePage(html, { owner, path, lang })` sanitises a completed document,
assigns the publisher's ownership/path, and supplies runtime references, toolbar,
editable regions, CMS rules and a fallback block library. It removes generated
scripts and forms, and CSS that reaches off the page. It refuses a document that
carries no stylesheet, because that one publishes as unstyled text.
`await nc.ai.buildContent({ title, description, href, css, blocks, pages, lang, pictures, previous, instruction })`
returns only the inside of `` for a new page in an existing design, sanitised and marked editable;
`nc.pages` puts it into the page's own shell. `nc.ai.uploadPictures(html)` swaps pictures made in this
browser for addresses on the owner's Blossom servers.
`nc.ai.previewHTML(html)` adds a restrictive CSP for display in an iframe with an
empty `sandbox` attribute. Preparing or previewing never publishes anything.
```js
await nc.ready // resolves to nc
nc.cfg // the parsed configuration
nc.pubkey / nc.npub / nc.isOwner
nc.status // idle | saving | saved | error
nc.dirty // set true to suppress auto-reload while work is unsaved
await nc.login("auto") // "nip07" | "nip46" | "bunker" | "nsec"
await nc.login("nsec", { key, password }) // nsec1…, 64 hex, or ncryptsec1… + password
await nc.login("bunker", { uri }) // bunker://…
const { uri, ready } = nc.connectRemote() // nostrconnect:// for Amber; await ready
await nc.logout() // also forgets this browser as the editor's, and the kept sign-in
await nc.resuming // settles once a kept sign-in is back (pubkey) or not (null)
nc.editRequested // is the edit gate open (always true without nc:edit-gate="hash")
nc.editorRemembered // has the owner signed in on this site in this browser
nc.rememberEditor(true | false) // set or forget that, and re-apply the gate
nc.dismissRememberedEditor() // close a gate only the memory opened, for this tab
nc.getHTML() // the exact string a save would store
await nc.save() // → { hash, bytes, manifest, version, aggregate }
// or { skipped: true } when nothing changed
await nc.save({ extraPaths, dropPaths }) // publish other files in the same manifest.
// A drop the document still references is refused
await nc.publishFiles(files, { servers, relays, onProgress }) // a whole site, from the browser
// onProgress stages: "upload" { path, done, total }, "server" { server, state, detail },
// "manifest" { uploaded, reused }. Server states: checking, present, absent, signing,
// sending, ok, failed. Every network call has a timeout (nc has none to override;
// blossom.js TIMEOUTS: head 20s, get 60s, put 120s, signer 180s)
await nc.currentManifest() // the live nsite manifest event
await nc.versions() // kind-5128 snapshots, newest first
await nc.readVersion(snap) // that version's HTML, fetched and hash-verified
await nc.restore(snap) // republish that version's path table as current
nc.editable.enable() / .disable() // re-run enable() after inserting new [editable] nodes
nc.editable.block("H2") // what the block menu calls
nc.reloadToLatest() // reload through a URL no cache can satisfy
// blocks: a page built out of markup rather than out of rows
nc.blocks.open() / .add(name, { container, before })
nc.blocks.partsOf(block) // what is inside one, depth-first, furniture excluded
nc.blocks.parts(block) // and the dialog that lists it, with remove and reorder
// moving a published site to a newer runtime
nc.version // the version of the engine this page is running
await nc.upgrade.check() // → a plan, or null when there is nothing newer
await nc.upgrade.prompt() // check, then offer it; what Settings calls
nc.unstamp(path) / nc.stamp(path, hash) // the content stamp in an asset URL
// structure: the DOM is the database, these are the operations on it
nc.dom.clone(el) / .remove(el) / .move(el, +1|-1, selector) // move reorders in one parent
nc.dom.moveTo(el, container, "beforeend") // and this crosses parents
nc.dom.insert(target, html, "beforeend") / .addFrom("template-id", container)
nc.dom.toggle(el, "hidden") / .set(el, "data-status", "done")
nc.dom.cloneClosest(btn, ".card") // from a control inside the block
nc.dom.removeClosest(btn, ".card") / .removeClosestAsk(btn, ".card", "this card")
nc.dom.moveClosest(btn, -1, ".card") / .toggleClosest(btn, ".card", "hidden")
nc.dom.moveToClosest(btn, ".card", ".col", +1|-1) // walk a card along the columns
nc.dom.all(".card") / .by(".card", "data-status") // reading it back
// state with no visual form, kept in a JSON script block in the page
nc.state.get() / .set({…}) / .update({…}) // optional id, default "app-state"
// a generated form for the page, from a