Audits · septembre 2026Builder « tout est vide » : diagnostic

Builder « tout est vide » : diagnostic

2026-09-26 (releve le 2026-09-27). Constat : le Builder paraissait vide et mal fonctionner (UI/UX, moteur, logique). Item : backlog/ai-platform/3069. La carte du chrome livre la veille : 2026-09-26-builder-chrome.md.

2026-09-26 (releve le 2026-09-27). Constat : le Builder paraissait vide et mal fonctionner (UI/UX, moteur, logique). Item : backlog/ai-platform/3069. La carte du chrome livre la veille : 2026-09-26-builder-chrome.md.

Ce document trace le chemin des donnees du montage jusqu'aux pixels, puis classe les causes par preuve. Les extraits de logs viennent du projet Vercel boostecom.app (production, lecture seule, fenetre de 72 h au 2026-09-27T00:40Z). Aucun secret n'y figure.

1. Le chemin, du montage aux pixels

mode-views.tsx  (kept layer "builder")
  └─ OnlookBuilder (onlook-builder.tsx)
       └─ ProjectProviders → Main (upstream/.../_components/main.tsx, la couture)
            ├─ useStartProject : GET builder-canvas → applyFrames(5 cadres)
            │     url = /api/stores/<id>/builder-frame?file=templates/<x>.json
            ├─ pages.scanPages : GET builder-library → arbre Pages / Sections + theme
            └─ BuilderChrome (chrome/)
                 ├─ Canvas → FrameView → FrameComponent (canvas/frame/view.tsx)
                 │     <iframe src=builder-frame…>  sandbox=allow-same-origin allow-scripts…
                 │       builder-frame GET ─307→ /api/preview/<id>/<chemin>?editor=1&file=…&preview_theme_id=…
                 │         proxy : fetch https://<Store.domain><chemin>, scripts coupes par CSP nonce,
                 │         injecte builderStampScript (shopify-document.ts) dans <head>
                 │       cadre ─ready→ parent ─hello→ cadre ─dom{blocks}→ parent
                 │         → frameEvent.handleDomProcessed → ast.mappings → Calques
                 │     clic : GestureScreen → view.getElementAtLoc → contentDocument.elementFromPoint
                 │         → closest('[data-oid]') → elements.click → rect pointille (BaseRect, selector-style.ts)
                 ├─ Calques : LayersTab (vendore) sur ast.mappings.filteredLayers
                 ├─ Pages / Sections : PagesTab (vendore) sur pages.tree
                 └─ Mirror : use-mirror-session → /mirror/frame (sandbox opaque) + /mirror/sections

Ce que le chemin n'utilise pas, verifie : le script de preload d'Onlook (ONLOOK_PRELOAD_SCRIPT_SRC, CDN jsdelivr) et l'enfant penpal. Un cadre dont l'URL contient /builder-frame est « local » (view.tsx:283-286) : ses methodes lisent iframe.contentDocument (localViewMethods, view.tsx:109-133) et onLoad ne tente jamais penpal sans runtime (view.tsx:525-534). Les shims code-provider (qui jette) et trpc (qui repond vide) ne sont atteints par aucun chemin du canevas. Le preload n'est donc pas la cause, et le reparer ne changerait rien.

2. Ce que la production montre

Requete72 hLecture
GET builder-frame65 × 307, 9 × 200chaque cadre de template redirige vers le proxy
GET/PUT builder-canvas88 × 200cadres et viewport persistes, rien ne casse
GET builder-library15 × 200la bibliotheque repond toujours 200, meme en echec
GET /api/preview/cmohv107…/…296 × 200le proxy sert les pages : /, /products/lapaisante-bague-anti-stress, /pages/contact, /search a 23:19:16

Aucune erreur d'execution sur builder-* dans la fenetre, hormis deux P2002 builderFrame.createMany du 2026-09-24/25, deja corriges par skipDuplicates (builder-canvas/route.ts:104-118). Le serveur livre donc tout ce qu'on lui demande : le vide est dans ce qu'il livre et dans la facon dont le client le lit.

