Abdulkader Safi

The editor was a dead end with one exit, and the preview lied about it

Clicking a nav link inside the builder's preview navigated the iframe to that page's live URL. The canvas then showed one page while the section list edited another, with nothing to tell you. Fixing it meant deciding what an iframe inside an editor is actually allowed to do.

4 min read

Share
A page builder with a sidebar listing pages grouped by type, beside a live preview of the site.

The builder in Atelier is a three-pane editor: sections on the left, the real page in an iframe in the middle, settings on the right. It has been that since the first release, and it had one exit.

Editing two pages meant: back arrow, the Pages list, find the row, Open builder. Five steps each way, and a client fixing a phrase in a header and the same phrase in a footer does that a dozen times in an afternoon.

The part that was actually wrong

The preview renders the real page through the real layout, which is the whole argument for it. A real layout has a real nav, with real hrefs.

So clicking Work in the preview navigated the iframe to /projects. The public version of it. The editor did not know, because nothing told it: the iframe just followed a link, the way an iframe does.

What you were left with was a canvas showing the projects page and a sidebar listing the home page's sections. Editing a section then changed a page you were not looking at. No error, no warning, no way to tell except noticing that the section list stopped matching the screen.

That is worse than the missing feature. A preview that silently stops previewing the thing you are editing undermines the one property the editor is built around.

What an iframe in an editor is allowed to do

The decision underneath the fix: the preview should never navigate itself. It is a preview of one page, and the moment it points somewhere else it is a browser wearing a preview's clothes.

So every click on an <a> inside the preview is now intercepted and posted up to the editor, which decides:

  • A page on this site opens in the builder. You are now editing the page you clicked.
  • Anything else, an external site or one of the host app's own routes, opens in a new tab. The builder stays exactly where it was.

Plus a Pages panel in the icon rail for when you know where you are going: every page grouped by type, a search box, the current one marked.

Both go through a real navigation rather than swapping the record inside the Livewire component, so the address bar names the page being edited. Refresh works. Back works. Sending someone the URL works. Swapping state in place would have left the address bar lying, which is the same class of problem I was fixing.

The resolver, and the rule I refused to write twice

"Which page is this URL" already existed, in the public controller, and it is subtler than it looks:

// /{slug} is the default locale. /{locale}/{slug} is everything else.
// The first segment is a locale only when it names one.

Get that wrong and /services/web-design reads services as a language and serves the services page with a 200. That exact bug shipped in August and took a while to spot, because a 200 with the wrong content looks like a content problem, not a routing one.

The editor needed the same answer. Writing a second copy of a rule that has already caused one silent bug is how you get the same bug twice, in two places, at different times. It moved into a PageResolver that both call, which is the smallest amount of work that makes a second occurrence impossible.

One thing I found on the way

While handling clicks in the preview I noticed forms were not handled at all. A block with a form, posting to the app's own route, submitted for real from inside the editor. On the demo site that meant a booking request from a draft page landing in the client's inbox as a genuine lead.

Submits are blocked in the preview now. Nobody reported it, because nobody had built a form block yet. Writing the demo site did that a week earlier, which is the whole argument for keeping a demo that behaves like a client project.

Where it stands

Shipped in v1.0.0. Click a link in the preview and you are editing that page. Click Pages and pick another. The URL follows.

The measure I care about: the number of times you have to leave the builder to do a normal afternoon's work went from a dozen to zero.

Last updated 13 Sep 2026 · filed under Laravel, filamentphp, plugin, tools, livewire, ui, ux

Every entry on this project

28 build notes, in order

Including the ones where nothing worked. You are on part 25.

Read the series →

FAQ

Frequently asked questions

Should an editor's preview iframe be allowed to follow links?

No, not on its own. A preview is a rendering of one specific record, and the surrounding editor holds state that belongs to that record: a section list, an inspector, an unsaved selection. If the frame navigates, all of that silently describes something else, and the user gets no signal because a page loading inside a frame looks normal. Intercept the click, hand the target to the editor, and let it decide whether to load that record, open a tab, or refuse. The same applies to form submissions inside a preview, which otherwise post for real.

Switch records in place, or navigate to the other record's editor URL?

Navigate, if the editor has a URL per record. The address bar then names what is being edited, so refresh, the back button, bookmarks and sending someone a link all behave the way people expect. Swapping the record inside a live component leaves the URL pointing at the previous one, and every piece of editor state has to be reset by hand, which is a long list you will find one item at a time. With SPA-style navigation the cost is small, since assets and scripts are not reloaded.

How do you decide whether a clicked URL belongs to your own content system?

Resolve it server-side against the same lookup your public routes use, and treat anything unresolvable as external. Comparing prefixes or guessing from the path shape breaks on locale prefixes, nested slugs and routes the host application owns. Also compare hosts against the request's own host rather than only a configured base URL, since local, tunnelled and proxied setups regularly differ. Anything that fails to resolve should still do something useful, such as opening in a new tab, rather than silently doing nothing.

When is it worth extracting a rule into its own class rather than duplicating ten lines?

When the rule is subtle and has already caused a bug. Ten obvious lines can live in two places without much risk. A rule with an exception in it, such as treating a URL's first segment as a language only when it matches a known one, will be copied slightly wrong, and the failure is usually a plausible-looking wrong result rather than an error. Extract it the second time you need it, keep the extraction narrow, and let each caller keep its own error handling around it.

Written by

Abdulkader Safi

Software Engineer

Lead engineer at DSRPT, from Lebanon and based in Kuwait. I write about the tools and bugs from real client work, with the numbers I measured.

About me → GitHub LinkedIn

Need this kind of work done on your project?

Start a project →

Keep reading

More from Filament Atelier

All 28 entries →
  • A version number changing from 0.5.0 to 1.0.0 beside a list of deferred items.
    Filament Atelier

    · part 28 of 28

    Tagging 1.0.0 with four features missing, on purpose

    The gate list had thirteen items. Six went in, seven did not, and the tag went out anyway. What the number promises is that the API stops moving, not that the feature list is finished, and conflating those two is how packages sit at 0.x for three years.

  • The same block of JavaScript appearing in three different Blade layout files.
    Filament Atelier

    · part 26 of 28

    A script in three layouts is a contract nobody signed

    The editor's preview needed a few lines of JavaScript in the page it renders. I put them in the shipped layout, then copied them into two more. Anyone writing their own layout had to copy them too, and missing them broke half the editor with no error at all.