# nsite-clay
> A self-editable nsite. One HTML file holds a page's markup, styling, content
> and state, and republishes itself: the browser serialises the document, stores
> it on Blossom, and publishes a Nostr event saying which bytes are now the page.
> Relays hold the pointer, Blossom holds the bytes, and there is no server in
> between.
This file is the brief for an agent. If you are building someone a website with
nsite-clay, read all of it before you start. It is long on purpose: it tells you
what the thing is, what to ask for, which template to begin from, and what will
go wrong. It links the documentation rather than restating it.
- Human walkthrough with screenshots: https://npub12edc7326qsryw5rw5yw0yh57fmj9r8jf4c8xazz6333w305qgnms9ypvj2.nsite.lol/docs.html
- Templates, browsable: https://npub12edc7326qsryw5rw5yw0yh57fmj9r8jf4c8xazz6333w305qgnms9ypvj2.nsite.lol/templates.html
- Runtime reference: https://github.com/jooray/nsite-clay/blob/main/docs/RUNTIME-API.md
- Where state lives: https://github.com/jooray/nsite-clay/blob/main/docs/state.md
- Source: https://github.com/jooray/nsite-clay
- What an nsite is, from the people who built the idea: https://nsite.run
---
## 1. What an nsite is
An nsite is web hosting on Nostr. Not long-form notes rendered as a blog: whole
HTML files, with their CSS, their images and their JavaScript.
Three pieces, and none of them is a server you rent:
**Blossom** stores bytes and names them by their own SHA-256. Upload a file, get
back its hash. Ask any Blossom server for that hash and you get exactly those
bytes or nothing, because the name *is* the checksum. Content-addressed storage,
so a file cannot be quietly swapped for a different one.
**A Nostr event** maps paths to hashes. `/index.html` is this hash, `/about.html`
is that one. The event is signed by the site owner's key, and it is *replaceable*,
meaning a newer one from the same key supersedes it. Publishing a new one is the
deploy. This is [NIP-5A](https://github.com/nostr-protocol/nips/blob/master/5A.md),
kind 15128 for a key's main site and 35128 for a named one.
**A gateway** is an ordinary HTTP server that reads the manifest, fetches the
blobs, checks the bytes hash to what the manifest claims, and serves them. Any
gateway can serve any site, because they all read the same public events.
### Why this works, and why it is not just a fashion
The address is derived from a key, so nobody assigns it and nobody can revoke it.
`https://.nsite.lol/` is yours because you hold the key, not because an
account exists somewhere.
Nothing is tied to one host. Six public gateways serve the same events today:
nsite.lol, nsite.run, nosto.re, nwb.tf, nsite.cloud, shakespeare.to. If one is
down, change the hostname. If all of them are down, run one yourself; the repo
ships one that fits in a file.
There is no account, no password reset, no billing relationship, and no terms of
service between a person and their own page.
### What nsite-clay adds
An nsite, on its own, is deployed from a terminal like any static site. nsite-clay
makes the page edit and republish *itself*, so the content management system is
inside the static HTML rather than behind it.
You change the DOM. Click around the page and edit text, or open your browser's
inspector and change it there; the page does not care which. The new version is
uploaded to Blossom and one replaceable event is republished saying which hash
`/` now points to. Any gateway renders it.
The idea and the editing model come from [HyperClay](https://hyperclay.com) and
[ClayJS](https://clayjs.com). This is that model with Nostr underneath instead of
a hosting platform.
---
## 2. The philosophy, briefly
**The document is the database.** There is no separate store. A checklist's state
is checkboxes in the markup, a board's cards are elements, a theme is an attribute.
Save the page and open the file in a text editor: everything is right there, in
HTML, readable by a person.
**The file at rest is the state.** No hydration step, no build. `curl` returns the
page as a reader sees it, which means crawlers index it and agents can read it
without executing anything.
**Ownership is a keypair.** "Who may edit this page" is a signature check, not a
row in somebody's users table.
**One key writes the document.** There is no shared editing, no per-visitor
content, and no conflict resolution. Two devices editing at once will lose one
side's work. Say this early rather than building toward it.
### Two places to put things
Relays are also a database, and confusing the two is the main way a page goes
wrong. The rule, in full at [docs/state.md](https://github.com/jooray/nsite-clay/blob/main/docs/state.md):
**The document holds what the page is.** Layout, copy, settings, application
state. A kanban board is the page; it has no life anywhere else.
**Relays hold what you published.** A blog post, a note, a photo: things with an
identity of their own that belong in every Nostr client, not only on this page.
Put a post only in the markup and Habla, njump and the rest never see it.
**A post can be both**, and usually should be. Publish the event, then bake a
rendered copy into the page with `nc.compose.bake(event)`. The markup carries
`nc:from="naddr1…"`, so the page is self-contained and readable with no
JavaScript and no relay, while the post stays a first-class Nostr object.
`nc.compose.refreshBaked()` pulls the copies back in line after an edit elsewhere.
**Neither** holds a draft, a filter or a scroll position. That is what the tab is
doing, not state. Serialisation captures attributes, so anything you put in
`dataset` is published to everyone, forever.
---
## 3. What you are actually building
One `index.html`. It links two files from the site root and nothing else:
```html
```
Your job is the markup, the styling and the words. Sign-in, saving, versions,
media upload, feeds and the post composer already exist. Writing your own is the
commonest mistake; check the runtime reference before implementing anything.
Never copy `nsite-clay.js` or `nsite-clay-base.css` into the site. They live at
the root so one upgrade fixes every page.
---
## 4. Start from a template
Do not start from a blank file.
```bash
npx nsite-clay init mysite --template=blog --npub=npub1…
npx nsite-clay deploy mysite --sec=nsec1…
```
Pick by what the page is *for*, then redesign it. The template gives you a
working page with the chrome wired up; the design is yours to change.
| Template | For | Look |
|---|---|---|
| `personal` | one person: who you are, what you write | deep blue-black, gold accent, articles as cards and notes as a timeline |
| `blog` | someone who already writes on Nostr | black, lunar, slow glow; articles in the main column, notes in a sidebar |
| `project` | a piece of software | warm paper and clay-red, numbered bands, terminal cards for the commands |
| `event` | a meetup, a gathering, a date and a place | black and white, poster-scale date and venue, downloadable calendar file |
| `links` | one page holding everything you publish | a contents page with numbered rows and printed destinations, not a stack of buttons |
| `gallery` | photographs | four rooms over one set: a canvas you drag, a sparse hang, a constellation, a plain column |
| `terminal` | a project, for people who like terminals | a shell session, phosphor green, readable top to bottom without pressing anything |
| `phrack` | a long technical write-up | a text file: monospace, 79 columns, ASCII rules, two colours |
| `brutal` | a manifesto, a zine, a loud event | thick borders, hard shadows, oversized type, flat colour |
| `eco` | an essay or a small studio | system fonts, almost no images, measures and prints its own page weight |
| `irc` | a log someone keeps adding to: notes, a journal, a changelog | a channel transcript, monospace, with a composer that appends a line and saves it |
Each is a complete page you can read for the pattern. Their rules are in
[CONTRACT.md](https://github.com/jooray/nsite-clay/blob/main/templates/_shared/CONTRACT.md),
which is short and binding: shared stylesheet from the root, `nc:edit-gate="hash"`,
the standard toolbar markup, CSS variables as the styling hook, no CDN links, no
autosave.
### Choosing when they are not sure
Ask what the page replaces. "My Linktree" is `links`. "My blog" is `blog` if they
post to Nostr already and `personal` if they mostly want an about page. "Our
project's site" is `project`, or `terminal` if the audience lives in one. "A
poster for a thing happening" is `event`. If they want to show pictures, `gallery`.
If they are a writer who cares about typography and hates decoration, `phrack` or
`eco`.
### Always ask
- **Their npub.** Required: it goes in `nc:owner` and it is the only key that can
ever change the page. Without a Nostr key nothing works.
- **What the page is for.** This picks the template.
- **What should come from Nostr**, if anything: their notes, their articles,
their photos, somebody else's.
- **What a stranger should do after thirty seconds.** Read something, come to a
thing, hire them, follow them, zap them.
- **What they do not want.** Most people have a strong opinion about at least one
thing they hate on websites, and it is cheaper to hear it now.
Also tell them, without being asked, that one key writes the document: there is
no shared editing and no second account.
### Ask as well, for a personal homepage
- Whether the page pulls their notes, their long-form posts, or both. Fetch a
sample first and design for what is actually there rather than what you
assumed: someone posting twice a day in two languages needs a different layout
from someone publishing an essay a month.
- What is at the top: a photo, a sentence, a list of what they are doing now.
- Which links matter, and whether they want a Lightning address on the page.
Take the picture, banner, name, about line, website and Lightning address from
their kind-0 profile rather than asking again. Everything above is judgement they
have and you do not.
---
## 5. Getting it online
```bash
npx nsite-clay deploy mysite --sec=nsec1… # a raw key
NOSTR_BUNKER_URI="bunker://…" npx nsite-clay deploy mysite # a remote signer
```
Prefer the bunker. It keeps the key off the machine running the deploy, which
matters when that machine is yours rather than theirs. Pass the URI in the
environment, never as an argument: an argument is visible to every process on the
box.
The page lands at `https://.nsite.lol/`. A CNAME at any gateway gives
a custom domain. `--site=blog` makes a second, separate site under the same key.
Develop against the local stack rather than public infrastructure:
```bash
npm run devnet # relay, Blossom and gateway on localhost, all in memory
```
Deploy into it with `--relays=ws://127.0.0.1:4869 --servers=http://127.0.0.1:4870`
and open `http://.localhost:4871/`. Browsers resolve anything under
`.localhost` to loopback, so the npub sits in the hostname exactly as in
production. Stop the process and everything it held is gone.
After the first deploy the page publishes itself and they never need a terminal
again. Tell them that.
---
## 6. Signing in: recommend a remote signer
Three ways in. Steer them to the second.
1. **A NIP-07 browser extension**, Alby or nos2x. Fine on their own machine.
2. **A remote signer over NIP-46**, Amber on Android or nsec.app. The key stays on
their phone and every save is approved there. The right default for a page
served by a gateway, and what you should suggest first.
3. **Pasting a key into the page.** It works, stays in the tab, is written
nowhere, and is still the weakest option. Say so rather than defaulting to it.
The toolbar handles all three. You write no sign-in code. The sign-in QR is
generated in the page, so the connection secret never leaves the browser.
Do not ask for their nsec. You do not need it and you should not have it.
---
## 7. The edit gate
Every template ships `nc:edit-gate="hash"` on ``. Readers see the page and
nothing to click. The owner adds `#edit` to the URL and the toolbar appears.
Tell them explicitly, because a page with no visible way in looks broken to
someone who has forgotten:
```
https://.nsite.lol/#edit
```
Put it in the handover note. `nc.settings.open()` turns the gate off for someone
who would rather have the toolbar always visible.
Autosave is off, deliberately: every save stores the whole page again and files a
version, so a timer publishes a dozen versions of one paragraph. Leaving with
unsaved work already warns them.
---
## 8. Making a page editable
Two attributes cover most of it.
- `editable` on a container: rich text, where Enter starts a paragraph and
selecting text raises a toolbar with headings, lists, quotes, links, pictures,
video and Nostr feeds.
- `editable="single-line"` on a heading, a caption or a list row: no Enter, no
block menu.
Mark everything a person would plausibly want to change. A template nobody can
edit without opening a text editor has missed the point. Aim for twenty or more
editable regions on a content page.
Form controls persist by default, because that is what "the document is the
database" means: a checked box is still checked for the next visitor. Put
`nc:no-persist` on a search box or a filter, where baking the last value into the
published file would be wrong.
---
## 9. When the page is an app, not a document
A board, a checklist or a directory needs more than typing. Put the controls on
the thing they act on and mark them `nc-gear`, so they appear only for a
signed-in owner who is editing:
```html
Fix Blossom sync
```
**Do not put `clay="no-save"` on a gear.** That strips it from the save, so the
pattern works exactly once and the published page has no controls. A gear is part
of the app and belongs in the file; the rules above keep it away from readers.
`clay="no-save"` is for markup that must never be written at all, such as a
composer holding a half-typed line. To have a control that both persists and is
never itself written, draw it from a script that persists.
`nc.dom` gives you `clone`, `remove`, `move`, `insert`, `addFrom` a ``,
`toggle` and `set`, plus `cloneClosest` and friends so a control addresses the
block it sits in. Every one arms the new markup for editing and marks the page
unsaved, which is the bookkeeping you would otherwise forget.
Read the document back as data with `nc.dom.all(".card")` and
`nc.dom.by(".card", "data-status")`. The DOM is the query engine; there is no
model to keep in sync with the view, because the view is the model.
State with no visual form goes in a JSON block through `nc.state.get()`,
`.set()` and `.update()`, which escapes the closing tag that would otherwise end
the block and the rest of your page.
`nc.dom` calls `nc.editable.refresh()` for you. You need it yourself only for
markup you inserted some other way, before focusing it, or the new region will
not be typeable.
---
## 10. Nostr content on the page
A feed is markup. It fetches at view time, verifies every signature in the
browser and sanitises what it shows:
```html
```
`nc:feed` is `notes` (kind 1), `articles` (kind 30023) or `images` (kind 20).
Also `nc:limit`, `nc:style` (`list` or `grid`), `nc:min-length` to drop
one-liners, `nc:topic` for a single tag, and `nc:feed-relays` to override.
Pinning an article uses its slug, not its event id: kind 30023 is replaceable, so
the id changes every time the author fixes a typo and an id-pin breaks.
Style feeds by overriding what they emit rather than rebuilding it. Each item is
an `` containing a `` with
`.nc-avatar`, `.nc-name` and `.nc-when` inside it, then the body as a sibling:
`.nc-title` and `.nc-summary` for an article, `.nc-text` for a note, `.nc-pics`
for pictures, and an optional `