Bondlayer editor JSON — capture runbook
Status: active · Since: 2026-07-17 · Owner: import pipeline
Consumes into: apps/bondlayer-worker/src/parser.py
Target format: Matriz Site JSON v1
Companion: import pipeline (runbook) · xlsx CMS export
Reverse-engineered from: archived …/tools/bondlayer-converter/docs/CLI-FETCHER-DESIGN.md
The import pipeline runbook assumes you already have
editor.jsonon disk. This doc is how you get it — the one manual step the whole pipeline hangs off. Bondlayer has no public export API; you capture the authenticated editor-design response out of a logged-in browser session.
1. What “editor JSON” is
Section titled “1. What “editor JSON” is”It’s Bondlayer’s project design payload — the single JSON the editor loads to
open a project. parser.py reads exactly two top-level keys, project and design.
A raw capture (verified against the live endpoint and all three files in
imports/ on 2026-07-26) is platform-nested:
{ "project": { "id": "sdjpmgddd8dlcouy", "name": "ACHRONIMA" }, "design": "{ …one big escaped JSON string… }" // ← usually stringified; json.loads it}…and that inner design object splits into per-platform and project-level halves:
{ "bond": "1.0.0", "updated_at": 1758558905643,
// ── per platform ────────────────────────────────────────────── "web": { "screens": {…}, "styles": {…}, "symbols": {…}, "repeaters": {…}, "settings": {…}, "content": {…}, "forms": {…}, "states": {…}, "variants": {…}, "fileVersion": … }, "mobile": { …same keys, usually near-empty… },
// ── project-level, shared by both platforms ─────────────────── "states": {…}, // global states — NOT under web/ "forms": {…}, "schemas": {…}, // collections; `web.schemas` is empty "fonts": {…}, "imagePresets": {…}, "variants": {…}}web.states / web.forms are byte-identical duplicates of the project-level ones (verified);
schemas exists only at project level.
Two shape quirks parser.py already absorbs — don’t be alarmed by either:
designarrives as a stringified JSON blob (one big escaped string) from the endpoint;parser.pyjson.loads-es it.- Flat vs nested.
parser.pyprefersdesign.web, else treatsdesignitself as the container — so it handles both. Raw captures are nested (above); a flattened variant exists wherescreens/repeaters/statessit directly underdesign, alongsidecontentand anexportedAtstamp. Flattened files are processed copies, not what the endpoint returns.
Reference captures in this repo:
| File | Shape | Notes |
|---|---|---|
imports/achronima_art/sdjpmgddd8dlcouy.editor.json |
raw / nested | 2.1 MB · 37 screens · 60 repeaters · 11 states |
imports/simaomartinez/nnp5fhzjcd.editor.json |
raw / nested | 2.3 MB · 29 screens · 171 repeaters · 0 states |
imports/12x12/nrkdmrhhrk.editor.json |
raw / nested | 5.6 MB · 51 screens · 141 repeaters · 15 states (incl. author-made) |
docs/Research/achronima-bondlayer-export.json |
flattened | 4.6 MB, pretty-printed, has content + exportedAt |
Diff your capture’s top-level shape against the matching row.
2. Capture it — four methods
Section titled “2. Capture it — four methods”⚠️ Verify before trusting Method A. The endpoint and headers were originally reverse-engineered from an archived 2026-06-24 snapshot (
archive/2026-06-24-doc-snapshot/tools/bondlayer-converter/docs/CLI-FETCHER-DESIGN.md). They were re-verified working on 2026-07-26, but the token location had already moved (localStorage → cookie — see the erratum below), so keep confirming against a liveapp.bondlayer.comsession (Method B shows you exactly what to look for). If the private endpoint no longer matches, Method B is the robust fallback — it needs no knowledge of Bondlayer’s API at all. Method D (in-memory store) needs neither the API nor a token and is the best option for exploratory work.
The endpoint (re-verified 2026-07-26)
Section titled “The endpoint (re-verified 2026-07-26)”GET https://backend.bondlayer.com/api/design/open/{siteId}Headers: Accept: application/json x-access-token: {jwt}siteId— the id in the editor URLapp.bondlayer.com/project/{siteId}/design/web. Not always 16 chars — a project captured on 2026-07-26 used a 10-char id (nnp5fhzjcd), so the URL-regex in Method A can miss; take the path segment, not a length-matched run.jwt— ⚠️ erratum: it is no longer inlocalStorage.accessToken(that key does not exist). As of 2026-07-26 it is the_bondUserTokencookie onapp.bondlayer.com(document.cookie, alongside_bondUserId). Remember todecodeURIComponentit. Still short-lived — capture in one sitting.
The endpoint itself was confirmed working and returns the familiar
{ design: "<stringified JSON>", … } shape (~2.3 MB for a 29-screen project).
Method A — console snippet (auto-downloads)
Section titled “Method A — console snippet (auto-downloads)”Open the project in the Bondlayer editor, then paste into the DevTools Console:
(async () => { const raw = document.cookie.split(';').map(s => s.trim()) .find(s => s.startsWith('_bondUserToken='))?.slice('_bondUserToken='.length); const token = raw && decodeURIComponent(raw); if (!token) return console.error('No _bondUserToken cookie — log in first.'); // /project/{siteId}/design/web — take the path segment, ids vary in length. const siteId = location.pathname.split('/')[2] || prompt('Bondlayer siteId:'); if (!siteId) return; const res = await fetch(`https://backend.bondlayer.com/api/design/open/${siteId}`, { headers: { Accept: 'application/json', 'x-access-token': token }, }); if (!res.ok) return console.error('Fetch failed', res.status, await res.text()); const json = await res.json(); const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: `${siteId}.editor.json`, }); a.click(); console.log('Saved', a.download, `(${(blob.size / 1e6).toFixed(1)} MB)`);})();The prompt() is there for the case where the editor URL isn’t the usual
/project/{siteId}/design/web shape. A copy of this snippet that the user keeps to hand lives at
imports/REFERENCE_user capture/Capture.txt.
Method B — Network-tab capture (no API knowledge required)
Section titled “Method B — Network-tab capture (no API knowledge required)”- Log in at
app.bondlayer.comand open the project in the editor. - DevTools → Network → filter Fetch/XHR → tick Preserve log.
- Reload the editor.
- Find the large JSON response (hundreds of KB – several MB) whose body has a
top-level
design(containingscreens/styles/symbols) and aproject. - Right-click it → Copy response (or Save response body) → save as
<site>.editor.json.
Method C — curl (token + siteId pasted by hand)
Section titled “Method C — curl (token + siteId pasted by hand)”curl -sS 'https://backend.bondlayer.com/api/design/open/<SITE_ID>' \ -H 'Accept: application/json' \ -H 'x-access-token: <JWT_FROM__bondUserToken_COOKIE>' \ -o '<SITE_ID>.editor.json'Method D — read the editor’s live in-memory store (no API, no token)
Section titled “Method D — read the editor’s live in-memory store (no API, no token)”Verified 2026-07-26. The editor keeps the whole project design in an Immutable.js store on
window.bond.design.current. toJS() yields the same object as the endpoint’s design key,
already parsed:
copy(JSON.stringify(bond.design.current.toJS(), null, 2)); // DevTools consoleTop-level keys observed: screens, styles, symbols, repeaters, schemas, forms, states,
settings, fonts, imagePresets, variants, content, fileVersion.
Two reasons to prefer this for research (not for bulk capture — it’s clipboard-bound):
- No credential handling — nothing to copy out of cookies.
- It reflects unsaved edits, so you can snapshot → change one control → snapshot → diff, and
read off exactly which keys that control owns, without ever saving. This is how
../Research/BONDLAYER-REPEATER-QUERY-SEMANTICS.mdwas produced; see its §0 for the diff helper.
window.bond.editor.current.toJS() is the sibling UI-state store (selection, current screen,
autosave, breakpoint, …) — useful for orienting, never part of a capture.
⚠️ Autosave. Projects can have
bond.editor.current.toJS().autosave === true. Any UI change you make to explore will persist to the real project. Do exploratory work on a scratch screen and delete it afterwards — and note that Bondlayer leaks orphanrepeaters,formsand privatestyleswhen a screen is deleted (see the query-semantics doc §6).
3. Verify the capture before parsing
Section titled “3. Verify the capture before parsing”A partial or truncated capture will fail deep in the pipeline. Sanity-check first:
cd apps/bondlayer-worker/srcpython3 parser.py ./<site>.editor.jsonYou should see a summary: screens, elements, styles, symbols, schemas, forms, etc. If it errors or reports 0 screens, the capture is incomplete — re-grab the full response body.
4. Hand it to the importer
Section titled “4. Hand it to the importer”The raw editor JSON is not directly ingestible by the Import Wizard — the
generic json adapter would flatten the whole site into one content item (see the
warning in packages/import/src/adapters/site-json.ts).
First convert it to a Matriz Site JSON v1 envelope with the offline CLI:
cd apps/bondlayer-worker/srcpython3 import_bondlayer.py --editor-json ./<site>.editor.json \ --default-language pt --output base.jsonbase.json carries schemaVersion: "matriz-site-json/v1". Ingest it either way:
- Import Wizard — drop
base.jsonin; thesite-jsonadapter claims it by schema version (probe 0.99) and runs provision → ingest verbatim. - Direct API —
POST /api/import/site/:siteIdwith the envelope as the body, or let the CLI push live with--api-url <workers-host> --api-token <jwt>.
Full sequence, merge with CMS records, and the two-arm architecture: import pipeline runbook.
5. What the editor JSON does not contain
Section titled “5. What the editor JSON does not contain”Per the archived data-locations table, the editor JSON is design-only. These live elsewhere and need a separate capture:
| Data | Where it actually lives | How to get it |
|---|---|---|
| Design / styles / screens | editor JSON | this doc |
| Triggers & states | editor JSON (element.attrs.triggers) |
this doc |
| CMS content records | Bondlayer CMS export | xlsx export → bondlayer-converter, merged into base.json |
| Custom code / custom CSS | published HTML | scrape <script src="*customcode*"> / <style> from the live site |
Erratum (2026-07-25): the custom-code row above is wrong — site-wide custom CSS is in the editor JSON at
design.settings.project.settings.webHeadStyles, and site-wide custom JS at the siblingwebBodyScripts(both verified against the Achronima export; see../Research/BONDLAYER-EXPORT-JSON-DEEP-DIVE.md§2/§6). A published-HTML scrape is only needed for anything loaded via external<script src="*customcode*">files.
So a full-fidelity migration = editor JSON + xlsx CMS exports (+ optional published-HTML scrape for custom code). The editor JSON alone gives you a styled, structured, but content-empty site.
6. Caveats
Section titled “6. Caveats”- Third-party surface, archived provenance. Everything in §2 depends on Bondlayer’s private app internals as captured on 2026-06-24. Treat Method A/C as a convenience and Method B as the source of truth.
- Tooling location (corrected 2026-07-26; deployment updated 2026-09-02). The Python importer
is live and maintained at
apps/bondlayer-worker/— it ships as thematriz-import-runtimeCloudflare Container (edge/) since theD:importer-containerscutover; it previously ran as a Render Docker service, now retired (seeplans/OWNER-OPS.md). Only the JS xlsx converter is frozen, atarchive/2026-07-13-bondlayer-tooling/bondlayer-converter/; there is nobondlayer-import/underarchive/. Earlier revisions of this doc pointed every Python path atarchive/— those paths never resolved. The live end-to-end import into Neon was still gated (G13) at last review — see the import & framework review. - Token handling. The
_bondUserTokencookie (formerlylocalStorage.accessToken) is a short-lived credential to a third-party account. Don’t commit it, don’t paste it into shared logs; it expires on its own. Method D avoids touching it entirely.