Browser editor internals
The editor is a static single-page application (SPA): Node/npm build it, and the Go CMS serves the resulting files and API on the same authenticated origin. There is no browser-side Kubernetes client and no Node process in production. For commands use build/test; for local sign-in use local development.
Editing engines and source of truth
Tiptap supplies rich Markdown editing; Source and Visual share the same Markdown source. A mode switch alone preserves exact source, while visual edits can normalize syntax. Unsupported structures stay in Source. Markdown preview uses the server's Goldmark renderer rather than trusting a second browser renderer. GrapesJS supplies HTML page and native theme component editing. It does not convert arbitrary visual pages to Markdown.
editor/style-controls.js registers the additional Extra properties for page
and theme editors. It also works around GrapesJS 0.22 view initialization:
composite controls must not write their shorthand into the first child input,
and numeric controls must populate their value/unit when rendered after model
selection. These view changes must not alter CSS models or undo history. Keep
the saved-style browser regression when upgrading GrapesJS.
editor/shadows.js maps shadow colours, offsets and inset flags independently
of their order in CSS. Browsers serialize colour before offsets; positional
parsing shifts every field. Keep function arguments together when splitting
tokens/layers, and apply the same mapping to box and text shadows on reload
and direct property updates.
Page documents and custom themes are source. The theme canvas shows labelled bindings while rendered decoration sits outside editable content and undo history. The server resolves bindings, menus, indexes, repeaters and renderer formats on preview and publication. Public generated HTML never becomes the editable input.
Authoring reads and theme refreshes
POST /api/content/render uses Store.ReadPreview, a consistent read of saved
settings, theme, pages and renderer packages. It does not create a publication
snapshot, calculate a whole-project revision or hardlink uploaded media. Media
is fetched through its normal API URL; a concurrent deletion can therefore make
an authoring preview's image unavailable. Publication still pins media in its
durable snapshot before rendering.
The editor requests themeShell: true for decoration around editable content.
This renders a fixed content marker and current draft metadata without running
article renderers. The selected layout and its shared parts determine whether
navigation needs the saved pages; a navigation-free layout reads no other pages
or installed renderer packages. Full content previews still read the pages and
packages required by indexes, repeaters and renderer-backed articles.
editor/theme-refresh.js coalesces load-time changes and aborts superseded
requests. Page-property loading updates controls without requesting another
refresh; page switches cancel the previous refresh before fetching the new
page. A generation check also prevents a late response from replacing the
current theme title. Refreshing Markdown decoration avoids refreshing the
hidden GrapesJS canvas.
Content/title saves with unchanged URL and menu skip the whole-site conflict and menu-graph scans under the store's writer lock. URL changes, menu changes, creation and overwrites retain their conflict/graph validation. Saves retain their atomic writes and filesystem synchronization.
Source directories
All following paths are under cms/editor/src:
| Path | Responsibility |
|---|---|
main.js, auth.js, util.js |
Boot orchestration, identity and shared helpers |
api/client.js |
Same-origin HTTP client, CSRF bootstrap and blocked-mutation state |
components/ |
UI dialogs/toasts and busy-state controls |
editor/ |
Page drafts, Markdown, GrapesJS blocks/controls, menus, media, preview, repeats and site bindings |
themes/ |
Theme workspace and native component conversion/editing |
plugins/ |
Local renderer/block/theme ZIP installation UI |
import/ |
WordPress inspection/review/apply UI |
publish/ |
Publish status, save-before-publish and rollback confirmation |
settings/ |
Site-title/description modal and save lifecycle |
session-recovery.js, login-draft.js |
Expiry handling, principal checks and per-tab checkpoints |
styles/ |
App and Markdown styles |
cms/editor/test/ has Node/jsdom tests; cms/editor/e2e/ has browser drivers
launched by Go API tests. index.html is the static app shell. dist/ and the
compiled browser-api.test are build outputs, not checked-in dependencies.
Authentication and interrupted work
Boot first calls /api/me; the server authenticates and authorizes every API
request. The UI displays email/name/roles, but hiding a control is not an access
boundary. /api/publish/status then bootstraps the separate CSRF cookie/token.
All API calls use relative same-origin URLs and browser credentials.
A mid-session 401 blocks writes while keeping authoring state. The expiry dialog has Carry on working for immediate local editing and Log in again for same-window login. Before navigation the tab checkpoints source, metadata, unsaved theme work and open forms in sessionStorage. No tokens, login secrets or WordPress passwords enter that checkpoint. Failed storage prevents navigation from silently discarding work. Checkpoints expire after 24 hours and are removed after recovery.
Recovery requires the same issuer and subject, not the same display name or
email. It refreshes CSRF and verifies the principal again before unblocking writes.
It restores drafts without automatically saving or publishing. Another-tab login
can also repair the session; the continue path verifies it in the background.
This is recovery assistance, not durable source backup or multi-user collaboration.
Logout POSTs to /oauth2/logout with CSRF then navigates to the returned
logoutURL. GET logout is refused. CMS auth/session details are in
authentication.
Security and previews
GrapesJS script/unsafe-attribute support, telemetry and outbound icon CDN loading are disabled. Font Awesome is bundled locally. DOMPurify sanitizes HTML entering the canvas. Server publication/preview sanitization remains necessary even when the browser sanitized input, because API clients can submit content directly.
The CMS SPA CSP restricts resources to its origin plus permitted data images/fonts
and inline styles; arbitrary inline/eval scripts are forbidden. A SHA-256 source
hash permits the fixed CMS file-gallery runtime in the preview. Preview uses a sandboxed iframe with
allow-scripts and without allow-same-origin. Its opaque origin cannot access
CMS cookies or parent/API state. The parent fetches authenticated media and
inlines bytes for the preview. Theme previews inline their own bundle assets.
External media can be absent in previews under these restrictions.
Use real-browser checks when modifying CSP, rendering, asset inlining, templates or session navigation. A srcdoc-string assertion alone does not prove visible content. The harness uses a signed-in identity fixture with real API/CSRF/render paths; separate auth tests exercise login/session protocols.
Bundle and development server
Vite builds dist/index.html and hashed assets, including a GrapesJS vendor
chunk and local fonts. base: './' makes asset references relative; the CMS/API
still expects its origin root and does not claim deployment under arbitrary
path prefixes. window.__vcEditor is development-only. See
package.json for current commands/dependencies.
Vite serves loopback 127.0.0.1:5173 with strictPort and no API/OAuth proxy.
Serving dist with an unrelated file server only inspects static assets. Use
built files served by the Go CMS for a complete authenticated local application.
