SvelteKit routes, edited in the +page.svelte that rendered them
Every element in a .svelte file reaches the browser carrying its file, line and column, added by the dev server PortBay launches and never written back to your files. Edits land in the +page.svelte that rendered them, and a page title lands in that route's own svelte:head. Pro, early access, opt-in.
How do I visually edit a SvelteKit page?
The trap in editing a SvelteKit site visually is not finding the markup — it is writing to the right file. A route renders through a site-wide shell, so a tool that resolves changes to src/app.html will happily set what looks like this page's title and quietly set every page's title instead. PortBay resolves the route first. Markup edits go to the src/routes/**/+page.svelte that actually rendered the element you clicked, because every element in a .svelte file carries its file, line and column while the dev server serves it. Page settings go to that same route's own svelte:head block, which SvelteKit hoists into the document head for exactly the routes that render it; the shell is the fallback only when no route file can be resolved at all. Pro, early access, opt-in under Settings.
Verified against the product, 2026-09-05.
A SvelteKit route and its head
PortBay runs your SvelteKit dev server at its own .test URL with trusted HTTPS and dispatches board cards into it. The visual editor edits that same running app in your real browser — one live page at one breakpoint, not a multi-frame design canvas.
Your first route edit
Four steps from a SvelteKit project to a change committed in a +page.svelte. None of them adds a dependency.
- Install PortBay and add your Svelte project
- Turn the visual editor on
- Start the project — the routes are already mapped
- Open Edit in browser and change a route
Install PortBay and add your Svelte project
Homebrew installs the app and the bundled portbay CLI. Add the project folder; PortBay reads your package.json, recognises SvelteKit, runs your dev server and serves it at its own .test hostname over trusted HTTPS.
brew tap portbay-app/portbay && brew install --cask portbayTurn the visual editor on
It ships to Pro under early access and is off by default. Sign in to Pro, then flip the Early Access toggle. The open-source build does not contain the editor at all — it is not compiled in.
Settings › Account › Early AccessStart the project — the routes are already mapped
PortBay writes a vite.config.mjs into its own data directory that imports and spreads your config and adds its plugin, then starts the dev server with one --config flag. Nothing is written inside your project, and the plugin applies while serving only, so a production build never loads it.
portbay start myappOpen Edit in browser and change a route
The project's Visual editing section has one button. It opens your normal browser at your normal .test URL with the edit bar docked at the top, booting in Preview mode where the page stays fully interactive. Navigate to the route you want, switch to Edit, and the edits resolve to that route's file. Pulling a section out into its own component file is a single edit here — Svelte is one of the four stacks where extraction is supported.
https://myapp.test/aboutOne file, three kinds of block, and one head
A .svelte file has no frontmatter fence and no template wrapper — the whole document is markup, with script and style as blocks inside it. That shape is why the head block is where a route's page settings genuinely belong.
The head values are writable when they are plain string literals, reached directly or through exactly one hop to a const or let in the script block. A title assembled from a substitution or returned by a function is refused by name, because a partial rewrite of a composed string is the kind of change that looks saved and renders wrong. The same standard governs the Tailwind lane: renaming a design token rewrites every class reference to it across your markup in one save, and refuses outright if it could not read the whole project rather than leaving half your files pointing at a name that no longer exists.
<!-- src/routes/about/+page.svelte --><script> let heading = $state("About us"); import Team from "$lib/Team.svelte";</script> <svelte:head> <title>About us</title> <!-- page settings write here --> <meta name="description" content="Who we are." /></svelte:head> <section class="prose"> <!-- stamped --> <h1>{heading}</h1> <!-- refused: an expression --> <p>We build tools for local development.</p><!-- stamped, text writes --> <Team /> <!-- stamped: the usage tier --></section> <!-- and NOT here: --><!-- src/app.html — the shell every route renders through. --><!-- Setting "this page's" title there sets every page's title, --><!-- so the route file is checked first and the shell is the --><!-- fallback only when no route resolves at all. -->
The pieces underneath
Visual editor
Click an element on your running site in your own browser and edit its text, classes and styles straight into the source file, across 21 stacks.
Explore →Agent dev environment
Stack detection, runtimes, HTTPS, databases and mail, provisioned per project — the running app your agents verify their work against.
Explore →Asked before downloading
Routes are the case it is built around. The cross-stack parity test drives a real SvelteKit fixture — a src/routes/about/+page.svelte served at /about — and all four write lanes land in that file. The fixture deliberately ships the src/app.html shell alongside it, because resolving a route edit to the shell instead of the route is the specific failure the route-first ordering exists to prevent. Plain Svelte components under src/lib are stamped the same way and edit the same way.
Into that route's own svelte:head block, creating one if the route has none. SvelteKit hoists a svelte:head into the document head for exactly the routes that render it, so for a +page.svelte that is precisely the one route it resolves to — which makes it the only place a per-page value can honestly go. src/app.html is the site-wide shell and stays the fallback for a route whose file cannot be resolved at all. Build output such as .svelte-kit is refused as a write target outright, since the next build erases it.
Text from a rune, a store or a prop is refused, by name. PortBay reads your source, not Svelte's runtime semantics, so an h1 whose contents are an expression is a text run holding braces — and rewriting those bytes would either destroy the binding or write a literal where a value belongs. The refusal says it is a template expression and points you at the source. Static copy in the same file edits normally, so in practice you edit the words you wrote and change the data for the words you computed.
Yes. A .svelte file is markup-first — the whole document is one markup region, with script and style as blocks inside it — and in markup a capitalised tag is a component and nothing else. So the usage site is stamped safely, and an element that Team rendered still traces back to the line where you used it. A .jsx or .tsx file does not get that tier, because the same tag shape there can be a TypeScript generic parameter list. It is the clearest concrete advantage the Svelte and Vue lanes have over the React one.
Every rendered copy carries the same coordinate, because they all came from the same line of your template. So editing one row edits the template, and therefore every row — which is usually what you meant, and when it is not, the thing to change is the data. The case that genuinely gets hard is an unstamped project, where PortBay has to fall back to a unique id or to counting same-tag siblings on the page; a loop makes those counts disagree with the source and PortBay hands you a picker instead of guessing which occurrence you meant.
No. The stamping plugin is compiled into the PortBay binary, written out to PortBay's own data directory, and handed to your dev server through a config generated outside your repository. There is no package to add and none to remove: uninstall PortBay and your package.json, lockfile and node_modules are byte-for-byte what they were. It is not on npm and is not meant to be, because nothing needs to serve it — which is also why you will never see an install command for it on this site.
No to both, and both are worth being plain about. It is a Pro feature, still in early access behind an opt-in toggle in Settings, and it is not compiled into the open-source build at all. And it is one live page in your real browser at one breakpoint at a time — the breakpoint control resizes that single canvas rather than opening a wall of frames. If a multi-frame infinite canvas is what you are shopping for, this is not that. Everything else on this page — the .test hostname, trusted HTTPS, the dev server, the task board — is in the free, open-source app.

Give your projects and your agents a real local home.
Download for macOSFree & open source · macOS 11+ on Apple Silicon · Pro from $10/mo