Files
pagerite/docs/editing.md
T
2026-08-19 16:11:42 +00:00

5.3 KiB

Editing interface

The Vue editor is a single tabbed EditorShell.vue mounted in a host div created inside the static document.

Tabs

The shell hosts four kept-alive tabs (ordered site-wide first — site, structure — then, after a visual break, the per-page tabs — article, banner):

  • PageEditor.vue — CodeMirror + server-rendered preview over WebSocket /_api/ws/editor, previewing into the visible article; editor scroll drives the article scroll — while any editor is open the window scroll is locked (body.editing), the panel exactly fills the available window height, and only #main scrolls; a format bar offers Markdown helpers — bold/italic/code/link/table/image upload, with Ctrl/Cmd-B/I/S bindings — for the hard-to-remember syntax. Edits content and title only, never the path.
  • BannerEditor.vue — per-page banner HTML + banner design selector, previewed into #page-banner.
  • SiteEditor.vue — site brand + optional custom brand HTML with image/video upload + theme selector + font picker + favicon upload — clicking the preview tile picks a new one — + site-wide custom CSS, CSS injected into <head id="pagerite-user">.
  • StructureEditor.vue — the vue-draggable structure tree with always-editable title/slug inputs per row.

Media uploads everywhere use the image icon buttons (pasting into the editor works too). The article, banner and site-settings pens are shorthands that open the shell on the matching tab; once open, clicking a pen switches tabs (and retargets the editors to the current page) instead of closing/remounting. The close button in the tab bar closes the shell (Escape too); tabs have no close buttons of their own. Closing only HIDES the shell — the Vue app stays mounted, so page-editor state (unsaved text included) survives until a real page reload; saving there is explicit (Ctrl+S) and refreshes the page regions in place. Admin panels never reload the page.

In-place page re-rendering shared by the banner/site/structure tabs lives in swapdoc.js (runScripts/loadPlain: fetch a page, swap the dynamic regions, replaceState). Placeholder texts are reserved for showing the actual default in effect when a field is left empty (e.g. the pending row's slug derived from its title); labels and help are real elements or tooltips, never placeholders.

Saving behavior

Everything saves immediately as you edit (brand/title/CSS debounced, slug on commit since it renames the path), theme change swaps the stylesheet in place, tree rows navigate in place without transitions when focused, and the front page is a root-only row whose empty slug is editable like any other.

Every non-empty list (and the root) ends with a non-draggable plus footer row (vuedraggable #footer slot): clicking it starts a new pending page at that level (its slug placeholder shows the slug derived live from the title being typed), and while dragging it is the list's "end of list" drop target. Committing a pending page PUTs it with empty markdown (creates an empty page that renders with its title — saving never deletes; deletion is the page editor's explicit choice: saving trimmed-empty text issues a REST DELETE), then switches to the page editor tab for the actual writing.

Dropping ON the lower part of a row moves the page under that row (the child list's container invisibly overlaps its own row's bottom via negative margin — Sortable inserts it as the first child natively), while a row's exposed top edge inserts a sibling before it. Row indentation is structural (each nested list margin-indents itself), so a dragged row previews its whole subtree at the target list's depth.

The shell is dynamic-imported onto the content page by pagerite.js when an edit pen is clicked (the pens are injected by pagerite.js after the session validates; they carry data-editor-src/data-editor-css/data-editor-mode). In dev, modules load from the Vite dev server (PAGERITE_VITE_URL), in prod from the hashed build assets resolved via frontend-build/.vite/manifest.json.

vite.config.js sets appType: 'mpa' (no SPA fallback) and builds with manifest: true, assetsDir: '_/assets' (so the build mirrors the URL space; frontend/public/favicon.ico lands at the build root and is served at /favicon.ico). JS inputs are src/main.js and src/pagerite.js, plus src/assets/pagerite.css as a separate stylesheet entry; theme and banner-design CSS are NOT built — they live in pagerite/themes/{name}/ and are served by the backend. There is no index.html source (it would shadow / and turn missing dev paths into an empty Vue shell). All outputs are ES modules. The build sets preserveEntrySignatures: 'exports-only' because main.js is consumed via dynamic import() for its openEditor/closeEditor exports — Vite app builds otherwise strip unused entry exports, leaving dead edit pens. In dev the backend links theme/banner-design stylesheets like in prod (/_themes/...); only the base CSS is Vite-injected from JS, and pagerite.js then re-appends the #pagerite-theme/#pagerite-banner/#pagerite-user elements to restore the canonical order (base < theme < design < custom CSS). Theme switches in the site editor simply swap the #pagerite-theme link href, identically in dev and prod.

vite-plugin-fastapi.js has an auto-upgrade marker — edit vite.config.js, not the plugin.