Dollie Editor SDK guide

Authoring Library

Private file-backed drafts in page folders, a Final canvas per page, and publication of a saved Final to a live website.

The authoring library is an optional workflow for hosts that draft pages privately before anything goes live: documents are JSON files in collections, drafts are grouped in page folders with one Final canvas and any number of variations, and a saved Final publishes to a live website through the host's own SSH connection.

The Laravel adapter owns the machinery (Dollie\Editor\Authoring); the React package ships the library UI (@dollie_ai/editor/authoring). Hosts keep their routes, controllers, Inertia pages, authorization, and the publication receiver.

Files and collections

Every document is a BuilderDocument JSON file with a sibling .revisions.json history (50 entries). A collection is one directory bound to one editor surface:

'authoring' => [
    'enabled' => true,
    'directory' => resource_path('editor/authoring'),
    'collections' => [
        'drafts' => ['label' => 'Drafts', 'surface' => 'landing', 'folders' => true],
        'templates' => ['label' => 'Templates', 'surface' => 'landing', 'template' => true],
    ],
],

A folders collection keeps each page in its own directory: final.json is the page's Final and every other file a variation. The physical directory decides membership; metadata.authoring keeps the display title. A template collection exposes each document's definition for insertion.

Page folders can be organised one level deep into groups: a directory named _<group> (for example _launch/home/final.json). Page folder names never start with an underscore, so the two cannot be confused. An optional _<group>/.group.json sets title (default: the headline of the name) and collapsed (default: only _archive starts collapsed; the Archive is also listed last). Document IDs stay unique across the collection and a page folder name may exist in only one place, so page IDs in links and routes do not change when a page is grouped. A document saved into an existing page always joins that page's folder, and its membership carries group; asking for a different group is refused. FilePageStore::movePage($page, $group) moves a page folder as a single directory rename, without rewriting any file.

