Skip to content

Visual Editor — Precise Editing

PortBay's visual editor lets you click an element on your running .test site and edit its text, classes, and styles, writing the change back into your real source files. By default it resolves a clicked element back to source with a text search — it looks for the rendered text or class= value in your project files and patches it when it finds exactly one match.

That works well for static HTML and unique CSS selectors, but it has hard limits:

  • React className can't be written back (the rewriter only handles class=).
  • Vue/Svelte scoped styles drop to an override stylesheet instead of the real rule.
  • Dynamic / loop content (.map(), v-for, {#each}) is ambiguous — N rendered copies look identical to a text search, so the edit is refused.
  • Repeated text across the page can resolve to the wrong element.

Precise editing removes those limits. A small dev-time build plugin stamps each rendered element with its authored source location:

html
<button data-pb-loc="src/components/Hero.jsx:42:7">Get started</button>

PortBay reads that attribute, opens the exact file at that line, verifies the element still matches what you clicked, and patches that one element — so React className, repeated .map() / v-for items, and scoped component styles all resolve deterministically.

Dev-only, by design

The data-pb-loc attribute is emitted only in development. It never reaches a production build, so there's no DOM bloat and no leaking of local file paths. The gate is NODE_ENV !== 'production'; set PORTBAY_LOC=1 to force it on, or PORTBAY_LOC=0 to force it off.

Is it on?

Open the edit bar on your site. When a project supports precise editing you'll see a Precise chip:

  • Precise (green dot) — the plugin is active; edits resolve to the exact source line.
  • Precise: off (amber dot) — the plugin isn't active yet. Click the chip for the exact install steps for your project (PortBay detects your framework, bundler, and package manager).

If you don't see the chip, PortBay didn't detect a supported bundler in the project (see Server-rendered templates below).

Install

The plugins are open source (MIT). Pick the one that matches your build pipeline.

Vite (React, Preact, Solid, Vue, Svelte, Astro)

bash
pnpm add -D @portbay/vite-plugin-loc
js
// vite.config.js — it runs enforce:'pre', so list order doesn't matter
import { defineConfig } from 'vite'
import portbayLoc from '@portbay/vite-plugin-loc'
// import your framework plugin, e.g. @vitejs/plugin-react / @vitejs/plugin-vue …

export default defineConfig({
  plugins: [portbayLoc(), /* react()/vue()/svelte()/… */],
})

Restart the dev server. That's it — every host element is stamped in vite dev, and nothing is emitted in vite build.

Babel (Create React App, custom webpack/Babel, Next.js Babel pipeline)

bash
npm i -D @portbay/babel-plugin-loc
js
// babel.config.js — dev only
module.exports = {
  plugins: [
    process.env.NODE_ENV !== 'production' && '@portbay/babel-plugin-loc',
  ].filter(Boolean),
}

Next.js: the default SWC compiler isn't instrumented yet. Use the Babel pipeline (add a Babel config so Next picks it up) or a Vite-based setup. A native SWC plugin is planned.

What gets stamped

Only real host elements (the lowercase intrinsic tags that produce a DOM node — <div>, <li>, <button>) are stamped. Components (<Hero>), TypeScript generics (useState<string>()), and framework wrappers (<template>, <slot>, <script>, <style>) are never touched. Each element carries the location of its own opening <, with the line as the firm anchor (the column is a tie-breaker, since compilers disagree on column bases).

Precise vs. fallback at a glance

EditFallback (text search)Precise (data-pb-loc)
Static HTML text / class=
React className
.map() / v-for / {#each} item❌ refused (ambiguous)✅ patches the one template element
Repeated text across the page⚠️ may mis-resolve✅ exact element
Vue / Svelte scoped styles⚠️ drops to override CSS✅ resolves in the component file
A computed class (cx(...), :class)refused with a precise file:line message

When the plugin is absent, PortBay falls back to the text-search resolver with zero behavior change — nothing you have working today regresses.

Server-rendered templates

There's no JS build step to hook for Blade, Twig, ERB, or Liquid, so the build plugins don't cover them. The resolver itself is framework-agnostic — it consumes data-pb-loc no matter who emitted it — so the moment any instrumentation stamps the attribute in your rendered HTML, those edits resolve precisely too. Light template instrumentation for these engines is on the roadmap; until then, server templates use the text-search fallback.

Troubleshooting

  • Chip says "off" after installing. Restart the dev server, then reload the page so the freshly stamped HTML is served. The chip reflects whether data-pb-loc is present in the current DOM.
  • No chip at all. PortBay couldn't detect a supported bundler. Confirm vite (or a Babel config) is present in the project root.
  • An edit refuses with a file:line message. The element's class is built from an expression (e.g. className={cx('a', b)} or Vue :class). Edit it in code — PortBay won't rewrite a dynamic expression as a string.
Was this helpful?
Feedback

PortBay is pre-MVP software. Use the docs as an operating guide, not a stability guarantee.