Content Management that can withstand the plundering hoards

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.