@nuasite/cms 0.42.1 → 0.43.0-beta.2

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/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "directory": "packages/astro-cms"
15
15
  },
16
16
  "license": "Apache-2.0",
17
- "version": "0.42.1",
17
+ "version": "0.43.0-beta.2",
18
18
  "module": "src/index.ts",
19
19
  "types": "src/index.ts",
20
20
  "type": "module",
@@ -26,6 +26,10 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
+ "@nuasite/cms-core": "0.43.0-beta.2",
30
+ "@nuasite/cms-types": "0.43.0-beta.2",
31
+ "@nuasite/cms-sidecar": "0.43.0-beta.2",
32
+ "@nuasite/collections-admin": "0.43.0-beta.2",
29
33
  "@astrojs/compiler": "^3.0.1",
30
34
  "@babel/parser": "^7.29.2",
31
35
  "node-html-parser": "^7.1.0",
@@ -33,6 +37,10 @@
33
37
  },
34
38
  "devDependencies": {
35
39
  "@babel/types": "^7.29.0",
40
+ "@types/react": "^19.2.7",
41
+ "@types/react-dom": "^19.2.3",
42
+ "react": "^19.2.1",
43
+ "react-dom": "^19.2.1",
36
44
  "@milkdown/core": "^7.20.0",
37
45
  "@milkdown/ctx": "^7.20.0",
38
46
  "@milkdown/plugin-listener": "^7.20.0",
@@ -63,11 +71,19 @@
63
71
  "astro": "6.1.4",
64
72
  "typescript": "^6.0.2",
65
73
  "vite": "^7.0.0",
66
- "@aws-sdk/client-s3": "^3.0.0"
74
+ "@aws-sdk/client-s3": "^3.0.0",
75
+ "react": "^19.0.0",
76
+ "react-dom": "^19.0.0"
67
77
  },
68
78
  "peerDependenciesMeta": {
69
79
  "@aws-sdk/client-s3": {
70
80
  "optional": true
81
+ },
82
+ "react": {
83
+ "optional": true
84
+ },
85
+ "react-dom": {
86
+ "optional": true
71
87
  }
72
88
  },
