Embedding the builder
This page is for a developer who puts the HotLayout builder — the editor where screens are assembled — into their own back-office, next to the tools their team already uses. It lists what the element takes, what it reports, and what the page around it has to provide.
You do not need this page to show screens in your app: that is the job of a renderer — see the quickstarts for Flutter, React, iOS and Android. You also do not need it if your team works in the HotLayout admin itself.
Related pages: index · delivery · data dictionary
What you embed#
The builder is a custom element, <hotlayout-builder>. Your page tells it which document to open, who is working and what they may do; the element reads and saves the draft, publishes, and tells your page when the document was renamed or published.
| You give | How |
|---|---|
| the document to open | three attributes: org-id, project-id, document-id |
| who is working | a one-time ticket from your backend, through the sessionProvider property |
| what this user may do | the access attribute, and the level your backend asks for |
| the interface language and colours | the locale and theme attributes |
| the project's data dictionary | the dataContract property |
| You get back | How |
|---|---|
| "the document was renamed" | the document-rename event |
| "the document was published" | the document-publish event |
How a person gets in#
Your people sign in to your back-office and never see a HotLayout login. Two pieces of yours take part:
- Your backend asks HotLayout for a ticket for the signed-in person. The request carries the embedding key of your organization, which never leaves your server.
- Your page hands that ticket to the element. The element exchanges it with HotLayout and opens the editor.
A ticket works once and lives one minute. When the editor needs a new session (for example, after a long break), it asks your page for another ticket.
Before you start, in the Embedding section of the admin, as an owner or an admin:
- The plan. Embedding is part of the Embedded plan, and the licence of the organization must be valid. Without it the admin only says so, and every ticket request is refused.
- Allowed domains. Add the exact origin of every page that will show the editor.
- The embedding key. Create it and put it into your backend's secrets. It is shown once.
1. Load the module#
The editor module is served from our domain. There is nothing to install:
https://hotlayout.com/embed/v1/hotlayout-builder.jsv1 is the version of the element's public API: the attributes, properties and events on this page. It changes only for a break in that API. The module exports one function, defineHotlayoutBuilder. Today the module is a single file. It is revalidated on every load and served so that a page on any domain may load it as a module.
With a script tag, in the page that shows the editor:
<script type="module">
import { defineHotlayoutBuilder } from 'https://hotlayout.com/embed/v1/hotlayout-builder.js'
defineHotlayoutBuilder()
</script>Or from your own code, loaded when the editor is first needed:
const { defineHotlayoutBuilder } = await import('https://hotlayout.com/embed/v1/hotlayout-builder.js')
defineHotlayoutBuilder()If your build tool tries to bundle that address, mark it as external. If your page sets a content security policy, allow https://hotlayout.com for scripts and connections.
2. Ask for a ticket on your backend#
For the person who opened the page, your backend calls:
POST https://hotlayout.com/api/embed/session
Authorization: Bearer <embedding key>
Content-Type: application/json{
"orgId": "your-org-id",
"user": { "id": "42", "email": "anna@example.com", "name": "Anna" },
"access": "edit"
}| Field | Meaning |
|---|---|
orgId |
the id of your organization, as it stands in the addresses of the admin |
user.id |
required: the id of the person in your system, up to 256 characters. It must stay the same for the same person: the count of people rests on it |
user.email, user.name |
optional, for the members list in the admin. Up to 320 and 200 characters |
access |
view, edit or publish; see access |
Any other field is refused with bad_request: a request that sent role instead of access should fail loudly, not get a viewer.
A successful answer is 200:
{ "ticket": "hlt_…", "expiresAt": "2026-10-14T09:31:00.000Z" }With curl, from a shell where the key is in an environment variable:
curl -sS https://hotlayout.com/api/embed/session \
-H "Authorization: Bearer $HOTLAYOUT_EMBED_KEY" \
-H "Content-Type: application/json" \
-d '{"orgId":"your-org-id","user":{"id":"42","name":"Anna"},"access":"edit"}'With Node, as the function your endpoint will call:
export async function issueTicket(user: { id: string; email?: string; name?: string }, access: 'view' | 'edit' | 'publish'): Promise<string> {
const response = await fetch('https://hotlayout.com/api/embed/session', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.HOTLAYOUT_EMBED_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ orgId: process.env.HOTLAYOUT_ORG_ID, user, access }),
})
const body = (await response.json()) as { ticket?: string; error?: string }
if (!response.ok || body.ticket === undefined) {
throw new Error(`HotLayout refused the ticket: ${body.error ?? response.status}`)
}
return body.ticket
}Rules for this call:
- Decide
accesson your side, from your own permissions. HotLayout trusts the level your backend names. It never gives more thanpublish: managing people and projects stays with the HotLayout accounts of your organization. - The key stays on the server. Whoever holds it can open the editor as any of your people. Never put it in page code, in a mobile app or in a repository. If it leaks, replace it in the admin: the old one stops working at once.
- Ask for a ticket per page open, and hand it to one person. A ticket works once, so do not store it or share it.
- Requests are rate-limited per organization (300 in ten minutes). A ticket per page open is far below that. A request over the limit gets
rate_limited.
3. Give the ticket to the element#
Set the sessionProvider property to a function that asks your backend for a ticket:
builder.sessionProvider = async () => {
const response = await fetch('/hotlayout/ticket', { method: 'POST', credentials: 'same-origin' })
if (!response.ok) throw new Error('no ticket')
return (await response.json()).ticket as string
}If your backend gets a refusal from HotLayout (seats_exhausted, not_licensed, rate_limited), pass its code on by throwing an error with a code property. The person then sees the text for that code instead of the general "host page could not provide a ticket":
builder.sessionProvider = async () => {
const response = await fetch('/hotlayout/ticket', { method: 'POST', credentials: 'same-origin' })
const body = await response.json()
if (!response.ok) throw Object.assign(new Error('no ticket'), { code: body.code as string | undefined })
return body.ticket as string
}/hotlayout/ticket is an endpoint of your back-office that checks the person's session and calls issueTicket from the previous step. The element calls the function:
- when the editor opens, once, before the document is read, so the editor never draws without knowing what the person may do;
- again a few minutes before the session ends — a session lasts 8 hours from the moment the ticket was exchanged, however often the element renews its access token in between;
- again when it could not renew its session by itself.
What the element does with the ticket: it sends it to HotLayout from the browser. HotLayout checks that the page that sends it is on your list of allowed domains, marks the ticket as used and returns a session. From then on the element renews the session itself and keeps it in memory only.
If the function throws, returns an empty string or the ticket is refused, the person sees an error screen with Try again (see When a session is refused). Try again calls your function again, so a new ticket is asked for.
Set only one of sessionProvider and tokenProvider. With both, tokenProvider is used and the console says so.
What the ticket does and does not promise#
Be exact about the protection, so you can explain it to your security team:
- A ticket is redeemed only by a page of an allowed domain. The browser tells HotLayout which page sends it, and the exchange is refused for any other. The check is on the exact origin: scheme, host and port, no wildcards and no subdomains.
- A ticket is issued only to your backend (it needs the key), lives one minute and is used up by the first exchange that reaches the check of the ticket, whatever its result: an allowed page, a page of another domain, a revoked licence. An exchange that is refused earlier does not use the ticket up: a body of the wrong shape, no
Originheader, or anOriginthat cannot be an allowed domain (nothttps://, nothttp://localhost, a path,null). A ticket is also valid only for the key it was issued with: after you replace or turn off the key, tickets issued with the old one are refused withticket_invalid. - The origin cannot be trusted outside a browser. A script that is not a browser can write any origin it likes. That is why the promise is not "only your domains can ever open the editor" but "a ticket can be redeemed once, within a minute, and only by a request that names one of your domains". Whoever should not get into the editor should not get a ticket from your backend.
- A session ends 8 hours after the exchange. Turning embedding off, an expired licence or a removed person takes effect for a session that is already open no later than when that session ends: the next ticket is refused. Replacing or turning off the key does not end a session that is already open either: it only stops new tickets. To end access sooner, remove the person under Members in the admin.
- The domain list also opens the storage to your pages. HotLayout adds the domains of organizations with a valid embedding licence to the storage's browser access list a short while after you change the list. A page on a domain that no organization has allowed cannot read drafts from the storage in a browser. The list is a browser rule, not the protection: every read still needs the token of a member of your organization.
4. Place the element#
Create the element like any other element:
import { defineHotlayoutBuilder } from 'https://hotlayout.com/embed/v1/hotlayout-builder.js'
/** The part of the element a host works with. */
interface HotlayoutBuilder extends HTMLElement {
sessionProvider: (() => Promise<string>) | null
tokenProvider: (() => Promise<string>) | null
dataContract: unknown
}
export function openBuilder(place: HTMLElement, address: { orgId: string; projectId: string; documentId: string }, getTicket: () => Promise<string>): HotlayoutBuilder {
// Once per page. A second call does nothing, so it is safe to call from every screen that needs the builder.
defineHotlayoutBuilder()
const builder = document.createElement('hotlayout-builder') as HotlayoutBuilder
// The three attributes name the document together. Without all three there is no address.
builder.setAttribute('org-id', address.orgId)
builder.setAttribute('project-id', address.projectId)
builder.setAttribute('document-id', address.documentId)
builder.setAttribute('locale', 'en')
// The highest level this element may show. The level your backend asked for can only lower it.
builder.setAttribute('access', 'edit')
// A function and an object cannot be attributes: these are properties.
builder.sessionProvider = getTicket
builder.dataContract = null
// A custom element is inline by default: give it a box.
builder.style.display = 'block'
builder.style.blockSize = '100%'
place.replaceChildren(builder)
return builder
}If your code loads the module with import() as in step 1, take defineHotlayoutBuilder from there instead of the static import.
The same in markup, for a page rendered from a template (the two properties are still set from script):
<hotlayout-builder
org-id="…"
project-id="…"
document-id="reward-catalog"
document-name="Reward catalog"
locale="en"
theme="light"
access="publish"
></hotlayout-builder>Rules of placement:
- Give the element a size. It fills the box you give it (
display: blockand a height: a fixed one,100%of a sized parent, a flex remainder). Without a height the editor has nothing to fill. - One builder per page. Two elements on one page would edit the same document and overwrite each other's draft; the element warns about it in the console. Everything the element reports there is prefixed with
[hotlayout-builder]. defineHotlayoutBuilder()before the element is used. It takes an optional element name; without one it registershotlayout-builder.- Moving the element keeps its state. Detaching it and attaching it elsewhere in the same task does not reset the editor; removing it for good unmounts it.
Set up in the admin#
The settings live in the admin, in Embedding in the menu of the organization (/admin/o/<organization id>/embed). Only an owner or an admin sees them; the user guide has a page that walks through the screen.
- Allowed domains. Up to 20 origins, each exact:
https://admin.example.com, orhttp://localhost:3000while developing.httpis accepted only forlocalhost. You can type the address as the browser shows it: a default port (:443,:80) is dropped and a non-Latin name becomes itsxn--form. A path, a wildcard or a subdomain pattern is not an origin. Another port is another origin. - Embedding key. One key per organization. Creating it shows it once; replacing it makes the old one stop working at once, together with the tickets it has issued and nobody has used yet; turning it off stops all sign-ins the same way. People who are already in the editor stay until their session ends (up to 8 hours). The key cannot be read again, not even by an owner.XX The key cannot be read again, not even by an owner.
- People and sessions. How many people count against the plan, and the recent sessions.
Who counts as a person#
The licence names a number of people in the editor. A person counts when they redeemed a ticket with access edit or publish within the last 30 days. People with view do not count.
- The same
user.idis one person, however many times they come back. - A person who is already counted always gets in. A new person over the limit is refused at the ticket request with
seats_exhausted; the seat is taken when the ticket is redeemed. - Removing a person from the members list does not free the seat at once: it frees itself 30 days after their last editing session.
Your people in the activity log#
Everything these people do that changes the organization, such as publishing a document, uploading an image or changing the draft, is written to the organization's activity log under their user.id (the externalId of the record), and under user.name if you passed it. The owner and the admins see it in the admin and can export it. The auditor will look for a person by that id, so send the same stable id every time, the one you use in your own system, and not a session or a counter.
Who the people are in the organization#
Every person from your system appears among the members of the organization, marked as coming through embedding, with the name or the id you passed. Their role comes from access on every sign-in: view is a viewer, edit an editor, publish a publisher. Change it in your system, and the next ticket applies it. Removing such a member in the admin lasts only until their next sign-in.
These people have no HotLayout password and no verified email, so they cannot create an organization or accept an invitation.
5. Attributes#
| Attribute | Values | Without it |
|---|---|---|
org-id, project-id, document-id |
the address of the document: the organization, the project and the document | see The address |
document-name |
a name for a document that does not exist in the storage yet | a new document gets a default name |
locale |
the language of the builder's interface: en or uk |
en |
theme |
light for the light look of the interface |
the dark look |
access |
view, edit or publish |
publish |
publish-blocked |
why publishing is not available right now: trial-ended or plan-expired |
publishing is available |
storage-url |
the base address of your own storage API, for self-hosted use | the storage of HotLayout |
licence-key |
the licence key of a self-hosted installation (hl_lic_…); read only together with your own storage |
with your own storage, the editor does not open |
The address: all three, or none#
org-id, project-id and document-id name a document only together. There is no half address:
- All three set — the builder reads the draft of that document from the storage and saves to that address. A document that has no draft yet is created on the first save.
- Any of them missing or empty — there is no address. The builder opens a starter document and saves nothing; use this only to look at the editor.
When your page moves to another document, change the three attributes in one synchronous step: the element reads the address once, after your code has finished. If you have to change them across an await, remove one attribute first — an incomplete address is "no address", never half of one.
In a framework that sets attributes one by one, make another document another element. With React, a key made of the full address does it — see In a React page.
What the user sees while the document opens:
| State | On screen |
|---|---|
| reading the draft | "Opening the document…" |
| the storage refused or did not answer within 20 seconds | an error screen with Try again — never an empty document, so a network failure cannot overwrite a draft |
| the draft was read | the editor |
document-name#
The name a new document starts with, so that the name in your list and the name in the editor are one from the first second. It is read once, when the document is opened. It does not touch a document that already has a draft, and changing the attribute later does nothing — from then on the name is changed in the editor, and you hear about it through document-rename.
locale#
The language of the builder's own interface, not of the screens that are built in it: a manager can work in English and build a Ukrainian screen. An unknown value changes nothing and is reported in the console. Removing the attribute returns to English.
theme#
The builder's interface is dark by default; theme="light" switches it to the light look. Like the language, the look is the host's choice: set it to match the page around the element. It has nothing to do with the theme of the screens.
access#
What the person may do, as a level. Each level adds one ability to the one before it.
access |
What the person gets |
|---|---|
view |
A different screen, not an editor with saving turned off: the map of the document's screens and a phone that can be clicked through. No editing tools, no fields, no publishing, no editing shortcuts. It shows the team's working draft and says so. |
edit |
The editor. The draft is saved; publishing is not offered. |
publish |
Everything, including Publish. |
- No attribute means
publish. A host that says nothing gets the full editor. - An unknown word means
view, with an error in the console: a host that wroteaccess="reviewer"meant to restrict. - With a ticket, the lower level wins. The element takes the lower of this attribute and the
accessyour backend put in the ticket request. A ticket forviewunderaccess="publish"gives a viewer; a ticket forpublishunderaccess="edit"gives an editor. So the attribute is the ceiling of this element, and your backend decides the person. - This is what is drawn, not what is allowed. The level keeps a person from seeing a button the storage would refuse. The protection is the membership and role of the account behind the token; for a ticket, that role is set from the ticket's
accesson every sign-in. - The
viewscreen does not refresh itself. To show a newer draft, create the element again.
publish-blocked#
The host can tell the editor that publishing is not available and why. access is about the person; this attribute is about the place: with it set, nobody publishes from this element, whatever their level.
publish-blocked |
What the editor says |
|---|---|
trial-ended |
The trial has ended. |
plan-expired |
The plan has expired. |
With the attribute set:
- Publish is disabled, and the reason stands as visible text in the bar at the bottom of the editor, not only in the button's tooltip. The text comes from the builder's dictionary, in the language of
locale. - The theme is read-only, with the same reason: a theme has no draft, so the only way to save it is to publish it.
- Editing goes on. The draft is saved as before.
Other rules:
- No attribute means publishing is available. Remove the attribute and the button comes back without reloading the element.
- An unknown word blocks nothing, with an error in the console: the editor would have a disabled button and nothing to say about it.
- The editor does not find this out itself. It does not know who its host is; the word is yours to set.
- This is what is drawn, not what is allowed. The storage refuses such a publication with or without the attribute.
6. Properties#
These are JavaScript properties of the element. They can be assigned before or after the element is placed, and also before defineHotlayoutBuilder() ran — the element picks such a value up when it connects.
sessionProvider#
type SessionProvider = () => Promise<string>A function that resolves to a ticket from your backend (see steps 2 and 3). This is how your own people open the editor. Assigning a new function (or null) starts a new session and shows the loading state again.
- The editor draws only after the ticket is exchanged, so a refusal is an error screen, never an empty document.
- Return a new ticket each call. A ticket works once. Do not cache the result.
- Both
sessionProviderandtokenProviderset:tokenProvideris used andsessionProvideris ignored, with an error in the console.
tokenProvider#
type TokenProvider = () => Promise<string>For people who have their own HotLayout account and are members of your organization — your own team that also works in the admin. It is not the way to let your back-office users in: that is sessionProvider.
A function that resolves to the access token of the signed-in account. The builder calls it whenever it talks to the storage: to open the draft, to save it, to publish, to list and upload media. Return a fresh token each time; refreshing it is yours.
- With a provider, the builder shows no sign-in form of its own.
- Without one (
null, and nosessionProvider), the builder runs its own sign-in and shows its form inside the element. In a back-office where the user is already signed in, setsessionProvider. - With your own storage (the
storage-urlattribute or thestorageproperty, see self-hosted) the token is optional and opaque: it goes to your API as it is. The same mode adds auserproperty —{ id, email?, name? }, who is working — and ignoressessionProvider.
dataContract#
The project's data dictionary: the object with paths and collections, the same shape as the data-contract.json file and as the dataContract key of the webhook body. The builder offers managers only these paths and checks every binding against them.
- An object replaces the dictionary whole. It is checked first; a dictionary that does not pass is refused with the reasons in the console, and the previous one stays in effect.
nullmeans "this project has no dictionary of its own": the standard one is used. Say it explicitly when you move between projects — the dictionary is one per page, and withoutnullthe next project would open with the previous project's dictionary.- Reading the property returns the dictionary in effect.
7. Events#
Both events bubble and cross the shadow boundary, so you can listen on the element itself or on any ancestor.
export function listen(builder: HTMLElement, documentId: string, on: { renamed: (name: string) => void; published: (at: Date) => void }): () => void {
const onRename = (event: Event) => {
const detail = (event as CustomEvent<{ documentId: string; name: string }>).detail
if (detail.documentId === documentId) on.renamed(detail.name)
}
const onPublish = (event: Event) => {
const detail = (event as CustomEvent<{ documentId: string; publishedAt: string }>).detail
if (detail.documentId === documentId) on.published(new Date(detail.publishedAt))
}
builder.addEventListener('document-rename', onRename)
builder.addEventListener('document-publish', onPublish)
return () => {
builder.removeEventListener('document-rename', onRename)
builder.removeEventListener('document-publish', onPublish)
}
}document-rename#
detail: { documentId: string, name: string }
The name of the opened document changed in the editor. A document's name lives in two places — inside the document and in your list of documents — and this event is how you keep them one.
- It fires on every change of the name, so while a person types it fires per keystroke. Write to your list after a pause in typing, and do not drop the last value when the user leaves the page.
- It is not fired when the document loads, and not when someone imports a JSON file of another document into the editor.
- Undoing a rename fires it too: the name really changed.
document-publish#
detail: { documentId: string, publishedAt: string } — publishedAt is the moment of publication, ISO 8601.
The opened document was published successfully. Use it to refresh whatever your page says about the publication state — a badge, a link to the published screen.
- One event per successful publication.
- It is not fired on load, on a failed or refused publication, or when the project theme is published.
- Without an address (no three attributes) neither event is fired.
Check detail.documentId against the document your page shows, as the example does: right after you move to another document, an event of the previous one may still arrive.
To deliver the published document to your backend, use the webhook — it is sent by our server and does not depend on a browser tab staying open.
In a React page#
React 19 sets a property on a custom element when the element has one, so the function and the dictionary can be passed as props — provided the element is defined before the first render. On an element that is not defined yet, sessionProvider would become a string attribute, and the builder would show its own sign-in form.
defineHotlayoutBuilder() // at module level, before any render
export function Editor({ orgId, projectId, documentId, name, canPublish, getTicket }: EditorProps) {
return createElement('hotlayout-builder', {
// Another document is another element: the three attributes never name a mix of two addresses.
key: `${orgId}/${projectId}/${documentId}`,
'org-id': orgId,
'project-id': projectId,
'document-id': documentId,
'document-name': name,
locale: 'en',
theme: 'light',
access: canPublish ? 'publish' : 'edit',
sessionProvider: getTicket,
dataContract: null,
style: { display: 'block', blockSize: '100%' },
})
}EditorProps is yours: { orgId: string; projectId: string; documentId: string; name: string; canPublish: boolean; getTicket: () => Promise<string> }. With React 18, set the two properties through a ref instead.
When a session is refused#
Two requests can be refused: the ticket request from your backend, and the exchange the element makes. A refusal comes as a JSON body { "error": "<code>" } with the status below.
| Code | Status | Where | Meaning | What the person sees |
|---|---|---|---|---|
unauthorized |
401 | ticket request | the key is missing, has the wrong form, is not the current key, or the organization does not exist (the answer is the same for all, so it does not reveal which) | nothing from HotLayout: your backend gets the error and decides what to show. If it throws, the person sees the "host page could not provide a ticket" text |
bad_request |
400 | ticket request; exchange | the method is not POST, the body has the wrong form, or an extra field |
the same, for the ticket request |
not_licensed |
403 | ticket request; exchange | the licence is not valid, or does not include embedding | "The organisation's licence is not active or does not include the embedded editor." |
seats_exhausted |
403 | ticket request; exchange | a new person over the limit of people in the editor | "The limit of people for the embedded editor is reached. Ask the administrator to raise the limit." |
rate_limited |
429 | ticket request | more than 300 ticket requests in ten minutes | "Too many attempts. Wait a minute and try again." |
ticket_invalid |
401 | exchange | the ticket is unknown, already used or expired. One code for all three, so the answer reveals nothing about a ticket | "The sign-in ticket from the host page is unknown, already used or expired. Tickets work once and for one minute — try again to get a new one." |
origin_not_allowed |
403 | exchange | the page that sends the ticket is not on the list of allowed domains | "This website is not on the organisation's list of allowed domains. Ask the administrator to add it in the Embedding section." |
internal |
500 | either | a failure on our side | the same as an unexpected answer |
The person always sees the same screen — "Could not start the editor session", the reason from the last column, and Try again. Three more reasons never come from a server answer:
| Cause | What the person sees |
|---|---|
| the network did not answer | "Could not reach the server. Check the connection and try again." |
sessionProvider threw an error with the code of a server refusal (seats_exhausted, not_licensed, rate_limited, …) |
the text of that code, from the table above |
sessionProvider threw without such a code, or returned an empty string |
"The host page could not provide a sign-in ticket. Try again; if it repeats — tell the administrator of the host page." |
| an answer the element does not understand | "The server gave an unexpected answer. Try again; if it repeats — tell the administrator about it." |
The code is also written to the console with the [hotlayout-builder] prefix, so you can see the exact cause during development. The texts are in the language of the locale attribute.
A ticket is used up by the first exchange, even a refused one. Try again therefore asks your function for a new ticket instead of repeating the old one.
What the page around the element provides#
Isolation#
The builder lives in a shadow root. Your page's CSS does not reach inside it, and its CSS does not reach your page: a global button {} rule of your back-office does not restyle the editor, and the editor resets nothing of yours. document.querySelector on your page does not find the builder's nodes, and for an event that leaves the element, event.target is the element itself.
Fonts#
The interface is designed in Geist and Geist Mono. A font face declared inside a shadow root is not registered by the browser, so the fonts are loaded by the page — yours. The builder does not add fonts to a document it does not own.
- With the two families available on the page — for example from the
@fontsource-variable/geistand@fontsource-variable/geist-monopackages — the interface looks as designed. - Without them it is drawn in the system font. Nothing breaks.
This is about the editor's own interface. The fonts of the screens come from the project theme — see Theme.
Width#
The thresholds measure the width of the element, not of the window: next to your own navigation the element is narrower than the window.
| Width of the element | What the editor does |
|---|---|
| 1160 px and wider | three columns: the section rail, the canvas, the properties and preview panel |
| below 1160 px | the properties and preview panel becomes a layer over the canvas, opened with the Side panel button in the header and closed with Esc |
| below 600 px | no editor: a note that it needs at least 600 px of width. Editing shortcuts are off while the note is shown |
Give the element at least 1160 px where you can.
What to check when something is off#
- A sign-in form inside the element. Neither
sessionProvidernortokenProvideris set, or it was passed as an attribute. In React, make suredefineHotlayoutBuilder()runs before the first render. - A thin strip instead of the editor. The element has no height.
- The starter document instead of yours, and nothing is saved. One of the three address attributes is missing or empty.
- "Could not open the document". The storage refused or did not answer: the token is not accepted, the account is not a member of the organization, or the network failed. Nothing was changed; Try again repeats the read.
- Every binding is marked as an unknown path. The builder works with the wrong dictionary: set
dataContractto the project's own, or tonull. A refused dictionary is reported in the console. - The document of the previous project is still shown. The address attributes were changed across an
await, or only one of them changed. Recreate the element for another document. - A warning about more than one builder. Leave exactly one element on the page.
- A note instead of the editor. The element is narrower than 600 px.
- The interface language did not change.
localetakesenoruk; anything else is reported in the console and ignored. - "Could not start the editor session". Read the reason on the screen and the code in the console; the table in When a session is refused says what to change. The usual first-day causes: the page's origin is not in the list (check scheme and port), no key was created, or the licence does not include embedding.
- The module does not load. A content security policy blocks
https://hotlayout.com, or a build tool tried to bundle the address. - The ticket is refused on the second page open. Your backend cached a ticket. Ask for a new one on every call of
sessionProvider.