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
- Register routes with parameters named
collection,document,page, andrevision, and map them underauthoring.routes(index,create,edit,update,restore,preview,move,move_to_group,create_page,open_final,present,publish_check,publish, and for global sectionscreate_global,republish_check,republish). Keys you leave out produce no links.move_to_groupreceives a POST ofgroup(an existing group id, or empty for none) orgroupTitle(a new group) for onepage. - Put
Dollie\Editor\Authoring\Http\Middleware\ActivateCollectionSurfaceon collection routes so validation and fills use the collection's surface. - Write thin controllers over
AuthoringLibraryandAuthoringPresenter; mapAuthoringException::$status,PageNotFoundException, andValidationExceptionto responses. - Optionally bind
Authoring\Contracts\EditorUrlResolverto 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 aglobalkey of its own. Hosts check it on save;authoring:syncrefuses 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$idis behind, each under its own lock with a revision labelledUpdated 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 apublished-pagessnapshot 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.