73
89
  "scripts": {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Local `/_nua/admin` SPA entry (cms-headless F7).
3
+ *
4
+ * Mounts the host-agnostic `@nuasite/collections-admin` SPA into the page served
5
+ * by the local-admin dev middleware. The only host-specific input is `apiBase`,
6
+ * which the HTML shell injects as `window.__NUA_ADMIN_API_BASE__` — it points at
7
+ * the in-process cms-sidecar mounted by the dev middleware (`…/cms/v1`). The same
8
+ * lib backs the webmaster Collections tab; only `apiBase` differs.
9
+ *
10
+ * This file is served as a virtual module through Astro's Vite dev server, so it
11
+ * is transformed (TSX → JS, React resolved) and HMR-capable like any app module.
12
+ */
13
+
14
+ import { CollectionsAdminApp } from '@nuasite/collections-admin'
15
+ import { StrictMode } from 'react'
16
+ import { createRoot } from 'react-dom/client'
17
+
18
+ function readApiBase(): string {
19
+ const value = window.__NUA_ADMIN_API_BASE__
20
+ if (typeof value !== 'string' || value === '') {
21
+ throw new Error('[nua-cms] /_nua/admin: window.__NUA_ADMIN_API_BASE__ is not set')
22
+ }
23
+ return value
24
+ }
25
+
26
+ const container = document.getElementById('nua-admin-root')
27
+ if (!container) {
28
+ throw new Error('[nua-cms] /_nua/admin: #nua-admin-root container is missing')
29
+ }
30
+
31
+ createRoot(container).render(
32
+ <StrictMode>
33
+ <CollectionsAdminApp apiBase={readApiBase()} />
34
+ </StrictMode>,
35
+ )
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Ambient globals for the local `/_nua/admin` SPA entry (cms-headless F7).
3
+ *
4
+ * The dev middleware injects the cms-sidecar `apiBase` into the HTML shell as a
5
+ * window global; the entry reads it to drive `<CollectionsAdminApp apiBase={…} />`.
6
+ */
7
+
8
+ declare global {
9
+ interface Window {
10
+ /** Base URL the in-process cms-sidecar is mounted at (the SPA adds nothing else; it is already the `/cms/v1` base). */
11
+ __NUA_ADMIN_API_BASE__?: string
12
+ }
13
+ }
14
+
15
+ export {}
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.settings.json",
3
+ "compilerOptions": {
4
+ "composite": false,
5
+ "noEmit": true,
6
+ "emitDeclarationOnly": false,
7
+ "jsx": "react-jsx",
8
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
9
+ "types": ["react", "react-dom"]
10
+ },
11
+ "references": [
12
+ { "path": "../../../collections-admin/src" },
13
+ { "path": "../../../cms-types/src" }
14
+ ]
15
+ }
@@ -0,0 +1 @@
1
+ {"fileNames":["../../../../node_modules/typescript/lib/lib.es5.d.ts","../../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../../node_modules/typescript/lib/lib.es2021.d.ts","../../../../node_modules/typescript/lib/lib.es2022.d.ts","../../../../node_modules/typescript/lib/lib.es2023.d.ts","../../../../node_modules/typescript/lib/lib.es2024.d.ts","../../../../node_modules/typescript/lib/lib.es2025.d.ts","../../../../node_modules/typescript/lib/lib.esnext.d.ts","../../../../node_modules/typescript/lib/lib.dom.d.ts","../../../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../../../node_modules/typescript/lib/lib.es2023.array.d.ts","../../../../node_modules/typescript/lib/lib.es2023.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2023.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../../../node_modules/typescript/lib/lib.es2024.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2024.object.d.ts","../../../../node_modules/typescript/lib/lib.es2024.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../../../node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2024.string.d.ts","../../../../node_modules/typescript/lib/lib.es2025.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2025.float16.d.ts","../../../../node_modules/typescript/lib/lib.es2025.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2025.iterator.d.ts","../../../../node_modules/typescript/lib/lib.es2025.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2025.regexp.d.ts","../../../../node_modules/typescript/lib/lib.esnext.array.d.ts","../../../../node_modules/typescript/lib/lib.esnext.collection.d.ts","../../../../node_modules/typescript/lib/lib.esnext.date.d.ts","../../../../node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../../../node_modules/typescript/lib/lib.esnext.error.d.ts","../../../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../../node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.esnext.temporal.d.ts","../../../../node_modules/typescript/lib/lib.esnext.typedarrays.d.ts","../../../../node_modules/typescript/lib/lib.decorators.d.ts","../../../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../../../node_modules/@types/react/global.d.ts","../../../../node_modules/csstype/index.d.ts","../../../../node_modules/@types/react/index.d.ts","../../../../node_modules/@types/react/jsx-runtime.d.ts","../../../collections-admin/dist/types/app.d.ts","../../../cms-types/dist/types/index.d.ts","../../../collections-admin/dist/types/client.d.ts","../../../collections-admin/dist/types/form-model.d.ts","../../../collections-admin/dist/types/index.d.ts","../../../../node_modules/@types/react-dom/client.d.ts","./entry.tsx","./env.d.ts","../../../../node_modules/@types/react-dom/index.d.ts"],"fileIdsList":[[92],[90,91],[92,93,98,99],[95],[94,95,96,97]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"1e9332c23e9a907175e0ffc6a49e236f97b48838cc8aec9ce7e4cec21e544b65","impliedFormat":1},{"version":"3753fbc1113dc511214802a2342280a8b284ab9094f6420e7aa171e868679f91","impliedFormat":1},{"version":"999ca32883495a866aa5737fe1babc764a469e4cde6ee6b136a4b9ae68853e4b","impliedFormat":1},{"version":"17f13ecb98cbc39243f2eee1f16d45cd8ec4706b03ee314f1915f1a8b42f6984","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"0bd714129fca875f7d4c477a1a392200b0bcd13fb2e80928cd334b63830ea047","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2c9037ae6cd2c52d80ceef0b3c5ffdb488627d71529cf4f63776daf11161c9a","affectsGlobalScope":true,"impliedFormat":1},{"version":"135d5cf4d345f59f1a9caadfafcd858d3d9cc68290db616cc85797224448cccc","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc238c3f81c2984751932b6aab223cd5b830e0ac6cad76389e5e9d2ffc03287d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a07f9b76d361f572620927e5735b77d6d2101c23cdd94383eb5b706e7b36357","affectsGlobalScope":true,"impliedFormat":1},{"version":"7c4e8dc6ab834cc6baa0227e030606d29e3e8449a9f67cdf5605ea5493c4db29","affectsGlobalScope":true,"impliedFormat":1},{"version":"de7ba0fd02e06cd9a5bd4ab441ed0e122735786e67dde1e849cced1cd8b46b78","affectsGlobalScope":true,"impliedFormat":1},{"version":"6148e4e88d720a06855071c3db02069434142a8332cf9c182cda551adedf3156","affectsGlobalScope":true,"impliedFormat":1},{"version":"d63dba625b108316a40c95a4425f8d4294e0deeccfd6c7e59d819efa19e23409","affectsGlobalScope":true,"impliedFormat":1},{"version":"0568d6befee03dd435bed4fc25c4e46865b24bdcb8c563fdc21f580a2c301904","affectsGlobalScope":true,"impliedFormat":1},{"version":"30d62269b05b584741f19a5369852d5d34895aa2ac4fd948956f886d15f9cc0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"ffbe6d7b295306b2ba88030f65b74c107d8d99bdcf596ea99c62a02f606108b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"996fb27b15277369c68a4ba46ed138b4e9e839a02fb4ec756f7997629242fd9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"79b712591b270d4778c89706ca2cfc56ddb8c3f895840e477388f1710dc5eda9","affectsGlobalScope":true,"impliedFormat":1},{"version":"20884846cef428b992b9bd032e70a4ef88e349263f63aeddf04dda837a7dba26","affectsGlobalScope":true,"impliedFormat":1},{"version":"5fcab789c73a97cd43828ee3cc94a61264cf24d4c44472ce64ced0e0f148bdb2","affectsGlobalScope":true,"impliedFormat":1},{"version":"db59a81f070c1880ad645b2c0275022baa6a0c4f0acdc58d29d349c6efcf0903","affectsGlobalScope":true,"impliedFormat":1},{"version":"673294292640f5722b700e7d814e17aaf7d93f83a48a2c9b38f33cbc940ad8b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"d786b48f934cbca483b3c6d0a798cb43bbb4ada283e76fb22c28e53ae05b9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"142efd4ce210576f777dc34df121777be89eda476942d6d6663b03dcb53be3ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"379bc41580c2d774f82e828c70308f24a005b490c25ba34d679d84bcf05c3d9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"ed484fb2aa8a1a23d0277056ec3336e0a0b52f9b8d6a961f338a642faf43235d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ffedae1d1c2d53fdbca1c96d3c7dda544281f7d262f99b6880634f8fd8d9820","affectsGlobalScope":true,"impliedFormat":1},{"version":"83a730b125d477dd264df8ba479afab27a3dae7152b005c214ab94dc7ee44fd3","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"2577e7e800bdece2956ca027cb5c17aa359abd968eb9355760110c853f4fb9da","impliedFormat":1},{"version":"b838d4c72740eb0afd284bf7575b74c624b105eff2e8c7b4aeead57e7ac320ff","impliedFormat":1},"af732cf34bfa338fff320258775aa469d8bc3e0f25b5c82e231046ae5dccabf7","c43c2a0107a2a099fe50bcc754e90b50aa6b265ce57e464a2489798b38e4dc53","18be101c2011a0c364fc7e5ee231d6a9397808e9a095b4bb25c171adf4f23ff5","35ac5e9e25a23b55393daa079c3e7b85817598eed090c2b6b92d156fbde16c6b","d1a6a20b9ba01e597090d73d614ea6e39dc0a5424261f6980280f189c05b36b1",{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},"3044ee76859e5dae484bafae0e31525f2a3378d41a3b24c495b2e8e7b690e52c",{"version":"55793e486e9c2ad120621fcba96981896ee79991dbfe795758e6e77e75d0df54","affectsGlobalScope":true},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1}],"root":[100,101],"options":{"allowImportingTsExtensions":true,"allowJs":true,"composite":false,"declaration":true,"declarationMap":true,"emitDeclarationOnly":false,"jsx":4,"module":200,"noEmitOnError":true,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noPropertyAccessFromIndexSignature":false,"noUncheckedIndexedAccess":true,"noUnusedLocals":false,"noUnusedParameters":false,"skipLibCheck":true,"strict":true,"target":99,"verbatimModuleSyntax":true},"referencedMap":[[99,1],[102,1],[92,2],[93,1],[100,3],[94,1],[96,4],[97,4],[98,5]],"affectedFilesPendingEmit":[[100,49]],"version":"6.0.2"}
@@ -1,3 +1,4 @@
1
+ import { createCmsCore, createNodeFs } from '@nuasite/cms-core'
1
2
  import fs from 'node:fs/promises'
