typograph-editor-documentation
    Preparing search index...

    Typograph — Embedding the editor and the viewer

    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>

    A 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.

    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();
    });

    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:

    • What the canvas draws. Selection handles, guides and the page bar's active badge are painted by the canvas library and stay #1071e5 for now.
    • The document itself. Page, swatch and text colours are content.
    • Icons, which are image files.
    • A few older corners of the chrome still carry fixed colours in 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.

    The 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.
    • The menu ids (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:

    • Templates has no PHP fallback. If neither host.templates nor set_templates_callback answers, the templates tab stays empty — there never was a PHP endpoint backing it.
    • The manifest does not fall back to PHP on a provider failure. If you supply 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.
    • The PHP fallbacks (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 every setup() 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. Pass host to the constructor, or use set_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.


    This 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:

    1. Open the document with the theme's manifest (host.manifest_data or host.manifest, applied after every open) and let its fonts load.
    2. In Replace fonts there is one row per font family the text uses. A family whose font file is not loaded is marked not loaded and drawn in a fallback face. Pick the font to set it in ("Helvetica Neue Std · FONT_4"). Several families can go to one font. ⚠️ Only loaded fonts are offered: a theme font that is missing from the list is not loaded. A family cannot be replaced into one that is itself being replaced in the same OK.
    3. OK sets their text in that font, and their references carry its handle. Save.
    4. To print with a theme, send the theme's manifest with the print job. The document does not have to be opened and saved with it first: the saved layout records which family each handle was laid out in (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:

    • No php/*.php request is made, ever. A missing provider means that path does nothing.
    • UI without a provider is hidden. This is based on 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.