@pramen/cms-editor 0.0.52 → 0.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @pramen/cms-editor
2
2
 
3
- A **visual block/page editor** for [`@pramen/cms`](../cms) — a standalone React SPA that
4
- talks to the CMS handlers over HTTP. It mutates through the *semantic* handlers
3
+ A **visual block/page editor** for [`@pramen/cms`](../cms) — a React SPA that talks to the
4
+ CMS handlers over HTTP, served by your own site (see [Deploy it](#deploy-it)). It mutates through the *semantic* handlers
5
5
  (`addBlock`/`updateBlock`/`reorderRegion`/`publishPage`/…), so field validation, region
6
6
  allow-lists, and the review/publish gates are all enforced server-side.
7
7
 
@@ -26,43 +26,89 @@ allow-lists, and the review/publish gates are all enforced server-side.
26
26
  ## Run it
27
27
 
28
28
  ```bash
29
- bun run --cwd packages/cms-editor build # dist/ (index.html + hashed JS)
29
+ bun run --cwd packages/cms-editor build # -> dist/editor.js + dist/editor.css
30
30
  bun run --cwd packages/cms-editor dev # watch + preview on http://localhost:5175
31
31
  ```
32
32
 
33
- It's a **standalone static SPA** (no server-package dependency local types + `fetch`).
34
- Deploy `dist/` anywhere; it talks cross-origin to your Worker, so set `CORS_ORIGINS` to allow
35
- the editor's origin. On first load, paste your Worker base URL, tenant, and an
36
- **editor/reviewer JWT**. (Co-hosting on the Worker via an `assets` binding is a possible
37
- follow-up.)
33
+ The build produces exactly **two files** and no `index.html`. That is deliberate: a baked
34
+ shell can only hard-code root-absolute asset paths, which works at the origin root and
35
+ nowhere else, and its companion `config.js` was a hand-edited untyped global that failed
36
+ silently the moment it 404'd.
38
37
 
39
- **SPA fallback (required).** Because views are real URLs, the host must serve `index.html`
40
- for any unmatched, extensionless path (deep link or refresh) and serve hashed assets from
41
- the root. The dev preview server already does this; on a static host configure a catch-all
42
- rewrite to `/index.html` (Cloudflare Pages/`assets` handle this by default). The bundle is
43
- referenced by an **absolute** path (`/main.<hash>.js`), so it loads correctly from any route
44
- depth — don't rewrite it to a relative path.
38
+ ## Deploy it
45
39
 
46
- ## Configure it (`/config.js`)
40
+ A **host serves it**, from a shell it renders. For an Astro site that is one line — see
41
+ [`@pramen/cms-astro`](../cms-astro):
47
42
 
48
- `dist/config.js` is loaded before the app boots and sets `window.PRAMEN_CMS_EDITOR`. Override
49
- that one file on your host — no rebuild, no fork. Every field is optional, and the shipped
50
- file has them all commented out; **add them one at a time**:
43
+ ```js
44
+ // astro.config.mjs
45
+ pramenCms({ backend: { url: "https://cms.example.workers.dev" }, admin: true })
46
+ ```
47
+
48
+ That injects a catch-all route at `/_pramen/admin`, so every view is a real server route on
49
+ the site's own origin: no `dist/` to copy, no SPA-fallback rewrite, and no second hostname
50
+ for the editor. The site's bundler emits and fingerprints `editor.js` / `editor.css` like
51
+ any other asset, which is what makes serving it under a prefix work.
52
+
53
+ It does not move the API, though. The editor still calls the CMS at the `backend.url` the
54
+ shell declares, so a CMS on its own Worker is still cross-origin and still needs
55
+ `CORS_ORIGINS` to allow the site. CORS goes away only when the CMS shares the site's
56
+ origin.
57
+
58
+ Everything the bundle needs at boot comes from that shell, and nothing else:
59
+
60
+ | What | How the shell provides it | Read by |
61
+ | --- | --- | --- |
62
+ | Where it is mounted | `data-base-path` on the mount node | `src/mount.ts` |
63
+ | Which Worker + tenant to call | `window.PRAMEN_CMS_EDITOR.backend` | `src/mount.ts` |
64
+ | Wordmark, sign-in URL, nav | the rest of `window.PRAMEN_CMS_EDITOR` | `src/brand.ts`, `src/app-context.tsx` |
65
+
66
+ The mount prefix is the constant the route was injected at, stamped onto the node by the
67
+ same code that injected it — so the router cannot be mounted somewhere the server does not
68
+ serve. Navigation is scoped to it, so a co-hosted editor intercepts only its own URLs
69
+ (`_404.tsx`'s catch-all matches every same-origin path, which un-scoped would mean a click
70
+ on the host's own `/blog` rendering the editor's "Nothing lives here").
71
+
72
+ **To write your own shell** (a Worker route, another framework), render: the stylesheet, a
73
+ `<div id="app" data-base-path="…">`, an inline script setting `window.PRAMEN_CMS_EDITOR`,
74
+ and `<script type="module" src="…editor.js">` — in that order. The dev preview in
75
+ `scripts/build.ts` is the smallest complete example.
76
+
77
+ ## Configure it
78
+
79
+ The editor's own options travel in `window.PRAMEN_CMS_EDITOR`, which the integration writes
80
+ from typed options (`admin: { … }`) — there is no file to edit:
51
81
 
52
82
  ```js
53
- window.PRAMEN_CMS_EDITOR = {
83
+ admin: {
54
84
  brand: { name: "Acme", suffix: "cms" }, // the wordmark — see below
55
85
  // signInUrl: "/signin/", // ONLY once that page exists — see the warning
56
86
  // hidePages: true, // collections-only deployments
57
- // extraNav: [{ label: "Curation", href: "/curate" }],
58
- };
87
+ // extraNav: [{ label: "Curation", href: "/curate", target: "_self" }],
88
+ }
59
89
  ```
60
90
 
61
- > **`signInUrl` must be a page that exists, and same-origin needs care.** An unauthenticated
62
- > load calls it after clearing the stored session, so if the path 404s into this SPA's own
63
- > catch-all (which a same-origin extensionless path does see the SPA-fallback note above),
64
- > the editor bounces between the redirect and itself with no session to recover from. Point
65
- > it at a page you have already deployed, and prefer a separate origin.
91
+ `extraNav` links open in a **new tab** by default, because the editor's catch-all route
92
+ matches every same-origin path a same-tab click would land on the editor's own 404 instead
93
+ of your tool. Add `target: "_self"` to ask for a same-tab navigation; it is honoured only
94
+ where the router provably will not claim the url:
95
+
96
+ | Link | Editor mounted under a prefix | Editor at the origin root |
97
+ | --- | --- | --- |
98
+ | Another origin (`https://tools.acme.com/x`) | same tab | same tab |
99
+ | Same origin, outside the mount (`/curate`) | same tab | new tab |
100
+ | Same origin, inside the mount | new tab | new tab |
101
+
102
+ Anything else — a relative href that resolves back inside the mount, a `javascript:` url, an
103
+ unparseable one — degrades to a new tab rather than stranding the editor on its 404. A
104
+ same-tab link runs the unsaved-changes guard first, so it cannot silently discard an edit in
105
+ progress.
106
+
107
+ > **`signInUrl` must be a page that exists.** An unauthenticated load calls it after
108
+ > clearing the stored session, so a path that 404s into this SPA's own catch-all leaves the
109
+ > editor bouncing between the redirect and itself with no session to recover from. Point it
110
+ > at a page you have already deployed. `?setup=1` always forces the built-in screen, for
111
+ > pasting a first-admin JWT.
66
112
 
67
113
  **Set `brand` when you deploy this for a client.** The editor ships as a package an agency
68
114
  installs on someone else's behalf, so the default wordmark — `pramen · cms editor`, in the
@@ -76,12 +122,8 @@ A malformed `brand` can never take the editor down: a non-string value is ignore
76
122
  than thrown on, and a `brand` that yields no usable name logs a console warning instead of
77
123
  silently shipping "pramen" to your client.
78
124
 
79
- The `<title>` in `index.html` is baked at build time, before any config exists, so the app
80
- re-applies the configured brand on boot; the static tag is the pre-hydration fallback, which
81
- means the default shows for the moment before the bundle runs.
82
-
83
- A clean build regenerates `config.js` when it is missing, but never overwrites one that is
84
- already there — including on every `dev` rebuild — so an edit you are previewing survives.
125
+ The shell's `<title>` is written before the bundle runs, so the app re-applies the
126
+ configured brand on boot; the server-rendered tag is the pre-hydration fallback.
85
127
 
86
128
  Settings → About still reports `pramen · cms-editor`. That row names the *software* you are
87
129
  running, not the deployment, which is what an About panel is for.