ONLY A STUDIO

Get in touch →

← NOTES

19 August 2026 3 MIN READ
Amir Dakhlia Co-founder

Building on LearnWorlds

In a closed LMS, the constraint isn't what the editor lets you build — it's what it lets you keep.

LEARNWORLDSLMSEDUCATIONAL DESIGNEDITORIAL DESIGNCONTENT MIGRATION
A three-column specimen of a LearnWorlds eBook's persistence boundary — the inline markup a text node keeps on save, the section and icon nodes it silently drops, and the custom CSS class that survives on the element.

Every closed platform gets scoped the same wrong way. Someone opens the editor, finds it can render what the design asks for, and calls the build feasible.

Rendering was never the question. On a hosted LMS the question is what the platform will still be showing after a save and a reload — and that line sits in a different place than the editor’s own interface suggests.

LearnWorlds is a good case to think through, because it’s honest about being a product rather than a framework, and because the boundary is sharp once you find it.

The editor owns the model

A LearnWorlds eBook renders inside an iframe. Open the inspector and it looks like a page: sections, columns, text nodes, classes you can reach and rewrite.

It isn’t a page. It’s a serialisation of the editor’s own model of the page. That distinction decides everything downstream.

Write into an existing text node through the DOM, save, reload — the content is there. Insert a new <section> the same way, and it’s gone. The editor rebuilds the document from its internal section list on save, and it never saw the one you added. No error, no warning. The work is simply not in the payload.

I found the line by losing a twenty-eight section rebuild to it. The test that gave me confidence had covered editing a single heading, and a single heading is the one case that works.

What a text node keeps

Inside a native text node, the survivors are a short list: <strong>, <span> and <div> carrying inline styles, <br>, non-breaking spaces, plain text. Lists survive in some contexts and not others — worth confirming against a saved block rather than assuming.

Everything else is dropped on the way out, or refuses to leave at all. Nest an icon node inside a text node and the Save button greys out. No message; a dead control and a lost session. An <iframe> does the same.

Then there’s the fact that reframes the whole build: custom classes on elements do survive. Add a class to a column and it comes back after reload.

That single behaviour sets the architecture. Content stays native and minimal, and everything else — icons, spacing, rules, colour, the entire visual layer — moves into the platform’s custom CSS. Anything you can express in a stylesheet is something you never have to negotiate with the serialiser. The design system lives outside the content, which is where it should have lived anyway.

Migration is counting, not writing

Because sections can’t be created in code, the shape of the destination is fixed before any content moves.

That inverts the usual order of a migration. The first step isn’t extracting the source. It’s counting the title and body slots that already exist in the destination, comparing that against what has to land there, and getting the missing ones duplicated by hand in the interface before a single line is written.

Estimate by slots available, not by words to move. A migration that reads as three hours of text transfer is three hours of transfer plus however long it takes a human to click duplicate forty times.

Both ends have a format contract

Source platforms leave residue, and it is never documented.

Course tools inject their own text into markup — link-tracking labels that fuse to the neighbouring word, figure captions that any bold-based heuristic reads as a heading, entity encoding applied twice. None of it is visible in the source interface. All of it arrives in the destination.

A migration between two hosted platforms is two undocumented format contracts, not one. Both are discoverable in an afternoon. Neither is discoverable after you’ve already written in bulk.

Find the boundary first

This generalises past LearnWorlds. It’s the same discipline a custom OJS theme needs, and the same one any hosted CMS rewards: every closed platform has a persistence boundary, and the editor’s interface is a poor guide to where it sits.

So find it before you design anything. Write one element, save, reload, and read what came back. Build the system on what survived.

The platform decides what it keeps. You decide whether you found that out on element one or element twenty-eight.

NEWSLETTER

Get notified when we publish.

Occasional notes from the studio. No frequency promises, no marketing — just the writing, when it ships.

Let's build something
great together.

LETS CONNECT

Get in touch