Let your users edit a rendered design
Your app renders a post or a carousel from a template, shows it to a user, and the user wants to change something — a word, a photo, the order of the slides. This guide opens that exact render in the embedded editor, lets the user edit it, and gets the new images back. The master template is never touched, and your app never handles design JSON.
- Render from a template and keep the
renderId. - When the user clicks Edit, create an edit session on your server.
- Show the returned
editorUrlin an iframe. - On save, take the new
renderIdand image URLs.
render.edited under Settings → Webhooks if you want a server-side record of every save.1. Render and keep the renderId
Every render response carries a renderId (rnd_…). Store it next to the post in your database — it is the handle for editing, rerendering and looking the render up later.
curl -X POST https://buildonce.app/v1/templates/tpl_123/render \
-H "Authorization: Bearer fl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "allPages": true, "modifications": { "page1@headline": "Rates hold steady" } }'2. Create an edit session
When the user clicks Edit, your server checks they may edit that post, then asks buildonce.app for an editor link for its render. Pass your page's origin: the editor will only talk to it.
app.post("/posts/:id/edit", requireUser, async (req, res) => {
const post = await db.posts.find(req.params.id, { owner: req.user.id });
const response = await fetch("https://buildonce.app/v1/embed/emb_123/edit-sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BUILDONCE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
renderId: post.renderId,
origin: "https://app.example.com",
allowPageChanges: true, // let them add, remove and reorder slides
allowUnlock: false, // keep the logo and disclaimer locked
}),
});
const { data } = await response.json();
res.json({ editorUrl: data.editorUrl, sessionId: data.sessionId });
});3. Show the editor
Put the editorUrl in an iframe and listen for the editor's messages. It has its own Save button; you can also save from your own button.
<iframe id="editor" style="width:100%;height:760px;border:0"></iframe>
<button id="save">Save</button>
<script type="module">
const EDITOR_ORIGIN = "https://buildonce.app";
const iframe = document.getElementById("editor");
const { editorUrl } = await fetch("/posts/42/edit", { method: "POST" }).then((r) => r.json());
iframe.src = editorUrl;
let unsaved = false;
window.addEventListener("message", (event) => {
if (event.origin !== EDITOR_ORIGIN) return;
const message = event.data;
if (message.type === "frameline:render:change") unsaved = message.dirty;
if (message.type === "frameline:render:saved") {
// message.renderId, message.files[i].url — in slide order
updatePreview(message.files.map((file) => file.url));
}
if (message.type === "frameline:render:error") alert(message.message);
});
document.getElementById("save").onclick = () =>
iframe.contentWindow.postMessage({ type: "frameline:render:save" }, EDITOR_ORIGIN);
window.addEventListener("beforeunload", (event) => {
if (unsaved) event.preventDefault();
});
</script>Unsaved work is autosaved as a draft of the post for 7 days. If the editor is closed and opened again on the same render — by the same person or a teammate — the draft comes back (restoredDraft: true on the loaded message).
Besides the four messages above, the editor also sends frameline:render:ready once the iframe has booted and frameline:render:loaded once the design is on the canvas (carrying renderId, pageCount and restoredDraft) — see the full message list in the API reference.
4. Store the result
Each save that changed something is a new render with a new renderId, pointing back at the one it was edited from (sourceRenderId). Store the new id: the next edit should start from it. The render.edited webhook carries the same data plus the sessionId, signed and retried, so your server can record the save even if the browser never reports back.
app.post("/webhooks/buildonce", express.raw({ type: "application/json" }), async (req, res) => {
if (!verify(req.body.toString(), req.get("BuildOnce-Signature"), process.env.BUILDONCE_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const { type, data } = JSON.parse(req.body);
if (type === "render.edited") {
await db.posts.updateByRenderId(data.sourceRenderId, {
renderId: data.renderId,
images: data.files.map((file) => file.url),
});
}
res.sendStatus(200);
});verify is in the API reference.
Good to know
- A save renders every slide and costs 1 credit per image; a save with no changes costs nothing. See Credits.
- Slides the user adds, removes or reorders come back that way in
files. SetallowPageChanges: falseto keep a fixed slide count. - Images and fonts the user uploads in the editor go to your workspace's asset library.
- Want new text without opening the editor? Use rerender with page-scoped modifications.
- The editor needs the design to show it, so a determined user can read it from their own browser. What leaves through the API, messages and webhook is only ids, image URLs and credits.
Was this page helpful?