@open_mind/editor-sdk 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/package.json +1 -1
- package/scripts/install-agent-assets.mjs +50 -5
- package/skills/fieldmatch-editable-components/SKILL.md +4 -4
- package/skills/fieldmatch-editable-components/references/component-patterns.md +6 -1
- package/templates/FIELDMATCH_EDITOR.md +3 -3
- package/templates/next-revalidate-route.ts +21 -0
package/README.md
CHANGED
|
@@ -5,10 +5,20 @@ Publicly downloadable React and Next.js integration SDK for the Fieldmatch visua
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @open_mind/editor-sdk
|
|
8
|
+
npm install @open_mind/editor-sdk@0.2.1
|
|
9
|
+
npx fieldmatch-editor-sdk-init
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
No npm token or custom registry is required. The initializer also creates or supplements `.env.local` without overwriting existing values. Fill in:
|
|
13
|
+
|
|
14
|
+
```ini
|
|
15
|
+
NEXT_PUBLIC_FIELDMATCH_SITE_ID=your-site-id
|
|
16
|
+
NEXT_PUBLIC_FIELDMATCH_EDITOR_ORIGIN=https://fieldmatch-canvas-editor.vercel.app
|
|
17
|
+
CMS_INTERNAL_URL=https://fieldmatch-canvas-cms.vercel.app
|
|
18
|
+
REVALIDATION_SECRET=a-shared-secret-configured-in-the-site-record
|
|
9
19
|
```
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
For a Next.js App Router project, it also creates `app/api/revalidate/route.ts` when that route is absent. Configure the matching revalidation URL and secret in the site's Fieldmatch record. It also:
|
|
12
22
|
|
|
13
23
|
- installs the repository skill at `.agents/skills/fieldmatch-editable-components`;
|
|
14
24
|
- creates `FIELDMATCH_EDITOR.md`, the integration registry and checklist; and
|
|
@@ -98,7 +108,7 @@ Field IDs are persistent content keys. Use stable names such as `page.section.el
|
|
|
98
108
|
|
|
99
109
|
Image fields support the Fieldmatch media library. Video fields currently accept a direct HTTP(S) URL; this version does not upload video files.
|
|
100
110
|
|
|
101
|
-
See the generated `FIELDMATCH_EDITOR.md`
|
|
111
|
+
The `$fieldmatch-editable-components` skill is mandatory whenever adding or changing visitor-visible text, links, images, or videos. It ensures every such item is wrapped using the matching SDK component. See the generated `FIELDMATCH_EDITOR.md` for the full integration workflow, CSP, preview, and verification guidance.
|
|
102
112
|
|
|
103
113
|
## License
|
|
104
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { cp, mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
4
4
|
import { dirname, resolve } from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
@@ -9,12 +9,13 @@ const targetRoot = resolve(process.env.INIT_CWD || process.cwd())
|
|
|
9
9
|
const force = process.argv.includes('--force')
|
|
10
10
|
const startMarker = '<!-- fieldmatch-editor-sdk:start -->'
|
|
11
11
|
const endMarker = '<!-- fieldmatch-editor-sdk:end -->'
|
|
12
|
+
const environmentTemplate = `# Fieldmatch visual-editor configuration\n# Fill the blank values. Keep this file out of version control.\nNEXT_PUBLIC_FIELDMATCH_SITE_ID=\nNEXT_PUBLIC_FIELDMATCH_EDITOR_ORIGIN=https://fieldmatch-canvas-editor.vercel.app\nCMS_INTERNAL_URL=https://fieldmatch-canvas-cms.vercel.app\nREVALIDATION_SECRET=\n`
|
|
12
13
|
const managedBlock = `${startMarker}
|
|
13
14
|
## Fieldmatch editable content
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
**Mandatory:** before creating or changing any visitor-visible text, link, image, or video, read and use the \`$fieldmatch-editable-components\` skill installed at \`.agents/skills/fieldmatch-editable-components\`. Wrap every such item with the matching Fieldmatch SDK component.
|
|
16
17
|
|
|
17
|
-
Read and maintain \`FIELDMATCH_EDITOR.md\` as the integration registry.
|
|
18
|
+
Read and maintain \`FIELDMATCH_EDITOR.md\` as the integration registry. Keep layout, styling, interaction behavior, authorization, calculations, and application state in code; do not use those as a reason to leave their visible text, links, images, or videos unwrapped.
|
|
18
19
|
${endMarker}`
|
|
19
20
|
|
|
20
21
|
async function hasDirectDependency() {
|
|
@@ -27,6 +28,47 @@ async function hasDirectDependency() {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
async function ensureEnvironmentFile() {
|
|
32
|
+
const environmentTarget = resolve(targetRoot, '.env.local')
|
|
33
|
+
let existing = ''
|
|
34
|
+
try {
|
|
35
|
+
existing = await readFile(environmentTarget, 'utf8')
|
|
36
|
+
} catch {
|
|
37
|
+
// Start with the managed defaults when the project has no local environment file.
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const missing = environmentTemplate
|
|
41
|
+
.split('\n')
|
|
42
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
43
|
+
.filter((line) => !new RegExp(`^${line.split('=')[0]}=`, 'm').test(existing))
|
|
44
|
+
|
|
45
|
+
if (!existing) {
|
|
46
|
+
await writeFile(environmentTarget, environmentTemplate)
|
|
47
|
+
} else if (missing.length) {
|
|
48
|
+
await writeFile(environmentTarget, `${existing.trimEnd()}\n\n# Fieldmatch visual-editor configuration\n${missing.join('\n')}\n`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function ensureNextRevalidationRoute() {
|
|
53
|
+
for (const applicationRoot of ['app', 'src/app']) {
|
|
54
|
+
const routeTarget = resolve(targetRoot, applicationRoot, 'api/revalidate/route.ts')
|
|
55
|
+
try {
|
|
56
|
+
await readFile(routeTarget, 'utf8')
|
|
57
|
+
return 'existing'
|
|
58
|
+
} catch {
|
|
59
|
+
try {
|
|
60
|
+
if (!(await stat(resolve(targetRoot, applicationRoot))).isDirectory()) continue
|
|
61
|
+
} catch {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
await mkdir(dirname(routeTarget), { recursive: true })
|
|
65
|
+
await cp(resolve(packageRoot, 'templates/next-revalidate-route.ts'), routeTarget)
|
|
66
|
+
return 'created'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 'not-next-app-router'
|
|
70
|
+
}
|
|
71
|
+
|
|
30
72
|
async function install() {
|
|
31
73
|
if (process.env.FIELDMATCH_SKIP_AGENT_SETUP === '1') return
|
|
32
74
|
if (targetRoot === packageRoot) return
|
|
@@ -39,6 +81,9 @@ async function install() {
|
|
|
39
81
|
force: true,
|
|
40
82
|
})
|
|
41
83
|
|
|
84
|
+
await ensureEnvironmentFile()
|
|
85
|
+
const revalidationRoute = await ensureNextRevalidationRoute()
|
|
86
|
+
|
|
42
87
|
const guideTarget = resolve(targetRoot, 'FIELDMATCH_EDITOR.md')
|
|
43
88
|
try {
|
|
44
89
|
await readFile(guideTarget, 'utf8')
|
|
@@ -64,11 +109,11 @@ async function install() {
|
|
|
64
109
|
}
|
|
65
110
|
await writeFile(agentsTarget, agents)
|
|
66
111
|
|
|
67
|
-
|
|
112
|
+
const routeMessage = revalidationRoute === 'created' ? ' Added app/api/revalidate/route.ts.' : revalidationRoute === 'existing' ? ' Reused the existing revalidation route.' : ''
|
|
113
|
+
process.stdout.write(`[Fieldmatch] Installed the required agent skill, integration instructions, and missing .env.local entries.${routeMessage} Fill NEXT_PUBLIC_FIELDMATCH_SITE_ID and REVALIDATION_SECRET, then restart your coding agent.\n`)
|
|
68
114
|
}
|
|
69
115
|
|
|
70
116
|
install().catch((error) => {
|
|
71
117
|
process.stderr.write(`[Fieldmatch] Agent setup skipped: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
72
118
|
process.exitCode = 0
|
|
73
119
|
})
|
|
74
|
-
|
|
@@ -5,16 +5,16 @@ description: Integrate or update React and Next.js components that should be edi
|
|
|
5
5
|
|
|
6
6
|
# Fieldmatch Editable Components
|
|
7
7
|
|
|
8
|
-
Connect
|
|
8
|
+
Connect every visitor-visible text, link, image, and video to Fieldmatch while preserving the site's structure, design, accessibility, and local fallbacks.
|
|
9
9
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
12
|
-
1. Read `FIELDMATCH_EDITOR.md
|
|
12
|
+
1. This skill is mandatory whenever a visitor-visible text, link, image, or video is created or changed. Read `FIELDMATCH_EDITOR.md` first. If it is absent, run `npx fieldmatch-editor-sdk-init` before integrating fields.
|
|
13
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.
|
|
14
|
+
3. Wrap every visitor-visible text, link, image, and video. Keep layout, CSS, component behavior, authorization, prices, calculated values, and application state in code, but still wrap the visible labels, copy, links, images, and videos that those features render.
|
|
15
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
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
|
|
17
|
+
6. Update the field registry in `FIELDMATCH_EDITOR.md` with every ID, page, type, source component, fallback, and limitation.
|
|
18
18
|
7. Verify normal rendering, editor selection, draft persistence, publish behavior, revalidation, and secret handling.
|
|
19
19
|
|
|
20
20
|
## Map content to components
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Fieldmatch component patterns
|
|
2
2
|
|
|
3
|
+
## Required coverage
|
|
4
|
+
|
|
5
|
+
Wrap every visitor-visible text, link, image, and video with the corresponding SDK component. Keep only layout, styling, interactivity, authorization, calculations, and state in application code. If one of those features renders visible copy or media, wrap that visible value too.
|
|
6
|
+
|
|
7
|
+
Run `npx fieldmatch-editor-sdk-init` before integration. It creates or supplements `.env.local` with `NEXT_PUBLIC_FIELDMATCH_SITE_ID`, `NEXT_PUBLIC_FIELDMATCH_EDITOR_ORIGIN`, `CMS_INTERNAL_URL`, and `REVALIDATION_SECRET`; supply the deployment-specific values before running the site.
|
|
8
|
+
|
|
3
9
|
## Server content and local fallbacks
|
|
4
10
|
|
|
5
11
|
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.
|
|
@@ -109,4 +115,3 @@ Publishing should call a protected site endpoint that verifies the shared revali
|
|
|
109
115
|
4. Confirm the public site has not changed before publishing.
|
|
110
116
|
5. Publish, then confirm revalidation makes the new content public.
|
|
111
117
|
6. Inspect the client bundle and network requests for leaked tokens or secrets.
|
|
112
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Fieldmatch editor integration
|
|
2
2
|
|
|
3
|
-
This file is the project-specific source of truth for content connected to `@open_mind/editor-sdk`. Keep it current whenever
|
|
3
|
+
This file is the project-specific source of truth for content connected to `@open_mind/editor-sdk`. Keep it current whenever visitor-visible text, links, images, or videos are added, renamed, moved, or removed. Do not place tokens or secrets here.
|
|
4
4
|
|
|
5
5
|
## Project configuration
|
|
6
6
|
|
|
@@ -21,10 +21,11 @@ This file is the project-specific source of truth for content connected to `@ope
|
|
|
21
21
|
|
|
22
22
|
- Render one `EditorProvider` around the editable area; do not create a provider per field.
|
|
23
23
|
- Fetch published content for normal visitors. Fetch draft content only in authenticated preview/editor mode.
|
|
24
|
+
- **Mandatory:** use the `$fieldmatch-editable-components` skill whenever adding or changing visitor-visible text, links, images, or videos. Wrap every such item with the matching SDK component.
|
|
24
25
|
- Every editable field must have a stable, unique ID such as `home.hero.title` and a meaningful local fallback.
|
|
25
26
|
- Do not silently rename or reuse an existing field ID. Treat an ID change as a content migration.
|
|
26
27
|
- 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
|
|
28
|
+
- Keep structure, styling, accessibility, behavior, permissions, prices, and application state in code. Their visible text, links, images, and videos remain editable fields.
|
|
28
29
|
- When wrapping custom components, make sure the final DOM element receives the props and event handlers supplied by the SDK.
|
|
29
30
|
- 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
31
|
- Keep the site CSP `frame-ancestors` restricted to the exact Fieldmatch editor origin. Never use a wildcard.
|
|
@@ -37,4 +38,3 @@ This file is the project-specific source of truth for content connected to `@ope
|
|
|
37
38
|
- [ ] Draft edits survive a refresh and remain invisible on the public site before publishing.
|
|
38
39
|
- [ ] Publish updates the public site and triggers revalidation successfully.
|
|
39
40
|
- [ ] No CMS token, preview token, or revalidation secret is shipped to the browser.
|
|
40
|
-
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
2
|
+
import { revalidatePath } from 'next/cache'
|
|
3
|
+
import { NextResponse } from 'next/server'
|
|
4
|
+
|
|
5
|
+
export async function POST(request: Request) {
|
|
6
|
+
const body = await request.text()
|
|
7
|
+
const secret = process.env.REVALIDATION_SECRET?.trim()
|
|
8
|
+
|
|
9
|
+
if (!secret && process.env.NODE_ENV === 'production') {
|
|
10
|
+
return NextResponse.json({ error: 'Revalidation is not configured.' }, { status: 503 })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const expected = createHmac('sha256', secret || 'development-revalidation-secret').update(body).digest()
|
|
14
|
+
const received = Buffer.from(request.headers.get('x-agency-signature') ?? '', 'hex')
|
|
15
|
+
if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
|
|
16
|
+
return NextResponse.json({ error: 'Invalid webhook signature.' }, { status: 403 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
revalidatePath('/')
|
|
20
|
+
return NextResponse.json({ revalidated: true })
|
|
21
|
+
}
|