Feat/wb detect missing frontend - #5556
Conversation
When no frontend is running at the preview domain, the editor used to sit on the "Loading preview..." spinner forever. Detect it and offer a way forward: read the setup instructions, or connect to the hosted sample frontend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Say plainly that pages built against the sample frontend can only be viewed inside the editor, and that previewing them elsewhere needs a frontend of the user's own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trade three sentences for one, and put the setup instructions behind a "Learn more" action next to "Disconnect" instead of leaving the banner with no way to reach them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🚓 Slop CopFootprint matches the stated feature scope with no signs of accidental deletions or leaked secrets, but several new/modified import lines violate the one-import-per-line rule. 🚨 Should this be in the PR?🟡 Low — Author-reported working-tree hygiene issues not visible in diffThe PR description itself notes stray files ( 📏 Code-style rule checks🟡 Low — Multiple named imports on one line in usePreviewConnection.ts
🟡 Low — Multiple named imports on one line in NoFrontendConnected.tsx
🟡 Low — Multiple named imports on one line in PreviewFrame.tsx
🟡 Low — Multiple named imports on one line in SampleFrontendBanner.tsx
🟡 Low — Modified import line in Preview.tsx still combines named importsThe changed line Automated, non-blocking heads-up from an LLM. It can be wrong — use your judgment. Regenerates on every push. |
PR handoff review
Base branch diffed against:
origin/next(HEAD016c33ba03, 0 commits behind base).Correctness judged against the Webiny MCP server:
get_started(),get_webiny_agent('website-builder-developer'),get_webiny_skill('webiny-website-builder'),get_webiny_skill('wb-preview-url-modifier'),get_webiny_skill('webiny-admin-website-builder-catalog'),list_webiny_skills().1. Intent
The Website Builder editor previously showed an indefinite "Loading preview..." spinner whenever
the Next.js frontend that renders the preview iframe was not running, giving no explanation and no
way out. This change adds a connection state machine around the preview iframe that distinguishes
"nothing is listening on the preview origin" from "something answered but never completed the
preview.readyhandshake", and replaces the spinner with an empty state that explains whichorigin failed, links to setup docs, and offers to point the preview at a Webiny-hosted sample
frontend. While the preview points at that sample frontend, a persistent info banner sits under
the address bar explaining that pages render only inside the editor, with a "Disconnect" action.
2. File map
All files are in
packages/app-website-builder(Admin-side WB editor). None are generated.src/BaseEditor/defaultConfig/Content/sampleFrontend.ts— new. Two hardcoded constants:SAMPLE_FRONTEND_DOMAIN(
https://wb-demo.webiny.com) andFRONTEND_SETUP_DOCS_URL(https://webiny.link/wb-frontend).SampleFrontendBanner.tsx— new. Renders anAlert(info/subtle) with "Learn more" and"Disconnect" actions when the effective preview domain equals
SAMPLE_FRONTEND_DOMAIN; returnsnullotherwise. Wrapped indata-affects-preview="height"souseReservedUISpacesubtracts itfrom the iframe height.
ContentPreviewConfig.tsx— registers the banner as a Content element namedsampleFrontendBanner,after="addressBar". Mechanical, three lines.src/BaseEditor/defaultConfig/Content/Preview/usePreviewConnection.ts— new. The state machine:connecting | connected | unreachable | unresponsive. Probes the preview origin with ano-corsHEADfetch for a fast "nothing is listening" signal, falls back to a 15s handshake timeout for"unresponsive", and while
unreachablepolls the origin every 4s, firingCommands.RefreshPreviewas soon as it answers. Exposesretry().PreviewFrame.tsx— new. WrapsIframe, owns the per-page-loadconnectedflag, forwardsonConnectedtoPreviewEvents, and turns an error status into the overlay element.NoFrontendConnected.tsx— new. The overlay:EmptyState type="layout"with per-status copy,three buttons (docs, load sample frontend, try again) and a footnote about the sample frontend's
limits. Hides the "load sample" button when the sample domain is already active.
Iframe.tsx— adds an optionaloverlayprop rendered in place ofOverlayLoader. The iframestays mounted underneath so a late handshake still resolves.
Preview.tsx— swapsIframeforPreviewFrame, drops the localonConnectedcallback infavour of passing
previewEventsdown, and addskey={${url}|${iframeTimestamp}}so theconnection state is recreated on every page load.
3. Decisions taken
HEADprobe of the origin for speed, and the existingpreview.readypostMessage handshake asthe authority. Neither the MCP server nor any skill describes a sanctioned way to detect frontend
liveness, so this mechanism is invented here.
Commands.RefreshPreviewon the user's behalf once the origin answers. Nothing else in the editorauto-executes an editor command on a timer.
wb-demo.webiny.com) hardcoded in core, withno DI abstraction or build parameter to override or disable it. The
webiny-admin-website-builder-catalogskill listsPreviewUrlModifieras the only preview-relatedextension point; it lists nothing covering a fallback/sample preview host, and the MCP server has
no guidance on this case either way.
webiny_wb_custom_preview_domainlocalStorage key viausePreviewDomain().setPreviewDomain.usePreviewDomain.ts:20-23documents that override as "adevelopers-only feature"; this change promotes it to a primary end-user action, and does not use
the existing
DeveloperModegate (packages/app-admin/src/components/DeveloperMode/DeveloperMode.tsx,WEBINY_ADMIN_DEV_MODE).PreviewDomain.tsx:42-48— a sibling component in the address bar — whose effect firesCommands.RefreshPreviewwheneverpreviewDomainchanges. Remove or decorate away the address barand the loading/box reset silently stops happening.
connected, with the boolean owned byPreviewFrameand the remount driven by akeyinPreview, instead of resetting state insidethe hook on url/timestamp change.
COPYrecord keyed by error status; the "unresponsive" and"unreachable" descriptions are sentence fragments completed by the origin string in JSX.
BaseEditordefault config, so it applies to every document editor(pages, blocks, templates, experiments), and it renders in read-only mode too.
4. Not done
and no way for a self-hosted or air-gapped customer to opt out of the offer.
indication in the UI that doing so sends the document to that host.
(
usePagePreviewLink) read the sameusePreviewDomainvalue, so they silently point atwb-demo.webiny.comtoo, with no banner and — by the change's own copy — no rendered page.connected, the hook returns early forever; adev server that dies mid-session leaves a stale preview with no notice until a manual refresh.
editor sits in the
unreachablestate, including in a hidden tab, and each failed probe emits aconsole network error.
usePreviewDomaintriggersgetSettings.execute()per hook instance;SampleFrontendBannermounts on every editor load, adding one settings request per load. No deduplication.
role/aria-live, and none of the new copy goes through any i18n mechanism.https://wb-demo.webiny.comnor the target ofhttps://webiny.link/wb-frontendcould be verified in this environment (no network access); both are unverified.
webiny.config.tsx.bac,lerna.json, and untrackedextensions/scratch files (includingextensions/previewUrlModifier/MyPreviewUrlModifier.ts)are present and must not ride along in a commit.
5. Test coverage
Nothing in this change is tested.
TEST FILES TOUCHEDis empty, and codegraph reports no coveringtests for
PreviewEvents,AwaitIframeUrl,usePreviewDomain, orIframeeither, so there is noexisting harness for this area to extend —
packages/app-website-builderhas jest tests only foruse cases (
src/features/pages/*.test.ts) and pure helpers (src/shared/PagePath.test.ts).Untested behaviours, by behaviour:
connecting → unreachableon a refused probe,connecting → unresponsiveon timeout, thesetStatus(current => current === "connecting" ? ... )guard that stops the timeout from overwriting
unreachable, and* → connectedon handshake.This is the only genuinely unit-testable piece in the change (a hook over fake timers and a
stubbed
fetch) and it has no test.iframe mounted behind the overlay.
Commands.RefreshPreviewexactly once when the origin comes up, and neverfires while
unresponsive(the guard that prevents a reload loop).disposedflags).PreviewFrameremounts, and connection state resets, on url or timestamp change.IframeprefersoverlayoverOverlayLoader, and still renders the loader whenoverlayis
null.preview to the configured domain.
6. Look here first