This document describes how to put Typograph on a page: what to load, where it mounts, and what a host container has to provide. It is intended for developers embedding Typograph into an existing application rather than serving it as a page of its own.
There are two products and two bundles. The editor (editor.js) authors
templates: full chrome, menus, parameter bar, every tool. The viewer
(viewer.js) fills templates in: no chrome, a floating toolbar, and only the
edits the template permits. Everything below about mounting, containers and
lifecycle applies to both — where they differ is called out.
For tailoring the interface once it is running, see API-EditorUI.md.
Two scripts. The colour engine is a bare-global script and stays the host's job; everything else — the chrome, the stylesheet, the webfont — the bundle brings itself.
<script type="text/javascript" src="https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js"></script>
<script type="module" src="https://cdn.typograph.nl/editor/latest/editor.js"></script>
For the viewer, load viewer.js in place of editor.js. The colour engine is
the same and is still yours to load.
There is no markup to author. Earlier versions required a page to reproduce 31 specific element ids; that is no longer true, and any page still carrying that markup keeps working — the bundle detects it and leaves it alone.
The same applies to the stylesheet and the webfont: if your page already links
stylesheets/style.css or Source Sans Pro, the bundle detects those by href and
does not add a second copy. A page built against the older instructions, which
carried both <link> tags itself, keeps using its own — including a customised
stylesheet.
Load the two scripts and you are done. The editor mounts on document.body and
fills the viewport.
<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8">
<script type="text/javascript" src=".../scripts/jsColorEngineWeb.js"></script>
<script type="module" src=".../editor.js"></script>
</head>
<body></body>
</html>
Construct a Typograph.Editor and give it a container. The chrome is built
inside that box instead of taking over the page.
<div id="app"></div>
<script type="module">
import { Editor } from 'https://cdn.typograph.nl/editor/latest/editor.js';
const editor = new Editor({ container: '#app' });
</script>
Editor is also available as window.Typograph.Editor if you would rather not
use a module script:
<script>
// After editor.js has loaded.
const editor = new Typograph.Editor({ container: '#app' });
</script>
containerA CSS selector, a bare element id, or an HTMLElement. All three work:
new Editor({ container: '#app' }); // selector
new Editor({ container: 'app' }); // bare id
new Editor({ container: document.getElementById('app') }); // element
If nothing matches, a warning is logged and the editor mounts on
document.body rather than failing silently.
Omit container entirely and you get the full-page editor.
This is the one requirement. The editor's layout is absolutely positioned
and resolves against the container's box, so a container with no height gives
you an editor with no height. A plain <div id="app"></div> collapses to zero.
Give it a definite size any way you like:
#app { position: absolute; top: 44px; left: 24px; right: 24px; bottom: 24px; }
#app { height: 90vh; }
/* Inside a flex or grid parent that gives it a track. */
#app { min-height: 0; }
The editor takes care of the rest: if the container is not already a positioned
box it is given position: relative, and if its overflow is visible it is given
overflow: hidden so nothing escapes the box you allotted. Both are restored on
destroy().
Your container keeps its own id and its own CSS. Inside the page, the editor adds
exactly one child element to your container, plus the two <link> tags in
<head> described above. Nothing else on the page is touched.
Menus, dialogs, colour pickers and the floating tool palettes are all confined to the box you gave the editor — none of them can be drawn over the rest of your page. A dialog that would be "centred on screen" in the full-page editor is centred in your container instead.
The one consequence worth knowing: a container much smaller than the editor's natural layout will clip its own overlays rather than let them escape. The chrome assumes roughly the room a desktop application gets.
The editor reserves 57px above the menu bar for a host's logo. If your application already has its own header, you will not want it:
new Editor({ container: '#app', header_space: false }); // remove it
new Editor({ container: '#app', header_space: 80 }); // or set your own height
Everything below moves with it. Omit the option and the layout is exactly what it has always been.
The editor shows its own texts in English or Dutch, and writes its dates in that language, in Europe/Amsterdam time: the menus, the toolbar and its tooltips, the parameter bar with the role panel, the content bar's panels, the dialogs, the comments panel, its pins and the mode switcher, and the canvas's own texts (the zoom control, the spelling menu, the page bar). The document's own text, font, style and swatch names, and the names elements and layers are given stay as they are; an element nobody named is listed by its type in the language:
new Editor({ container: '#app', locale: 'nl' }); // 'en' or 'nl'; 'nl-NL' counts as 'nl'
A page that uses the full-page editor without constructing one sets the language
on <html> before editor.js loads; the viewer reads it too:
<html data-typograph-locale="nl">
The option wins over the attribute. Without either, and for a language the
editor has no texts for, the editor is English, as it has always been. Only the
editor's own texts change: the document's text, and the ids of menu items that
MenuAPI and gEditorUI address them by, stay what they are.
gEditorUI.get_locale() answers the language in force.
Construct the Editor before the window load event. Construct it later and the
editor will already have mounted itself on document.body under the full-page
defaults; container is then ignored and a warning says so. A module script in
the page body, as above, is early enough.
<div id="app"></div>
<script type="module">
import { Viewer } from 'https://cdn.typograph.nl/editor/latest/viewer.js';
const viewer = new Viewer({ container: '#app' });
</script>
Also available as window.Typograph.Viewer. Everything above applies unchanged:
the same container forms, the same height requirement, the same containment,
the same get_document() and destroy().
One difference in behaviour. The editor mounts itself when nothing constructs
it, so a page that only loads editor.js still gets an editor — history the
viewer does not have. viewer.js does nothing until you construct a Viewer.
Not the editor with things switched off. Canvas answers permission questions
differently in viewer mode: an element with no authored edit_options falls back
to a default that allows text editing and image content edits — moving, scaling
and rotating the image inside its frame — and refuses moving, resizing or
rotating the frames themselves. Grant those in a template and the same code
allows them, with no change to your host.
The toolbar floats and follows the selection, showing the controls for whatever is selected and undo/redo when nothing is. Drag it by its handle and it stays where you put it — following the selection is a convenience, your placement is an instruction.
new Viewer({
container: '#app',
show_rulers: true, // default true
page_bar: {
visible: true, // the bar itself
show_name: true, // each page's name
show_page_number: true, // the number badge
show_add_page: true, // the "new page" control
allow_reorder: false, // drag pages into a new order (default false)
allow_delete: false, // a delete control on the selected page (default false)
mode: 'narrow', // 'narrow' beside the canvas, or 'overlay' over it
},
host: {}, // providers, as for the Editor
legacy_php_fallback: true, // false: no php/*.php request, ever
locale: 'en', // or 'nl': the viewer's own texts, as for the Editor
});
All are optional; omit any and canvas's own default applies. host,
legacy_php_fallback and locale work exactly as for the editor — see
Templates, images, the manifest and clips.
page_bar matters more than it looks. The page bar is the viewer's only
navigation, but a filled-in template is not an authoring surface — the names it
carries are the template author's, not your user's, and there may be nothing to
number. Turn off what your product should not show.
show_add_page also settles an older rule: canvas suppressed the control in
viewer mode whenever a page was visible. Saying nothing keeps that. Setting it
either way makes your answer the whole answer.
allow_reorder and allow_delete let your user change the document's page
structure, so they are off unless you turn them on. With allow_reorder the
user drags a page in the page bar to a new place; with allow_delete the selected
page gets a small delete control, which asks "Delete page 3?" first and only
deletes on a second click. The last page is never offered. Both are ordinary
document changes: one undo step each, typograph_document_changed for your
autosave, and typograph_page_rearranged / typograph_page_deleted as in the
editor (see Page Action Events).
mode says how the open page bar sits against the canvas. With 'narrow', the
default, the canvas starts at the bar's right edge and the page is fitted and
centred in what is left, and the bar runs the full height. With 'overlay' the
bar lies over a full-width canvas as a floating panel, as it did before release
20261022.3. Switch it later with typograph.set_page_bar_mode('narrow' | 'overlay'); typograph.get_page_bar_mode() reads it. In the editor the user
switches it from the View menu.
No menu bar, content bar or parameter bar; no path tool, marquee, guides or multi-select. These are absent by construction, not hidden — a template cannot turn them on. Numeric position and size entry is the parameter bar's job and is therefore absent too; direct manipulation is the whole interface.
Replacing an image has no picker. React to the selection event and swap the image through the manifest API — the choice of what replaces it belongs to your application, not to Typograph.
No Find and Replace dialog either: it is a menu item, and there is no menu bar to
carry it. The API behind it is canvas-level rather than part of the editor's
chrome, so it works here unchanged — window.typograph.find() and
replace_all() are available in a viewer embed, and a host that needs to fill
placeholders, run a search or offer its own control can call them directly. See
FindReplace.md. If the filling in happens on your server before
the document reaches the viewer, TextModel.md is the same job
done there.
const editor = new Editor({ container: '#app' });
editor.get_document(); // the canvas document, once booted
editor.destroy(); // remove the editor's DOM and styles
Viewer has the same two:
const viewer = new Viewer({ container: '#app' });
viewer.get_document();
viewer.destroy();
From setup until the document can be looked at, the editor and the viewer show one loader over the
canvas, in the page's light or dark theme. Open your document from your typograph_editor_ready
handler (editor) or typograph_setup_ready handler (viewer), and the loader stays until it is open
and its images are in: at most 8 seconds for the images, and 20 seconds after the fonts whatever
happens. Opened later, or not at all, the blank document shows once the fonts are in.
The loader fills the container. When you bring your own #typograph and it is not positioned, the
loader lays itself over that element's box instead of over the nearest positioned ancestor; the
container's own position is not changed, because the canvas places itself against it.
get_document()Returns the canvas TypographDocument — the same object as
window.typograph.get_typograph_document(). Available once the editor has
booted; wait for typograph_editor_ready if you are unsure.
document.addEventListener('typograph_editor_ready', () => {
const doc = editor.get_document();
});
destroy()Removes the chrome and the injected stylesheet, and restores anything the editor wrote on your container. Your own content inside the container is left untouched.
Mounting a new editor afterwards is not supported. The canvas document has no
teardown entry point yet, so its state would survive a second boot. Constructing
another Editor after destroy() logs a warning and does nothing; reload the
page instead. Use destroy() to release the DOM when your view is torn down,
not as half of a mount/unmount cycle.
Constructing a second Editor destroys the first, and so does a second Viewer. The one exception is
the editor that started itself because the page constructed none: an Editor constructed after that
takes it over — same document, same chrome — and hands it its host and legacy_php_fallback.
The two are separate bundles and do not know about each other, so do not load
both against the same page expecting them to cooperate — pick one. Two editors side by side
would need the canvas library's single global document undone, which is
deliberately deferred — see
ASSESSMENT-editor-viewer-canvas-architecture.md §3.4 in the canvas repository.
The chrome's colours, radii and shadows come from a set of CSS custom
properties defined once, at the top of stylesheets/style.css:
:where(:root) {
--typograph-color-primary: #1B6AE3;
--typograph-color-surface: #fff;
--typograph-color-text: #141824;
/* … */
}
Overriding them is the supported way to rebrand the editor. Set the ones you
want on your own :root:
:root {
--typograph-color-primary: #0b7a53;
--typograph-color-primary-hover: #096646;
--typograph-color-primary-soft: rgba(11, 122, 83, .10);
}
Your value always wins, whichever stylesheet loads first. The defaults sit
inside :where(), which contributes no specificity, so any plain :root rule
beats them — there is no need to load your CSS after the bundle's or to add
!important.
They are on :root rather than on the editor's wrapper because dialogs, menus
and popups are mounted outside it; a property set on your container would not
reach them. Set them on :root.
The names describe a role, not a colour: primary, surface, line,
text-muted, stage. The full list is at the top of style.css. Recolour by
role — setting primary also changes the active tool, the selected tab, focus
rings, switches, the link locks, the comment pins and the open edge tabs, which
is the point.
Things the tokens do not reach:
#1071e5 for now.style.css: tooltips, the start screen, the text-style samples, and some
image-manager and clip controls. They move to the tokens in a follow-up.style.css from the same release as editor.jsThe chrome is styled by classes that the script adds, and the stylesheet is
where those classes are defined. A new editor.js against an old stylesheet
shows controls with no styling at all where the old version painted them from
script.
If you do nothing, this cannot happen: the bundle links its own stylesheet from
beside itself. It can happen if your page links stylesheets/style.css
itself — the bundle then detects that link and leaves it alone, see
What to load. If you do, link it from the same place as
editor.js, or from a copy you update together with it. Do not carry a
customised copy of style.css forward from an older release; move your
changes into token overrides instead.
Every rule in style.css only matches inside the editor's own wrapper, which carries
the class typograph. A .spinner, .button or .menu of your own is not touched
by the editor's stylesheet. The only thing set on :root is the --typograph-*
tokens, and they style nothing on their own.
The other direction is covered too: every class, id and animation name the editor's
stylesheet styles starts with typograph-, so a .spinner { … } of yours does not reach the
editor's fields. Ids the editor and canvas only use to find their own elements — such as
#parmetersbarcontent or #zoomin — are not prefixed yet; avoid them on your page, since a
page element with the same id could be the one the editor finds. Rules on bare elements are the exception. A button { … } or
input { … } on your page still reaches the editor's buttons and fields.
Since TYP-805 every id and class name the stylesheet styles carries the typograph-
prefix: .menubar became .typograph-menubar, #menulayer became #typograph-menulayer,
and .typograph .toolbar is now .typograph .typograph-toolbar. CSS or scripts aimed at
the old names must take the new ones — Renamed ids and classes lists every
one. The exceptions:
#typograph, the canvas's container, keeps its name.menutitle-file, menutitle-file_Save, …) keep theirs: they are the
MenuAPI's public ids.#arrow_page_bar_resizer is now #arrow_typograph-page_bar_resizer, because the
arrow's id is built from its button's.A page that still authors the old chrome markup itself keeps working. The editor renames the ids it finds there and adds the new class names next to the old ones, so your own CSS on that markup keeps matching.
Rules that recoloured the old chrome may now fight the new one, which draws its
colours from classes rather than inline styles, so an old !important or a
more specific selector will still win — possibly against a background it was
never meant for. Check such rules after upgrading and replace them with token
overrides where you can.
The templates panel, the images panel, the manifest that backs it, and the clips panel each
resolve their content through canvas's host contract (TYP-697) rather than only the older
setters. The order tried, per panel, is:
| Panel | Resolution order |
|---|---|
| Templates | host.templates → set_templates_callback → (nothing — the panel simply stays empty, as it always did when no callback was set) |
| Images | host.images → set_images_callback → the manifest (push → pull → php/get_manifest.php) |
| Manifest | host.manifest_data (push) → host.manifest (pull) → php/get_manifest.php |
| Clips | host.clips → set_clips_callback → php/get_clips.php |
When the manifest is applied. The host's manifest (host.manifest_data, else host.manifest)
is applied every time a document has been opened, in the editor and in the viewer, on top of the
document's own manifest: fonts and swatches are merged by uuid, images by id. A pull provider is
therefore called once per document opened. This is what lets a theme manifest bind FONT_n to a
customer's font families, and recolour by uuid: runs that name FONT_1 are drawn in whatever family
the manifest gives FONT_1, and everything filled with COLOR_4 in whatever colour or gradient the
manifest defines as COLOR_4. The swatches used to be dropped on this path, so a document kept the
swatches it was saved with (TYP-962).
It used to happen only when the Images panel loaded a document without images of its own, so a
converted template, and anything in the viewer, never received it (TYP-909). The PHP fallback is
still only tried from the Images panel.
Two things worth being precise about:
host.templates nor set_templates_callback
answers, the templates tab stays empty — there never was a PHP endpoint backing it.host.manifest
(or host.manifest_data) and it throws or rejects, the editor logs the error and stops there;
it does not silently retry against php/get_manifest.php. Supplying manifest is a promise
that you are the source of truth for it. There is also no legacy setter for the manifest — it
is new with this contract.php/get_manifest.php, php/get_clips.php) still exist behind both the
provider and the legacy-callback paths, as relative requests your page has to serve. The editor
repository no longer ships a php/ directory; the reference implementation lives in
typograph-demo. Version 2 removes the fallbacks.A host-supplied list entry may be a plain url string or an object with a url field; an entry
with no usable url is dropped, logging one batched console.error per resolve rather than one per
bad entry. Clips also read an optional name. Templates read an optional name and id, which
the editor only hands back, in typograph_template_applied (see docs/API-EditorUI.md), together
with the entry's position in the list. id is not part of canvas's HostTemplateRef type
({name, url}): pass it as an extra field.
Pass a host block to the constructor. It is forwarded into the editor's own internal
typograph.setup() call, so the registry is populated before the canvas boots — no waiting for
typograph_editor_ready:
const editor = new Editor({
container: '#app',
host: {
templates: async () => {
const res = await fetch('/api/templates');
return res.json(); // HostTemplateRef[]
},
images: async (folder) => {
const res = await fetch('/api/images' + (folder ? `?folder=${folder}` : ''));
return res.json(); // HostImageRef[]
},
manifest: async () => {
const res = await fetch('/api/manifest');
return res.json(); // { images: [...], fonts: [...] }
},
// manifest_data is the push alternative to manifest, not an addition to it —
// supplying both throws.
clips: async () => {
const res = await fetch('/api/clips');
return res.json(); // HostClipRef[]
},
},
});
See canvas's docs/HostContract.md for the HostTemplateRef, HostImageRef, HostManifest,
and HostClipRef shapes and for the manifest/manifest_data either-or rule. new Viewer({ host })
takes the same block.
Never call
typograph.setup()yourself with the editor or the viewer on the page. Both call it themselves, and everysetup()builds a new document: a second call from your page replaces the editor's document with an empty one — the page "flips" — and throws away the providers and callbacks set on the first. Passhostto the constructor, or useset_providers()below.
An Editor constructed after the editor already booted (the legacy auto-boot on load got there
first) does not set up again: its host and legacy_php_fallback go onto the existing document.
If your host only learns its providers after the editor is already up — an async config fetch, a login step, whatever — configure them directly on the canvas document instead:
document.addEventListener('typograph_editor_ready', () => {
const doc = editor.get_document();
doc.host.set_providers({
templates: async () => {
const res = await fetch('/api/templates');
return res.json(); // HostTemplateRef[]
},
// ...same shape as above
});
});
doc.host is the same HostRegistry that host: {...} on the constructor (or a
typograph.setup({ host: {...} }) call) builds; calling set_providers() on it directly merges
into it rather than replacing it, so this works whether or not you also passed host up front.
This is the escape hatch for late-known providers, not the default path — prefer the constructor
option above when you have the providers at construction time.
Merging only ever adds: a later set_providers() call can add a provider that was missing before
or overwrite one that was already there, but nothing removes an entry that has already been
registered. In particular, once either manifest or manifest_data has been supplied — from the
constructor or from a later set_providers() call — that choice is effectively fixed for the
life of the page: a subsequent call supplying the other variant throws rather than switching over.
Pick one and stay on it, including across a re-mount while an async config step is still pending.
set_templates_callback, set_images_callback, and set_clips_callback — the older,
synchronous string-in/string-out setters on the document instance — keep working exactly as
before, and each is tried as the fallback ahead of the PHP endpoint (see the table above). Each
one now also logs exactly one [TYP-697] deprecation warning per document (not per call), naming
the host field that replaces it. That warning is only a warning: existing integrations do not need
to change anything today. New integrations pass host to the constructor.
Replace the setters with one host block on the constructor — not with a typograph.setup() call
(see the warning above). The providers do not take the callbacks' shapes, so a callback cannot
be handed over unchanged:
| Setter callback | Provider |
|---|---|
list: (message) => JSON string |
() => array (or a promise of one) |
save: ({ name, data }) => void |
(name, data) => void (or a promise) |
clip: ({ name, data, thumbnail }) => void |
(name, data, thumbnail) => void (or a promise) |
// before
const doc = editor.get_document();
doc.set_templates_callback(getTemplates); // returns a JSON string
doc.set_clips_callback(getClips);
doc.set_save_document_callback(saveTemplate); // takes { name, data }
doc.set_save_clip_callback(saveClip); // takes { name, data, thumbnail }
// after — the same functions, adapted to the provider shapes
const editor = new Typograph.Editor({
container: '#app',
host: {
templates: () => JSON.parse(getTemplates()),
clips: () => JSON.parse(getClips()),
save_document: (name, data) => saveTemplate({ name, data }),
save_template: (name, data) => saveTemplate({ name, data }),
save_clip: (name, data, thumbnail) => saveClip({ name, data, thumbnail }),
},
legacy_php_fallback: false, // once every capability you use has a provider
});
Leave legacy_php_fallback out until you have checked that nothing you use still reaches a
php/*.php path — with it false, UI without a provider is hidden.
FONT_n handlesThis is for a host that drives documents from its own theme manifests (print on demand). A host that keeps its fonts per project, as our asset manager does, needs none of it.
A font reference carries a uuid, and the uuid is a lookup: the manifest says which family it
is. A theme manifest binds a handle, such as FONT_1, to a family: FONT_1 can be Barlow for one
customer and Helvetica for another, and every piece of text whose font carries that handle is set in
the theme's family. A converted InDesign document already carries handles: the converter numbers
its font families FONT_1, FONT_2, … in alphabetical order — every family the text uses,
including one whose font file was missing from the package. The theme manifest is the one place
that says what each handle is; the editor does not re-key them.
What a handle means is the manifest's alone (TYP-938). The text names its font by handle and style,
never by family, so applying a theme rewrites nothing in the document: it binds the handles, and
the text follows. A family missing from the package is a manifest entry without a url, which
names the family the handle stands for; a theme manifest that binds the same handle supplies the
file. Until then it is reported missing (validate(), font_missing) and drawn in a substitute
face.
Fewer theme fonts than families? Use File ▸ POD settings ▸ Replace fonts:
host.manifest_data or host.manifest, applied
after every open) and let its fonts load.layout_fonts), and when the job's manifest binds a handle to another family,
the document is laid out again with the theme's fonts before it is printed. If those fonts
cannot be loaded, the job fails and names them, rather than printing without that text.Through the canvas API a host has the same: typograph.font_handles() lists the families the text
sets, and typograph.substitute_font(from_uuid, to_uuid) sets one in another font.
assign_font_handle() is retired (20261019), and so is validate()'s font_not_printed: the
state it reported, a stored layout naming a family the print could not find, is what the
re-layout at print time removes.
Alongside the read side above, the editor's own code now consults five more host providers —
save_document, save_clip, upload_image, remove_file and delete_files. See canvas's
docs/HostContract.md for their exact signatures and the HostImageRef shape.
| Call site | Resolution order |
|---|---|
SaveDocument (File → Save) |
host.save_document → save_document_callback (legacy) → php/save_page_json.php |
"save as clip" (store_clip) |
host.save_clip → save_clip_callback (legacy) → php/save_clip_json.php |
upload_image consumers (below) |
host.upload_image → the matching php/*.php endpoint. No legacy setter exists for this one — it is new with the host contract. |
remove_file consumer (below) |
host.remove_file → php/removefile.php. No legacy setter. |
delete_files consumer (below) |
host.delete_files → php/handle_delete_files.php. No legacy setter. |
SaveDocument and store_clip do not wait for the provider before returning — they trigger the
save and log a console.error if the returned promise rejects, rather than blocking the UI on it.
This is unchanged from the legacy-callback and PHP paths they replace.
upload_image, remove_file and delete_files have no caller in the editor UI. The asset and
clip manager dialogs that used to call them were never reachable from the bundle and have been
removed. Setting these providers does no harm, but nothing the user can click calls them. They
exist for your own UI through doc.host.get(...), and for canvas's RemoveDocument().
upload_image also has a second, unrelated call site: upload_page_preview() in
menucommands.ts, which would POST a rendered snapshot of the current page as a preview image
after a save. It is permanently disabled behind a hard-coded flag (k_upload_page_preview = false) — page thumbnails in this product come from a browser-side snapshot the host takes itself,
not from a publisher render, and that stays true regardless of what host.upload_image is set to.
Do not document or rely on this path as an available feature.
Pass collaboration: { url, document_id, token } and everyone with the same template open sees the
others' pointers and selections. Off without it. See Collaboration.md.
By default on latest, every row above ends in a relative php/*.php call when no provider or
callback answers. The first such call per endpoint logs a [TYP-697] warning. A host that doesn't
serve those paths turns them off:
const editor = new Editor({ container: '#app', host: { /* … */ }, legacy_php_fallback: false });
// or, once the document exists (e.g. on typograph_editor_ready):
editor.get_document().host.set_legacy_php_fallback(false);
With the fallbacks off, two things change:
php/*.php request is made, ever. A missing provider means that path does nothing.window.typograph.is_available(name);
see API-EditorUI.md → "Capability-driven UI" for the full table. Providers you add later
through host.set_providers() make their UI appear straight away.While the fallbacks are on (the default), the editor hides nothing, so an existing host sees exactly the editor it always saw. Version 2 removes the flag, the PHP branches and the legacy setters.
tests/backend-endpoints.test.ts counts every hard-coded php/*.php call in src/ and checks it
against a written baseline. Adding a new hard-coded backend call — instead of going through the
host provider → legacy-callback → PHP fallback chain described above — fails that test on
purpose. If you're adding a feature that talks to a backend, that's the test to make peace with
first.
Icons, cursors, the stylesheet and its images all resolve against editor.js's
own URL, not the page embedding it. Loading the bundle from the CDN pulls its
assets from the CDN too, with nothing to configure — you can embed the editor
from a page on any origin.
This is why editor.js is shipped as an ES module and must be loaded with
type="module": the resolution uses import.meta.url, which does not exist in a
classic script.
Language data defaults to https://cdn.typograph.nl/languages, which works from
any host. Override RichTextEditor.languageAssetBaseUrl only if you are
self-hosting it.