FilePageStore implements PageStore for one collection directory. AuthoringLibrary adds the trusted operations: listing, page grouping, groups, creating documents and pages, opening a Final, moving variations, and moving page folders between groups (movePage, which also creates a group from a title). AuthoringPresenter turns them into the authoring Inertia prop (including groups in display order and each page's group), with every link generated from the named routes in authoring.routes.

Host wiring

  1. Register routes with parameters named collection, document, page, and revision, and map them under authoring.routes (index, create, edit, update, restore, preview, move, move_to_group, create_page, open_final, present, publish_check, publish, and for global sections create_global, republish_check, republish). Keys you leave out produce no links. move_to_group receives a POST of group (an existing group id, or empty for none) or groupTitle (a new group) for one page.
  2. Put Dollie\Editor\Authoring\Http\Middleware\ActivateCollectionSurface on collection routes so validation and fills use the collection's surface.
  3. Write thin controllers over AuthoringLibrary and AuthoringPresenter; map AuthoringException::$status, PageNotFoundException, and ValidationException to responses.
  4. Optionally bind Authoring\Contracts\EditorUrlResolver to add an expiring editor link to documents.

Console commands authoring:organize (move flat files into page folders; folders already in place, grouped or not, never move) and authoring:sync (copy a library between environments without overwriting server edits) register when the module is enabled. Sync fingerprints include page and group membership, so regrouping on the server is an edit that stops the sync; a page folder regrouped at the source moves whole on the destination without new revisions, and group titles sync under the same conflict rule.

React

import { AuthoringProvider, AuthoringSidebar, PublishFinal } from '@dollie_ai/editor/authoring';
import { inertiaAuthoringAdapter } from '@dollie_ai/editor/authoring-inertia';

<AuthoringProvider adapter={inertiaAuthoringAdapter}>
    <AuthoringSidebar context={authoring} currentId={document.id} hasChanges={hasChanges} onInsert={insert} />
</AuthoringProvider>

The adapter supplies the link component, forms, and JSON calls. Without a provider, anchors and fetch are used. AuthoringSidebar renders the folder view for collections with page folders and a flat list otherwise. PageFolderList takes the context's groups and shows ungrouped pages first, then each group as a collapsible section (a search opens every group with a match); MovePageToGroup is the page-level "Move to group" dialog and groupPages the shared split. The Templates picker is unaffected by groups. CopyableSectionBoundary is a SectionRenderer boundary with a copy action in the section clipboard format; useVariationPreviewEscape lets an embedded preview close its parent dialog.

Global sections

A global collection holds Global Sections: one section per document, flat like templates, with the same revisions, locking, conflict tokens and sync.

'globals' => ['label' => 'Global sections', 'surface' => 'landing', 'global' => true],

AuthoringCollections::isGlobal(), globalCollectionFor($surface) and linkingCollections($surface) describe it; describe() carries a global flag, and on the React side globalCollectionFor() finds it while documentCollectionFor(), templateCollectionFor() and folderCollectionFor() never return it. Library items of a global collection carry their definition and, through the presenter, their usage (count, stale, documents). globalSectionsFromContext(context) turns them into the editor runtime's globalSections.items.

GlobalSectionLibrary holds the trusted operations:

  • create($surface, $name, $section) stores a placed section under a readable, permanent id taken from the name (Closing CTA → closing-cta) and refuses a taken id. AuthoringLibrary::createDocument() refuses global collections, so a global always starts from a section.
  • documentProblem($definition) enforces the document rule: exactly one section, without a global key of its own. Hosts check it on save; authoring:sync refuses documents that break it.
  • resolve($surface, $definition) refreshes a page's linked sections. Run it before saving a page (agents saving files too), so snapshots are never behind.
  • usage($surface) scans every document of the surface's other collections (drafts, Finals, variations, templates) and reports, per global, which documents link it and which copies are behind.
  • propagate($surface, $id) rewrites every document whose linked copy of $id is behind, each under its own lock with a revision labelled Updated global section "Name". Local fields, framing and links to other globals are kept; unlinked copies are untouched; a document changed meanwhile is retried once, then reported as skipped. Call it after saving (or restoring) a global.
  • outOfDate($surface, $definition) lists stale or missing links. PublicationReview::check() uses it to refuse a Final that is behind: "2 linked sections are out of date. Open Final and save."
  • livePagesBehind($surface, $id, $snapshot, $assetOrigins) reads a published-pages snapshot and returns the live pages whose copy of the global is older, matched to their Final when that Final differs from the live page only by the refreshed links. The rest are reported for a manual Publish to live.

Live pages keep the version they were published with; the published definition keeps global: { id } as inert provenance and the receiver needs no change. Add the route keys create_global (POST JSON name, section), republish_check and republish (per global document) to surface the batch Republish affected pages flow: check reviews each matched Final through PublicationReview::check() and returns tokens; republish confirms them one publication at a time. GlobalSaveConfirmDialog ("Save and update N pages") and GlobalUsagePanel ("Used on N pages", the save report and the republish flow) are the React pieces.

Edit globals on one library (the local one): authoring:sync carries the globals collection and the propagated drafts as ordinary updates and never re-resolves links on the receiving side. An older destination without the collection ignores the folder; its pages still render their snapshots.

Publish a Final

PublicationReview::check() inspects the destination and returns an encrypted review token bound to the author, the saved revision, and the exact destination; publish() confirms it within fifteen minutes. RemotePublisher runs the host's receiver script inside the live container over ssh and docker exec, without a live endpoint or browser-held credentials.

The receiver is a host PHP file returning static function (array $input): array that handles inspect and publish, validates against the destination catalog, checks route ownership and assets, and saves through the destination's PageStore. Configure authoring.remote_publish (enabled, url, host, container or container_pattern, receiver).

Both operations include authoringOrigin (the local application's URL, captured in the review token). A host with the same public image library in both environments can rebase its asset paths from that exact origin to site-relative URLs before validation and saving. Rebase only host-owned asset namespaces, preserve external CDNs, and verify the destination files exist. The original Final remains unchanged.

Read published benchmarks

php artisan authoring:published-pages > /tmp/live-pages.json

This read-only command uses the same RemotePublisher configuration and local SSH connection. It sends { "operation": "published-pages" } to the configured host receiver, which must implement that operation. The receiver executes in the live application without a deployment or public endpoint. Hosts own the published-page query and public route policy; the SDK does not assume a model or table.

The receiver returns { source, fetchedAt, pages: [...] }. source is the live origin, fetchedAt an ISO timestamp, and each page includes id, title, slug, url, revision, publishedAt, updatedAt, definition and pageData. Definitions retain stored props, including illustration configuration; component defaults are not expanded. Include only publicly published pages, never private drafts. Empty results return pages: [].

The command prints JSON only on success and exits nonzero on failure. Refresh this snapshot before using published work as a copy/design benchmark; a local Final is an assembly canvas and is not evidence of what is currently live.

Content view

php artisan authoring:published-pages --view=content > /tmp/live-content.json

The SDK projects ordered sections as { component, content, illustrationContent? }. Schema field kinds preserve nested copy, numbers, actions, conversations and records; presentation fields, internal IDs and image URLs are omitted. Illustration entries include a slot path, their component name and copy. Declared defaultProps and selected presets are merged shallowly with saved props, preserving explicit empty strings and arrays. Catalogue insert example values are never used as defaults.

For this view, the host receiver additionally returns catalog.sections and catalog.illustrations from the live manifest, with id, fields, optional defaultProps, presets and propsDriven. These schemas are consumed locally and omitted from the output. An optional contentPolicy supplies excludedComponents (shared chrome) and bodyXPath (one public HTML content root; default //main). The full view remains unchanged and does not fetch public HTML.

Missing or partial schemas/props trigger a public HTML fallback for that page. The SDK fetches only same-origin HTTPS URLs, does not follow redirects, and strips scripts, styles, navigation and hidden markup from the selected body. It retains ordered text blocks and links as renderedContent, labelled source: live-html and scope: page-body. For a single-component page it is attached to that section; otherwise it remains at page level because section ownership cannot be inferred. Rendered blocks already present in structured copy are omitted, while new link destinations are retained. The fallback reflects server-rendered content, not every interactive state. PHP DOM is needed for this fallback.

Fetch/extraction failures produce a warning beside the available structured copy; they must not be treated as complete benchmarks. A missing live catalogue fails the command rather than substituting potentially different local schemas.