2
3
  import type { IncomingMessage, ServerResponse } from 'node:http'
3
4
  import path from 'node:path'
@@ -118,7 +119,15 @@ export function createDevMiddleware(
118
119
 
119
120
  // CMS API endpoints (local dev server backend)
120
121
  if (options.enableCmsApi) {
121
- const projectRoot = getProjectRoot()
122
+ // One cms-core instance per project root. Structural routes (entry/page/redirect
123
+ // CRUD, media) delegate to this brain over a node:fs adapter; the media adapter
124
+ // and component dirs mirror the selection @nuasite/cms makes for the dev server.
125
+ const cmsFs = createNodeFs(getProjectRoot())
126
+ const core = createCmsCore(cmsFs, {
127
+ contentDir: config.contentDir,
128
+ media: options.mediaAdapter,
129
+ componentDirs: config.componentDirs,
130
+ })
122
131
 
123
132
  server.middlewares.use((req, res, next) => {
124
133
  const url = req.url || ''
@@ -136,6 +145,8 @@ export function createDevMiddleware(
136
145
  res,
137
146
  route,
138
147
  manifestWriter,
148
+ core,
149
+ fs: cmsFs,
139
150
  contentDir: config.contentDir,
140
151
  mediaAdapter: options.mediaAdapter,
141
152
  maxUploadSize: options.maxUploadSize,
@@ -22,7 +22,10 @@ const confirmDeleteSlug = signal<string | null>(null)
22
22
  const EMPTY_ENTRIES: never[] = []
23
23
 
24
24
  export function CollectionsBrowser() {
25
- const visible = isCollectionsBrowserOpen.value
25
+ // Collection management (browse/list/open entries) can be owned by the host
26
+ // app; when disabled, the in-preview browser is hidden entirely.
27
+ const collectionManagementEnabled = config.value.features?.collectionManagement !== false
28
+ const visible = collectionManagementEnabled && isCollectionsBrowserOpen.value
26
29
  const selected = selectedBrowserCollection.value
27
30
 
28
31
  const collectionDefinitions = manifest.value.collectionDefinitions ?? {}
@@ -95,6 +95,11 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
95
95
  })
96
96
  }
97
97
 
98
+ // When collection management is disabled (e.g. it lives in the host app's
99
+ // Collections tab), hide the in-preview collection browser entry points.
100
+ // Inline editing of the current page's content ("Edit Content") is kept.
101
+ const collectionManagementEnabled = signals.config.value.features?.collectionManagement !== false
102
+
98
103
  if (collectionDefinitions) {
99
104
  const entries = Object.entries(collectionDefinitions)
100
105
  if (entries.length > 0) {
@@ -111,12 +116,14 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
111
116
  ordered.push([name, def, false])
112
117
  }
113
118
 
114
- const contentItems: MenuItem[] = ordered.map(([name, def, indented]) => ({
115
- label: def.label,
116
- icon: <GridIcon />,
117
- indented,
118
- onClick: () => callbacks.onOpenCollection?.(name),
119
- }))
119
+ const contentItems: MenuItem[] = collectionManagementEnabled
120
+ ? ordered.map(([name, def, indented]) => ({
121
+ label: def.label,
122
+ icon: <GridIcon />,
123
+ indented,
124
+ onClick: () => callbacks.onOpenCollection?.(name),
125
+ }))
126
+ : []
120
127
 
121
128
  if (currentPageCollection && callbacks.onEditContent) {
122
129
  contentItems.unshift({
@@ -134,11 +141,13 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
134
141
  })
135
142
  }
136
143
 
137
- menuSections.push({
138
- label: 'Content',
139
- icon: <GridIcon />,
140
- items: contentItems,
141
- })
144
+ if (contentItems.length > 0) {
145
+ menuSections.push({
146
+ label: 'Content',
147
+ icon: <GridIcon />,
148
+ items: contentItems,
149
+ })
150
+ }
142
151
  }
143
152
  }
144
153
 
@@ -1470,8 +1470,10 @@ export function setConfig(newConfig: CmsConfig): void {
1470
1470
 
1471
1471
  export function setFeatures(features: CmsConfig['features']): void {
1472
1472
  const current = config.value.features
1473
- if (current?.selectElement === features?.selectElement) return
1474
- config.value = { ...config.value, features: { ...current, ...features } }
1473
+ const next = { ...current, ...features }
1474
+ // Bail out when nothing actually changes, to avoid re-triggering effects.
1475
+ if (current?.selectElement === next.selectElement && current?.collectionManagement === next.collectionManagement) return
1476
+ config.value = { ...config.value, features: next }
1475
1477
  }
1476
1478
 
1477
1479
  // ============================================================================
@@ -1,17 +1,16 @@
1
+ import type { CmsCore, CmsFileSystem } from '@nuasite/cms-core'
2
+ import { listProjectImages } from '@nuasite/cms-core'
1
3
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
4
  import path from 'node:path'
3
5
  import { scanCollections } from '../collection-scanner'
4
6
  import { getProjectRoot } from '../config'
5
7
  import { expectedDeletions } from '../dev-middleware'
6
8
  import type { ManifestWriter } from '../manifest-writer'
7
- import { listProjectImages } from '../media/project-images'
8
9
  import type { MediaStorageAdapter } from '../media/types'
9
10
  import { handleAddArrayItem, handleRemoveArrayItem } from './array-ops'
10
11
  import { tryAstroImageUpload } from './astro-image-upload'
11
12
  import { handleInsertComponent, handleRemoveComponent } from './component-ops'
12
- import { handleCreateMarkdown, handleDeleteMarkdown, handleGetMarkdownContent, handleRenameMarkdown, handleUpdateMarkdown } from './markdown-ops'
13
- import { handleCheckSlugExists, handleCreatePage, handleDeletePage, handleDuplicatePage, handleGetLayouts } from './page-ops'
14
- import { handleAddRedirect, handleDeleteRedirect, handleGetRedirects, handleUpdateRedirect } from './redirect-ops'
13
+ import { handleCheckSlugExists } from './page-ops'
15
14
  import { BodyTooLargeError, parseJsonBody, parseMultipartFile, readBody, sendError, sendJson } from './request-utils'
16
15
  import { handleUpdate } from './source-writer'
17
16
 
@@ -20,6 +19,10 @@ export interface RouteContext {
20
19
  res: ServerResponse
21
20
  route: string
22
21
  manifestWriter: ManifestWriter
22
+ /** The framework-agnostic brain. Structural routes delegate here. */
23
+ core: CmsCore
24
+ /** Raw FileSystem port (for the few helpers that scan the project directly). */
25
+ fs: CmsFileSystem
23
26
  contentDir: string
24
27
  mediaAdapter?: MediaStorageAdapter
25
28
  maxUploadSize: number
@@ -39,6 +42,37 @@ function getQuery(ctx: RouteContext): URLSearchParams {
39
42
  return new URL(ctx.req.url!, `http://${ctx.req.headers.host}`).searchParams
40
43
  }
41
44
 
45
+ /**
46
+ * Derive `{ collection, slug }` from a root-relative content-entry `filePath`.
47
+ *
48
+ * The dev API is addressed by `filePath` (e.g. `src/content/blog/hello.md`),
49
+ * while cms-core is addressed by `{ collection, slug }`. The collection is the
50
+ * first path segment under `contentDir`; the slug is the remainder with the
51
+ * extension stripped (and a trailing `/index` collapsed for index-layout
52
+ * entries). cms-core's path resolution is the exact inverse: a flat `<slug>.<ext>`
53
+ * resolves first, an index `<slug>/index.{md,mdx}` after — so the derived pair
54
+ * resolves back to the same file.
55
+ */
56
+ function filePathToEntry(contentDir: string, filePath: string): { collection: string; slug: string } | null {
57
+ const normalized = filePath.replace(/^\/+/, '')
58
+ const prefix = `${contentDir.replace(/\/+$/, '')}/`
59
+ if (!normalized.startsWith(prefix)) return null
60
+
61
+ const rel = normalized.slice(prefix.length)
62
+ const firstSlash = rel.indexOf('/')
63
+ if (firstSlash < 0) return null
64
+
65
+ const collection = rel.slice(0, firstSlash)
66
+ const entryPath = rel.slice(firstSlash + 1)
67
+ if (!collection || !entryPath) return null
68
+
69
+ const withoutExt = entryPath.replace(/\.(md|mdx|json|yaml|yml)$/, '')
70
+ const slug = withoutExt.replace(/\/index$/, '')
71
+ if (!slug) return null
72
+
73
+ return { collection, slug }
74
+ }
75
+
42
76
  // -- Route helper factories --
43
77
 
44
78
  /** POST route: parse JSON body → handler(body, manifestWriter) → sendJson */
@@ -49,19 +83,19 @@ function post<T>(route: string, handler: (body: T, mw: ManifestWriter) => Promis
49
83
  }]
50
84
  }
51
85
 
52
- /** POST route: parse JSON body → handler(body) → sendJson with success-based status */
53
- function postWithStatus<T>(route: string, handler: (body: T) => Promise<{ success: boolean }>): [string, RouteHandler] {
54
- return [`POST:${route}`, async ({ req, res }) => {
86
+ /** POST route through cms-core: parse JSON body → handler(body, core) → sendJson with success-based status */
87
+ function postCore<T>(route: string, handler: (body: T, core: CmsCore) => Promise<{ success: boolean }>): [string, RouteHandler] {
88
+ return [`POST:${route}`, async ({ req, res, core }) => {
55
89
  const body = await parseJsonBody<T>(req)
56
- const result = await handler(body)
90
+ const result = await handler(body, core)
57
91
  sendJson(res, result, result.success ? 200 : 400)
58
92
  }]
59
93
  }
60
94
 
61
- /** GET route: handler() → sendJson */
62
- function get(route: string, handler: () => Promise<unknown>): [string, RouteHandler] {
63
- return [`GET:${route}`, async ({ res }) => {
64
- sendJson(res, await handler())
95
+ /** GET route through cms-core: handler(core) → sendJson */
96
+ function getCore(route: string, handler: (core: CmsCore) => Promise<unknown>): [string, RouteHandler] {
97
+ return [`GET:${route}`, async ({ res, core }) => {
98
+ sendJson(res, await handler(core))
65
99
  }]
66
100
  }
67
101
 
@@ -82,54 +116,150 @@ const ALLOWED_UPLOAD_TYPES = new Set([
82
116
  'application/pdf',
83
117
  ])
84
118
 
119
+ /** Frontmatter shape the create route enriches with title/date for markdown entries. */
120
+ interface CreateMarkdownBody {
121
+ collection: string
122
+ title: string
123
+ slug: string
124
+ frontmatter?: Record<string, unknown>
125
+ content?: string
126
+ fileExtension?: string
127
+ }
128
+
129
+ interface UpdateMarkdownBody {
130
+ filePath: string
131
+ frontmatter?: Record<string, unknown>
132
+ content?: string
133
+ }
134
+
135
+ interface DeleteMarkdownBody {
136
+ filePath: string
137
+ }
138
+
139
+ interface RenameMarkdownBody {
140
+ filePath: string
141
+ newSlug: string
142
+ }
143
+
144
+ interface DuplicatePageBody {
145
+ sourcePagePath: string
146
+ slug: string
147
+ title?: string
148
+ layoutPath?: string
149
+ createRedirect?: boolean
150
+ }
151
+
152
+ interface DeletePageBody {
153
+ pagePath: string
154
+ createRedirect?: boolean
155
+ redirectTo?: string
156
+ }
157
+
158
+ const DATA_EXTENSIONS = new Set(['json', 'yaml', 'yml'])
159
+
85
160
  /** O(1) route lookup map: "METHOD:route" → handler */
86
161
  const routeMap = new Map<string, RouteHandler>([
87
- // Source editing
162
+ // Source editing — manifest-coupled, stays in @nuasite/cms
88
163
  post('update', (body: Parameters<typeof handleUpdate>[0], mw) => handleUpdate(body, mw)),
89
164
  post('insert-component', (body: Parameters<typeof handleInsertComponent>[0], mw) => handleInsertComponent(body, mw)),
90
165
  post('remove-component', (body: Parameters<typeof handleRemoveComponent>[0], mw) => handleRemoveComponent(body, mw)),
91
166
  post('add-array-item', (body: Parameters<typeof handleAddArrayItem>[0], mw) => handleAddArrayItem(body, mw)),
92
167
  post('remove-array-item', (body: Parameters<typeof handleRemoveArrayItem>[0], mw) => handleRemoveArrayItem(body, mw)),
93
168
 
94
- // Markdown CRUD
95
- custom('GET', 'markdown/content', async ({ req, res }) => {
169
+ // Markdown / entry CRUD — structural, delegated to cms-core
170
+ custom('GET', 'markdown/content', async ({ req, res, core, contentDir }) => {
96
171
  const filePath = getQuery({ req } as RouteContext).get('filePath')
97
172
  if (!filePath) {
98
173
  sendError(res, 'filePath query parameter required')
99
174
  return
100
175
  }
101
- const result = await handleGetMarkdownContent(filePath)
176
+ const entry = filePathToEntry(contentDir, filePath)
177
+ const result = entry ? await core.getEntry(entry.collection, entry.slug) : null
102
178
  if (!result) {
103
179
  sendError(res, 'File not found', 404)
104
180
  return
105
181
  }
106
- sendJson(res, result)
182
+ sendJson(res, { content: result.content, frontmatter: result.frontmatter, filePath })
183
+ }),
184
+ custom('POST', 'markdown/update', async ({ req, res, core, contentDir }) => {
185
+ const body = await parseJsonBody<UpdateMarkdownBody>(req)
186
+ const entry = filePathToEntry(contentDir, body.filePath)
187
+ if (!entry) {
188
+ sendJson(res, { success: false, error: `Invalid content path: ${body.filePath}` })
189
+ return
190
+ }
191
+ // cms-core's updateEntry resolves component definitions internally for MDX imports.
192
+ const result = await core.updateEntry({
193
+ collection: entry.collection,
194
+ slug: entry.slug,
195
+ frontmatter: body.frontmatter,
196
+ body: body.content,
197
+ })
198
+ sendJson(res, { success: result.success, ...(result.error ? { error: result.error } : {}) })
107
199
  }),
108
- custom('POST', 'markdown/update', async ({ req, res, manifestWriter }) => {
109
- const body = await parseJsonBody<Parameters<typeof handleUpdateMarkdown>[0]>(req)
110
- const result = await handleUpdateMarkdown(body, manifestWriter.getComponentDefinitions())
111
- sendJson(res, result)
200
+ custom('POST', 'markdown/rename', async ({ req, res, core, contentDir }) => {
201
+ const body = await parseJsonBody<RenameMarkdownBody>(req)
202
+ const entry = filePathToEntry(contentDir, body.filePath)
203
+ if (!entry) {
204
+ sendJson(res, { success: false, error: `Invalid content path: ${body.filePath}` })
205
+ return
206
+ }
207
+ const result = await core.renameEntry(entry.collection, entry.slug, body.newSlug)
208
+ if (!result.success) {
209
+ sendJson(res, { success: false, error: result.error })
210
+ return
211
+ }
212
+ const newSlug = result.sourcePath ? lastSlug(result.sourcePath) : undefined
213
+ sendJson(res, { success: true, newFilePath: result.sourcePath, newSlug })
112
214
  }),
113
- post('markdown/rename', (body: Parameters<typeof handleRenameMarkdown>[0]) => handleRenameMarkdown(body)),
114
- custom('POST', 'markdown/create', async ({ req, res, manifestWriter, contentDir }) => {
115
- const body = await parseJsonBody<Parameters<typeof handleCreateMarkdown>[0]>(req)
116
- const result = await handleCreateMarkdown(body)
215
+ custom('POST', 'markdown/create', async ({ req, res, core, manifestWriter, contentDir }) => {
216
+ const body = await parseJsonBody<CreateMarkdownBody>(req)
217
+ const ext = body.fileExtension ?? 'md'
218
+ const isData = DATA_EXTENSIONS.has(ext)
219
+ // Markdown entries get title + an ISO date injected; data entries take frontmatter verbatim.
220
+ const frontmatter = isData
221
+ ? { ...(body.frontmatter ?? {}) }
222
+ : { title: body.title, date: new Date().toISOString().split('T')[0]!, ...(body.frontmatter ?? {}) }
223
+
224
+ const result = await core.createEntry({
225
+ collection: body.collection,
226
+ slug: body.slug || body.title,
227
+ frontmatter,
228
+ body: body.content,
229
+ fileExtension: body.fileExtension,
230
+ })
231
+
117
232
  if (result.success) {
118
233
  manifestWriter.setCollectionDefinitions(await scanCollections(contentDir))
119
234
  }
120
- sendJson(res, result, result.success ? 200 : 400)
235
+ const slug = result.sourcePath ? lastSlug(result.sourcePath) : undefined
236
+ sendJson(
237
+ res,
238
+ {
239
+ success: result.success,
240
+ ...(result.sourcePath ? { filePath: result.sourcePath } : {}),
241
+ ...(slug ? { slug } : {}),
242
+ ...(result.error ? { error: result.error } : {}),
243
+ },
244
+ result.success ? 200 : 400,
245
+ )
121
246
  }),
122
- custom('POST', 'markdown/delete', async ({ req, res, manifestWriter, contentDir }) => {
123
- const body = await parseJsonBody<Parameters<typeof handleDeleteMarkdown>[0]>(req)
124
- const fullPath = path.resolve(getProjectRoot(), body.filePath?.replace(/^\//, '') ?? '')
247
+ custom('POST', 'markdown/delete', async ({ req, res, core, manifestWriter, contentDir }) => {
248
+ const body = await parseJsonBody<DeleteMarkdownBody>(req)
249
+ const entry = filePathToEntry(contentDir, body.filePath)
250
+ if (!entry) {
251
+ sendJson(res, { success: false, error: `Invalid content path: ${body.filePath}` }, 400)
252
+ return
253
+ }
254
+ const fullPath = path.resolve(getProjectRoot(), body.filePath.replace(/^\//, ''))
125
255
  expectedDeletions.add(fullPath)
126
- const result = await handleDeleteMarkdown(body)
256
+ const result = await core.deleteEntry(entry.collection, entry.slug)
127
257
  if (result.success) {
128
258
  manifestWriter.setCollectionDefinitions(await scanCollections(contentDir))
129
259
  } else {
130
260
  expectedDeletions.delete(fullPath)
131
261
  }
132
- sendJson(res, result, result.success ? 200 : 400)
262
+ sendJson(res, { success: result.success, ...(result.error ? { error: result.error } : {}) }, result.success ? 200 : 400)
133
263
  }),
134
264
 
135
265
  // Media
@@ -143,7 +273,7 @@ const routeMap = new Map<string, RouteHandler>([
143
273
  }),
144
274
  custom('GET', 'media/project-images', async (ctx) => {
145
275
  const excludeDir = ctx.mediaAdapter?.staticFiles?.dir
146
- const items = await listProjectImages({ excludeDir })
276
+ const items = await listProjectImages(ctx.fs, { excludeDir })
147
277
  sendJson(ctx.res, { items })
148
278
  }),
149
279
  custom('POST', 'media/upload', async (ctx) => {
@@ -225,24 +355,24 @@ const routeMap = new Map<string, RouteHandler>([
225
355
  sendJson(ctx.res, result, result.success ? 200 : 400)
226
356
  }),
227
357
 
228
- // Page operations
229
- postWithStatus('page/create', (body: Parameters<typeof handleCreatePage>[0]) => handleCreatePage(body)),
230
- custom('POST', 'page/duplicate', async ({ req, res }) => {
231
- const body = await parseJsonBody<Parameters<typeof handleDuplicatePage>[0]>(req)
232
- const result = await handleDuplicatePage(body)
358
+ // Page operations — structural, delegated to cms-core
359
+ postCore('page/create', (body: Parameters<CmsCore['createPage']>[0], core) => core.createPage(body)),
360
+ custom('POST', 'page/duplicate', async ({ req, res, core }) => {
361
+ const body = await parseJsonBody<DuplicatePageBody>(req)
362
+ const result = await core.duplicatePage(body)
233
363
  if (result.success && body.createRedirect) {
234
- await handleAddRedirect({ source: body.sourcePagePath, destination: result.url!, statusCode: 307 })
364
+ await core.addRedirect({ source: body.sourcePagePath, destination: result.url!, statusCode: 307 })
235
365
  }
236
366
  sendJson(res, result, result.success ? 200 : 400)
237
367
  }),
238
- custom('POST', 'page/delete', async ({ req, res }) => {
239
- const body = await parseJsonBody<Parameters<typeof handleDeletePage>[0]>(req)
240
- const result = await handleDeletePage(body)
368
+ custom('POST', 'page/delete', async ({ req, res, core }) => {
369
+ const body = await parseJsonBody<DeletePageBody>(req)
370
+ const result = await core.deletePage(body)
241
371
  if (result.success && result.filePath) {
242
372
  expectedDeletions.add(path.resolve(getProjectRoot(), result.filePath))
243
373
  }
244
374
  if (result.success && body.createRedirect && body.redirectTo) {
245
- await handleAddRedirect({ source: body.pagePath, destination: body.redirectTo, statusCode: 307 })
375
+ await core.addRedirect({ source: body.pagePath, destination: body.redirectTo, statusCode: 307 })
246
376
  }
247
377
  sendJson(res, result, result.success ? 200 : 400)
248
378
  }),
@@ -254,18 +384,32 @@ const routeMap = new Map<string, RouteHandler>([
254
384
  }
255
385
  sendJson(ctx.res, await handleCheckSlugExists(slug))
256
386
  }),
257
- get('page/layouts', async () => ({ layouts: await handleGetLayouts() })),
387
+ getCore('page/layouts', async (core) => ({ layouts: await core.getLayouts() })),
258
388
 
259
- // Redirects
260
- get('redirects', () => handleGetRedirects()),
261
- postWithStatus('redirects/add', (body: Parameters<typeof handleAddRedirect>[0]) => handleAddRedirect(body)),
262
- postWithStatus('redirects/update', (body: Parameters<typeof handleUpdateRedirect>[0]) => handleUpdateRedirect(body)),
263
- postWithStatus('redirects/delete', (body: Parameters<typeof handleDeleteRedirect>[0]) => handleDeleteRedirect(body)),
389
+ // Redirects — structural, delegated to cms-core
390
+ getCore('redirects', (core) => core.listRedirects()),
391
+ postCore('redirects/add', (body: Parameters<CmsCore['addRedirect']>[0], core) => core.addRedirect(body)),
392
+ postCore('redirects/update', (body: Parameters<CmsCore['updateRedirect']>[0], core) => core.updateRedirect(body)),
393
+ postCore('redirects/delete', (body: Parameters<CmsCore['deleteRedirect']>[0], core) => core.deleteRedirect(body)),
264
394
 
265
395
  // Deployment
266
396
  get('deployment/status', async () => ({ currentDeployment: null, pendingCount: 0, deploymentEnabled: false })),
267
397
  ])
268
398
 
399
+ /** GET route returning a fixed/static payload (no cms-core needed). */
400
+ function get(route: string, handler: () => Promise<unknown>): [string, RouteHandler] {
401
+ return [`GET:${route}`, async ({ res }) => {
402
+ sendJson(res, await handler())
403
+ }]
404
+ }
405
+
406
+ /** Last path segment of a root-relative source path, with the extension and a trailing `/index` stripped. */
407
+ function lastSlug(sourcePath: string): string {
408
+ const withoutExt = sourcePath.replace(/\.(md|mdx|json|yaml|yml)$/, '').replace(/\/index$/, '')
409
+ const slash = withoutExt.lastIndexOf('/')
410
+ return slash >= 0 ? withoutExt.slice(slash + 1) : withoutExt
411
+ }
412
+
269
413
  export async function handleCmsApiRoute(ctx: RouteContext): Promise<void> {
270
414
  const { req, res, route } = ctx
271
415