Edit in the page. Re-render on the server. Publish on purpose.
Authoring runs on its own port, against the same content the delivery port serves. A block is selected on the rendered page rather than in a form beside it, the inspector edits exactly the fields that block’s model declares, and every commit goes back as a typed page model that the server re-renders. Nothing in that loop waits on a build.
The fourth step looks like something you could optimise away, and cannot. EDS blocks are not idempotent by design — Adobe’s own boilerplate collapses rows into cells, unwraps wrappers and moves nodes — so running decorate() a second time over its own output destroys content: a hero loses its buttons, a rich-text body is truncated to its first paragraph. The contract is that decorate() runs once, against server markup, so restoring that markup is the precondition for re-running it. This is not hypothetical. One re-decoration path shipped without the re-fetch and silently corrupted every block whose JavaScript was touched during a dev session; all five call sites now go through the one function whose comment explains why the fetch is not optional.
The editor
In the page, not beside it
The authoring port serves a content-free shell; the editor fetches the same <main> the delivery port would render — plus instrumentation — and decorates it in the browser.
Selection is a click on the rendered page. The left panel holds the insert palette and the site tree; the right holds Properties and the Outline together, each collapsible, each remembering whether you left it open. Existing blocks are dragged from their Outline row rather than from a handle on the page, so a click on the page always means select — there is no drag-versus-click ambiguity to resolve.
Structural edits — adding, moving or deleting a block or a section — re-render the whole <main> and re-run the full EDS decoration pipeline, section decoration included; skipping that half leaves moved content full-width and resurrects the section-metadata block. Field edits stay scoped to their one block. Both take the same route: mutate the typed page, write it, fetch the fragment, swap it in, decorate.
Pages are created, renamed and deleted from the site tree. A rename moves the page and, when it has children, its whole subtree under a single lock, and carries that page’s revision history across with it.
The typing mirror is a DOM write, not a save. It finds the element carrying that field’s resource identifier and writes text or HTML into it — no save, no re-decoration, no change to the page the editor holds in memory. Blur runs the real pipeline and replaces the local takeover with the server’s own render, which is why a decorated field can look momentarily different while you type and correct itself when you leave it. Clicks that land before the WebAssembly bundle has booted are lost outright; a click that merely lands mid-decoration is re-tried on the next frame and once more shortly after.
Publish and unpublish are deliberately outside the journal: a revision is a draft body, and there is no field on it to say whether reverting one means reverting the draft or the published copy. The same split explains scheduling — a page with a publish time set is served normally on the delivery port, because that is the author’s own draft view, and 404s on the published port until its time arrives. There is no scheduler process; the boundary is checked on the way past.
The page model
What an edit writes
A ledge page is one JSON document: ordered sections, each holding ordered nodes, each node either a block with a model and its fields or a piece of default content. The editor edits that document and the server renders it — there is no reverse path, no reading the page back out of the DOM. This is the shape the repository ships as seed content, and the shape the authoring port reads and writes:
seed/content/local-first-authoring.json{
"path": "/local-first-authoring",
"metadata": {
"title": "Local-first authoring — ledge",
"description": "Edit an EDS page in the page itself…"
},
"sections": [
{
"name": "Hero",
"style": ["grid"],
"nodes": [
{
"type": "block",
"name": "hero",
"model": "hero",
"fields": {
"eyebrow": "Local-first authoring",
"heading": "<h1>Edit in the page.</h1>"
}
}
]
}
]
}
The section name above is an editor-only label: it shows in the outline and reaches the authoring DOM as an instrumentation attribute, and delivery is byte-identical whether it is set or not. The style list is the one per-section datum EDS itself carries, and it travels the standard way — as the section-metadata block’s single row, which stock decorateSections turns into classes.
The link at the foot of every page here returns something derived from this, not this: the same section-by-section traversal, but with each block’s rendered HTML and its pointer in place of its fields, rendered exactly as delivery would render it. On the published port that endpoint is opt-in and off by default — this site turns it on so the link works where you are actually reading.
Publishing
One page, on purpose
Publishing copies one page from the preview store to the published store, together with the media, sheets and editable form definitions it references — resources first, so a failed publish never leaves a live page pointing at something that is not there. The button opens a plan listing each of those resources with its change state: changed ones checked, ones with no published copy checked and locked, unchanged ones folded away behind a count. Holding Shift skips the plan.
The plan also carries a link report. Internal links are always checked, against the content store, with no network involved — and a path that exists only as a redirect source counts as resolving, because the redirect map is consulted ahead of real pages at request time. External checking is opt-in and off by default. Neither list blocks the publish: a publish that could not complete because a third-party host was slow would be a worse failure than the one being caught.
The response comes back as soon as the page and its source assets are written; image renditions are generated afterwards, in the background. A rendition that fails to generate is a cache miss rather than a fault — the serve path falls back to the source image — so the cost of that failure is image weight, not a broken page.
Publishing is per page and explicit. There is no whole-site build and no bulk publish or unpublish — both are on the deferred list, not implemented and pending a switch. Deleting a page is refused while a published copy exists, so the order is unpublish, then delete; the effect is that every published page always keeps an author draft behind it.
Keep reading
Two more pieces of the same system
What the editor renders on the authoring port and what delivery serves differ by exactly one thing: instrumentation, added at render time and never stored.
When the inspector is not enough: block templates, sandboxed WebAssembly plugins with their own authoring panels, and custom field types.
Waitlist
One mail, when there is a binary.
No newsletter, no drip sequence, no passing your address to anyone. One message when ledge is downloadable, and that is the entire promise.