@bettercms-ai/next 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -0
- package/dist/bettercms-snapshot.js +292 -0
- package/dist/bettercms-snapshot.js.map +1 -0
- package/dist/blocks.d.ts +36 -0
- package/dist/blocks.js +496 -0
- package/dist/blocks.js.map +1 -0
- package/dist/form.d.ts +28 -0
- package/dist/form.js +203 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +516 -0
- package/dist/index.js +583 -0
- package/dist/index.js.map +1 -0
- package/dist/live.d.ts +15 -0
- package/dist/live.js +27 -0
- package/dist/live.js.map +1 -0
- package/dist/search.d.ts +32 -0
- package/dist/search.js +111 -0
- package/dist/search.js.map +1 -0
- package/dist/visual-editing.d.ts +7 -0
- package/dist/visual-editing.js +35 -0
- package/dist/visual-editing.js.map +1 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var BetterCMSError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
constructor(message, status, code) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "BetterCMSError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/client.ts
|
|
14
|
+
var DEFAULT_BASE_URL = "https://api.bettercms.ai/api/v1/delivery";
|
|
15
|
+
var DEFAULT_REVALIDATE = 60;
|
|
16
|
+
function statusToCode(status) {
|
|
17
|
+
if (status === 404) return "CONTENT_NOT_FOUND";
|
|
18
|
+
if (status === 401) return "UNAUTHORIZED";
|
|
19
|
+
if (status === 403) return "FORBIDDEN";
|
|
20
|
+
if (status === 429) return "RATE_LIMITED";
|
|
21
|
+
if (status === 422) return "VALIDATION_ERROR";
|
|
22
|
+
return "INTERNAL_ERROR";
|
|
23
|
+
}
|
|
24
|
+
function mapEntry(raw) {
|
|
25
|
+
return {
|
|
26
|
+
slug: raw.slug,
|
|
27
|
+
status: raw.status,
|
|
28
|
+
fields: raw.data,
|
|
29
|
+
publishedAt: raw.publishedAt ?? null,
|
|
30
|
+
updatedAt: raw.updatedAt ?? null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createBetterCMS(config) {
|
|
34
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
35
|
+
const previewBaseUrl = (config.previewBaseUrl ?? baseUrl.replace(/\/delivery$/, "/preview")).replace(/\/+$/, "");
|
|
36
|
+
const defaultRevalidate = config.revalidate ?? DEFAULT_REVALIDATE;
|
|
37
|
+
function headers() {
|
|
38
|
+
const h = { Accept: "application/json" };
|
|
39
|
+
if (config.apiKey) h["Authorization"] = `Bearer ${config.apiKey}`;
|
|
40
|
+
return h;
|
|
41
|
+
}
|
|
42
|
+
function cacheInit(revalidate, tags, forceNoStore = false) {
|
|
43
|
+
const rv = revalidate ?? defaultRevalidate;
|
|
44
|
+
if (forceNoStore) return { headers: headers(), cache: "no-store" };
|
|
45
|
+
if (rv === false) {
|
|
46
|
+
return tags?.length ? { headers: headers(), next: { revalidate: false, tags } } : { headers: headers(), cache: "no-store" };
|
|
47
|
+
}
|
|
48
|
+
return { headers: headers(), next: { revalidate: rv, tags } };
|
|
49
|
+
}
|
|
50
|
+
async function requestJSON(url, init, { nullOn404 = false } = {}) {
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(url, init);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new BetterCMSError(
|
|
56
|
+
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
57
|
+
0,
|
|
58
|
+
"NETWORK_ERROR"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (res.status === 404 && nullOn404) return null;
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
let message = res.statusText || "Request failed";
|
|
64
|
+
try {
|
|
65
|
+
const body = await res.json();
|
|
66
|
+
message = body.error ?? body.message ?? message;
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
throw new BetterCMSError(message, res.status, statusToCode(res.status));
|
|
70
|
+
}
|
|
71
|
+
return await res.json();
|
|
72
|
+
}
|
|
73
|
+
function entryQuery(opts) {
|
|
74
|
+
const params = new URLSearchParams();
|
|
75
|
+
if (opts?.depth != null) params.set("depth", String(opts.depth));
|
|
76
|
+
if (opts?.select?.length) params.set("select", opts.select.join(","));
|
|
77
|
+
const qs = params.toString();
|
|
78
|
+
return qs ? `?${qs}` : "";
|
|
79
|
+
}
|
|
80
|
+
const client = {
|
|
81
|
+
async getEntry(slug, opts) {
|
|
82
|
+
const encoded = encodeURIComponent(slug);
|
|
83
|
+
const query = entryQuery(opts);
|
|
84
|
+
if (opts?.preview) {
|
|
85
|
+
if (!opts.previewToken) {
|
|
86
|
+
throw new BetterCMSError(
|
|
87
|
+
"preview: true requires a previewToken",
|
|
88
|
+
400,
|
|
89
|
+
"VALIDATION_ERROR"
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const tokenParam = `token=${encodeURIComponent(opts.previewToken)}`;
|
|
93
|
+
const sep = query ? "&" : "?";
|
|
94
|
+
const url2 = `${previewBaseUrl}/${encoded}${query}${sep}${tokenParam}`;
|
|
95
|
+
const body = await requestJSON(
|
|
96
|
+
url2,
|
|
97
|
+
cacheInit(void 0, void 0, true),
|
|
98
|
+
{ nullOn404: true }
|
|
99
|
+
);
|
|
100
|
+
return body ? mapEntry(body.data) : null;
|
|
101
|
+
}
|
|
102
|
+
const url = `${baseUrl}/${config.workspace}/content-entries/${encoded}${query}`;
|
|
103
|
+
const raw = await requestJSON(
|
|
104
|
+
url,
|
|
105
|
+
cacheInit(opts?.revalidate, opts?.tags),
|
|
106
|
+
{ nullOn404: true }
|
|
107
|
+
);
|
|
108
|
+
return raw ? mapEntry(raw) : null;
|
|
109
|
+
},
|
|
110
|
+
async listEntries(model, opts) {
|
|
111
|
+
const params = new URLSearchParams();
|
|
112
|
+
if (model) params.set("model", model);
|
|
113
|
+
if (opts?.page != null) params.set("page", String(opts.page));
|
|
114
|
+
if (opts?.perPage != null) params.set("perPage", String(opts.perPage));
|
|
115
|
+
if (opts?.depth != null) params.set("depth", String(opts.depth));
|
|
116
|
+
if (opts?.select?.length) params.set("select", opts.select.join(","));
|
|
117
|
+
const url = `${baseUrl}/${config.workspace}/content-entries?${params}`;
|
|
118
|
+
const body = await requestJSON(
|
|
119
|
+
url,
|
|
120
|
+
cacheInit(opts?.revalidate, opts?.tags)
|
|
121
|
+
);
|
|
122
|
+
const data = body?.data;
|
|
123
|
+
if (!data) {
|
|
124
|
+
return {
|
|
125
|
+
items: [],
|
|
126
|
+
page: opts?.page ?? 1,
|
|
127
|
+
perPage: opts?.perPage ?? 20,
|
|
128
|
+
totalItems: 0,
|
|
129
|
+
totalPages: 1,
|
|
130
|
+
hasNextPage: false,
|
|
131
|
+
hasPreviousPage: false
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
items: data.items.map((r) => mapEntry(r)),
|
|
136
|
+
page: data.page,
|
|
137
|
+
perPage: data.perPage,
|
|
138
|
+
totalItems: data.totalItems,
|
|
139
|
+
totalPages: data.totalPages,
|
|
140
|
+
hasNextPage: data.hasNextPage,
|
|
141
|
+
hasPreviousPage: data.hasPreviousPage
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
async getPage(slug, opts) {
|
|
145
|
+
const url = `${baseUrl}/${config.workspace}/content/${encodeURIComponent(slug)}`;
|
|
146
|
+
const body = await requestJSON(
|
|
147
|
+
url,
|
|
148
|
+
cacheInit(opts?.revalidate, opts?.tags),
|
|
149
|
+
{ nullOn404: true }
|
|
150
|
+
);
|
|
151
|
+
if (!body) return null;
|
|
152
|
+
const { entry, publishedAt } = body.data;
|
|
153
|
+
return {
|
|
154
|
+
slug: entry.slug,
|
|
155
|
+
title: entry.title,
|
|
156
|
+
metaTitle: entry.metaTitle ?? null,
|
|
157
|
+
metaDescription: entry.metaDescription ?? null,
|
|
158
|
+
metaJson: entry.metaJson ?? null,
|
|
159
|
+
blocks: entry.blocks ?? [],
|
|
160
|
+
publishedAt: publishedAt ?? null,
|
|
161
|
+
updatedAt: entry.updatedAt ?? null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
return client;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/metadata.ts
|
|
169
|
+
import { resolveSeo } from "@bettercms-ai/sdk";
|
|
170
|
+
function buildMetadata(page, siteDefaults = {}) {
|
|
171
|
+
const seo = resolveSeo(page, siteDefaults);
|
|
172
|
+
return {
|
|
173
|
+
title: seo.title || void 0,
|
|
174
|
+
description: seo.description || void 0,
|
|
175
|
+
alternates: seo.canonical ? { canonical: seo.canonical } : void 0,
|
|
176
|
+
openGraph: {
|
|
177
|
+
title: seo.og.title || void 0,
|
|
178
|
+
description: seo.og.description || void 0,
|
|
179
|
+
type: seo.og.type || "website",
|
|
180
|
+
url: seo.og.url || void 0,
|
|
181
|
+
images: seo.og.image ? [{ url: seo.og.image }] : void 0
|
|
182
|
+
},
|
|
183
|
+
twitter: {
|
|
184
|
+
card: seo.twitter.card || "summary",
|
|
185
|
+
title: seo.twitter.title || void 0,
|
|
186
|
+
description: seo.twitter.description || void 0,
|
|
187
|
+
images: seo.twitter.image ? [seo.twitter.image] : void 0,
|
|
188
|
+
site: seo.twitter.site || void 0
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/index.ts
|
|
194
|
+
import { resolveSeo as resolveSeo2 } from "@bettercms-ai/sdk";
|
|
195
|
+
|
|
196
|
+
// src/snapshot.ts
|
|
197
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
198
|
+
import { dirname, resolve } from "path";
|
|
199
|
+
var SNAPSHOT_VERSION = 1;
|
|
200
|
+
var DEFAULT_SNAPSHOT_FILE = "bcms-content.json";
|
|
201
|
+
function serializeSnapshot(snapshot) {
|
|
202
|
+
return JSON.stringify(snapshot, null, 2);
|
|
203
|
+
}
|
|
204
|
+
function parseSnapshot(json) {
|
|
205
|
+
const parsed = JSON.parse(json);
|
|
206
|
+
if (parsed.version !== SNAPSHOT_VERSION) {
|
|
207
|
+
throw new BetterCMSError(
|
|
208
|
+
`Unsupported bcms-content.json version ${parsed.version} (expected ${SNAPSHOT_VERSION}). Regenerate it with "bettercms-snapshot".`,
|
|
209
|
+
0,
|
|
210
|
+
"SNAPSHOT_VERSION_MISMATCH"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return parsed;
|
|
214
|
+
}
|
|
215
|
+
function writeSnapshot(path, snapshot) {
|
|
216
|
+
const out = resolve(process.cwd(), path);
|
|
217
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
218
|
+
writeFileSync(out, serializeSnapshot(snapshot), "utf8");
|
|
219
|
+
}
|
|
220
|
+
function readSnapshot(path) {
|
|
221
|
+
const out = resolve(process.cwd(), path);
|
|
222
|
+
let json;
|
|
223
|
+
try {
|
|
224
|
+
json = readFileSync(out, "utf8");
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (err?.code === "ENOENT") return null;
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
return parseSnapshot(json);
|
|
230
|
+
}
|
|
231
|
+
async function buildSnapshot(opts) {
|
|
232
|
+
const client = opts.client ?? createBetterCMS(opts.config);
|
|
233
|
+
const models = {};
|
|
234
|
+
for (const slug of opts.modelSlugs) {
|
|
235
|
+
const all = [];
|
|
236
|
+
let page = 1;
|
|
237
|
+
for (; ; ) {
|
|
238
|
+
const res = await client.listEntries(slug, { page, perPage: 100, revalidate: false });
|
|
239
|
+
all.push(...res.items);
|
|
240
|
+
if (!res.hasNextPage) break;
|
|
241
|
+
page += 1;
|
|
242
|
+
}
|
|
243
|
+
models[slug] = all;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
version: SNAPSHOT_VERSION,
|
|
247
|
+
workspace: opts.config.workspace,
|
|
248
|
+
generatedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
249
|
+
models
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function isPreview() {
|
|
253
|
+
const v = globalThis.process?.env?.BETTERCMS_PREVIEW;
|
|
254
|
+
return v != null && v !== "" && v !== "0" && v !== "false";
|
|
255
|
+
}
|
|
256
|
+
var snapshotCache = /* @__PURE__ */ new Map();
|
|
257
|
+
function emptyPage(items) {
|
|
258
|
+
return {
|
|
259
|
+
items,
|
|
260
|
+
page: 1,
|
|
261
|
+
perPage: items.length,
|
|
262
|
+
totalItems: items.length,
|
|
263
|
+
totalPages: 1,
|
|
264
|
+
hasNextPage: false,
|
|
265
|
+
hasPreviousPage: false
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function getContent(config, opts = {}) {
|
|
269
|
+
const path = opts.snapshotPath ?? DEFAULT_SNAPSHOT_FILE;
|
|
270
|
+
if (isPreview()) {
|
|
271
|
+
const live = createBetterCMS(config);
|
|
272
|
+
return {
|
|
273
|
+
getEntry: (slug) => live.getEntry(slug, { revalidate: false }),
|
|
274
|
+
listEntries: (model) => live.listEntries(model, { revalidate: false })
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const resolved = resolve(process.cwd(), path);
|
|
278
|
+
function load() {
|
|
279
|
+
const cached = snapshotCache.get(resolved);
|
|
280
|
+
if (cached) return cached;
|
|
281
|
+
const snap = readSnapshot(path);
|
|
282
|
+
if (!snap) {
|
|
283
|
+
throw new BetterCMSError(
|
|
284
|
+
`BetterCMS content snapshot "${path}" not found. A static (output: "export") build reads content from this snapshot; without it pages fall back to a live no-store fetch, become dynamic routes, and output: "export" silently drops them (empty out/). Run "bettercms-snapshot" before "next build" (make it your build:preview prestep), or set BETTERCMS_PREVIEW=1 to read live in a preview build.`,
|
|
285
|
+
0,
|
|
286
|
+
"SNAPSHOT_MISSING"
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
snapshotCache.set(resolved, snap);
|
|
290
|
+
return snap;
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
async getEntry(slug) {
|
|
294
|
+
const snap = load();
|
|
295
|
+
for (const entries of Object.values(snap.models)) {
|
|
296
|
+
const found = entries.find((e) => e.slug === slug);
|
|
297
|
+
if (found) return found;
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
},
|
|
301
|
+
async listEntries(model) {
|
|
302
|
+
const snap = load();
|
|
303
|
+
const items = model ? snap.models[model] ?? [] : Object.values(snap.models).flat();
|
|
304
|
+
return emptyPage(items);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/llms-txt.ts
|
|
310
|
+
function oneLine(text) {
|
|
311
|
+
return text.replace(/[\r\n]+/g, " ").trim();
|
|
312
|
+
}
|
|
313
|
+
function inlineText(text) {
|
|
314
|
+
return oneLine(text).replace(/^([#>\-*+=|])/, "\\$1");
|
|
315
|
+
}
|
|
316
|
+
function codeSpan(text) {
|
|
317
|
+
return oneLine(text).replace(/`/g, "");
|
|
318
|
+
}
|
|
319
|
+
function fieldLine(f) {
|
|
320
|
+
const req = f.required ? ", required" : "";
|
|
321
|
+
const label = f.label && f.label !== f.key ? ` \u2014 ${inlineText(f.label)}` : "";
|
|
322
|
+
return ` - \`${codeSpan(f.key)}\` (${codeSpan(f.type)}${req})${label}`;
|
|
323
|
+
}
|
|
324
|
+
function generateLlmsTxt(models, opts = {}) {
|
|
325
|
+
const title = opts.title ?? "BetterCMS content";
|
|
326
|
+
const sorted = [...models].sort(
|
|
327
|
+
(a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0
|
|
328
|
+
);
|
|
329
|
+
const lines = [`# ${inlineText(title)}`, ""];
|
|
330
|
+
if (opts.description) {
|
|
331
|
+
lines.push(`> ${inlineText(opts.description)}`, "");
|
|
332
|
+
}
|
|
333
|
+
const base = opts.baseUrl && opts.workspace ? `${opts.baseUrl.replace(/\/+$/, "")}/${opts.workspace}/content-entries` : void 0;
|
|
334
|
+
lines.push("## Content models", "");
|
|
335
|
+
if (sorted.length === 0) {
|
|
336
|
+
lines.push("_No content models are defined yet._", "");
|
|
337
|
+
}
|
|
338
|
+
for (const model of sorted) {
|
|
339
|
+
const name = model.name ?? model.slug;
|
|
340
|
+
lines.push(`### ${inlineText(name)} (\`${codeSpan(model.slug)}\`)`);
|
|
341
|
+
if (model.description) lines.push("", inlineText(model.description));
|
|
342
|
+
if (base) {
|
|
343
|
+
lines.push("", `- List: \`GET ${base}?model=${codeSpan(model.slug)}\``);
|
|
344
|
+
lines.push(`- Entry: \`GET ${base}/{slug}\``);
|
|
345
|
+
}
|
|
346
|
+
if (model.fields.length) {
|
|
347
|
+
lines.push("", "Fields:");
|
|
348
|
+
for (const f of model.fields) lines.push(fieldLine(f));
|
|
349
|
+
}
|
|
350
|
+
lines.push("");
|
|
351
|
+
}
|
|
352
|
+
if (opts.notes?.length) {
|
|
353
|
+
lines.push("## Notes", "");
|
|
354
|
+
for (const note of opts.notes) lines.push(`- ${inlineText(note)}`);
|
|
355
|
+
lines.push("");
|
|
356
|
+
}
|
|
357
|
+
return lines.join("\n");
|
|
358
|
+
}
|
|
359
|
+
function llmsTxtRoute(config) {
|
|
360
|
+
const { models, getModels, cacheControl, ...opts } = config;
|
|
361
|
+
return async () => {
|
|
362
|
+
const resolved = getModels ? await getModels() : models ?? [];
|
|
363
|
+
const body = generateLlmsTxt(resolved, opts);
|
|
364
|
+
return new Response(body, {
|
|
365
|
+
status: 200,
|
|
366
|
+
headers: {
|
|
367
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
368
|
+
"Cache-Control": cacheControl ?? "public, max-age=3600"
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
async function fetchModels(opts) {
|
|
374
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
375
|
+
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
376
|
+
const res = await doFetch(`${base}/management/content/models`, {
|
|
377
|
+
headers: { Authorization: `Bearer ${opts.apiKey}`, Accept: "application/json" }
|
|
378
|
+
});
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
throw new Error(`Management API returned ${res.status} ${res.statusText}`);
|
|
381
|
+
}
|
|
382
|
+
const body = await res.json();
|
|
383
|
+
return (body.data ?? []).map((m) => ({
|
|
384
|
+
slug: m.slug,
|
|
385
|
+
name: m.name,
|
|
386
|
+
description: m.description ?? null,
|
|
387
|
+
fields: m.fields ?? []
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/preview.ts
|
|
392
|
+
async function isDraftEnabled() {
|
|
393
|
+
const { draftMode } = await import("next/headers");
|
|
394
|
+
const dm = await draftMode();
|
|
395
|
+
return dm.isEnabled;
|
|
396
|
+
}
|
|
397
|
+
async function getPreviewToken() {
|
|
398
|
+
const { cookies } = await import("next/headers");
|
|
399
|
+
const jar = await cookies();
|
|
400
|
+
return jar.get("bcms-preview-token")?.value ?? null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/draft-route.ts
|
|
404
|
+
var PREVIEW_TOKEN_COOKIE = "bcms-preview-token";
|
|
405
|
+
function safeRedirectPath(to) {
|
|
406
|
+
return to.startsWith("/") && !to.startsWith("//") ? to : "/";
|
|
407
|
+
}
|
|
408
|
+
function decodeTokenSlug(token) {
|
|
409
|
+
try {
|
|
410
|
+
const part = token.split(".")[1];
|
|
411
|
+
if (!part) return null;
|
|
412
|
+
const json = JSON.parse(
|
|
413
|
+
Buffer.from(part, "base64url").toString("utf8")
|
|
414
|
+
);
|
|
415
|
+
return json.entrySlug ?? null;
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function createDraftModeRoute(config) {
|
|
421
|
+
const apiUrl = config.apiUrl.replace(/\/+$/, "");
|
|
422
|
+
async function GET(request) {
|
|
423
|
+
const url = new URL(request.url);
|
|
424
|
+
const token = url.searchParams.get("token");
|
|
425
|
+
const redirectTo = url.searchParams.get("redirect") ?? "/";
|
|
426
|
+
if (!token) return new Response("Missing token", { status: 400 });
|
|
427
|
+
const slug = decodeTokenSlug(token);
|
|
428
|
+
if (!slug) return new Response("Malformed token", { status: 400 });
|
|
429
|
+
const res = await fetch(
|
|
430
|
+
`${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`
|
|
431
|
+
);
|
|
432
|
+
if (!res.ok) return new Response("Invalid or expired token", { status: 401 });
|
|
433
|
+
const { draftMode, cookies } = await import("next/headers");
|
|
434
|
+
(await draftMode()).enable();
|
|
435
|
+
(await cookies()).set(PREVIEW_TOKEN_COOKIE, token, {
|
|
436
|
+
httpOnly: true,
|
|
437
|
+
sameSite: "lax",
|
|
438
|
+
secure: url.protocol === "https:",
|
|
439
|
+
path: "/",
|
|
440
|
+
maxAge: 60 * 60 * 24
|
|
441
|
+
});
|
|
442
|
+
return Response.redirect(new URL(safeRedirectPath(redirectTo), url.origin), 302);
|
|
443
|
+
}
|
|
444
|
+
return { GET };
|
|
445
|
+
}
|
|
446
|
+
function createDisableDraftRoute() {
|
|
447
|
+
async function GET(request) {
|
|
448
|
+
const url = new URL(request.url);
|
|
449
|
+
const redirectTo = url.searchParams.get("redirect") ?? "/";
|
|
450
|
+
const { draftMode, cookies } = await import("next/headers");
|
|
451
|
+
(await draftMode()).disable();
|
|
452
|
+
(await cookies()).delete(PREVIEW_TOKEN_COOKIE);
|
|
453
|
+
return Response.redirect(new URL(safeRedirectPath(redirectTo), url.origin), 302);
|
|
454
|
+
}
|
|
455
|
+
return { GET };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/index.ts
|
|
459
|
+
import { default as default2, imageUrl } from "@bettercms-ai/image-url";
|
|
460
|
+
|
|
461
|
+
// src/forms-read.ts
|
|
462
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
463
|
+
import { resolve as resolve2 } from "path";
|
|
464
|
+
function readForms(path = "bcms-content.json") {
|
|
465
|
+
let raw;
|
|
466
|
+
try {
|
|
467
|
+
raw = readFileSync2(resolve2(process.cwd(), path), "utf8");
|
|
468
|
+
} catch {
|
|
469
|
+
return { forms: [], turnstileSiteKey: null };
|
|
470
|
+
}
|
|
471
|
+
const snap = JSON.parse(raw);
|
|
472
|
+
return { forms: snap.forms ?? [], turnstileSiteKey: snap.turnstileSiteKey ?? null };
|
|
473
|
+
}
|
|
474
|
+
function getForm(idOrName, path) {
|
|
475
|
+
return readForms(path).forms.find((f) => f.id === idOrName || f.name === idOrName);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/pages-read.ts
|
|
479
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
480
|
+
import { resolve as resolve3 } from "path";
|
|
481
|
+
function readPages(path = "bcms-content.json") {
|
|
482
|
+
let raw;
|
|
483
|
+
try {
|
|
484
|
+
raw = readFileSync3(resolve3(process.cwd(), path), "utf8");
|
|
485
|
+
} catch {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
const snap = JSON.parse(raw);
|
|
489
|
+
return snap.pages ?? [];
|
|
490
|
+
}
|
|
491
|
+
function getPage(slug, path) {
|
|
492
|
+
return readPages(path).find((p) => p.slug === slug);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/components-read.ts
|
|
496
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
497
|
+
import { resolve as resolve4 } from "path";
|
|
498
|
+
function readComponents(path = "bcms-content.json") {
|
|
499
|
+
let raw;
|
|
500
|
+
try {
|
|
501
|
+
raw = readFileSync4(resolve4(process.cwd(), path), "utf8");
|
|
502
|
+
} catch {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
const snap = JSON.parse(raw);
|
|
506
|
+
return snap.components ?? [];
|
|
507
|
+
}
|
|
508
|
+
function getComponent(idOrSlug, path) {
|
|
509
|
+
return readComponents(path).find(
|
|
510
|
+
(c) => c.id === idOrSlug || c.slug === idOrSlug || c.name === idOrSlug
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/live-config.ts
|
|
515
|
+
function defineBetterCMSLive(config) {
|
|
516
|
+
const base = config.apiUrl.replace(/\/+$/, "");
|
|
517
|
+
const liveSrc = `${base}/api/v1/delivery/${config.workspace}/live?key=${encodeURIComponent(config.apiKey)}`;
|
|
518
|
+
return { liveSrc };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/revalidate-route.ts
|
|
522
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
523
|
+
function safeEqual(a, b) {
|
|
524
|
+
const ab = Buffer.from(a);
|
|
525
|
+
const bb = Buffer.from(b);
|
|
526
|
+
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
527
|
+
}
|
|
528
|
+
function createRevalidateRoute(config = {}) {
|
|
529
|
+
async function POST(request) {
|
|
530
|
+
const body = await request.text();
|
|
531
|
+
if (config.secret) {
|
|
532
|
+
const provided = request.headers.get("x-bettercms-signature") ?? "";
|
|
533
|
+
const expected = `sha256=${createHmac("sha256", config.secret).update(body).digest("hex")}`;
|
|
534
|
+
if (!safeEqual(provided, expected)) {
|
|
535
|
+
return new Response("Invalid signature", { status: 401 });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
let payload;
|
|
539
|
+
try {
|
|
540
|
+
payload = JSON.parse(body);
|
|
541
|
+
} catch {
|
|
542
|
+
return new Response("Invalid JSON", { status: 400 });
|
|
543
|
+
}
|
|
544
|
+
const tags = payload.data?.tags ?? [];
|
|
545
|
+
const { revalidateTag } = await import("next/cache");
|
|
546
|
+
for (const tag of tags) revalidateTag(tag);
|
|
547
|
+
return Response.json({ revalidated: true, tags });
|
|
548
|
+
}
|
|
549
|
+
return { POST };
|
|
550
|
+
}
|
|
551
|
+
export {
|
|
552
|
+
BetterCMSError,
|
|
553
|
+
DEFAULT_SNAPSHOT_FILE,
|
|
554
|
+
PREVIEW_TOKEN_COOKIE,
|
|
555
|
+
SNAPSHOT_VERSION,
|
|
556
|
+
buildMetadata,
|
|
557
|
+
buildSnapshot,
|
|
558
|
+
createBetterCMS,
|
|
559
|
+
createDisableDraftRoute,
|
|
560
|
+
createDraftModeRoute,
|
|
561
|
+
createRevalidateRoute,
|
|
562
|
+
defineBetterCMSLive,
|
|
563
|
+
fetchModels,
|
|
564
|
+
generateLlmsTxt,
|
|
565
|
+
getComponent,
|
|
566
|
+
getContent,
|
|
567
|
+
getForm,
|
|
568
|
+
getPage,
|
|
569
|
+
getPreviewToken,
|
|
570
|
+
imageUrl,
|
|
571
|
+
default2 as imageUrlBuilder,
|
|
572
|
+
isDraftEnabled,
|
|
573
|
+
llmsTxtRoute,
|
|
574
|
+
parseSnapshot,
|
|
575
|
+
readComponents,
|
|
576
|
+
readForms,
|
|
577
|
+
readPages,
|
|
578
|
+
readSnapshot,
|
|
579
|
+
resolveSeo2 as resolveSeo,
|
|
580
|
+
serializeSnapshot,
|
|
581
|
+
writeSnapshot
|
|
582
|
+
};
|
|
583
|
+
//# sourceMappingURL=index.js.map
|