@open_mind/editor-sdk 0.2.0
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/LICENSE.md +42 -0
- package/README.md +105 -0
- package/dist/chunk-CAWU5SFJ.js +20 -0
- package/dist/chunk-CAWU5SFJ.js.map +1 -0
- package/dist/chunk-FVPTAW36.js +10 -0
- package/dist/chunk-FVPTAW36.js.map +1 -0
- package/dist/chunk-NDL3YEJY.js +34 -0
- package/dist/chunk-NDL3YEJY.js.map +1 -0
- package/dist/client.d.ts +50 -0
- package/dist/client.js +124 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol-DC8OkoEg.d.ts +38 -0
- package/dist/runtime-A5GXJO6L.js +24 -0
- package/dist/runtime-A5GXJO6L.js.map +1 -0
- package/dist/security.d.ts +3 -0
- package/dist/security.js +7 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +7 -0
- package/dist/server.js.map +1 -0
- package/package.json +69 -0
- package/scripts/install-agent-assets.mjs +74 -0
- package/skills/fieldmatch-editable-components/SKILL.md +39 -0
- package/skills/fieldmatch-editable-components/agents/openai.yaml +4 -0
- package/skills/fieldmatch-editable-components/references/component-patterns.md +112 -0
- package/templates/FIELDMATCH_EDITOR.md +40 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Fieldmatch Customer SDK License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fieldmatch. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is source-available, not open-source. Its public availability does not grant any rights except those expressly stated below.
|
|
6
|
+
|
|
7
|
+
## 1. Limited customer license
|
|
8
|
+
|
|
9
|
+
Subject to these terms, Fieldmatch grants an authorized Fieldmatch customer a limited, non-exclusive, non-transferable, non-sublicensable, revocable license to install and use an unmodified copy of this SDK solely to connect websites owned or controlled by that customer to Fieldmatch-approved services during an active commercial relationship with Fieldmatch.
|
|
10
|
+
|
|
11
|
+
The customer may make copies strictly necessary for installation, deployment, testing, and backup, and may allow its employees and contractors to use those copies solely on its behalf and subject to these terms.
|
|
12
|
+
|
|
13
|
+
## 2. Restrictions
|
|
14
|
+
|
|
15
|
+
Except where applicable law does not permit the restriction, you may not:
|
|
16
|
+
|
|
17
|
+
- modify, adapt, translate, or create derivative works of the SDK;
|
|
18
|
+
- copy the SDK except as expressly permitted above;
|
|
19
|
+
- distribute, publish, sublicense, rent, lease, sell, transfer, or otherwise provide the SDK to any third party;
|
|
20
|
+
- reverse engineer, decompile, disassemble, or attempt to derive non-public implementation details from the SDK;
|
|
21
|
+
- use the SDK or its source code to build, train, improve, benchmark, or support a competing product or service;
|
|
22
|
+
- bypass or interfere with authentication, authorization, usage controls, or other technical safeguards; or
|
|
23
|
+
- remove or alter copyright, license, attribution, or proprietary notices.
|
|
24
|
+
|
|
25
|
+
These restrictions do not prevent a customer from changing its own website, integration code, configuration, generated `AGENTS.md` instructions, or generated `FIELDMATCH_EDITOR.md` guide.
|
|
26
|
+
|
|
27
|
+
## 3. Ownership and services
|
|
28
|
+
|
|
29
|
+
Fieldmatch and its licensors retain all right, title, and interest in the SDK. No trademark rights or implied licenses are granted. Access to Fieldmatch-hosted services, accounts, content, and data is governed separately and requires valid authorization.
|
|
30
|
+
|
|
31
|
+
## 4. Termination
|
|
32
|
+
|
|
33
|
+
This license terminates automatically if you breach these terms or cease to be an authorized Fieldmatch customer. On termination, you must stop using and delete all copies of the SDK, except where retention is required by law.
|
|
34
|
+
|
|
35
|
+
## 5. Disclaimer and limitation of liability
|
|
36
|
+
|
|
37
|
+
THE SDK IS PROVIDED “AS IS” AND “AS AVAILABLE,” WITHOUT WARRANTIES OF ANY KIND, TO THE MAXIMUM EXTENT PERMITTED BY LAW. TO THE MAXIMUM EXTENT PERMITTED BY LAW, FIELDMATCH WILL NOT BE LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR LOSS OF DATA, PROFITS, REVENUE, OR BUSINESS. FIELDMATCH'S TOTAL LIABILITY ARISING FROM THE SDK WILL NOT EXCEED THE AMOUNT PAID BY THE CUSTOMER FOR THE SDK DURING THE SIX MONTHS BEFORE THE EVENT GIVING RISE TO THE CLAIM.
|
|
38
|
+
|
|
39
|
+
## 6. Governing law
|
|
40
|
+
|
|
41
|
+
These terms are governed by the laws of the State of Israel, without regard to conflict-of-law rules. The competent courts in Tel Aviv-Jaffa, Israel have exclusive jurisdiction. Signed commercial terms between Fieldmatch and a customer prevail to the extent they expressly conflict with this license.
|
|
42
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @open_mind/editor-sdk
|
|
2
|
+
|
|
3
|
+
Publicly downloadable React and Next.js integration SDK for the Fieldmatch visual editor. The package contains only the website-side adapter; access to customer sites and content remains protected by the Fieldmatch CMS authentication and authorization layer.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @open_mind/editor-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
No npm token or custom registry is required. Installation also:
|
|
12
|
+
|
|
13
|
+
- installs the repository skill at `.agents/skills/fieldmatch-editable-components`;
|
|
14
|
+
- creates `FIELDMATCH_EDITOR.md`, the integration registry and checklist; and
|
|
15
|
+
- adds a managed Fieldmatch section to the root `AGENTS.md` without replacing existing instructions.
|
|
16
|
+
|
|
17
|
+
Restart Codex or another compatible coding agent after the first installation so it reloads `AGENTS.md`. If lifecycle scripts are disabled, run:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx fieldmatch-editor-sdk-init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Set `FIELDMATCH_SKIP_AGENT_SETUP=1` to opt out of generating agent files.
|
|
24
|
+
|
|
25
|
+
## Fetch content on the server
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { getSiteContent } from '@open_mind/editor-sdk/server'
|
|
29
|
+
|
|
30
|
+
const response = await getSiteContent({
|
|
31
|
+
siteId: process.env.NEXT_PUBLIC_FIELDMATCH_SITE_ID!,
|
|
32
|
+
page: 'home',
|
|
33
|
+
apiUrl: process.env.FIELDMATCH_CMS_URL,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const content = response.fields
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Normal traffic should use published content. Draft mode requires a server-only preview token; never expose it through a `NEXT_PUBLIC_` variable.
|
|
40
|
+
|
|
41
|
+
## Add the provider
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { EditorProvider } from '@open_mind/editor-sdk/client'
|
|
45
|
+
|
|
46
|
+
<EditorProvider
|
|
47
|
+
siteId={process.env.NEXT_PUBLIC_FIELDMATCH_SITE_ID!}
|
|
48
|
+
page="home"
|
|
49
|
+
editorOrigin={process.env.NEXT_PUBLIC_FIELDMATCH_EDITOR_ORIGIN!}
|
|
50
|
+
initialContent={content}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</EditorProvider>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use one provider around the editable page area.
|
|
57
|
+
|
|
58
|
+
## Mark content as editable
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import {
|
|
62
|
+
EditableImage,
|
|
63
|
+
EditableLink,
|
|
64
|
+
EditableText,
|
|
65
|
+
EditableVideo,
|
|
66
|
+
} from '@open_mind/editor-sdk/client'
|
|
67
|
+
|
|
68
|
+
<EditableText id="home.hero.title" page="home" label="Hero title">
|
|
69
|
+
<h1>Your local fallback title</h1>
|
|
70
|
+
</EditableText>
|
|
71
|
+
|
|
72
|
+
<EditableText id="home.hero.description" page="home" label="Hero description" type="textarea">
|
|
73
|
+
<p>Your local fallback description.</p>
|
|
74
|
+
</EditableText>
|
|
75
|
+
|
|
76
|
+
<EditableLink id="home.hero.cta.href" page="home" label="CTA destination" href="/contact">
|
|
77
|
+
Contact us
|
|
78
|
+
</EditableLink>
|
|
79
|
+
|
|
80
|
+
<EditableImage
|
|
81
|
+
id="home.hero.image"
|
|
82
|
+
page="home"
|
|
83
|
+
label="Hero image"
|
|
84
|
+
src="/hero.jpg"
|
|
85
|
+
alt="Product on a neutral background"
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<EditableVideo
|
|
89
|
+
id="home.hero.video"
|
|
90
|
+
page="home"
|
|
91
|
+
label="Hero video"
|
|
92
|
+
src="https://cdn.example.com/hero.mp4"
|
|
93
|
+
controls
|
|
94
|
+
/>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Field IDs are persistent content keys. Use stable names such as `page.section.element`; renaming an ID requires a content migration. Every field should retain a useful local fallback.
|
|
98
|
+
|
|
99
|
+
Image fields support the Fieldmatch media library. Video fields currently accept a direct HTTP(S) URL; this version does not upload video files.
|
|
100
|
+
|
|
101
|
+
See the generated `FIELDMATCH_EDITOR.md` and use the `$fieldmatch-editable-components` skill for the full integration workflow, CSP, preview, and verification guidance.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
The package is publicly downloadable but is not open-source. Use is limited to authorized Fieldmatch customers and unmodified customer integrations. See [LICENSE.md](./LICENSE.md).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
async function getSiteContent(options) {
|
|
3
|
+
const mode = options.mode ?? "published";
|
|
4
|
+
if (mode === "draft" && !options.previewToken) throw new Error("A preview token is required for draft content.");
|
|
5
|
+
const apiUrl = options.apiUrl ?? process.env.CMS_INTERNAL_URL ?? process.env.NEXT_PUBLIC_CMS_URL;
|
|
6
|
+
if (!apiUrl) throw new Error("CMS API URL is not configured.");
|
|
7
|
+
const request = options.fetcher ?? fetch;
|
|
8
|
+
const response = await request(`${apiUrl}/api/v1/sites/${encodeURIComponent(options.siteId)}/pages/${encodeURIComponent(options.page)}/content?mode=${mode}`, {
|
|
9
|
+
headers: options.previewToken ? { authorization: `Bearer ${options.previewToken}` } : void 0,
|
|
10
|
+
cache: mode === "draft" ? "no-store" : "force-cache",
|
|
11
|
+
...mode === "published" ? { next: { tags: [`agency-site:${options.siteId}`, `agency-page:${options.siteId}:${options.page}`], revalidate: 300 } } : {}
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) throw new Error(`Content request failed (${response.status}).`);
|
|
14
|
+
return response.json();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
getSiteContent
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=chunk-CAWU5SFJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import type { ContentResponse } from './protocol'\n\nexport interface GetSiteContentOptions {\n siteId: string\n page: string\n mode?: 'published' | 'draft'\n apiUrl?: string\n previewToken?: string\n fetcher?: typeof fetch\n}\n\nexport async function getSiteContent(options: GetSiteContentOptions): Promise<ContentResponse> {\n const mode = options.mode ?? 'published'\n if (mode === 'draft' && !options.previewToken) throw new Error('A preview token is required for draft content.')\n const apiUrl = options.apiUrl ?? process.env.CMS_INTERNAL_URL ?? process.env.NEXT_PUBLIC_CMS_URL\n if (!apiUrl) throw new Error('CMS API URL is not configured.')\n const request = options.fetcher ?? fetch\n const response = await request(`${apiUrl}/api/v1/sites/${encodeURIComponent(options.siteId)}/pages/${encodeURIComponent(options.page)}/content?mode=${mode}`, {\n headers: options.previewToken ? { authorization: `Bearer ${options.previewToken}` } : undefined,\n cache: mode === 'draft' ? 'no-store' : 'force-cache',\n ...(mode === 'published' ? { next: { tags: [`agency-site:${options.siteId}`, `agency-page:${options.siteId}:${options.page}`], revalidate: 300 } } : {}),\n } as RequestInit)\n if (!response.ok) throw new Error(`Content request failed (${response.status}).`)\n return response.json() as Promise<ContentResponse>\n}\n"],"mappings":";AAWA,eAAsB,eAAe,SAA0D;AAC7F,QAAM,OAAO,QAAQ,QAAQ;AAC7B,MAAI,SAAS,WAAW,CAAC,QAAQ,aAAc,OAAM,IAAI,MAAM,gDAAgD;AAC/G,QAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,oBAAoB,QAAQ,IAAI;AAC7E,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,gCAAgC;AAC7D,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,WAAW,MAAM,QAAQ,GAAG,MAAM,iBAAiB,mBAAmB,QAAQ,MAAM,CAAC,UAAU,mBAAmB,QAAQ,IAAI,CAAC,iBAAiB,IAAI,IAAI;AAAA,IAC5J,SAAS,QAAQ,eAAe,EAAE,eAAe,UAAU,QAAQ,YAAY,GAAG,IAAI;AAAA,IACtF,OAAO,SAAS,UAAU,aAAa;AAAA,IACvC,GAAI,SAAS,cAAc,EAAE,MAAM,EAAE,MAAM,CAAC,eAAe,QAAQ,MAAM,IAAI,eAAe,QAAQ,MAAM,IAAI,QAAQ,IAAI,EAAE,GAAG,YAAY,IAAI,EAAE,IAAI,CAAC;AAAA,EACxJ,CAAgB;AAChB,MAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,2BAA2B,SAAS,MAAM,IAAI;AAChF,SAAO,SAAS,KAAK;AACvB;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/security.ts"],"sourcesContent":["export function editorFrameAncestors(editorOrigin: string): string {\n const origin = new URL(editorOrigin).origin\n return `frame-ancestors 'self' ${origin}`\n}\n"],"mappings":";AAAO,SAAS,qBAAqB,cAA8B;AACjE,QAAM,SAAS,IAAI,IAAI,YAAY,EAAE;AACrC,SAAO,0BAA0B,MAAM;AACzC;","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/protocol.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var fieldTypes = ["text", "textarea", "richText", "image", "video", "link", "buttonLabel"];
|
|
4
|
+
var fieldSchema = z.object({
|
|
5
|
+
id: z.string().min(3).max(160).regex(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/i),
|
|
6
|
+
page: z.string().min(1).max(80),
|
|
7
|
+
label: z.string().min(1).max(120),
|
|
8
|
+
type: z.enum(fieldTypes),
|
|
9
|
+
required: z.boolean().optional(),
|
|
10
|
+
maxLength: z.number().int().positive().max(2e4).optional(),
|
|
11
|
+
initialValue: z.unknown().optional()
|
|
12
|
+
});
|
|
13
|
+
var protocolVersion = 1;
|
|
14
|
+
var envelope = z.object({ version: z.literal(protocolVersion), nonce: z.string().optional() });
|
|
15
|
+
var editorMessageSchema = z.discriminatedUnion("type", [
|
|
16
|
+
envelope.extend({ type: z.literal("EDITOR_READY"), payload: z.object({ siteId: z.string(), page: z.string() }) }),
|
|
17
|
+
envelope.extend({ type: z.literal("EDITOR_INIT"), payload: z.object({ siteId: z.string(), page: z.string(), permissions: z.array(z.enum(["preview", "edit"])), content: z.record(z.string(), z.unknown()) }) }),
|
|
18
|
+
envelope.extend({ type: z.literal("REGISTER_FIELDS"), payload: z.object({ siteId: z.string(), page: z.string(), fields: z.array(fieldSchema) }) }),
|
|
19
|
+
envelope.extend({ type: z.literal("FIELD_SELECTED"), payload: z.object({ siteId: z.string(), page: z.string(), fieldId: z.string(), fieldType: z.enum(fieldTypes) }) }),
|
|
20
|
+
envelope.extend({ type: z.literal("FIELD_HOVERED"), payload: z.object({ fieldId: z.string().nullable() }) }),
|
|
21
|
+
envelope.extend({ type: z.literal("PREVIEW_UPDATE"), payload: z.object({ fieldId: z.string(), value: z.unknown() }) }),
|
|
22
|
+
envelope.extend({ type: z.literal("CONTENT_SAVED"), payload: z.object({ fieldId: z.string(), savedAt: z.string() }) }),
|
|
23
|
+
envelope.extend({ type: z.literal("NAVIGATE_PAGE"), payload: z.object({ page: z.string(), href: z.string() }) })
|
|
24
|
+
]);
|
|
25
|
+
function parseEditorMessage(value) {
|
|
26
|
+
const parsed = editorMessageSchema.safeParse(value);
|
|
27
|
+
return parsed.success ? parsed.data : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
protocolVersion,
|
|
32
|
+
parseEditorMessage
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=chunk-NDL3YEJY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol.ts"],"sourcesContent":["import { z } from 'zod'\n\nexport const fieldTypes = ['text', 'textarea', 'richText', 'image', 'video', 'link', 'buttonLabel'] as const\nexport type FieldType = (typeof fieldTypes)[number]\n\nexport type ImageValue = { assetId: string; url: string; alt: string }\nexport type ContentValue = string | ImageValue | Record<string, unknown>\n\nexport const fieldSchema = z.object({\n id: z.string().min(3).max(160).regex(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/i),\n page: z.string().min(1).max(80),\n label: z.string().min(1).max(120),\n type: z.enum(fieldTypes),\n required: z.boolean().optional(),\n maxLength: z.number().int().positive().max(20_000).optional(),\n initialValue: z.unknown().optional(),\n})\nexport type EditableFieldSchema = z.infer<typeof fieldSchema>\n\nexport interface ContentResponse {\n siteId: string\n page: string\n mode: 'published' | 'draft'\n fields: Record<string, ContentValue>\n schemas: EditableFieldSchema[]\n updatedAt: string\n}\n\nexport const protocolVersion = 1 as const\nconst envelope = z.object({ version: z.literal(protocolVersion), nonce: z.string().optional() })\n\nexport const editorMessageSchema = z.discriminatedUnion('type', [\n envelope.extend({ type: z.literal('EDITOR_READY'), payload: z.object({ siteId: z.string(), page: z.string() }) }),\n envelope.extend({ type: z.literal('EDITOR_INIT'), payload: z.object({ siteId: z.string(), page: z.string(), permissions: z.array(z.enum(['preview', 'edit'])), content: z.record(z.string(), z.unknown()) }) }),\n envelope.extend({ type: z.literal('REGISTER_FIELDS'), payload: z.object({ siteId: z.string(), page: z.string(), fields: z.array(fieldSchema) }) }),\n envelope.extend({ type: z.literal('FIELD_SELECTED'), payload: z.object({ siteId: z.string(), page: z.string(), fieldId: z.string(), fieldType: z.enum(fieldTypes) }) }),\n envelope.extend({ type: z.literal('FIELD_HOVERED'), payload: z.object({ fieldId: z.string().nullable() }) }),\n envelope.extend({ type: z.literal('PREVIEW_UPDATE'), payload: z.object({ fieldId: z.string(), value: z.unknown() }) }),\n envelope.extend({ type: z.literal('CONTENT_SAVED'), payload: z.object({ fieldId: z.string(), savedAt: z.string() }) }),\n envelope.extend({ type: z.literal('NAVIGATE_PAGE'), payload: z.object({ page: z.string(), href: z.string() }) }),\n])\nexport type EditorMessage = z.infer<typeof editorMessageSchema>\n\nexport function parseEditorMessage(value: unknown): EditorMessage | null {\n const parsed = editorMessageSchema.safeParse(value)\n return parsed.success ? parsed.data : null\n}\n"],"mappings":";AAAA,SAAS,SAAS;AAEX,IAAM,aAAa,CAAC,QAAQ,YAAY,YAAY,SAAS,SAAS,QAAQ,aAAa;AAM3F,IAAM,cAAc,EAAE,OAAO;AAAA,EAClC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,MAAM,iCAAiC;AAAA,EACtE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EAC9B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAChC,MAAM,EAAE,KAAK,UAAU;AAAA,EACvB,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC/B,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAM,EAAE,SAAS;AAAA,EAC5D,cAAc,EAAE,QAAQ,EAAE,SAAS;AACrC,CAAC;AAYM,IAAM,kBAAkB;AAC/B,IAAM,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,eAAe,GAAG,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AAExF,IAAM,sBAAsB,EAAE,mBAAmB,QAAQ;AAAA,EAC9D,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,cAAc,GAAG,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAAA,EAChH,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,aAAa,GAAG,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,GAAG,aAAa,EAAE,MAAM,EAAE,KAAK,CAAC,WAAW,MAAM,CAAC,CAAC,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;AAAA,EAC9M,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,iBAAiB,GAAG,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,GAAG,QAAQ,EAAE,MAAM,WAAW,EAAE,CAAC,EAAE,CAAC;AAAA,EACjJ,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,gBAAgB,GAAG,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,GAAG,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC,EAAE,CAAC;AAAA,EACtK,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,eAAe,GAAG,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;AAAA,EAC3G,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,gBAAgB,GAAG,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AAAA,EACrH,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,eAAe,GAAG,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAAA,EACrH,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,eAAe,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AACjH,CAAC;AAGM,SAAS,mBAAmB,OAAsC;AACvE,QAAM,SAAS,oBAAoB,UAAU,KAAK;AAClD,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;","names":[]}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ReactElement, VideoHTMLAttributes } from 'react';
|
|
3
|
+
import { F as FieldType, a as ContentValue } from './protocol-DC8OkoEg.js';
|
|
4
|
+
import 'zod';
|
|
5
|
+
|
|
6
|
+
interface EditorProviderProps {
|
|
7
|
+
siteId: string;
|
|
8
|
+
page: string;
|
|
9
|
+
editorOrigin: string;
|
|
10
|
+
initialContent: Record<string, ContentValue>;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
declare function EditorProvider({ siteId, page, editorOrigin, initialContent, children }: EditorProviderProps): react.JSX.Element;
|
|
14
|
+
declare function EditableText({ id, page, label, type, maxLength, required, initialValue, children }: {
|
|
15
|
+
id: string;
|
|
16
|
+
page: string;
|
|
17
|
+
label: string;
|
|
18
|
+
type?: Exclude<FieldType, 'image' | 'video'>;
|
|
19
|
+
maxLength?: number;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
initialValue?: string;
|
|
22
|
+
children: ReactElement<Record<string, unknown>>;
|
|
23
|
+
}): ReactElement<Record<string, unknown>, string | react.JSXElementConstructor<any>>;
|
|
24
|
+
declare function EditableImage({ id, page, label, src, alt, assetId, className }: {
|
|
25
|
+
id: string;
|
|
26
|
+
page: string;
|
|
27
|
+
label: string;
|
|
28
|
+
src: string;
|
|
29
|
+
alt: string;
|
|
30
|
+
assetId?: string;
|
|
31
|
+
className?: string;
|
|
32
|
+
}): react.JSX.Element;
|
|
33
|
+
declare function EditableVideo({ id, page, label, src, className, style, ...videoProps }: {
|
|
34
|
+
id: string;
|
|
35
|
+
page: string;
|
|
36
|
+
label: string;
|
|
37
|
+
src: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
} & Omit<VideoHTMLAttributes<HTMLVideoElement>, 'src' | 'children'>): react.JSX.Element;
|
|
40
|
+
declare function EditableLink({ id, page, label, href, children, className }: {
|
|
41
|
+
id: string;
|
|
42
|
+
page: string;
|
|
43
|
+
label: string;
|
|
44
|
+
href: string;
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
className?: string;
|
|
47
|
+
}): react.JSX.Element;
|
|
48
|
+
declare function useEditableContent<T extends ContentValue>(id: string, fallback: T): T;
|
|
49
|
+
|
|
50
|
+
export { EditableImage, EditableLink, EditableText, EditableVideo, EditorProvider, type EditorProviderProps, useEditableContent };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
protocolVersion
|
|
4
|
+
} from "./chunk-NDL3YEJY.js";
|
|
5
|
+
|
|
6
|
+
// src/client.tsx
|
|
7
|
+
import { Children, cloneElement, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { jsx } from "react/jsx-runtime";
|
|
9
|
+
var EditorContext = createContext(null);
|
|
10
|
+
function EditorProvider({ siteId, page, editorOrigin, initialContent, children }) {
|
|
11
|
+
const [enabled, setEnabled] = useState(false);
|
|
12
|
+
const [content, setContent] = useState(initialContent);
|
|
13
|
+
const nonce = useRef(void 0);
|
|
14
|
+
const schemas = useRef(/* @__PURE__ */ new Map());
|
|
15
|
+
const register = useCallback((schema) => {
|
|
16
|
+
const current = schemas.current.get(schema.id);
|
|
17
|
+
schemas.current.set(schema.id, { schema, registrations: (current?.registrations ?? 0) + 1 });
|
|
18
|
+
return () => {
|
|
19
|
+
const registered = schemas.current.get(schema.id);
|
|
20
|
+
if (!registered || registered.registrations <= 1) schemas.current.delete(schema.id);
|
|
21
|
+
else schemas.current.set(schema.id, { ...registered, registrations: registered.registrations - 1 });
|
|
22
|
+
};
|
|
23
|
+
}, []);
|
|
24
|
+
const send = useCallback((message) => {
|
|
25
|
+
if (!enabled) return;
|
|
26
|
+
window.parent.postMessage({ ...message, nonce: nonce.current }, new URL(editorOrigin).origin);
|
|
27
|
+
}, [editorOrigin, enabled]);
|
|
28
|
+
const select = useCallback((schema) => send({ version: protocolVersion, type: "FIELD_SELECTED", payload: { siteId, page, fieldId: schema.id, fieldType: schema.type } }), [page, send, siteId]);
|
|
29
|
+
const hover = useCallback((fieldId) => send({ version: protocolVersion, type: "FIELD_HOVERED", payload: { fieldId } }), [send]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const params = new URLSearchParams(window.location.search);
|
|
32
|
+
if (params.get("editorMode") !== "true" || window.parent === window) return;
|
|
33
|
+
let stop;
|
|
34
|
+
void import("./runtime-A5GXJO6L.js").then(({ startEditorRuntime }) => {
|
|
35
|
+
stop = startEditorRuntime({ siteId, page, editorOrigin, schemas: () => [...schemas.current.values()].map(({ schema }) => schema), onMessage(message) {
|
|
36
|
+
if (message.type !== "EDITOR_INIT") return;
|
|
37
|
+
nonce.current = message.nonce ?? "";
|
|
38
|
+
setContent((current) => ({ ...current, ...message.payload.content }));
|
|
39
|
+
setEnabled(true);
|
|
40
|
+
} });
|
|
41
|
+
});
|
|
42
|
+
return () => stop?.();
|
|
43
|
+
}, [editorOrigin, page, siteId]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!enabled) return;
|
|
46
|
+
const origin = new URL(editorOrigin).origin;
|
|
47
|
+
const listener = (event) => {
|
|
48
|
+
if (event.origin !== origin || event.source !== window.parent) return;
|
|
49
|
+
const candidate = event.data;
|
|
50
|
+
if (candidate.type === "PREVIEW_UPDATE" && candidate.nonce === nonce.current && candidate.payload && "fieldId" in candidate.payload) {
|
|
51
|
+
const payload = candidate.payload;
|
|
52
|
+
setContent((current) => ({ ...current, [payload.fieldId]: payload.value }));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener("message", listener);
|
|
56
|
+
return () => window.removeEventListener("message", listener);
|
|
57
|
+
}, [editorOrigin, enabled]);
|
|
58
|
+
const value = useMemo(() => ({ enabled, content, register, select, hover, send }), [content, enabled, hover, register, select, send]);
|
|
59
|
+
return /* @__PURE__ */ jsx(EditorContext.Provider, { value, children });
|
|
60
|
+
}
|
|
61
|
+
function useEditable(schema) {
|
|
62
|
+
const context = useContext(EditorContext);
|
|
63
|
+
useEffect(() => context?.register(schema), [context, schema.id, schema.label, schema.maxLength, schema.page, schema.required, schema.type]);
|
|
64
|
+
const interactions = context?.enabled ? {
|
|
65
|
+
onClick: (event) => {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
event.stopPropagation();
|
|
68
|
+
context.select(schema);
|
|
69
|
+
},
|
|
70
|
+
onMouseEnter: () => context.hover(schema.id),
|
|
71
|
+
onMouseLeave: () => context.hover(null),
|
|
72
|
+
"data-agency-editable": schema.id
|
|
73
|
+
} : {};
|
|
74
|
+
return { context, interactions };
|
|
75
|
+
}
|
|
76
|
+
var editableStyle = { outline: "1px solid color-mix(in srgb, #7c3aed 55%, transparent)", outlineOffset: "3px", cursor: "pointer" };
|
|
77
|
+
var emptyEditableStyle = { display: "inline-block", minWidth: "8rem", minHeight: "1.5em", backgroundColor: "color-mix(in srgb, #7c3aed 8%, transparent)" };
|
|
78
|
+
function textContent(value) {
|
|
79
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
80
|
+
const parts = Children.toArray(value);
|
|
81
|
+
return parts.every((part) => typeof part === "string" || typeof part === "number") ? parts.join("") : void 0;
|
|
82
|
+
}
|
|
83
|
+
function EditableText({ id, page, label, type = "text", maxLength, required, initialValue, children }) {
|
|
84
|
+
const existing = children.props;
|
|
85
|
+
const fallback = initialValue ?? textContent(existing.children);
|
|
86
|
+
const schema = useMemo(() => ({ id, page, label, type, ...maxLength ? { maxLength } : {}, ...required === void 0 ? {} : { required }, ...fallback === void 0 ? {} : { initialValue: fallback } }), [fallback, id, label, maxLength, page, required, type]);
|
|
87
|
+
const { context, interactions } = useEditable(schema);
|
|
88
|
+
const value = context?.content[id];
|
|
89
|
+
const renderedValue = typeof value === "string" ? value : existing.children;
|
|
90
|
+
const empty = context?.enabled && renderedValue === "";
|
|
91
|
+
return cloneElement(children, { ...interactions, ...empty ? { "aria-label": `${label}: empty field`, title: `${label} \u2014 click to add content` } : {}, style: context?.enabled ? { ...existing.style, ...editableStyle, ...empty ? emptyEditableStyle : {} } : existing.style }, empty ? "\xA0" : renderedValue);
|
|
92
|
+
}
|
|
93
|
+
function EditableImage({ id, page, label, src, alt, assetId = "external", className }) {
|
|
94
|
+
const schema = useMemo(() => ({ id, page, label, type: "image", initialValue: { assetId, url: src, alt } }), [alt, assetId, id, label, page, src]);
|
|
95
|
+
const { context, interactions } = useEditable(schema);
|
|
96
|
+
const value = context?.content[id];
|
|
97
|
+
const image = value && typeof value === "object" && "url" in value ? value : { url: src, alt };
|
|
98
|
+
return /* @__PURE__ */ jsx("img", { src: image.url, alt: image.alt, className, ...interactions, style: context?.enabled ? editableStyle : void 0 });
|
|
99
|
+
}
|
|
100
|
+
function EditableVideo({ id, page, label, src, className, style, ...videoProps }) {
|
|
101
|
+
const schema = useMemo(() => ({ id, page, label, type: "video", initialValue: src }), [id, label, page, src]);
|
|
102
|
+
const { context, interactions } = useEditable(schema);
|
|
103
|
+
const value = context?.content[id];
|
|
104
|
+
return /* @__PURE__ */ jsx("video", { ...videoProps, ...interactions, src: typeof value === "string" ? value : src, className, style: context?.enabled ? { ...style, ...editableStyle } : style });
|
|
105
|
+
}
|
|
106
|
+
function EditableLink({ id, page, label, href, children, className }) {
|
|
107
|
+
const schema = useMemo(() => ({ id, page, label, type: "link", initialValue: href }), [href, id, label, page]);
|
|
108
|
+
const { context, interactions } = useEditable(schema);
|
|
109
|
+
const value = context?.content[id];
|
|
110
|
+
return /* @__PURE__ */ jsx("a", { href: typeof value === "string" ? value : href, className, ...interactions, style: context?.enabled ? editableStyle : void 0, children: Children.toArray(children) });
|
|
111
|
+
}
|
|
112
|
+
function useEditableContent(id, fallback) {
|
|
113
|
+
const context = useContext(EditorContext);
|
|
114
|
+
return context?.content[id] ?? fallback;
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
EditableImage,
|
|
118
|
+
EditableLink,
|
|
119
|
+
EditableText,
|
|
120
|
+
EditableVideo,
|
|
121
|
+
EditorProvider,
|
|
122
|
+
useEditableContent
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.tsx"],"sourcesContent":["'use client'\n\nimport { protocolVersion, type ContentValue, type EditableFieldSchema, type EditorMessage, type FieldType } from './protocol'\nimport { Children, cloneElement, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type CSSProperties, type MouseEvent, type ReactElement, type ReactNode, type VideoHTMLAttributes } from 'react'\n\ninterface EditorContextValue {\n enabled: boolean\n content: Record<string, ContentValue>\n register: (schema: EditableFieldSchema) => () => void\n select: (schema: EditableFieldSchema) => void\n hover: (id: string | null) => void\n send: (message: EditorMessage) => void\n}\nconst EditorContext = createContext<EditorContextValue | null>(null)\n\nexport interface EditorProviderProps {\n siteId: string\n page: string\n editorOrigin: string\n initialContent: Record<string, ContentValue>\n children: ReactNode\n}\n\nexport function EditorProvider({ siteId, page, editorOrigin, initialContent, children }: EditorProviderProps) {\n const [enabled, setEnabled] = useState(false)\n const [content, setContent] = useState(initialContent)\n const nonce = useRef<string | undefined>(undefined)\n const schemas = useRef(new Map<string, { schema: EditableFieldSchema; registrations: number }>())\n const register = useCallback((schema: EditableFieldSchema) => {\n const current = schemas.current.get(schema.id)\n schemas.current.set(schema.id, { schema, registrations: (current?.registrations ?? 0) + 1 })\n return () => {\n const registered = schemas.current.get(schema.id)\n if (!registered || registered.registrations <= 1) schemas.current.delete(schema.id)\n else schemas.current.set(schema.id, { ...registered, registrations: registered.registrations - 1 })\n }\n }, [])\n const send = useCallback((message: EditorMessage) => {\n if (!enabled) return\n window.parent.postMessage({ ...message, nonce: nonce.current }, new URL(editorOrigin).origin)\n }, [editorOrigin, enabled])\n const select = useCallback((schema: EditableFieldSchema) => send({ version: protocolVersion, type: 'FIELD_SELECTED', payload: { siteId, page, fieldId: schema.id, fieldType: schema.type } }), [page, send, siteId])\n const hover = useCallback((fieldId: string | null) => send({ version: protocolVersion, type: 'FIELD_HOVERED', payload: { fieldId } }), [send])\n\n useEffect(() => {\n const params = new URLSearchParams(window.location.search)\n if (params.get('editorMode') !== 'true' || window.parent === window) return\n let stop: (() => void) | undefined\n void import('./runtime').then(({ startEditorRuntime }) => {\n stop = startEditorRuntime({ siteId, page, editorOrigin, schemas: () => [...schemas.current.values()].map(({ schema }) => schema), onMessage(message) {\n if (message.type !== 'EDITOR_INIT') return\n nonce.current = message.nonce ?? ''\n setContent((current) => ({ ...current, ...message.payload.content } as Record<string, ContentValue>))\n setEnabled(true)\n } })\n })\n return () => stop?.()\n }, [editorOrigin, page, siteId])\n\n useEffect(() => {\n if (!enabled) return\n const origin = new URL(editorOrigin).origin\n const listener = (event: MessageEvent<unknown>) => {\n if (event.origin !== origin || event.source !== window.parent) return\n const candidate = event.data as Partial<EditorMessage>\n if (candidate.type === 'PREVIEW_UPDATE' && candidate.nonce === nonce.current && candidate.payload && 'fieldId' in candidate.payload) {\n const payload = candidate.payload as { fieldId: string; value: ContentValue }\n setContent((current) => ({ ...current, [payload.fieldId]: payload.value }))\n }\n }\n window.addEventListener('message', listener)\n return () => window.removeEventListener('message', listener)\n }, [editorOrigin, enabled])\n\n const value = useMemo(() => ({ enabled, content, register, select, hover, send }), [content, enabled, hover, register, select, send])\n return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>\n}\n\nfunction useEditable(schema: EditableFieldSchema) {\n const context = useContext(EditorContext)\n useEffect(() => context?.register(schema), [context, schema.id, schema.label, schema.maxLength, schema.page, schema.required, schema.type])\n const interactions = context?.enabled ? {\n onClick: (event: MouseEvent) => { event.preventDefault(); event.stopPropagation(); context.select(schema) },\n onMouseEnter: () => context.hover(schema.id), onMouseLeave: () => context.hover(null),\n 'data-agency-editable': schema.id,\n } : {}\n return { context, interactions }\n}\n\nconst editableStyle: CSSProperties = { outline: '1px solid color-mix(in srgb, #7c3aed 55%, transparent)', outlineOffset: '3px', cursor: 'pointer' }\nconst emptyEditableStyle: CSSProperties = { display: 'inline-block', minWidth: '8rem', minHeight: '1.5em', backgroundColor: 'color-mix(in srgb, #7c3aed 8%, transparent)' }\n\nfunction textContent(value: ReactNode): string | undefined {\n if (typeof value === 'string' || typeof value === 'number') return String(value)\n const parts = Children.toArray(value)\n return parts.every((part) => typeof part === 'string' || typeof part === 'number') ? parts.join('') : undefined\n}\n\nexport function EditableText({ id, page, label, type = 'text', maxLength, required, initialValue, children }: { id: string; page: string; label: string; type?: Exclude<FieldType, 'image' | 'video'>; maxLength?: number; required?: boolean; initialValue?: string; children: ReactElement<Record<string, unknown>> }) {\n const existing = children.props as { children?: ReactNode; style?: CSSProperties }\n const fallback = initialValue ?? textContent(existing.children)\n const schema = useMemo(() => ({ id, page, label, type, ...(maxLength ? { maxLength } : {}), ...(required === undefined ? {} : { required }), ...(fallback === undefined ? {} : { initialValue: fallback }) }), [fallback, id, label, maxLength, page, required, type])\n const { context, interactions } = useEditable(schema)\n const value = context?.content[id]\n const renderedValue = typeof value === 'string' ? value : existing.children\n const empty = context?.enabled && renderedValue === ''\n return cloneElement(children, { ...interactions, ...(empty ? { 'aria-label': `${label}: empty field`, title: `${label} — click to add content` } : {}), style: context?.enabled ? { ...existing.style, ...editableStyle, ...(empty ? emptyEditableStyle : {}) } : existing.style }, empty ? '\\u00a0' : renderedValue)\n}\n\nexport function EditableImage({ id, page, label, src, alt, assetId = 'external', className }: { id: string; page: string; label: string; src: string; alt: string; assetId?: string; className?: string }) {\n const schema = useMemo(() => ({ id, page, label, type: 'image' as const, initialValue: { assetId, url: src, alt } }), [alt, assetId, id, label, page, src])\n const { context, interactions } = useEditable(schema)\n const value = context?.content[id]\n const image = value && typeof value === 'object' && 'url' in value ? value as { url: string; alt: string } : { url: src, alt }\n return <img src={image.url} alt={image.alt} className={className} {...interactions} style={context?.enabled ? editableStyle : undefined} />\n}\n\nexport function EditableVideo({ id, page, label, src, className, style, ...videoProps }: { id: string; page: string; label: string; src: string; className?: string } & Omit<VideoHTMLAttributes<HTMLVideoElement>, 'src' | 'children'>) {\n const schema = useMemo(() => ({ id, page, label, type: 'video' as const, initialValue: src }), [id, label, page, src])\n const { context, interactions } = useEditable(schema)\n const value = context?.content[id]\n return <video {...videoProps} {...interactions} src={typeof value === 'string' ? value : src} className={className} style={context?.enabled ? { ...style, ...editableStyle } : style} />\n}\n\nexport function EditableLink({ id, page, label, href, children, className }: { id: string; page: string; label: string; href: string; children: ReactNode; className?: string }) {\n const schema = useMemo(() => ({ id, page, label, type: 'link' as const, initialValue: href }), [href, id, label, page])\n const { context, interactions } = useEditable(schema)\n const value = context?.content[id]\n return <a href={typeof value === 'string' ? value : href} className={className} {...interactions} style={context?.enabled ? editableStyle : undefined}>{Children.toArray(children)}</a>\n}\n\nexport function useEditableContent<T extends ContentValue>(id: string, fallback: T): T {\n const context = useContext(EditorContext)\n return (context?.content[id] as T | undefined) ?? fallback\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
declare const fieldTypes: readonly ["text", "textarea", "richText", "image", "video", "link", "buttonLabel"];
|
|
4
|
+
type FieldType = (typeof fieldTypes)[number];
|
|
5
|
+
type ImageValue = {
|
|
6
|
+
assetId: string;
|
|
7
|
+
url: string;
|
|
8
|
+
alt: string;
|
|
9
|
+
};
|
|
10
|
+
type ContentValue = string | ImageValue | Record<string, unknown>;
|
|
11
|
+
declare const fieldSchema: z.ZodObject<{
|
|
12
|
+
id: z.ZodString;
|
|
13
|
+
page: z.ZodString;
|
|
14
|
+
label: z.ZodString;
|
|
15
|
+
type: z.ZodEnum<{
|
|
16
|
+
text: "text";
|
|
17
|
+
textarea: "textarea";
|
|
18
|
+
richText: "richText";
|
|
19
|
+
image: "image";
|
|
20
|
+
video: "video";
|
|
21
|
+
link: "link";
|
|
22
|
+
buttonLabel: "buttonLabel";
|
|
23
|
+
}>;
|
|
24
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
25
|
+
maxLength: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
initialValue: z.ZodOptional<z.ZodUnknown>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
type EditableFieldSchema = z.infer<typeof fieldSchema>;
|
|
29
|
+
interface ContentResponse {
|
|
30
|
+
siteId: string;
|
|
31
|
+
page: string;
|
|
32
|
+
mode: 'published' | 'draft';
|
|
33
|
+
fields: Record<string, ContentValue>;
|
|
34
|
+
schemas: EditableFieldSchema[];
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type { ContentResponse as C, EditableFieldSchema as E, FieldType as F, ImageValue as I, ContentValue as a };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseEditorMessage,
|
|
3
|
+
protocolVersion
|
|
4
|
+
} from "./chunk-NDL3YEJY.js";
|
|
5
|
+
|
|
6
|
+
// src/runtime.ts
|
|
7
|
+
function startEditorRuntime(options) {
|
|
8
|
+
const origin = new URL(options.editorOrigin).origin;
|
|
9
|
+
const ready = { version: protocolVersion, type: "EDITOR_READY", payload: { siteId: options.siteId, page: options.page } };
|
|
10
|
+
window.parent.postMessage(ready, origin);
|
|
11
|
+
const onWindowMessage = (event) => {
|
|
12
|
+
if (event.origin !== origin || event.source !== window.parent) return;
|
|
13
|
+
const message = parseEditorMessage(event.data);
|
|
14
|
+
if (!message || message.type !== "EDITOR_INIT" || message.payload.siteId !== options.siteId || message.payload.page !== options.page) return;
|
|
15
|
+
options.onMessage(message);
|
|
16
|
+
window.parent.postMessage({ version: protocolVersion, nonce: message.nonce, type: "REGISTER_FIELDS", payload: { siteId: options.siteId, page: options.page, fields: options.schemas() } }, origin);
|
|
17
|
+
};
|
|
18
|
+
window.addEventListener("message", onWindowMessage);
|
|
19
|
+
return () => window.removeEventListener("message", onWindowMessage);
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
startEditorRuntime
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=runtime-A5GXJO6L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["import { parseEditorMessage, protocolVersion, type EditableFieldSchema, type EditorMessage } from './protocol'\n\nexport interface RuntimeOptions {\n siteId: string\n page: string\n editorOrigin: string\n schemas: () => EditableFieldSchema[]\n onMessage: (message: EditorMessage) => void\n}\n\nexport function startEditorRuntime(options: RuntimeOptions): () => void {\n const origin = new URL(options.editorOrigin).origin\n const ready: EditorMessage = { version: protocolVersion, type: 'EDITOR_READY', payload: { siteId: options.siteId, page: options.page } }\n window.parent.postMessage(ready, origin)\n const onWindowMessage = (event: MessageEvent<unknown>) => {\n if (event.origin !== origin || event.source !== window.parent) return\n const message = parseEditorMessage(event.data)\n if (!message || message.type !== 'EDITOR_INIT' || message.payload.siteId !== options.siteId || message.payload.page !== options.page) return\n options.onMessage(message)\n window.parent.postMessage({ version: protocolVersion, nonce: message.nonce, type: 'REGISTER_FIELDS', payload: { siteId: options.siteId, page: options.page, fields: options.schemas() } } satisfies EditorMessage, origin)\n }\n window.addEventListener('message', onWindowMessage)\n return () => window.removeEventListener('message', onWindowMessage)\n}\n"],"mappings":";;;;;;AAUO,SAAS,mBAAmB,SAAqC;AACtE,QAAM,SAAS,IAAI,IAAI,QAAQ,YAAY,EAAE;AAC7C,QAAM,QAAuB,EAAE,SAAS,iBAAiB,MAAM,gBAAgB,SAAS,EAAE,QAAQ,QAAQ,QAAQ,MAAM,QAAQ,KAAK,EAAE;AACvI,SAAO,OAAO,YAAY,OAAO,MAAM;AACvC,QAAM,kBAAkB,CAAC,UAAiC;AACxD,QAAI,MAAM,WAAW,UAAU,MAAM,WAAW,OAAO,OAAQ;AAC/D,UAAM,UAAU,mBAAmB,MAAM,IAAI;AAC7C,QAAI,CAAC,WAAW,QAAQ,SAAS,iBAAiB,QAAQ,QAAQ,WAAW,QAAQ,UAAU,QAAQ,QAAQ,SAAS,QAAQ,KAAM;AACtI,YAAQ,UAAU,OAAO;AACzB,WAAO,OAAO,YAAY,EAAE,SAAS,iBAAiB,OAAO,QAAQ,OAAO,MAAM,mBAAmB,SAAS,EAAE,QAAQ,QAAQ,QAAQ,MAAM,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,EAAE,EAAE,GAA2B,MAAM;AAAA,EAC3N;AACA,SAAO,iBAAiB,WAAW,eAAe;AAClD,SAAO,MAAM,OAAO,oBAAoB,WAAW,eAAe;AACpE;","names":[]}
|
package/dist/security.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { C as ContentResponse } from './protocol-DC8OkoEg.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
4
|
+
interface GetSiteContentOptions {
|
|
5
|
+
siteId: string;
|
|
6
|
+
page: string;
|
|
7
|
+
mode?: 'published' | 'draft';
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
previewToken?: string;
|
|
10
|
+
fetcher?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
declare function getSiteContent(options: GetSiteContentOptions): Promise<ContentResponse>;
|
|
13
|
+
|
|
14
|
+
export { type GetSiteContentOptions, getSiteContent };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open_mind/editor-sdk",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Public integration SDK for connecting React and Next.js sites to the Fieldmatch visual editor.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
9
|
+
"./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
|
|
10
|
+
"./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
|
|
11
|
+
"./security": { "types": "./dist/security.d.ts", "import": "./dist/security.js" }
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"fieldmatch-editor-sdk-init": "scripts/install-agent-assets.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"skills",
|
|
19
|
+
"scripts",
|
|
20
|
+
"templates",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"init-agent": "node ./scripts/install-agent-assets.mjs --force",
|
|
27
|
+
"postinstall": "node ./scripts/install-agent-assets.mjs",
|
|
28
|
+
"prepack": "npm run build",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"zod": "^4.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.18.3",
|
|
37
|
+
"@types/react": "latest",
|
|
38
|
+
"react": "latest",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"typescript": "^5.9.2",
|
|
41
|
+
"vitest": "^3.2.4"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": ">=18.2.0"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"registry": "https://registry.npmjs.org"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/fieldmatch/editor-sdk.git"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/fieldmatch/editor-sdk#readme",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/fieldmatch/editor-sdk/issues"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"fieldmatch",
|
|
60
|
+
"visual-editor",
|
|
61
|
+
"cms",
|
|
62
|
+
"react",
|
|
63
|
+
"nextjs"
|
|
64
|
+
],
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=18"
|
|
67
|
+
},
|
|
68
|
+
"license": "SEE LICENSE IN LICENSE.md"
|
|
69
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
8
|
+
const targetRoot = resolve(process.env.INIT_CWD || process.cwd())
|
|
9
|
+
const force = process.argv.includes('--force')
|
|
10
|
+
const startMarker = '<!-- fieldmatch-editor-sdk:start -->'
|
|
11
|
+
const endMarker = '<!-- fieldmatch-editor-sdk:end -->'
|
|
12
|
+
const managedBlock = `${startMarker}
|
|
13
|
+
## Fieldmatch editable content
|
|
14
|
+
|
|
15
|
+
When creating or changing content that should be editable through Fieldmatch, use the \`$fieldmatch-editable-components\` skill installed at \`.agents/skills/fieldmatch-editable-components\`.
|
|
16
|
+
|
|
17
|
+
Read and maintain \`FIELDMATCH_EDITOR.md\` as the integration registry. Only content explicitly intended for customer editing should use the SDK; keep layout, styling, behavior, authorization, prices, and application state in code unless requirements say otherwise.
|
|
18
|
+
${endMarker}`
|
|
19
|
+
|
|
20
|
+
async function hasDirectDependency() {
|
|
21
|
+
try {
|
|
22
|
+
const manifest = JSON.parse(await readFile(resolve(targetRoot, 'package.json'), 'utf8'))
|
|
23
|
+
return ['dependencies', 'devDependencies', 'optionalDependencies']
|
|
24
|
+
.some((key) => manifest[key]?.['@open_mind/editor-sdk'])
|
|
25
|
+
} catch {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function install() {
|
|
31
|
+
if (process.env.FIELDMATCH_SKIP_AGENT_SETUP === '1') return
|
|
32
|
+
if (targetRoot === packageRoot) return
|
|
33
|
+
if (!force && !(await hasDirectDependency())) return
|
|
34
|
+
|
|
35
|
+
const skillTarget = resolve(targetRoot, '.agents/skills/fieldmatch-editable-components')
|
|
36
|
+
await mkdir(dirname(skillTarget), { recursive: true })
|
|
37
|
+
await cp(resolve(packageRoot, 'skills/fieldmatch-editable-components'), skillTarget, {
|
|
38
|
+
recursive: true,
|
|
39
|
+
force: true,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const guideTarget = resolve(targetRoot, 'FIELDMATCH_EDITOR.md')
|
|
43
|
+
try {
|
|
44
|
+
await readFile(guideTarget, 'utf8')
|
|
45
|
+
} catch {
|
|
46
|
+
await writeFile(guideTarget, await readFile(resolve(packageRoot, 'templates/FIELDMATCH_EDITOR.md'), 'utf8'))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const agentsTarget = resolve(targetRoot, 'AGENTS.md')
|
|
50
|
+
let agents = ''
|
|
51
|
+
try {
|
|
52
|
+
agents = await readFile(agentsTarget, 'utf8')
|
|
53
|
+
} catch {
|
|
54
|
+
// A repository-level AGENTS.md is optional until this integration creates one.
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const start = agents.indexOf(startMarker)
|
|
58
|
+
const end = agents.indexOf(endMarker)
|
|
59
|
+
if (start !== -1 && end !== -1 && end >= start) {
|
|
60
|
+
const after = end + endMarker.length
|
|
61
|
+
agents = `${agents.slice(0, start)}${managedBlock}${agents.slice(after)}`
|
|
62
|
+
} else {
|
|
63
|
+
agents = `${agents.trimEnd()}${agents.trim() ? '\n\n' : ''}${managedBlock}\n`
|
|
64
|
+
}
|
|
65
|
+
await writeFile(agentsTarget, agents)
|
|
66
|
+
|
|
67
|
+
process.stdout.write('[Fieldmatch] Installed agent skill and integration instructions. Restart your coding agent so it reloads AGENTS.md.\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
install().catch((error) => {
|
|
71
|
+
process.stderr.write(`[Fieldmatch] Agent setup skipped: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
72
|
+
process.exitCode = 0
|
|
73
|
+
})
|
|
74
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fieldmatch-editable-components
|
|
3
|
+
description: Integrate or update React and Next.js components that should be editable through @open_mind/editor-sdk. Use when creating editable text, headings, descriptions, button labels, links, images, videos, or page-level Fieldmatch content wiring. Do not use for layout-only or backend-only changes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fieldmatch Editable Components
|
|
7
|
+
|
|
8
|
+
Connect deliberately chosen website content to Fieldmatch while preserving the site's structure, design, accessibility, and local fallbacks.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Read `FIELDMATCH_EDITOR.md`. If it is absent, create it from the SDK's documented project guide structure before integrating fields.
|
|
13
|
+
2. Inspect the installed SDK version, existing server content fetch, preview handling, and nearest `EditorProvider`. Reuse one provider around the editable page area; never add a provider per field.
|
|
14
|
+
3. Identify only content explicitly intended for customer editing. Keep layout, CSS, component behavior, authorization, prices, calculated values, and application state in code unless the requirement expressly includes them.
|
|
15
|
+
4. Assign each field a stable, unique ID in `page.section.element` form. Do not silently rename or reuse IDs. An ID rename is a content migration.
|
|
16
|
+
5. Choose the matching SDK component and preserve the original DOM semantics, classes, accessibility attributes, behavior, and meaningful local fallback.
|
|
17
|
+
6. Update the field registry in `FIELDMATCH_EDITOR.md` with the ID, page, type, source component, fallback, and limitations.
|
|
18
|
+
7. Verify normal rendering, editor selection, draft persistence, publish behavior, revalidation, and secret handling.
|
|
19
|
+
|
|
20
|
+
## Map content to components
|
|
21
|
+
|
|
22
|
+
- Heading, short label, or inline text: `EditableText` with `type="text"`.
|
|
23
|
+
- Paragraph or multiline copy: `EditableText` with `type="textarea"`.
|
|
24
|
+
- Formatted copy: `EditableText` with `type="richText"`. This SDK version transports a string, not a structured rich-text document; do not promise unsupported formatting controls.
|
|
25
|
+
- Button text: `EditableText` with `type="buttonLabel"` around the label element. Keep button behavior in code.
|
|
26
|
+
- Link destination: `EditableLink`. Make the visible label a separate `EditableText` field when both should be editable.
|
|
27
|
+
- Image: `EditableImage`, with a useful `src` and accurate `alt` fallback. The editor supports Fieldmatch media-library image selection.
|
|
28
|
+
- Video: `EditableVideo`, with a direct HTTP(S) `src`. The current SDK edits the URL only; it does not upload videos or edit poster images.
|
|
29
|
+
|
|
30
|
+
Read [references/component-patterns.md](references/component-patterns.md) whenever implementing markup, the provider, preview mode, CSP, or revalidation.
|
|
31
|
+
|
|
32
|
+
## Invariants
|
|
33
|
+
|
|
34
|
+
- Fetch published content for public traffic and draft content only through authenticated preview mode.
|
|
35
|
+
- Never expose preview tokens, CMS secrets, or revalidation secrets to browser code.
|
|
36
|
+
- Restrict `frame-ancestors` to the exact Fieldmatch editor origin; do not use wildcards.
|
|
37
|
+
- Keep a meaningful local value inside every editable component so the site remains usable before registration or during CMS outages.
|
|
38
|
+
- For a custom child component, verify it forwards the SDK's props and event handlers to the final DOM element.
|
|
39
|
+
- Do not modify the SDK's installed source inside a customer repository. Extend the customer's integration code or request an SDK change from Fieldmatch.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Fieldmatch component patterns
|
|
2
|
+
|
|
3
|
+
## Server content and local fallbacks
|
|
4
|
+
|
|
5
|
+
Fetch content on the server and merge it with local defaults. Public requests use `published`; only authenticated preview flows may request `draft` with a server-only token.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { getSiteContent } from '@open_mind/editor-sdk/server'
|
|
9
|
+
|
|
10
|
+
const defaults = {
|
|
11
|
+
'home.hero.title': 'A reliable local title',
|
|
12
|
+
'home.hero.image': { assetId: 'local', url: '/hero.jpg', alt: 'Product' },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const response = await getSiteContent({ siteId, page: 'home', mode: 'published', apiUrl })
|
|
16
|
+
const content = { ...defaults, ...response.fields }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Handle CMS failures according to the site's availability requirements. A marketing site normally falls back to local or last-known published content rather than exposing a server error.
|
|
20
|
+
|
|
21
|
+
## Provider
|
|
22
|
+
|
|
23
|
+
Render one provider around the page region that contains editable fields.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<EditorProvider siteId={siteId} page="home" editorOrigin={editorOrigin} initialContent={content}>
|
|
27
|
+
<HomePage />
|
|
28
|
+
</EditorProvider>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`editorOrigin` must be the exact trusted origin, for example `https://fieldmatch-canvas-editor.vercel.app`.
|
|
32
|
+
|
|
33
|
+
## Text and button labels
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<EditableText id="home.hero.title" page="home" label="Hero title" maxLength={90}>
|
|
37
|
+
<h1 className="hero-title">A reliable local title</h1>
|
|
38
|
+
</EditableText>
|
|
39
|
+
|
|
40
|
+
<button type="button" onClick={openDialog}>
|
|
41
|
+
<EditableText id="home.hero.cta.label" page="home" label="CTA label" type="buttonLabel">
|
|
42
|
+
<span>Start now</span>
|
|
43
|
+
</EditableText>
|
|
44
|
+
</button>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Keep the click behavior on the button. Make the label editable independently.
|
|
48
|
+
|
|
49
|
+
## Links
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<EditableLink id="home.hero.cta.href" page="home" label="CTA destination" href="/contact">
|
|
53
|
+
<EditableText id="home.hero.cta.label" page="home" label="CTA label" type="buttonLabel">
|
|
54
|
+
<span>Contact us</span>
|
|
55
|
+
</EditableText>
|
|
56
|
+
</EditableLink>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use separate IDs for the URL and visible copy.
|
|
60
|
+
|
|
61
|
+
## Images
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<EditableImage
|
|
65
|
+
id="home.hero.image"
|
|
66
|
+
page="home"
|
|
67
|
+
label="Hero image"
|
|
68
|
+
src="/images/hero.jpg"
|
|
69
|
+
alt="Gold ring photographed from the side"
|
|
70
|
+
className="hero-image"
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Keep meaningful alt text. The editor can select images from the Fieldmatch media library.
|
|
75
|
+
|
|
76
|
+
## Videos
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<EditableVideo
|
|
80
|
+
id="home.hero.video"
|
|
81
|
+
page="home"
|
|
82
|
+
label="Hero video"
|
|
83
|
+
src="https://cdn.example.com/hero.mp4"
|
|
84
|
+
muted
|
|
85
|
+
playsInline
|
|
86
|
+
autoPlay
|
|
87
|
+
loop
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The editable value is a direct HTTP(S) video URL. Video upload, source arrays, captions, and poster editing are not currently provided. Model a poster as a separate `EditableImage` only if the rendering component supports it.
|
|
92
|
+
|
|
93
|
+
## Preview and security
|
|
94
|
+
|
|
95
|
+
- Determine preview mode on the server through an authenticated route or signed token.
|
|
96
|
+
- Pass only the content snapshot and public editor origin to browser components.
|
|
97
|
+
- Add the exact editor origin to CSP with `editorFrameAncestors(editorOrigin)` from `@open_mind/editor-sdk/security`.
|
|
98
|
+
- Never use `frame-ancestors *` and never expose a CMS write token in `NEXT_PUBLIC_*`.
|
|
99
|
+
|
|
100
|
+
## Revalidation
|
|
101
|
+
|
|
102
|
+
Publishing should call a protected site endpoint that verifies the shared revalidation secret, then invalidates the same tags used by `getSiteContent`. Record the endpoint and verification status in `FIELDMATCH_EDITOR.md` without recording the secret.
|
|
103
|
+
|
|
104
|
+
## Verification
|
|
105
|
+
|
|
106
|
+
1. Open the site normally and confirm published/local content renders.
|
|
107
|
+
2. Open the Fieldmatch editor and confirm each intended field highlights and can be selected.
|
|
108
|
+
3. Edit each supported field type and refresh the preview to confirm the draft persists.
|
|
109
|
+
4. Confirm the public site has not changed before publishing.
|
|
110
|
+
5. Publish, then confirm revalidation makes the new content public.
|
|
111
|
+
6. Inspect the client bundle and network requests for leaked tokens or secrets.
|
|
112
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Fieldmatch editor integration
|
|
2
|
+
|
|
3
|
+
This file is the project-specific source of truth for content connected to `@open_mind/editor-sdk`. Keep it current whenever editable content is added, renamed, moved, or removed. Do not place tokens or secrets here.
|
|
4
|
+
|
|
5
|
+
## Project configuration
|
|
6
|
+
|
|
7
|
+
- Site ID: `TODO`
|
|
8
|
+
- CMS public URL: `TODO`
|
|
9
|
+
- Editor origin: `TODO`
|
|
10
|
+
- Editable page keys: `TODO`
|
|
11
|
+
- Preview mode implementation: `TODO`
|
|
12
|
+
- Revalidation endpoint/status: `TODO`
|
|
13
|
+
|
|
14
|
+
## Editable field registry
|
|
15
|
+
|
|
16
|
+
| Field ID | Page | Type | Component/file | Fallback source | Notes |
|
|
17
|
+
| --- | --- | --- | --- | --- | --- |
|
|
18
|
+
| `TODO` | `TODO` | `text` | `TODO` | `TODO` | `TODO` |
|
|
19
|
+
|
|
20
|
+
## Integration rules
|
|
21
|
+
|
|
22
|
+
- Render one `EditorProvider` around the editable area; do not create a provider per field.
|
|
23
|
+
- Fetch published content for normal visitors. Fetch draft content only in authenticated preview/editor mode.
|
|
24
|
+
- Every editable field must have a stable, unique ID such as `home.hero.title` and a meaningful local fallback.
|
|
25
|
+
- Do not silently rename or reuse an existing field ID. Treat an ID change as a content migration.
|
|
26
|
+
- Use `EditableText` for text, textarea, rich-text strings, and button labels; `EditableLink` for destinations; `EditableImage` for images; and `EditableVideo` for direct video URLs.
|
|
27
|
+
- Keep structure, styling, accessibility, behavior, permissions, prices, and application state in code unless the product requirement explicitly makes them editable content.
|
|
28
|
+
- When wrapping custom components, make sure the final DOM element receives the props and event handlers supplied by the SDK.
|
|
29
|
+
- Image selection supports the Fieldmatch media library. Video currently accepts a direct HTTP(S) URL; video upload and poster editing are not provided by this SDK version.
|
|
30
|
+
- Keep the site CSP `frame-ancestors` restricted to the exact Fieldmatch editor origin. Never use a wildcard.
|
|
31
|
+
|
|
32
|
+
## Verification checklist
|
|
33
|
+
|
|
34
|
+
- [ ] The normal site renders local or published content without editor query parameters.
|
|
35
|
+
- [ ] The preview URL loads inside Fieldmatch and every intended field is highlighted and selectable.
|
|
36
|
+
- [ ] Text, link, image, and video fields display the correct editor control.
|
|
37
|
+
- [ ] Draft edits survive a refresh and remain invisible on the public site before publishing.
|
|
38
|
+
- [ ] Publish updates the public site and triggers revalidation successfully.
|
|
39
|
+
- [ ] No CMS token, preview token, or revalidation secret is shipped to the browser.
|
|
40
|
+
|