Editing in place
On a page your app serves on this machine (Next.js, Vite, any dev server), Zest edits the page where it runs: with its motion, its scroll position and its open tabs, as you see it. No other server starts. The Zest service, which runs at login on port 4470, does the work, and your app's dev server forwards /__zest to it.
Set it up once
- Start the service at login and mark this browser:
`` zest service install ``
- Let it open pages from your repo:
`` zest service add path/to/repo ``
That covers every git worktree of the repo too. The repo says which file is behind each address: a sites entry in its zest.json for plain HTML files, or your team's extension for pages an app renders (its resolve(url) and pages hooks, see A live page).
- Forward
/__zestto the service in development. Copy the snippet for your dev server below.
- Put the Zest Edit button on the page, or use the bookmark from
zest service hello.
Next.js
In next.config.mjs (or .js), development only:
const nextConfig = {
async rewrites() {
// Zest Edit: the editor's calls go to the Zest service on this machine, in development only
if (process.env.NODE_ENV !== 'development') return [];
return [{ source: '/__zest/:path*', destination: 'http://127.0.0.1:4470/__zest/:path*' }];
},
};
export default nextConfig;
If your config already has rewrites, add the entry to the list it returns (or to afterFiles, when it returns { beforeFiles, afterFiles, fallback }). Next.js forwards the request with the service's own address as its Host and adds X-Forwarded-Host, which Zest checks against the page.
Vite
In vite.config.js (or .ts):
export default defineConfig({
server: {
// Zest Edit: the editor's calls go to the Zest service on this machine (vite dev only, never a build)
proxy: { '/__zest': { target: 'http://127.0.0.1:4470', changeOrigin: true } },
},
});
Keep changeOrigin: true. Without it Vite sends your app's own Host (localhost:5173), and Zest refuses a request that is not addressed to itself.
Any other dev server
Forward every path under /__zest/ to http://127.0.0.1:4470, with the Host header set to 127.0.0.1:4470. Keep the request's Origin, Referer and Cookie headers, and pass answers through as they stream (the editor listens for live events). Do it in development only.
Use it
The button on a page whose app forwards /__zest offers one thing: Edit here. The editor loads into the running page. E, N and I work as they do anywhere (Editing), and the page keeps running underneath.
For the rare time you want a still copy of the page, with its scripts taken out, the ⋯ menu beside Edit here offers Open a still copy. The service serves it, on its own port. Notes left either way are the same notes.
Stop editing in the corner takes the editor off the page. Until you press it, the editor comes back after a reload, yours or your app's.
Where the editor sits
The bar sits at the bottom of the window and the toasts in the bottom-left corner. Your framework's own dev overlays float there too, so Zest steps around them: it lifts its bar, toasts and panel above Next.js's indicator (nextjs-portal), Astro's dev toolbar and any control your page marks with data-zest-avoid, so neither covers the other's buttons. While a dev overlay covers the whole page (Vite's or Next.js's error screen), Zest's bar and toasts step aside until it closes.
While the editor is on, Zest pads the bottom of the page by the bar's height, so your page's last line can scroll above the bar and take clicks. The padding goes with the editor, and a freeze frame never carries it. A page that scrolls inside a box of its own, not the window, does not get the room: tuck the bar away with − to reach its end.
When you save
A change to words goes where any save goes: your team's extension writes the app's source (pages.edit), or Zest writes the HTML file. Then your app's own hot reload repaints the page, and the editor carries on. Zest never holds a part of the page across a repaint: it finds the selection, an open note and every pin again by their selector and their words. If the app replaces the very part you are typing in, Zest keeps your words as a note for your agent, pinned where they were, and says so.
Moving to another page of the app (a link the app handles itself) reloads it, so the editor opens that page's file. A change to the query or the hash alone (a tab that keeps itself in ?tab=2) does not: the page stays in that state.
What a note carries
Every note and every edit keeps what the page showed at that moment, as state:
| Field | What |
|---|---|
url, route | The page's address, and its path |
viewport | The window's width and height |
scroll | How far the page was scrolled |
movement | The data-movement value of the nearest part that has one, so your agent knows which motion was playing |
dom | The markup around the part, up to its section, with scripts taken out (at most 8 KB) |
attrs | The part's class, aria-* and data-*, and the data-state of every part around it |
shot | A freeze frame: a picture of the window as you saw it |
A note on the whole page takes these from the part in the middle of the window.
The freeze frame is made from the page itself, not from a new visit: Zest copies the page's markup the instant you start the note (with the styles a script set, a CSS animation's place, form values and canvases), and headless Chrome draws it at your window size and scroll, with no script running. It arrives on the note a few seconds later, as a picture beside the note and in state.shot. An edit Zest saved keeps its state without a picture; a picture is taken for notes, where your agent has work to do. Without Chrome, the note is saved without one, and the editor says so once.
Your agent is told that you made the note on the running page at that moment, and to find the source that produces what you saw there, including what depends on state or motion, and to change it so the page reads as asked at that state.
Limits
- Pages on this machine only:
httponlocalhost,127.0.0.1or[::1]. - The freeze frame does not keep the scroll position inside a scrolling box, a video's frame, or what a page draws inside its own shadow roots.
- Replacing an image on a live page works as on its copy: the extension writes the change, and an inserted picture lives beside the copy until the extension's
placemoves it. - A plain HTML file with no app behind it:
zest openasks the running service to serve it, at a private address on port 4470. With no service running, Zest starts a server for that folder as before (port 4477 and up).
How the service keeps other sites out: Security. When something does not work: Troubleshooting.