@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/dist/editor.js +2221 -2219
- package/package.json +18 -2
- package/src/admin/entry.tsx +35 -0
- package/src/admin/env.d.ts +15 -0
- package/src/admin/tsconfig.json +15 -0
- package/src/admin/tsconfig.tsbuildinfo +1 -0
- package/src/dev-middleware.ts +12 -1
- package/src/editor/components/collections-browser.tsx +4 -1
- package/src/editor/components/toolbar.tsx +20 -11
- package/src/editor/signals.ts +4 -2
- package/src/handlers/api-routes.ts +192 -48
- package/src/handlers/page-ops.ts +4 -189
- package/src/index.ts +137 -59
- package/src/local-admin.ts +232 -0
- package/src/media/types.ts +11 -55
- package/src/mode.ts +53 -0
- package/src/tsconfig.json +10 -1
- package/src/types.ts +38 -225
- package/src/handlers/markdown-ops.ts +0 -474
- package/src/handlers/redirect-ops.ts +0 -163
- package/src/media/contember.ts +0 -85
- package/src/media/local.ts +0 -152
- package/src/media/project-images.ts +0 -81
- package/src/media/s3.ts +0 -154
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-mode admin server (cms-headless F7).
|
|
3
|
+
*
|
|
4
|
+
* In `local` mode (`pletivo dev` on a developer machine), `@nuasite/cms` gives the
|
|
5
|
+
* same full-page collections admin as the webmaster tab — reusing the exact
|
|
6
|
+
* `@nuasite/collections-admin` SPA — instead of the cramped in-iframe collection
|
|
7
|
+
* form. This module wires two things into Astro's Vite dev server:
|
|
8
|
+
*
|
|
9
|
+
* 1. an **in-process** cms-sidecar (`@nuasite/cms-sidecar` `createServer` over
|
|
10
|
+
* `createCmsCore(createNodeFs(root))`) mounted at `/_nua/cms-admin-api/*`,
|
|
11
|
+
* forwarding to the sidecar's `/cms/v1` contract;
|
|
12
|
+
* 2. the collections-admin SPA served at `/_nua/admin`, with
|
|
13
|
+
* `apiBase = /_nua/cms-admin-api/cms/v1`.
|
|
14
|
+
*
|
|
15
|
+
* Both are **lazy**: nothing is built on `pletivo dev` startup. The sidecar core
|
|
16
|
+
* is created on the first `/_nua/admin` (or API) hit and reused thereafter, so
|
|
17
|
+
* dev startup stays fast.
|
|
18
|
+
*
|
|
19
|
+
* The sidecar runs in-process (not a child `bunx` process) on purpose: the dev
|
|
20
|
+
* server already runs under Bun/Node, the sidecar's `createServer` is a pure
|
|
21
|
+
* Web-standard `fetch` handler, and `createCmsCore(createNodeFs(root))` is the
|
|
22
|
+
* very same brain the legacy `/_nua/cms` dev API already builds here. In-process
|
|
23
|
+
* avoids a port allocation, a `bunx` cold-start, and a network hop on every
|
|
24
|
+
* request — and keeps `pletivo dev` a single self-contained process.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createCmsCore, createNodeFs } from '@nuasite/cms-core'
|
|
28
|
+
import { type CmsSidecarServer, createServer } from '@nuasite/cms-sidecar'
|
|
29
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
30
|
+
import { getProjectRoot } from './config'
|
|
31
|
+
import { readBody } from './handlers/request-utils'
|
|
32
|
+
import type { MediaStorageAdapter } from './media/types'
|
|
33
|
+
|
|
34
|
+
/** Minimal Vite dev-server surface this module needs (kept loose to dodge Vite version skew). */
|
|
35
|
+
export interface AdminViteServerLike {
|
|
36
|
+
middlewares: {
|
|
37
|
+
use: (middleware: (req: IncomingMessage, res: ServerResponse, next: () => void) => void) => void
|
|
38
|
+
}
|
|
39
|
+
transformIndexHtml: (url: string, html: string) => Promise<string>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LocalAdminOptions {
|
|
43
|
+
/** Content collections directory, relative to the project root. */
|
|
44
|
+
contentDir: string
|
|
45
|
+
/** Component directories, used by the core for MDX import resolution. */
|
|
46
|
+
componentDirs: string[]
|
|
47
|
+
/** Media adapter for the sidecar's `/media` routes (defaults to `local` in this mode). */
|
|
48
|
+
mediaAdapter?: MediaStorageAdapter
|
|
49
|
+
/** Max upload size in bytes for sidecar media uploads. */
|
|
50
|
+
maxUploadSize: number
|
|
51
|
+
/** Virtual-module id of the SPA entry the HTML shell loads (transformed by Vite). */
|
|
52
|
+
entryModuleId: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** URL the collections-admin SPA is served at. */
|
|
56
|
+
export const ADMIN_ROUTE = '/_nua/admin'
|
|
57
|
+
|
|
58
|
+
/** Local mount of the in-process sidecar. The SPA's `apiBase` targets `${API_PREFIX}/cms/v1`. */
|
|
59
|
+
export const ADMIN_API_PREFIX = '/_nua/cms-admin-api'
|
|
60
|
+
|
|
61
|
+
/** `apiBase` passed to the SPA — the sidecar serves its routes under `/cms/v1`. */
|
|
62
|
+
export const ADMIN_API_BASE = `${ADMIN_API_PREFIX}/cms/v1`
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the HTML shell for the admin SPA. It loads the virtual entry module
|
|
66
|
+
* (which imports the lib's stylesheet and mounts `<CollectionsAdminApp apiBase={…} />`),
|
|
67
|
+
* and injects the resolved `apiBase` as a window global so the entry needs no
|
|
68
|
+
* build config. The host-agnostic SPA is reused verbatim — only `apiBase` differs
|
|
69
|
+
* from the webmaster tab.
|
|
70
|
+
*/
|
|
71
|
+
function adminShellHtml(entryModuleId: string, apiBase: string): string {
|
|
72
|
+
return `<!doctype html>
|
|
73
|
+
<html lang="en">
|
|
74
|
+
<head>
|
|
75
|
+
<meta charset="utf-8" />
|
|
76
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
77
|
+
<title>Collections — Nua CMS</title>
|
|
78
|
+
<style>
|
|
79
|
+
html, body, #nua-admin-root { height: 100%; margin: 0; }
|
|
80
|
+
body { background: #f8fafc; }
|
|
81
|
+
</style>
|
|
82
|
+
<script>window.__NUA_ADMIN_API_BASE__ = ${JSON.stringify(apiBase)};</script>
|
|
83
|
+
</head>
|
|
84
|
+
<body>
|
|
85
|
+
<div id="nua-admin-root"></div>
|
|
86
|
+
<script type="module" src=${JSON.stringify(entryModuleId)}></script>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Lazily create the in-process cms-sidecar over the project's `node:fs`. The core
|
|
94
|
+
* is built once on first use and reused; the media adapter mirrors the dev
|
|
95
|
+
* server's selection (local `public/uploads` by default in this mode).
|
|
96
|
+
*/
|
|
97
|
+
function makeLazySidecar(options: LocalAdminOptions): () => CmsSidecarServer {
|
|
98
|
+
let server: CmsSidecarServer | null = null
|
|
99
|
+
return () => {
|
|
100
|
+
if (server) return server
|
|
101
|
+
const root = getProjectRoot()
|
|
102
|
+
const fs = createNodeFs(root)
|
|
103
|
+
const core = createCmsCore(fs, {
|
|
104
|
+
contentDir: options.contentDir,
|
|
105
|
+
media: options.mediaAdapter,
|
|
106
|
+
componentDirs: options.componentDirs,
|
|
107
|
+
})
|
|
108
|
+
server = createServer({
|
|
109
|
+
core,
|
|
110
|
+
fs,
|
|
111
|
+
root,
|
|
112
|
+
// In-process: the core ships with the site's @nuasite/cms version, so the
|
|
113
|
+
// package version is not separately meaningful — report it as local.
|
|
114
|
+
coreVersion: 'local',
|
|
115
|
+
contentDir: options.contentDir,
|
|
116
|
+
maxUploadSize: options.maxUploadSize,
|
|
117
|
+
})
|
|
118
|
+
return server
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Build a Web `Request` from a Node `IncomingMessage` (+ already-buffered body). */
|
|
123
|
+
function toWebRequest(req: IncomingMessage, path: string, body: Buffer | undefined): Request {
|
|
124
|
+
const host = req.headers.host ?? 'localhost'
|
|
125
|
+
const url = `http://${host}${path}`
|
|
126
|
+
const headers = new Headers()
|
|
127
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
128
|
+
if (value === undefined) continue
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
for (const v of value) headers.append(key, v)
|
|
131
|
+
} else {
|
|
132
|
+
headers.set(key, value)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const method = req.method ?? 'GET'
|
|
136
|
+
const init: RequestInit = { method, headers }
|
|
137
|
+
if (method !== 'GET' && method !== 'HEAD' && body !== undefined && body.length > 0) {
|
|
138
|
+
// `Buffer` is not directly a DOM `BodyInit`; copy into a standalone
|
|
139
|
+
// `Uint8Array` (a valid `ArrayBufferView` body) over its own `ArrayBuffer`.
|
|
140
|
+
const bytes = new Uint8Array(body.byteLength)
|
|
141
|
+
bytes.set(body)
|
|
142
|
+
init.body = bytes
|
|
143
|
+
}
|
|
144
|
+
return new Request(url, init)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Write a Web `Response` back to a Node `ServerResponse`. */
|
|
148
|
+
async function writeWebResponse(res: ServerResponse, response: Response): Promise<void> {
|
|
149
|
+
res.statusCode = response.status
|
|
150
|
+
response.headers.forEach((value, key) => {
|
|
151
|
+
res.setHeader(key, value)
|
|
152
|
+
})
|
|
153
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
154
|
+
res.end(buffer)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Register the local-mode admin middleware on the Vite dev server. The caller
|
|
159
|
+
* must only invoke this in `local` mode — in `hosted` mode the managed sandbox
|
|
160
|
+
* sidecar + the webmaster tab own these responsibilities, so the plugin never
|
|
161
|
+
* registers this middleware (verified by the no-op test).
|
|
162
|
+
*/
|
|
163
|
+
export function createLocalAdminMiddleware(server: AdminViteServerLike, options: LocalAdminOptions): void {
|
|
164
|
+
const getSidecar = makeLazySidecar(options)
|
|
165
|
+
const shell = adminShellHtml(options.entryModuleId, ADMIN_API_BASE)
|
|
166
|
+
|
|
167
|
+
// 1. In-process sidecar API. Strip the local mount prefix and forward the rest
|
|
168
|
+
// (which begins with `/cms/v1/...`) to the sidecar's Web `fetch` handler.
|
|
169
|
+
server.middlewares.use((req, res, next) => {
|
|
170
|
+
const rawUrl = req.url ?? ''
|
|
171
|
+
if (!rawUrl.startsWith(`${ADMIN_API_PREFIX}/`) && rawUrl !== ADMIN_API_PREFIX) {
|
|
172
|
+
next()
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
const forwardedPath = rawUrl.slice(ADMIN_API_PREFIX.length) || '/'
|
|
176
|
+
|
|
177
|
+
readBody(req, options.maxUploadSize)
|
|
178
|
+
.then(async (body) => {
|
|
179
|
+
const sidecar = getSidecar()
|
|
180
|
+
const request = toWebRequest(req, forwardedPath, body)
|
|
181
|
+
const response = await sidecar.fetch(request)
|
|
182
|
+
await writeWebResponse(res, response)
|
|
183
|
+
})
|
|
184
|
+
.catch((error) => {
|
|
185
|
+
console.error('[nua-cms] /_nua/admin API error:', error)
|
|
186
|
+
if (!res.headersSent) {
|
|
187
|
+
res.statusCode = 500
|
|
188
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
189
|
+
}
|
|
190
|
+
res.end(JSON.stringify({ error: 'Internal server error', code: 'io_error' }))
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// 2. Admin SPA shell at /_nua/admin (and any sub-path under it — the SPA owns
|
|
195
|
+
// its own internal view-state navigation). Health-check the in-process
|
|
196
|
+
// sidecar before serving so a broken project surfaces a clear error rather
|
|
197
|
+
// than a blank SPA. The HTML is run through Vite's transform so the dev
|
|
198
|
+
// client + the virtual entry module resolve and HMR works.
|
|
199
|
+
server.middlewares.use((req, res, next) => {
|
|
200
|
+
const pathname = (req.url ?? '').split('?')[0] ?? ''
|
|
201
|
+
const isAdmin = pathname === ADMIN_ROUTE || pathname.startsWith(`${ADMIN_ROUTE}/`)
|
|
202
|
+
if (!isAdmin || (req.method !== 'GET' && req.method !== 'HEAD')) {
|
|
203
|
+
next()
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const serve = async () => {
|
|
208
|
+
const sidecar = getSidecar()
|
|
209
|
+
const health = await sidecar.fetch(new Request('http://localhost/health'))
|
|
210
|
+
if (!health.ok) {
|
|
211
|
+
res.statusCode = 503
|
|
212
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8')
|
|
213
|
+
res.end('Nua CMS local sidecar is not healthy.')
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
const html = await server.transformIndexHtml(pathname, shell)
|
|
217
|
+
res.statusCode = 200
|
|
218
|
+
res.setHeader('content-type', 'text/html; charset=utf-8')
|
|
219
|
+
res.setHeader('cache-control', 'no-store')
|
|
220
|
+
res.end(html)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
serve().catch((error) => {
|
|
224
|
+
console.error('[nua-cms] /_nua/admin serve error:', error)
|
|
225
|
+
if (!res.headersSent) {
|
|
226
|
+
res.statusCode = 500
|
|
227
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8')
|
|
228
|
+
}
|
|
229
|
+
res.end('Failed to load Nua CMS admin.')
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
}
|
package/src/media/types.ts
CHANGED
|
@@ -1,55 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface MediaFolderItem {
|
|
15
|
-
/** Folder name (last segment) */
|
|
16
|
-
name: string
|
|
17
|
-
/** Full relative path from media root (e.g. 'photos/vacation') */
|
|
18
|
-
path: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type MediaTypeFilter = 'all' | 'photo' | 'graphic' | 'video' | 'document'
|
|
22
|
-
|
|
23
|
-
export interface MediaListOptions {
|
|
24
|
-
limit?: number
|
|
25
|
-
cursor?: string
|
|
26
|
-
/** List contents of this subfolder (relative to media root) */
|
|
27
|
-
folder?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface MediaListResult {
|
|
31
|
-
items: MediaItem[]
|
|
32
|
-
/** Subfolders in the current directory */
|
|
33
|
-
folders: MediaFolderItem[]
|
|
34
|
-
hasMore: boolean
|
|
35
|
-
cursor?: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface MediaUploadResult {
|
|
39
|
-
success: boolean
|
|
40
|
-
url?: string
|
|
41
|
-
filename?: string
|
|
42
|
-
annotation?: string
|
|
43
|
-
id?: string
|
|
44
|
-
error?: string
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface MediaStorageAdapter {
|
|
48
|
-
list(options?: MediaListOptions): Promise<MediaListResult>
|
|
49
|
-
upload(file: Buffer, filename: string, contentType: string, options?: { folder?: string }): Promise<MediaUploadResult>
|
|
50
|
-
delete(id: string): Promise<{ success: boolean; error?: string }>
|
|
51
|
-
/** Create an empty folder. Folders are also created implicitly on upload. */
|
|
52
|
-
createFolder?(folder: string): Promise<{ success: boolean; error?: string }>
|
|
53
|
-
/** Local filesystem info for direct file serving in dev (bypasses Vite's public dir cache) */
|
|
54
|
-
staticFiles?: { urlPrefix: string; dir: string }
|
|
55
|
-
}
|
|
1
|
+
// Media storage adapter types now live in @nuasite/cms-types (the shared wire model).
|
|
2
|
+
// Re-exported here so existing `../media/types` imports keep working unchanged.
|
|
3
|
+
export type {
|
|
4
|
+
MediaFolderItem,
|
|
5
|
+
MediaItem,
|
|
6
|
+
MediaListOptions,
|
|
7
|
+
MediaListResult,
|
|
8
|
+
MediaStorageAdapter,
|
|
9
|
+
MediaTypeFilter,
|
|
10
|
+
MediaUploadResult,
|
|
11
|
+
} from '@nuasite/cms-types'
|
package/src/mode.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS run mode — `local` vs `hosted` (cms-headless F7).
|
|
3
|
+
*
|
|
4
|
+
* The marker plugin behaves differently by host:
|
|
5
|
+
*
|
|
6
|
+
* - **`local`** (default for `pletivo dev` on a developer machine): the plugin
|
|
7
|
+
* lazily spawns an in-process cms-sidecar over the project's `node:fs` and
|
|
8
|
+
* serves the `@nuasite/collections-admin` SPA at `/_nua/admin`. The inline
|
|
9
|
+
* widget runs in-page as usual.
|
|
10
|
+
* - **`hosted`** (inside the agent sandbox): the sidecar is a managed sandbox
|
|
11
|
+
* service (cms-headless F2) and the collections admin is a webmaster tab (F3),
|
|
12
|
+
* so the plugin must NOT spawn a sidecar and must NOT serve `/_nua/admin`. It
|
|
13
|
+
* stays a pure marker + CDN-inject plugin (F5).
|
|
14
|
+
*
|
|
15
|
+
* Detection is automatic and zero-config for local dev: the agent runtime sets a
|
|
16
|
+
* sandbox environment variable that the `pletivo dev` process inherits, which
|
|
17
|
+
* flips the mode to `hosted`. An explicit `mode` in the plugin config always
|
|
18
|
+
* wins over auto-detection.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type CmsMode = 'local' | 'hosted'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Environment signals that mean "this `pletivo dev` runs inside the managed
|
|
25
|
+
* agent sandbox" (⇒ `hosted`):
|
|
26
|
+
*
|
|
27
|
+
* - `SANDBOX_ID` — set by the webmaster sandbox deployer when it starts the agent
|
|
28
|
+
* process (the Contember project id); every service the agent spawns, including
|
|
29
|
+
* `pletivo dev`, inherits it. It is never set on a developer machine. This is
|
|
30
|
+
* the canonical "we are in the sandbox" marker used across the agent runtime.
|
|
31
|
+
* - `CMS_SIDECAR_LOCAL_EDVABE` — set (to `'1'`) by the deployer for the local
|
|
32
|
+
* edvabe sandbox variant; the managed sidecar bundle is bind-mounted there.
|
|
33
|
+
* Also implies a managed sidecar, hence `hosted`.
|
|
34
|
+
*/
|
|
35
|
+
const HOSTED_ENV_KEYS: readonly string[] = ['SANDBOX_ID', 'CMS_SIDECAR_LOCAL_EDVABE']
|
|
36
|
+
|
|
37
|
+
/** Whether any hosted-sandbox env signal is present (and non-empty). */
|
|
38
|
+
export function detectHostedFromEnv(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
39
|
+
return HOSTED_ENV_KEYS.some((key) => {
|
|
40
|
+
const value = env[key]
|
|
41
|
+
return typeof value === 'string' && value.trim() !== ''
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the effective CMS mode. An explicit `override` (from the plugin config)
|
|
47
|
+
* always wins; otherwise the mode is auto-detected from the environment, defaulting
|
|
48
|
+
* to `local` when no sandbox signal is present.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveCmsMode(override?: CmsMode, env: NodeJS.ProcessEnv = process.env): CmsMode {
|
|
51
|
+
if (override !== undefined) return override
|
|
52
|
+
return detectHostedFromEnv(env) ? 'hosted' : 'local'
|
|
53
|
+
}
|
package/src/tsconfig.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "../tsconfig.settings.json",
|
|
3
|
+
"exclude": [
|
|
4
|
+
"admin"
|
|
5
|
+
],
|
|
3
6
|
"compilerOptions": {
|
|
4
7
|
"outDir": "../dist/src",
|
|
5
8
|
"composite": true,
|
|
@@ -19,5 +22,11 @@
|
|
|
19
22
|
"react-dom": ["../../../node_modules/preact/compat/"],
|
|
20
23
|
"react-dom/*": ["../../../node_modules/preact/compat/*"]
|
|
21
24
|
}
|
|
22
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"references": [
|
|
27
|
+
{ "path": "../../cms-core/src" },
|
|
28
|
+
{ "path": "../../cms-types/src" },
|
|
29
|
+
{ "path": "../../collections-admin/src" },
|
|
30
|
+
{ "path": "../../cms-sidecar/src" },
|
|
31
|
+
]
|
|
23
32
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
import type { CollectionDefinition, CollectionEntry, ComponentDefinition } from '@nuasite/cms-types'
|
|
2
|
+
|
|
3
|
+
// Structural contract types live in @nuasite/cms-types (the shared wire model).
|
|
4
|
+
// Re-exported here so existing `@nuasite/cms` imports keep working unchanged.
|
|
5
|
+
export type {
|
|
6
|
+
CollectionDefinition,
|
|
7
|
+
CollectionEntry,
|
|
8
|
+
CollectionEntryInfo,
|
|
9
|
+
ComponentDefinition,
|
|
10
|
+
ComponentProp,
|
|
11
|
+
FieldDefinition,
|
|
12
|
+
FieldHints,
|
|
13
|
+
FieldType,
|
|
14
|
+
MutationResult,
|
|
15
|
+
} from '@nuasite/cms-types'
|
|
16
|
+
export type {
|
|
17
|
+
AddRedirectRequest,
|
|
18
|
+
CreatePageRequest,
|
|
19
|
+
DeletePageRequest,
|
|
20
|
+
DeleteRedirectRequest,
|
|
21
|
+
DuplicatePageRequest,
|
|
22
|
+
GetRedirectsResponse,
|
|
23
|
+
LayoutInfo,
|
|
24
|
+
PageOperationResponse,
|
|
25
|
+
RedirectOperationResponse,
|
|
26
|
+
RedirectRule,
|
|
27
|
+
UpdateRedirectRequest,
|
|
28
|
+
} from '@nuasite/cms-types'
|
|
29
|
+
|
|
1
30
|
/** SEO tracking options */
|
|
2
31
|
export interface SeoOptions {
|
|
3
32
|
/** Whether to track SEO elements (default: true) */
|
|
@@ -23,25 +52,6 @@ export interface CmsMarkerOptions {
|
|
|
23
52
|
seo?: SeoOptions
|
|
24
53
|
}
|
|
25
54
|
|
|
26
|
-
export interface ComponentProp {
|
|
27
|
-
name: string
|
|
28
|
-
type: string
|
|
29
|
-
required: boolean
|
|
30
|
-
defaultValue?: string
|
|
31
|
-
description?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ComponentDefinition {
|
|
35
|
-
name: string
|
|
36
|
-
file: string
|
|
37
|
-
props: ComponentProp[]
|
|
38
|
-
description?: string
|
|
39
|
-
slots?: string[]
|
|
40
|
-
previewUrl?: string
|
|
41
|
-
/** Viewport width (in px) used to render the preview iframe (default: 1280) */
|
|
42
|
-
previewWidth?: number
|
|
43
|
-
}
|
|
44
|
-
|
|
45
55
|
/** Background image metadata for elements using bg-[url()] */
|
|
46
56
|
export interface BackgroundImageMetadata {
|
|
47
57
|
/** Full Tailwind class, e.g. bg-[url('/path.png')] */
|
|
@@ -219,140 +229,6 @@ export interface ComponentInstance {
|
|
|
219
229
|
isInlineArray?: boolean
|
|
220
230
|
}
|
|
221
231
|
|
|
222
|
-
/** Represents a content collection entry (markdown file) */
|
|
223
|
-
export interface CollectionEntry {
|
|
224
|
-
/** Collection name (e.g., 'services', 'blog') */
|
|
225
|
-
collectionName: string
|
|
226
|
-
/** Entry slug (e.g., '3d-tisk') */
|
|
227
|
-
collectionSlug: string
|
|
228
|
-
/** Path to the markdown file relative to project root */
|
|
229
|
-
sourcePath: string
|
|
230
|
-
/** Frontmatter fields with their values and line numbers */
|
|
231
|
-
frontmatter: Record<string, { value: string; line: number }>
|
|
232
|
-
/** Full markdown body content */
|
|
233
|
-
body: string
|
|
234
|
-
/** Line number where body starts (1-indexed) */
|
|
235
|
-
bodyStartLine: number
|
|
236
|
-
/** ID of the wrapper element containing the rendered markdown */
|
|
237
|
-
wrapperId?: string
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Field types for collection schema inference */
|
|
241
|
-
export type FieldType =
|
|
242
|
-
| 'text'
|
|
243
|
-
| 'textarea'
|
|
244
|
-
| 'date'
|
|
245
|
-
| 'datetime'
|
|
246
|
-
| 'time'
|
|
247
|
-
| 'year'
|
|
248
|
-
| 'month'
|
|
249
|
-
| 'boolean'
|
|
250
|
-
| 'number'
|
|
251
|
-
| 'image'
|
|
252
|
-
| 'file'
|
|
253
|
-
| 'url'
|
|
254
|
-
| 'email'
|
|
255
|
-
| 'tel'
|
|
256
|
-
| 'color'
|
|
257
|
-
| 'select'
|
|
258
|
-
| 'array'
|
|
259
|
-
| 'object'
|
|
260
|
-
| 'reference'
|
|
261
|
-
|
|
262
|
-
/** Editor hints for enhanced field rendering (extracted from `n.*()` options in content config) */
|
|
263
|
-
export interface FieldHints {
|
|
264
|
-
min?: number | string
|
|
265
|
-
max?: number | string
|
|
266
|
-
step?: number
|
|
267
|
-
placeholder?: string
|
|
268
|
-
maxLength?: number
|
|
269
|
-
minLength?: number
|
|
270
|
-
rows?: number
|
|
271
|
-
accept?: string
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/** Definition of a single field in a collection's schema */
|
|
275
|
-
export interface FieldDefinition {
|
|
276
|
-
/** Field name as it appears in frontmatter */
|
|
277
|
-
name: string
|
|
278
|
-
/** Inferred or specified field type */
|
|
279
|
-
type: FieldType
|
|
280
|
-
/** Whether the field is required (present in all entries) */
|
|
281
|
-
required: boolean
|
|
282
|
-
/** Default value for the field */
|
|
283
|
-
defaultValue?: unknown
|
|
284
|
-
/** Options for 'select' type fields */
|
|
285
|
-
options?: string[]
|
|
286
|
-
/** Item type for 'array' fields */
|
|
287
|
-
itemType?: FieldType
|
|
288
|
-
/** Nested fields for 'object' type */
|
|
289
|
-
fields?: FieldDefinition[]
|
|
290
|
-
/** Sample values seen across entries */
|
|
291
|
-
examples?: unknown[]
|
|
292
|
-
/** Where the field renders in the editor UI */
|
|
293
|
-
position?: 'sidebar' | 'header'
|
|
294
|
-
/** Group name for visual grouping with section headers */
|
|
295
|
-
group?: string
|
|
296
|
-
/** Referenced collection name for 'reference' type fields */
|
|
297
|
-
collection?: string
|
|
298
|
-
/** Hide from the editor UI (e.g. derived/computed fields) */
|
|
299
|
-
hidden?: boolean
|
|
300
|
-
/** Source field name this field is derived from (e.g. categoryHref derived from category) */
|
|
301
|
-
derivedFrom?: string
|
|
302
|
-
/** Editor hints for enhanced field rendering */
|
|
303
|
-
hints?: FieldHints
|
|
304
|
-
/** True when the field uses Astro's `image()` schema (entry-relative paths through astro:assets). */
|
|
305
|
-
astroImage?: boolean
|
|
306
|
-
/** Semantic role used by the editor UI to position special fields without name matching.
|
|
307
|
-
* - `publish-toggle`: boolean controlling whether the entry is published (e.g. `draft`, `isDraft`, `published`).
|
|
308
|
-
* - `publish-date`: the publish/release date field (e.g. `date`, `publishDate`, `publishedAt`). */
|
|
309
|
-
role?: 'publish-toggle' | 'publish-date'
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** Per-entry metadata for collection browsing */
|
|
313
|
-
export interface CollectionEntryInfo {
|
|
314
|
-
slug: string
|
|
315
|
-
title?: string
|
|
316
|
-
sourcePath: string
|
|
317
|
-
draft?: boolean
|
|
318
|
-
/** URL pathname of the rendered page for this entry */
|
|
319
|
-
pathname?: string
|
|
320
|
-
/** Full entry data for data collections (JSON/YAML) */
|
|
321
|
-
data?: Record<string, unknown>
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Definition of a content collection with inferred schema */
|
|
325
|
-
export interface CollectionDefinition {
|
|
326
|
-
/** Collection identifier (directory name) */
|
|
327
|
-
name: string
|
|
328
|
-
/** Human-readable label for the collection */
|
|
329
|
-
label: string
|
|
330
|
-
/** Path to the collection directory */
|
|
331
|
-
path: string
|
|
332
|
-
/** Number of entries in the collection */
|
|
333
|
-
entryCount: number
|
|
334
|
-
/** Inferred field definitions */
|
|
335
|
-
fields: FieldDefinition[]
|
|
336
|
-
/** Whether the collection has draft support */
|
|
337
|
-
supportsDraft?: boolean
|
|
338
|
-
/** Collection type: 'content' for markdown, 'data' for JSON/YAML */
|
|
339
|
-
type?: 'content' | 'data'
|
|
340
|
-
/** File extension used by entries */
|
|
341
|
-
fileExtension: 'md' | 'mdx' | 'json' | 'yaml' | 'yml'
|
|
342
|
-
/** Per-entry metadata for browsing */
|
|
343
|
-
entries?: CollectionEntryInfo[]
|
|
344
|
-
/** Frontmatter field name to sort entries by (detected from `.orderBy()` in content config) */
|
|
345
|
-
orderBy?: string
|
|
346
|
-
/** Sort direction for orderBy field */
|
|
347
|
-
orderDirection?: 'asc' | 'desc'
|
|
348
|
-
/**
|
|
349
|
-
* Name of the collection this one is nested under in the CMS browser, when it shares a base
|
|
350
|
-
* directory with another collection (e.g. a nested `*/otazky/*` collection grouped under the
|
|
351
|
-
* `*/index.md` collection at the same base). Purely presentational grouping.
|
|
352
|
-
*/
|
|
353
|
-
parentCollection?: string
|
|
354
|
-
}
|
|
355
|
-
|
|
356
232
|
/** Manifest metadata for versioning and conflict detection */
|
|
357
233
|
export interface ManifestMetadata {
|
|
358
234
|
/** Manifest schema version */
|
|
@@ -651,6 +527,13 @@ export type CmsPostMessage =
|
|
|
651
527
|
|
|
652
528
|
export interface CmsFeatures {
|
|
653
529
|
selectElement?: boolean
|
|
530
|
+
/**
|
|
531
|
+
* Controls the in-preview collection browser/management UI (browse collections,
|
|
532
|
+
* list/open entries). Defaults to enabled. When `false`, the widget hides this
|
|
533
|
+
* UI because collection editing is owned elsewhere (e.g. the webmaster
|
|
534
|
+
* Collections tab); inline text/image/color editing stays unaffected.
|
|
535
|
+
*/
|
|
536
|
+
collectionManagement?: boolean
|
|
654
537
|
}
|
|
655
538
|
|
|
656
539
|
// ============================================================================
|
|
@@ -670,75 +553,5 @@ export interface CmsSetFeaturesMessage {
|
|
|
670
553
|
/** All possible CMS postMessage types sent from the parent to the editor iframe */
|
|
671
554
|
export type CmsInboundMessage = CmsDeselectElementMessage | CmsSetFeaturesMessage
|
|
672
555
|
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
// ============================================================================
|
|
676
|
-
|
|
677
|
-
export interface CreatePageRequest {
|
|
678
|
-
title: string
|
|
679
|
-
slug: string
|
|
680
|
-
layoutPath?: string
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
export interface DuplicatePageRequest {
|
|
684
|
-
sourcePagePath: string
|
|
685
|
-
slug: string
|
|
686
|
-
title?: string
|
|
687
|
-
createRedirect?: boolean
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
export interface DeletePageRequest {
|
|
691
|
-
pagePath: string
|
|
692
|
-
createRedirect?: boolean
|
|
693
|
-
redirectTo?: string
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
export interface PageOperationResponse {
|
|
697
|
-
success: boolean
|
|
698
|
-
filePath?: string
|
|
699
|
-
slug?: string
|
|
700
|
-
url?: string
|
|
701
|
-
error?: string
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
export interface LayoutInfo {
|
|
705
|
-
name: string
|
|
706
|
-
path: string
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// ============================================================================
|
|
710
|
-
// Redirect Operations (shared between server handlers and editor UI)
|
|
711
|
-
// ============================================================================
|
|
712
|
-
|
|
713
|
-
export interface RedirectRule {
|
|
714
|
-
source: string
|
|
715
|
-
destination: string
|
|
716
|
-
statusCode: number
|
|
717
|
-
lineIndex: number
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
export interface AddRedirectRequest {
|
|
721
|
-
source: string
|
|
722
|
-
destination: string
|
|
723
|
-
statusCode?: number
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
export interface UpdateRedirectRequest {
|
|
727
|
-
lineIndex: number
|
|
728
|
-
source: string
|
|
729
|
-
destination: string
|
|
730
|
-
statusCode?: number
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
export interface DeleteRedirectRequest {
|
|
734
|
-
lineIndex: number
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export interface RedirectOperationResponse {
|
|
738
|
-
success: boolean
|
|
739
|
-
error?: string
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
export interface GetRedirectsResponse {
|
|
743
|
-
rules: RedirectRule[]
|
|
744
|
-
}
|
|
556
|
+
// Page & Redirect operation request/response types now live in @nuasite/cms-types
|
|
557
|
+
// and are re-exported at the top of this file.
|