Le magasin du proprietaire (storeId cmohv107f0007xvt61bkbsior) a pour domaine cutsdemo.com (log tracking-scan.scan_failed, url = https://cutsdemo.com, meme storeId). La page d'accueil de ce domaine, lue le 2026-09-27 :

  • TriplePixelData={TripleName:"c0ffee.myshopify.com",…,plat:"SHOPIFY",isHeadless:true} ;
  • feuilles servies depuis cdn.shopify.com/oxygen-v2/… et window.__reactRouterContext.streamController… ;
  • 0 element id="shopify-section-…" sur 476 Ko de HTML.

cutsdemo.com est un storefront headless (Hydrogen, heberge sur Oxygen), pas le theme Online Store que le Builder edite.

3. Les causes, classees

C1. Le canevas rend le domaine de la boutique, pas le theme edite (prouve en production)

builder-frame redirige vers le proxy de la Preview (builder-frame/route.ts:395-406, previewEditorPath dans shopify-document.ts:288-303). Le proxy va chercher https://<Store.domain> (api/preview/[storeId]/[[...path]]/route.ts:848) et lui passe preview_theme_id. Sur une boutique dont le domaine sert l'Online Store, c'est juste. Sur cutsdemo.com, Hydrogen ignore preview_theme_id et rend son application React : builderStampScript ne trouve aucun [id^='shopify-section-'], le message dom part avec zero bloc, les Calques n'ont qu'une racine, et aucun noeud ne porte data-oid. Un clic tombe alors dans le repli de readFrameElement (view.tsx:119-121) : le noeud touche, oid: null, domId = son nom de balise ("div"). L'inspecteur s'ouvre sur un « div » dont les ecritures visent [data-oid="div"]. Le canevas montre une page, mais ni le theme qu'on edite, ni rien qu'on puisse selectionner : c'est le « tout est vide ». Et une section Mirror installee dans le theme n'apparaitra jamais sur ce domaine.

C2. Le marquage des sections court contre le parseur (prouve par le code, toutes boutiques)

Le proxy injecte builderStampScript juste apres <head> (route.ts:1120-1135). Le script poste ready tout de suite et ne marque les sections qu'a la reception de hello (shopify-document.ts:339-356), un aller-retour par le parent (view.tsx:400-410). Le cadre est de meme origine, donc sur le meme fil : le hello peut etre traite pendant que le parseur n'a pas atteint le <body>. stamp() ne tourne qu'une fois : les sections parsees apres restent sans data-oid, absentes des Calques et non selectionnables. Sur une page d'accueil de 200 a 500 Ko, c'est le cas courant, pas l'exception.

Meme script, second defaut : il obeissait a tout postMessage, de n'importe quelle source, et postait vers *. Un script plus complet (storefrontEditorScript) existait, mais seul mountStorefrontDocument, que plus rien n'appelait, l'utilisait.

Hors de ce chemin, et non corrige ici : « Modifier le texte sur le canevas » n'atteint pas un cadre local. text.start demande isChildTextEditable, que localViewMethods laisse a la valeur de repli false (view.tsx:33-75) : l'edition s'arrete avant le premier message.

C3. Un echec est peint comme un contenu (prouve par le code)

Chaque echec de builder-frame rend html([]) (route.ts:274, 284, 286, 304, 306, 330, 347) : renderBuilderDocument sans bloc peint une fausse section « Template / index / This theme has no sections in templates/index.json. » (shopify-document.ts:1076), en anglais. Shopify non connecte, jeton expire, aucun theme, portee manquante : quatre causes, une seule image, qui ressemble a du contenu. builder-library repond 200 avec Funnel et Models seulement quand le pont echoue (builder-library/route.ts:56-63) et avale l'echec de la liste des themes (79-102) : l'onglet Sections affiche « No pages found » sans dire pourquoi.

C4. Aucun etat de chargement, vide ou erreur (prouve par le code)

  • useStartProject renvoie toujours isProjectReady: true, error: null (use-start-project.tsx:141) : le canevas est vide tant que builder-canvas n'a pas repondu, puis blanc pendant que cinq pages de boutique se rendent en parallele par le proxy.
  • Pages et Sections sont le PagesTab d'Onlook : « Search pages », « No pages found », « Create a new page » en anglais, et le bouton de creation appelle createPageInSandbox sur un bac a sable qui n'existe pas (page-tab/index.tsx:191-222, pages/index.ts:199-213).
  • Les Calques n'ont pas d'etat : un cadre qui n'a encore rien dit et un cadre sans section donnent le meme panneau vide.

C5. Le theme choisi peut etre impossible a previsualiser (risque, non observe)

pickBuilderTheme prend le premier role different de MAIN (builder-library.ts:39-41), donc aussi ARCHIVED ou LOCKED, que Shopify refuse de previsualiser, et un theme DEVELOPMENT ephemere de la CLI avant un vrai brouillon UNPUBLISHED.

C6. Mirror dit ses erreurs en code brut (prouve par le code)

use-mirror-session.ts affiche out.json.message ?? out.json.error (messages serveur en anglais, ou un code comme rights_not_attested) et install.refused avec le code de refus. Une erreur du cadre source n'a pas de « Reessayer » (mirror-stage.tsx:80-84). Sans brouillon, l'onglet dit d'aller creer une copie ailleurs alors qu'une route sure le fait deja (POST /api/stores/[storeId]/branches { create: true }, utilisee par le Worktree). Les trois temps (Source, Resultat, Installer) sont implicites.

4. Ce qui est corrige, et ou

CauseCorrectif
C1builder-frame choisit l'hote qui rend le theme (themeRenderSource) : le proxy quand Store.domain est le domaine principal de l'Online Store, sinon un rendu serveur depuis ce domaine principal (shop.primaryDomain), monte avec le script d'edition sous CSP a nonce. builder-library renvoie source et le chrome dit « storefront headless » quand c'est le cas.
C2Un seul script d'edition (builderFrameScript) : il n'annonce ready et ne marque qu'apres DOMContentLoaded, repond a chaque hello avec les memes ids, n'obeit qu'a son parent, poste vers sa propre origine, et dit l'etat de la page (ok, password, no_sections). Un clic hors de toute section selectionne le cadre, plus jamais un noeud sans oid (view.tsx).
C3Un echec rend un document d'etat (stateDocument) qui porte un code, jamais une fausse section. builder-library renvoie status.
C4Le chrome lit l'etat de chaque cadre (use-frame-states.ts), de la bibliotheque et du canevas, et rend un squelette, un vide qui dit pourquoi avec l'action qui corrige, ou une erreur avec « Reessayer ». Pages et Sections sont a nous (chrome/library/), traduits.
C5pickBuilderTheme classe UNPUBLISHED, puis DEVELOPMENT, puis DEMO, ecarte ARCHIVED et LOCKED, et retombe sur MAIN.
C6Codes d'erreur Mirror traduits, « Reessayer » sur le cadre, « Creer un brouillon du theme » sans brouillon, et trois etapes explicites.

5. Ce qui n'est pas verifie

  • Le rendu serveur depuis shop.primaryDomain n'a pas ete execute contre la vraie boutique : pas d'acces Admin depuis cette session. Si Shopify renvoie cutsdemo.com comme domaine principal de l'Online Store, la regle garde le proxy et le chrome affiche l'etat « aucune section de theme » avec l'explication headless, pas le theme.
  • Une Online Store protegee par mot de passe, rendue cote serveur, montre l'etat « mot de passe » avec le lien vers les preferences Shopify ; le mot de passe saisi dans la Preview ne s'applique qu'au chemin proxy.
  • Aucun navigateur n'a ete lance (pas de .env, la seule base est la production) : la course du parseur (C2) est prouvee par lecture et par un harnais happy-dom (builder-frame-script.test.ts, qui echoue sur l'ancien script), pas par une capture.
  • L'edition de texte sur le canevas (voir C2) reste inatteignable pour un cadre local ; un item de suivi est a ouvrir.