next-content-overlay 1.0.0 → 1.1.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/README.md +310 -232
- package/dist/server/index.d.ts +54 -8
- package/dist/server/index.js +7 -3
- package/package.json +56 -56
package/README.md
CHANGED
|
@@ -1,232 +1,310 @@
|
|
|
1
|
-
# next-content-overlay
|
|
2
|
-
|
|
3
|
-
**Plug-and-play inline CMS for Next.js — click text on the page, edit it, publish. No database.**
|
|
4
|
-
|
|
5
|
-
`next-content-overlay` gives any Next.js App Router project a Squarespace-style inline editor in three files. Your content lives in a plain JSON file in your repo. Version control handles history. There is no CMS platform, no vendor lock-in, no config hell.
|
|
6
|
-
|
|
7
|
-
It started as a CLI-only tool in v0.1 (`init → scan → edit → publish`). As of **v1.0** the primary interface is a React component + server bundle that you drop into a layout — and the CLI is still there as a secondary workflow when you'd rather edit copy from the terminal.
|
|
8
|
-
|
|
9
|
-
> **From v0.1 → v1.0:** the CLI still ships and still works. The big upgrade is that you can now edit text visually, on the page, with drafts and version history, without leaving the browser. See [CHANGELOG.md](CHANGELOG.md) for the full migration guide.
|
|
10
|
-
|
|
11
|
-
## Install
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm i -D next-content-overlay
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Quickstart (visual editor — 3 files)
|
|
18
|
-
|
|
19
|
-
### 1. Wrap your root layout
|
|
20
|
-
|
|
21
|
-
```tsx
|
|
22
|
-
// app/layout.tsx
|
|
23
|
-
import { ContentOverlayProvider, EditModeToggle } from "next-content-overlay";
|
|
24
|
-
import { getContent } from "next-content-overlay/server";
|
|
25
|
-
|
|
26
|
-
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
27
|
-
const content = await getContent();
|
|
28
|
-
return (
|
|
29
|
-
<html>
|
|
30
|
-
<body>
|
|
31
|
-
<ContentOverlayProvider initialContent={content}>
|
|
32
|
-
{children}
|
|
33
|
-
<EditModeToggle />
|
|
34
|
-
</ContentOverlayProvider>
|
|
35
|
-
</body>
|
|
36
|
-
</html>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### 2. Wrap any text in `<Editable>`
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
// app/page.tsx
|
|
45
|
-
import { Editable } from "next-content-overlay";
|
|
46
|
-
|
|
47
|
-
export default function Page() {
|
|
48
|
-
return (
|
|
49
|
-
<main>
|
|
50
|
-
<Editable k="hero.title" as="h1">Welcome to my site</Editable>
|
|
51
|
-
<Editable k="hero.subtitle" as="p" multiline>
|
|
52
|
-
Build something amazing with zero config.
|
|
53
|
-
</Editable>
|
|
54
|
-
</main>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### 3. Mount the API handler
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
// app/api/content-overlay/[...action]/route.ts
|
|
63
|
-
import { createContentAPI } from "next-content-overlay/server";
|
|
64
|
-
|
|
65
|
-
const handler = createContentAPI();
|
|
66
|
-
export const GET = handler;
|
|
67
|
-
export const POST = handler;
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Or scaffold it with one command:
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
npx content-overlay setup
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
**That's it.** Press `Ctrl+Shift+E`, click any `<Editable>` text, edit it, save. Click **Publish Drafts** when you're ready to ship.
|
|
77
|
-
|
|
78
|
-
## How it works
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
┌───────────────────────┐ ┌──────────────────────┐ ┌──────────────────────────┐
|
|
82
|
-
│ <Editable> component │ ───▶ │ createContentAPI() │ ───▶ │ content/site.json │
|
|
83
|
-
│ (click-to-edit UI) │ │ (Next.js route) │ │ .overlay-content/*.json │
|
|
84
|
-
└───────────────────────┘ └──────────────────────┘ └──────────────────────────┘
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
- **`content/site.json`** — your published content, committed to git.
|
|
88
|
-
- **`.overlay-content/draft.json`** — working drafts (per-key versioned).
|
|
89
|
-
- **`.overlay-content/history.json`** — per-key version history for restore.
|
|
90
|
-
|
|
91
|
-
All JSON. All in your repo. No database, no external service.
|
|
92
|
-
|
|
93
|
-
## Components API
|
|
94
|
-
|
|
95
|
-
### `<ContentOverlayProvider>`
|
|
96
|
-
|
|
97
|
-
| Prop | Type | Default | Description |
|
|
98
|
-
|------------------|-------------------------------|---------------------------|-------------|
|
|
99
|
-
| `initialContent` | `Record<string, string>` | `{}` | Published content map from `getContent()` for SSR hydration. |
|
|
100
|
-
| `basePath` | `string` | `"/api/content-overlay"` | Mount path for the API route. |
|
|
101
|
-
| `shortcut` | `string` | `"ctrl+shift+e"` | Keyboard shortcut to toggle edit mode. |
|
|
102
|
-
|
|
103
|
-
### `<Editable>`
|
|
104
|
-
|
|
105
|
-
| Prop | Type | Default | Description |
|
|
106
|
-
|-------------|----------------------|-------------|-------------|
|
|
107
|
-
| `k` | `string` | *(required)* | Stable content key (e.g. `"hero.title"`). |
|
|
108
|
-
| `as` | `keyof JSX.IntrinsicElements` | `"span"` | Element to render in view mode. |
|
|
109
|
-
| `children` | `ReactNode` | — | Default text used until a published value exists. |
|
|
110
|
-
| `multiline` | `boolean` | `false` | Allow newlines; uses textarea instead of single-line input. |
|
|
111
|
-
| `maxLength` | `number` | `5000` | Soft character limit. |
|
|
112
|
-
| `deletable` | `boolean` | `false` | Show a Delete button in the editor. |
|
|
113
|
-
| `className` | `string` | — | Passed through to the rendered element. |
|
|
114
|
-
|
|
115
|
-
### `<EditModeToggle>`
|
|
116
|
-
|
|
117
|
-
| Prop | Type | Default |
|
|
118
|
-
|------------|--------------------------------------------------|-----------------|
|
|
119
|
-
| `position` | `"bottom-left" \| "bottom-right" \| "top-left" \| "top-right"` | `"bottom-left"` |
|
|
120
|
-
| `className`| `string` | — |
|
|
121
|
-
|
|
122
|
-
### `useContentOverlay()`
|
|
123
|
-
|
|
124
|
-
Hook for reading edit-mode state (`isAdmin`, `isEditMode`, `pendingCount`, etc.) from custom components.
|
|
125
|
-
|
|
126
|
-
## Server API
|
|
127
|
-
|
|
128
|
-
| Export | From | Description |
|
|
129
|
-
|--------------------|--------------------------------|-------------|
|
|
130
|
-
| `createContentAPI` | `next-content-overlay/server` | Factory returning a catch-all route handler. |
|
|
131
|
-
| `getContent` | `next-content-overlay/server` | Reads `content/site.json` for SSR. |
|
|
132
|
-
| `ContentStorage` | `next-content-overlay/server` | Class wrapping the file-backed store. Useful for scripts. |
|
|
133
|
-
| `defaultAdminCheck`| `next-content-overlay/server` | The built-in admin check (dev-mode + secret cookie). |
|
|
134
|
-
|
|
135
|
-
### `createContentAPI(options?)`
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
createContentAPI({
|
|
139
|
-
isAdmin: (request) => boolean | Promise<boolean>, // Custom admin check
|
|
140
|
-
contentDir: process.cwd(), // Where to read/write files
|
|
141
|
-
revalidatePaths: ["/"], // Next.js paths to revalidate on publish
|
|
142
|
-
});
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Sub-routes mounted under `[...action]`:
|
|
146
|
-
|
|
147
|
-
| Method | Path | Purpose |
|
|
148
|
-
|--------|-----------------------------|---------|
|
|
149
|
-
| GET | `/me` | Admin check + unpublished count |
|
|
150
|
-
| GET | `/content?keys=a,b&includeDraft=1` | Fetch published or draft values |
|
|
151
|
-
| POST | `/save` | Upsert a draft (increments version, logs history) |
|
|
152
|
-
| POST | `/publish` | Promote drafts to `content/site.json` |
|
|
153
|
-
| GET | `/history?key=hero.title` | List version history for a key |
|
|
154
|
-
| POST | `/restore` | Restore a historical version as a new draft |
|
|
155
|
-
| POST | `/login` | Validate the shared secret and set a cookie |
|
|
156
|
-
|
|
157
|
-
##
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
1
|
+
# next-content-overlay
|
|
2
|
+
|
|
3
|
+
**Plug-and-play inline CMS for Next.js — click text on the page, edit it, publish. No database.**
|
|
4
|
+
|
|
5
|
+
`next-content-overlay` gives any Next.js App Router project a Squarespace-style inline editor in three files. Your content lives in a plain JSON file in your repo (by default — v1.1 lets you plug in any backend). Version control handles history. There is no CMS platform, no vendor lock-in, no config hell.
|
|
6
|
+
|
|
7
|
+
It started as a CLI-only tool in v0.1 (`init → scan → edit → publish`). As of **v1.0** the primary interface is a React component + server bundle that you drop into a layout — and the CLI is still there as a secondary workflow when you'd rather edit copy from the terminal.
|
|
8
|
+
|
|
9
|
+
> **From v0.1 → v1.0:** the CLI still ships and still works. The big upgrade is that you can now edit text visually, on the page, with drafts and version history, without leaving the browser. See [CHANGELOG.md](CHANGELOG.md) for the full migration guide.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i -D next-content-overlay
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart (visual editor — 3 files)
|
|
18
|
+
|
|
19
|
+
### 1. Wrap your root layout
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// app/layout.tsx
|
|
23
|
+
import { ContentOverlayProvider, EditModeToggle } from "next-content-overlay";
|
|
24
|
+
import { getContent } from "next-content-overlay/server";
|
|
25
|
+
|
|
26
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
27
|
+
const content = await getContent();
|
|
28
|
+
return (
|
|
29
|
+
<html>
|
|
30
|
+
<body>
|
|
31
|
+
<ContentOverlayProvider initialContent={content}>
|
|
32
|
+
{children}
|
|
33
|
+
<EditModeToggle />
|
|
34
|
+
</ContentOverlayProvider>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Wrap any text in `<Editable>`
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// app/page.tsx
|
|
45
|
+
import { Editable } from "next-content-overlay";
|
|
46
|
+
|
|
47
|
+
export default function Page() {
|
|
48
|
+
return (
|
|
49
|
+
<main>
|
|
50
|
+
<Editable k="hero.title" as="h1">Welcome to my site</Editable>
|
|
51
|
+
<Editable k="hero.subtitle" as="p" multiline>
|
|
52
|
+
Build something amazing with zero config.
|
|
53
|
+
</Editable>
|
|
54
|
+
</main>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Mount the API handler
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// app/api/content-overlay/[...action]/route.ts
|
|
63
|
+
import { createContentAPI } from "next-content-overlay/server";
|
|
64
|
+
|
|
65
|
+
const handler = createContentAPI();
|
|
66
|
+
export const GET = handler;
|
|
67
|
+
export const POST = handler;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or scaffold it with one command:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx content-overlay setup
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**That's it.** Press `Ctrl+Shift+E`, click any `<Editable>` text, edit it, save. Click **Publish Drafts** when you're ready to ship.
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
┌───────────────────────┐ ┌──────────────────────┐ ┌──────────────────────────┐
|
|
82
|
+
│ <Editable> component │ ───▶ │ createContentAPI() │ ───▶ │ content/site.json │
|
|
83
|
+
│ (click-to-edit UI) │ │ (Next.js route) │ │ .overlay-content/*.json │
|
|
84
|
+
└───────────────────────┘ └──────────────────────┘ └──────────────────────────┘
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- **`content/site.json`** — your published content, committed to git.
|
|
88
|
+
- **`.overlay-content/draft.json`** — working drafts (per-key versioned).
|
|
89
|
+
- **`.overlay-content/history.json`** — per-key version history for restore.
|
|
90
|
+
|
|
91
|
+
All JSON. All in your repo. No database, no external service.
|
|
92
|
+
|
|
93
|
+
## Components API
|
|
94
|
+
|
|
95
|
+
### `<ContentOverlayProvider>`
|
|
96
|
+
|
|
97
|
+
| Prop | Type | Default | Description |
|
|
98
|
+
|------------------|-------------------------------|---------------------------|-------------|
|
|
99
|
+
| `initialContent` | `Record<string, string>` | `{}` | Published content map from `getContent()` for SSR hydration. |
|
|
100
|
+
| `basePath` | `string` | `"/api/content-overlay"` | Mount path for the API route. |
|
|
101
|
+
| `shortcut` | `string` | `"ctrl+shift+e"` | Keyboard shortcut to toggle edit mode. |
|
|
102
|
+
|
|
103
|
+
### `<Editable>`
|
|
104
|
+
|
|
105
|
+
| Prop | Type | Default | Description |
|
|
106
|
+
|-------------|----------------------|-------------|-------------|
|
|
107
|
+
| `k` | `string` | *(required)* | Stable content key (e.g. `"hero.title"`). |
|
|
108
|
+
| `as` | `keyof JSX.IntrinsicElements` | `"span"` | Element to render in view mode. |
|
|
109
|
+
| `children` | `ReactNode` | — | Default text used until a published value exists. |
|
|
110
|
+
| `multiline` | `boolean` | `false` | Allow newlines; uses textarea instead of single-line input. |
|
|
111
|
+
| `maxLength` | `number` | `5000` | Soft character limit. |
|
|
112
|
+
| `deletable` | `boolean` | `false` | Show a Delete button in the editor. |
|
|
113
|
+
| `className` | `string` | — | Passed through to the rendered element. |
|
|
114
|
+
|
|
115
|
+
### `<EditModeToggle>`
|
|
116
|
+
|
|
117
|
+
| Prop | Type | Default |
|
|
118
|
+
|------------|--------------------------------------------------|-----------------|
|
|
119
|
+
| `position` | `"bottom-left" \| "bottom-right" \| "top-left" \| "top-right"` | `"bottom-left"` |
|
|
120
|
+
| `className`| `string` | — |
|
|
121
|
+
|
|
122
|
+
### `useContentOverlay()`
|
|
123
|
+
|
|
124
|
+
Hook for reading edit-mode state (`isAdmin`, `isEditMode`, `pendingCount`, etc.) from custom components.
|
|
125
|
+
|
|
126
|
+
## Server API
|
|
127
|
+
|
|
128
|
+
| Export | From | Description |
|
|
129
|
+
|--------------------|--------------------------------|-------------|
|
|
130
|
+
| `createContentAPI` | `next-content-overlay/server` | Factory returning a catch-all route handler. |
|
|
131
|
+
| `getContent` | `next-content-overlay/server` | Reads `content/site.json` for SSR. |
|
|
132
|
+
| `ContentStorage` | `next-content-overlay/server` | Class wrapping the file-backed store. Useful for scripts. |
|
|
133
|
+
| `defaultAdminCheck`| `next-content-overlay/server` | The built-in admin check (dev-mode + secret cookie). |
|
|
134
|
+
|
|
135
|
+
### `createContentAPI(options?)`
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
createContentAPI({
|
|
139
|
+
isAdmin: (request) => boolean | Promise<boolean>, // Custom admin check
|
|
140
|
+
contentDir: process.cwd(), // Where to read/write files
|
|
141
|
+
revalidatePaths: ["/"], // Next.js paths to revalidate on publish
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Sub-routes mounted under `[...action]`:
|
|
146
|
+
|
|
147
|
+
| Method | Path | Purpose |
|
|
148
|
+
|--------|-----------------------------|---------|
|
|
149
|
+
| GET | `/me` | Admin check + unpublished count |
|
|
150
|
+
| GET | `/content?keys=a,b&includeDraft=1` | Fetch published or draft values |
|
|
151
|
+
| POST | `/save` | Upsert a draft (increments version, logs history) |
|
|
152
|
+
| POST | `/publish` | Promote drafts to `content/site.json` |
|
|
153
|
+
| GET | `/history?key=hero.title` | List version history for a key |
|
|
154
|
+
| POST | `/restore` | Restore a historical version as a new draft |
|
|
155
|
+
| POST | `/login` | Validate the shared secret and set a cookie |
|
|
156
|
+
|
|
157
|
+
## Making edits persist
|
|
158
|
+
|
|
159
|
+
Where you run the editor determines how your changes stick around. The
|
|
160
|
+
default storage backend is **file-based** — drafts and published content live
|
|
161
|
+
in JSON files in your repo:
|
|
162
|
+
|
|
163
|
+
- **Local dev (`npm run dev`)** — edits write to `content/site.json` and
|
|
164
|
+
`.overlay-content/*` on your disk. Commit those files to git to ship them.
|
|
165
|
+
This is the intended workflow with the default backend: edit visually, then
|
|
166
|
+
`git add content/ && git commit && git push`.
|
|
167
|
+
- **Deployed host (Vercel, Netlify, Cloudflare, etc.)** — the filesystem is
|
|
168
|
+
ephemeral, so any "publish" made against the live site with the default
|
|
169
|
+
backend is **lost on the next deploy**. Treat the deployed editor as a
|
|
170
|
+
preview only, or plug in a durable storage adapter (see below).
|
|
171
|
+
- **Want edits from the live site to persist?** Implement a `StorageAdapter`
|
|
172
|
+
backed by your own database, blob store, or the GitHub API. The seam is
|
|
173
|
+
built in as of v1.1 — see the next section.
|
|
174
|
+
|
|
175
|
+
**TL;DR:** for local-only or solo workflows, edit → publish → commit → push.
|
|
176
|
+
For team / production editing, plug in a custom storage adapter.
|
|
177
|
+
|
|
178
|
+
## Pluggable storage (v1.1+)
|
|
179
|
+
|
|
180
|
+
The default `ContentStorage` writes to JSON files. If you want edits to persist
|
|
181
|
+
somewhere durable — Postgres, Supabase, Redis, S3, GitHub via the API, your own
|
|
182
|
+
internal service — implement the `StorageAdapter` interface and pass an
|
|
183
|
+
instance into `createContentAPI` and `getContent`.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import type { StorageAdapter } from "next-content-overlay/server";
|
|
187
|
+
|
|
188
|
+
export class PostgresAdapter implements StorageAdapter {
|
|
189
|
+
constructor(private db: MyDbClient) {}
|
|
190
|
+
|
|
191
|
+
async getPublished() { /* SELECT key, value FROM content_published */ }
|
|
192
|
+
async getContent(keys, includeDraft) { /* fetch by keys, overlay drafts if admin */ }
|
|
193
|
+
async saveDraft(key, value, baseValue) { /* INSERT ... RETURNING version */ }
|
|
194
|
+
async publish(keys) { /* copy drafts → published in a tx */ }
|
|
195
|
+
async getHistory(key, limit) { /* SELECT ... ORDER BY version DESC */ }
|
|
196
|
+
async restoreVersion(key, version) { /* clone old row as new draft */ }
|
|
197
|
+
async getUnpublishedCount() { /* SELECT count(*) WHERE draft <> published */ }
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Wire it into the route handler and your SSR helper:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// app/api/content-overlay/[...action]/route.ts
|
|
205
|
+
import { createContentAPI } from "next-content-overlay/server";
|
|
206
|
+
import { PostgresAdapter } from "@/lib/content-adapter";
|
|
207
|
+
|
|
208
|
+
const storage = new PostgresAdapter(db);
|
|
209
|
+
const handler = createContentAPI({ storage });
|
|
210
|
+
export const GET = handler;
|
|
211
|
+
export const POST = handler;
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// app/layout.tsx
|
|
216
|
+
import { getContent } from "next-content-overlay/server";
|
|
217
|
+
import { storage } from "@/lib/content-adapter";
|
|
218
|
+
|
|
219
|
+
const content = await getContent({ storage });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
That's the whole integration. Once your adapter is wired in, edits made from
|
|
223
|
+
the live deployed site survive deploys, and your non-technical teammates can
|
|
224
|
+
edit copy without a git commit.
|
|
225
|
+
|
|
226
|
+
The `StorageAdapter` interface is intentionally tiny (7 methods, all
|
|
227
|
+
key/value + versioning). The file-backed `ContentStorage` is the reference
|
|
228
|
+
implementation — read [`src/server/storage.ts`](src/server/storage.ts) when
|
|
229
|
+
building your own.
|
|
230
|
+
|
|
231
|
+
> **No official DB drivers ship in this package.** That's deliberate — picking
|
|
232
|
+
> Postgres vs. Supabase vs. Mongo vs. GitHub is your call, not the library's.
|
|
233
|
+
> Community adapters welcome via PR.
|
|
234
|
+
|
|
235
|
+
## Auth & admin access
|
|
236
|
+
|
|
237
|
+
Three layered defaults, pick whichever fits:
|
|
238
|
+
|
|
239
|
+
1. **No config** — you're automatically admin in `NODE_ENV === "development"`.
|
|
240
|
+
2. **`CONTENT_OVERLAY_SECRET`** env var — in production, users hit `Ctrl+Shift+E`, type the
|
|
241
|
+
secret once in a modal, and an httpOnly cookie keeps them signed in.
|
|
242
|
+
3. **Custom callback** — `createContentAPI({ isAdmin: (req) => myAuthCheck(req) })` to
|
|
243
|
+
integrate with your existing session / Supabase / Auth.js setup.
|
|
244
|
+
|
|
245
|
+
## CLI commands (secondary workflow)
|
|
246
|
+
|
|
247
|
+
The v0.1 CLI still ships and shares storage with the visual editor — so a change
|
|
248
|
+
made in one shows up in the other.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
content-overlay init [--force] # Create config + content files
|
|
252
|
+
content-overlay scan # Scan JSX for text strings
|
|
253
|
+
content-overlay edit <key> <val> # Update a draft value
|
|
254
|
+
content-overlay publish # Promote drafts to published content
|
|
255
|
+
content-overlay setup [--force] # Scaffold the visual-editor API route
|
|
256
|
+
content-overlay --help
|
|
257
|
+
content-overlay --version
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Generated files:
|
|
261
|
+
|
|
262
|
+
| File | Purpose |
|
|
263
|
+
|------|---------|
|
|
264
|
+
| `content-overlay.config.json` | Scan dirs, extensions, file paths |
|
|
265
|
+
| `content/site.json` | Published content (your app reads this) |
|
|
266
|
+
| `.overlay-content/content-map.json` | Auto-generated key → source mapping |
|
|
267
|
+
| `.overlay-content/draft.json` | Working drafts (versioned `DraftEntry` objects) |
|
|
268
|
+
| `.overlay-content/history.json` | Per-key version history |
|
|
269
|
+
| `.overlay-content/last-publish.json` | Publish metadata |
|
|
270
|
+
|
|
271
|
+
## 5-minute demo
|
|
272
|
+
|
|
273
|
+
A runnable Next.js demo app is included:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
cd examples/next-demo
|
|
277
|
+
npm install
|
|
278
|
+
npm run dev
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Open `http://localhost:3000`, press `Ctrl+Shift+E`, click the headline, edit it, save,
|
|
282
|
+
then click **Publish Drafts**. Refresh — the new text is in `content/site.json`.
|
|
283
|
+
|
|
284
|
+
## Migrating from v0.1
|
|
285
|
+
|
|
286
|
+
See [CHANGELOG.md](CHANGELOG.md#100---plug-and-play-inline-cms) for the full migration
|
|
287
|
+
guide. The short version: the CLI keeps working, `content/site.json` is unchanged, and
|
|
288
|
+
the draft file format auto-migrates on first read (with a backup).
|
|
289
|
+
|
|
290
|
+
## Local development
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
npm install
|
|
294
|
+
npm run check
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`npm run check` runs typecheck, tests, build, and license check.
|
|
298
|
+
|
|
299
|
+
## Show your setup
|
|
300
|
+
|
|
301
|
+
If you're using `next-content-overlay` in a project, we'd love to see it in action.
|
|
302
|
+
Share a short screen capture in [Show & Tell Discussions](../../discussions/categories/show-and-tell).
|
|
303
|
+
|
|
304
|
+
## Contributing
|
|
305
|
+
|
|
306
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
MIT — see [LICENSE](LICENSE).
|
package/dist/server/index.d.ts
CHANGED
|
@@ -9,13 +9,53 @@ type HistoryEntry = {
|
|
|
9
9
|
changeType: "save_draft" | "publish" | "restore";
|
|
10
10
|
changedAt: string;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Pluggable storage backend. The default is file-backed (`ContentStorage`),
|
|
14
|
+
* but you can supply your own — e.g. Postgres, Supabase, Redis, GitHub API —
|
|
15
|
+
* to make edits persist across deploys on hosts with ephemeral filesystems.
|
|
16
|
+
*
|
|
17
|
+
* Pass an instance to `createContentAPI({ storage })` and to `getContent({ storage })`.
|
|
18
|
+
*/
|
|
19
|
+
interface StorageAdapter {
|
|
20
|
+
/** Flat map of all published key→value pairs. Used by SSR and the API. */
|
|
21
|
+
getPublished(): Promise<Record<string, string>>;
|
|
22
|
+
/** Resolve a set of keys, optionally overlaying drafts for admins. */
|
|
23
|
+
getContent(keys: string[], includeDraft: boolean): Promise<{
|
|
24
|
+
content: Record<string, string>;
|
|
25
|
+
versions: Record<string, number>;
|
|
26
|
+
}>;
|
|
27
|
+
/** Upsert a draft value for a key, returning the new draft version. */
|
|
28
|
+
saveDraft(key: string, value: string, baseValue?: string): Promise<{
|
|
29
|
+
version: number;
|
|
30
|
+
}>;
|
|
31
|
+
/** Promote drafts to published. If keys are omitted, publish all changed drafts. */
|
|
32
|
+
publish(keys?: string[]): Promise<{
|
|
33
|
+
publishedCount: number;
|
|
34
|
+
changedKeys: string[];
|
|
35
|
+
}>;
|
|
36
|
+
/** Newest-first version history for a key. */
|
|
37
|
+
getHistory(key: string, limit?: number): Promise<HistoryEntry[]>;
|
|
38
|
+
/** Restore a historical version as a new draft. */
|
|
39
|
+
restoreVersion(key: string, version: number): Promise<{
|
|
40
|
+
value: string;
|
|
41
|
+
version: number;
|
|
42
|
+
}>;
|
|
43
|
+
/** Count of keys whose draft differs from published. */
|
|
44
|
+
getUnpublishedCount(): Promise<number>;
|
|
45
|
+
}
|
|
12
46
|
type ContentAPIOptions = {
|
|
13
47
|
/** Custom admin check. Defaults to env var / dev-mode check. */
|
|
14
48
|
isAdmin?: (request: Request) => Promise<boolean> | boolean;
|
|
15
|
-
/** Project root directory. Default: process.cwd() */
|
|
49
|
+
/** Project root directory. Default: process.cwd(). Ignored if `storage` is provided. */
|
|
16
50
|
contentDir?: string;
|
|
17
51
|
/** Paths to revalidate after publish (Next.js revalidatePath). */
|
|
18
52
|
revalidatePaths?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Custom storage backend. Defaults to the file-backed `ContentStorage`
|
|
55
|
+
* rooted at `contentDir`. Supply your own adapter to persist edits to a
|
|
56
|
+
* database or remote service.
|
|
57
|
+
*/
|
|
58
|
+
storage?: StorageAdapter;
|
|
19
59
|
};
|
|
20
60
|
|
|
21
61
|
type RouteContext = {
|
|
@@ -38,7 +78,7 @@ type RouteContext = {
|
|
|
38
78
|
declare function createContentAPI(options?: ContentAPIOptions): (request: Request, context: RouteContext) => Promise<Response>;
|
|
39
79
|
|
|
40
80
|
type DraftStore = Record<string, DraftEntry>;
|
|
41
|
-
declare class ContentStorage {
|
|
81
|
+
declare class ContentStorage implements StorageAdapter {
|
|
42
82
|
private contentPath;
|
|
43
83
|
private draftPath;
|
|
44
84
|
private historyPath;
|
|
@@ -83,14 +123,20 @@ declare class ContentStorage {
|
|
|
83
123
|
private withLock;
|
|
84
124
|
}
|
|
85
125
|
|
|
126
|
+
type GetContentOptions = {
|
|
127
|
+
/** Project root directory. Default: process.cwd(). Ignored if `storage` is provided. */
|
|
128
|
+
rootDir?: string;
|
|
129
|
+
/** Custom storage adapter. If provided, reads published content from it. */
|
|
130
|
+
storage?: StorageAdapter;
|
|
131
|
+
};
|
|
86
132
|
/**
|
|
87
|
-
* Read published content
|
|
88
|
-
*
|
|
133
|
+
* Read published content for SSR. Defaults to reading `content/site.json`
|
|
134
|
+
* from disk, but accepts a custom `StorageAdapter` so database-backed setups
|
|
135
|
+
* can hydrate the provider the same way.
|
|
89
136
|
*
|
|
90
|
-
*
|
|
91
|
-
* @returns Record<string, string> of published content
|
|
137
|
+
* Backward compatible: `getContent()` and `getContent("/my/root")` still work.
|
|
92
138
|
*/
|
|
93
|
-
declare function getContent(
|
|
139
|
+
declare function getContent(optionsOrRootDir?: string | GetContentOptions): Promise<Record<string, string>>;
|
|
94
140
|
|
|
95
141
|
/**
|
|
96
142
|
* Default admin check for content-overlay API routes.
|
|
@@ -101,4 +147,4 @@ declare function getContent(rootDir?: string): Promise<Record<string, string>>;
|
|
|
101
147
|
*/
|
|
102
148
|
declare function defaultAdminCheck(request: Request): boolean;
|
|
103
149
|
|
|
104
|
-
export { ContentStorage, createContentAPI, defaultAdminCheck, getContent };
|
|
150
|
+
export { type ContentAPIOptions, ContentStorage, type DraftEntry, type HistoryEntry, type StorageAdapter, createContentAPI, defaultAdminCheck, getContent };
|
package/dist/server/index.js
CHANGED
|
@@ -300,7 +300,7 @@ function buildTokenCookie(secret) {
|
|
|
300
300
|
|
|
301
301
|
// src/server/createContentAPI.ts
|
|
302
302
|
function createContentAPI(options = {}) {
|
|
303
|
-
const storage = new ContentStorage(options.contentDir ?? process.cwd());
|
|
303
|
+
const storage = options.storage ?? new ContentStorage(options.contentDir ?? process.cwd());
|
|
304
304
|
const checkAdmin = options.isAdmin ?? defaultAdminCheck;
|
|
305
305
|
return async function handler(request, context) {
|
|
306
306
|
const { action } = await context.params;
|
|
@@ -427,8 +427,12 @@ function json(data, status = 200) {
|
|
|
427
427
|
|
|
428
428
|
// src/server/content.ts
|
|
429
429
|
import path3 from "path";
|
|
430
|
-
async function getContent(
|
|
431
|
-
const
|
|
430
|
+
async function getContent(optionsOrRootDir) {
|
|
431
|
+
const options = typeof optionsOrRootDir === "string" ? { rootDir: optionsOrRootDir } : optionsOrRootDir ?? {};
|
|
432
|
+
if (options.storage) {
|
|
433
|
+
return options.storage.getPublished();
|
|
434
|
+
}
|
|
435
|
+
const contentPath = path3.join(options.rootDir ?? process.cwd(), "content", "site.json");
|
|
432
436
|
const raw = await readJsonFile(contentPath);
|
|
433
437
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
434
438
|
const result = {};
|
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "next-content-overlay",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Plug-and-play inline CMS for Next.js App Router — edit text on the page, no database needed",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"content-overlay": "./dist/cli/index.js"
|
|
9
|
-
},
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"import": "./dist/react/index.js",
|
|
13
|
-
"types": "./dist/react/index.d.ts"
|
|
14
|
-
},
|
|
15
|
-
"./server": {
|
|
16
|
-
"import": "./dist/server/index.js",
|
|
17
|
-
"types": "./dist/server/index.d.ts"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"files": [
|
|
21
|
-
"dist",
|
|
22
|
-
"README.md",
|
|
23
|
-
"LICENSE"
|
|
24
|
-
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsup",
|
|
27
|
-
"dev": "tsx src/cli/index.ts",
|
|
28
|
-
"typecheck": "tsc --noEmit",
|
|
29
|
-
"test": "tsx --test tests/**/*.test.ts",
|
|
30
|
-
"license:check": "node scripts/license-check.mjs",
|
|
31
|
-
"check": "npm run typecheck && npm run test && npm run build && npm run license:check",
|
|
32
|
-
"prepack": "npm run check"
|
|
33
|
-
},
|
|
34
|
-
"peerDependencies": {
|
|
35
|
-
"next": ">=14.0.0",
|
|
36
|
-
"react": ">=18.0.0",
|
|
37
|
-
"react-dom": ">=18.0.0"
|
|
38
|
-
},
|
|
39
|
-
"peerDependenciesMeta": {
|
|
40
|
-
"react": { "optional": true },
|
|
41
|
-
"react-dom": { "optional": true }
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@types/node": "^22.13.10",
|
|
45
|
-
"@types/react": "^19.0.0",
|
|
46
|
-
"next": "^15.0.0",
|
|
47
|
-
"react": "^19.0.0",
|
|
48
|
-
"react-dom": "^19.0.0",
|
|
49
|
-
"tsup": "^8.4.0",
|
|
50
|
-
"tsx": "^4.19.3",
|
|
51
|
-
"typescript": "^5.8.2"
|
|
52
|
-
},
|
|
53
|
-
"engines": {
|
|
54
|
-
"node": ">=20.0.0"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "next-content-overlay",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Plug-and-play inline CMS for Next.js App Router — edit text on the page, no database needed",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"content-overlay": "./dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/react/index.js",
|
|
13
|
+
"types": "./dist/react/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"import": "./dist/server/index.js",
|
|
17
|
+
"types": "./dist/server/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsx src/cli/index.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
30
|
+
"license:check": "node scripts/license-check.mjs",
|
|
31
|
+
"check": "npm run typecheck && npm run test && npm run build && npm run license:check",
|
|
32
|
+
"prepack": "npm run check"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"next": ">=14.0.0",
|
|
36
|
+
"react": ">=18.0.0",
|
|
37
|
+
"react-dom": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"react": { "optional": true },
|
|
41
|
+
"react-dom": { "optional": true }
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.13.10",
|
|
45
|
+
"@types/react": "^19.0.0",
|
|
46
|
+
"next": "^15.0.0",
|
|
47
|
+
"react": "^19.0.0",
|
|
48
|
+
"react-dom": "^19.0.0",
|
|
49
|
+
"tsup": "^8.4.0",
|
|
50
|
+
"tsx": "^4.19.3",
|
|
51
|
+
"typescript": "^5.8.2"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|