@dispatchcms/next 0.0.6 → 0.0.7
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 +28 -33
- package/dist/index.d.mts +11 -14
- package/dist/index.d.ts +11 -14
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -9
- package/src/client.ts +17 -20
- package/src/index.ts +4 -4
- package/test/client.cache.test.ts +28 -32
- package/test/getPage.test.ts +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dispatchcms/next
|
|
2
2
|
|
|
3
|
-
Fetch published
|
|
3
|
+
Fetch published entries from a [Dispatch](https://github.com/dispatch-cms/dispatch) CMS in your Next.js app.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -36,43 +36,41 @@ You can omit `initDispatch` entirely if `NEXT_PUBLIC_DISPATCH_SITE_KEY` is set;
|
|
|
36
36
|
|
|
37
37
|
## Caching
|
|
38
38
|
|
|
39
|
-
The package does **not** cache responses in memory. Each call to `
|
|
39
|
+
The package does **not** cache responses in memory. Each call to `getEntries()` or `getEntry()` fetches from the API, so your app always sees up-to-date data. For performance, rely on Next.js: use Server Components and the default `fetch` caching, or `revalidate` / ISR.
|
|
40
40
|
|
|
41
41
|
## API
|
|
42
42
|
|
|
43
|
-
### `
|
|
43
|
+
### `getEntries(collection, siteKey?)`
|
|
44
44
|
|
|
45
|
-
Returns
|
|
45
|
+
Returns published entries for a collection, ordered as in the CMS.
|
|
46
46
|
|
|
47
|
-
- **Returns:** `Promise<
|
|
47
|
+
- **Returns:** `Promise<Entry[]>`
|
|
48
48
|
- **Optional:** pass `siteKey` to override the configured site for this call.
|
|
49
49
|
|
|
50
|
-
### `
|
|
50
|
+
### `getEntry(collection, slug, siteKey?)`
|
|
51
51
|
|
|
52
|
-
Returns
|
|
52
|
+
Returns one published entry by collection and slug, or `null` if not found.
|
|
53
53
|
|
|
54
|
-
- **Returns:** `Promise<
|
|
55
|
-
- **Optional:** pass `siteKey` as the second argument to override the configured site.
|
|
54
|
+
- **Returns:** `Promise<Entry | null>`
|
|
56
55
|
|
|
57
|
-
### `
|
|
56
|
+
### `getEntryByPreviewToken(token)`
|
|
58
57
|
|
|
59
|
-
Returns
|
|
58
|
+
Returns one entry by its preview token (draft or published). No site key is required; the token is the secret.
|
|
60
59
|
|
|
61
|
-
- **Returns:** `Promise<
|
|
62
|
-
- **Example:** Implement a route at `/preview` (or `/blog/preview`) that reads `token` from the query and renders the post with the same layout as your live post page (see below).
|
|
60
|
+
- **Returns:** `Promise<Entry | null>`
|
|
63
61
|
|
|
64
62
|
## Types
|
|
65
63
|
|
|
66
64
|
Exportable types:
|
|
67
65
|
|
|
68
|
-
- **`
|
|
66
|
+
- **`Entry`** – An entry from a collection. Main fields: `id`, `collection`, `title`, `slug`, `data` (field values; rich text fields are TipTap JSON), `published`, `published_at`, `sort_order`, `created_at`, `updated_at`, `site_id`. A migrated blog stores the body at `data.body` and the excerpt at `data.excerpt`.
|
|
69
67
|
- **`DispatchConfig`** – Options for `initDispatch`: `{ siteKey: string }`.
|
|
70
68
|
|
|
71
69
|
Use your editor’s type hints or the generated `.d.ts` for the full shape.
|
|
72
70
|
|
|
73
71
|
## Rendering content
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
Rich text field values are TipTap (ProseMirror) JSON. Render them with TipTap’s static renderer so formatting, links, and images match the CMS:
|
|
76
74
|
|
|
77
75
|
- **Server Components:** Use `renderToHTMLString` from `@tiptap/static-renderer/pm/html-string` with the same extensions as the CMS (e.g. Document, Paragraph, Heading, Bold, Italic, Blockquote, BulletList, ListItem, Link, Image), then render the HTML in a wrapper `div` (the output is safe TipTap-generated HTML).
|
|
78
76
|
- **Client:** Use `renderToReactElement` from `@tiptap/static-renderer/pm/react` for a React node with no `dangerouslySetInnerHTML`.
|
|
@@ -85,15 +83,15 @@ Use the same extensions as the CMS editor for full parity. See [TipTap static re
|
|
|
85
83
|
|
|
86
84
|
```tsx
|
|
87
85
|
// app/blog/page.tsx
|
|
88
|
-
import {
|
|
86
|
+
import { getEntries } from "@dispatchcms/next";
|
|
89
87
|
|
|
90
88
|
export default async function BlogPage() {
|
|
91
|
-
const
|
|
89
|
+
const entries = await getEntries("posts");
|
|
92
90
|
return (
|
|
93
91
|
<ul>
|
|
94
|
-
{
|
|
95
|
-
<li key={
|
|
96
|
-
<a href={`/blog/${
|
|
92
|
+
{entries.map((entry) => (
|
|
93
|
+
<li key={entry.id}>
|
|
94
|
+
<a href={`/blog/${entry.slug}`}>{entry.title}</a>
|
|
97
95
|
</li>
|
|
98
96
|
))}
|
|
99
97
|
</ul>
|
|
@@ -101,21 +99,20 @@ export default async function BlogPage() {
|
|
|
101
99
|
}
|
|
102
100
|
```
|
|
103
101
|
|
|
104
|
-
**Single
|
|
102
|
+
**Single entry (Server Component):**
|
|
105
103
|
|
|
106
104
|
```tsx
|
|
107
105
|
// app/blog/[slug]/page.tsx
|
|
108
|
-
import {
|
|
106
|
+
import { getEntry } from "@dispatchcms/next";
|
|
109
107
|
import { notFound } from "next/navigation";
|
|
110
108
|
|
|
111
109
|
export default async function PostPage({ params }: { params: { slug: string } }) {
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
110
|
+
const entry = await getEntry("posts", params.slug);
|
|
111
|
+
if (!entry) notFound();
|
|
114
112
|
return (
|
|
115
113
|
<article>
|
|
116
|
-
<h1>{
|
|
117
|
-
{
|
|
118
|
-
{/* Render post.content with TipTap: renderToHTMLString(extensions, post.content) then a div, or use renderToReactElement in a client component */}
|
|
114
|
+
<h1>{entry.title}</h1>
|
|
115
|
+
{/* Render entry.data.body with TipTap */}
|
|
119
116
|
</article>
|
|
120
117
|
);
|
|
121
118
|
}
|
|
@@ -129,7 +126,7 @@ To support public preview links from the CMS (e.g. `https://yoursite.com/preview
|
|
|
129
126
|
|
|
130
127
|
```tsx
|
|
131
128
|
// app/preview/page.tsx
|
|
132
|
-
import {
|
|
129
|
+
import { getEntryByPreviewToken } from "@dispatchcms/next";
|
|
133
130
|
import { notFound } from "next/navigation";
|
|
134
131
|
|
|
135
132
|
export default async function PreviewPage({
|
|
@@ -139,14 +136,12 @@ export default async function PreviewPage({
|
|
|
139
136
|
}) {
|
|
140
137
|
const { token } = await searchParams;
|
|
141
138
|
if (!token) notFound();
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
139
|
+
const entry = await getEntryByPreviewToken(token);
|
|
140
|
+
if (!entry) notFound();
|
|
144
141
|
return (
|
|
145
142
|
<article>
|
|
146
143
|
<p className="text-sm text-muted-foreground">Preview</p>
|
|
147
|
-
<h1>{
|
|
148
|
-
{post.excerpt && <p>{post.excerpt}</p>}
|
|
149
|
-
{/* Render post.content with TipTap (same as your live post page) */}
|
|
144
|
+
<h1>{entry.title}</h1>
|
|
150
145
|
</article>
|
|
151
146
|
);
|
|
152
147
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
type
|
|
1
|
+
type Entry = {
|
|
2
2
|
id: string;
|
|
3
3
|
created_at: string;
|
|
4
|
-
published_at: string;
|
|
4
|
+
published_at: string | null;
|
|
5
5
|
updated_at: string;
|
|
6
6
|
site_id: string;
|
|
7
|
+
collection: string;
|
|
7
8
|
title: string;
|
|
8
|
-
slug: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
featured_image: string | null;
|
|
9
|
+
slug: string | null;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
sort_order: number;
|
|
12
12
|
published: boolean;
|
|
13
13
|
};
|
|
14
14
|
type DispatchConfig = {
|
|
@@ -18,12 +18,9 @@ declare function initDispatch(options: DispatchConfig): void;
|
|
|
18
18
|
declare function getConfig(): {
|
|
19
19
|
siteKey: string;
|
|
20
20
|
};
|
|
21
|
-
declare function
|
|
22
|
-
declare function
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
* No site key required. Use this in your app's preview route (e.g. /preview?token=...).
|
|
26
|
-
*/
|
|
27
|
-
declare function getPostByPreviewToken(token: string): Promise<Post | null>;
|
|
21
|
+
declare function getEntries(collection: string, siteKey?: string): Promise<Entry[]>;
|
|
22
|
+
declare function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null>;
|
|
23
|
+
/** Draft or published entry. The token is the secret; no site key is required. */
|
|
24
|
+
declare function getEntryByPreviewToken(token: string): Promise<Entry | null>;
|
|
28
25
|
|
|
29
|
-
export { type DispatchConfig, type
|
|
26
|
+
export { type DispatchConfig, type Entry, getConfig, getEntries, getEntry, getEntryByPreviewToken, initDispatch };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
type
|
|
1
|
+
type Entry = {
|
|
2
2
|
id: string;
|
|
3
3
|
created_at: string;
|
|
4
|
-
published_at: string;
|
|
4
|
+
published_at: string | null;
|
|
5
5
|
updated_at: string;
|
|
6
6
|
site_id: string;
|
|
7
|
+
collection: string;
|
|
7
8
|
title: string;
|
|
8
|
-
slug: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
featured_image: string | null;
|
|
9
|
+
slug: string | null;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
sort_order: number;
|
|
12
12
|
published: boolean;
|
|
13
13
|
};
|
|
14
14
|
type DispatchConfig = {
|
|
@@ -18,12 +18,9 @@ declare function initDispatch(options: DispatchConfig): void;
|
|
|
18
18
|
declare function getConfig(): {
|
|
19
19
|
siteKey: string;
|
|
20
20
|
};
|
|
21
|
-
declare function
|
|
22
|
-
declare function
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
* No site key required. Use this in your app's preview route (e.g. /preview?token=...).
|
|
26
|
-
*/
|
|
27
|
-
declare function getPostByPreviewToken(token: string): Promise<Post | null>;
|
|
21
|
+
declare function getEntries(collection: string, siteKey?: string): Promise<Entry[]>;
|
|
22
|
+
declare function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null>;
|
|
23
|
+
/** Draft or published entry. The token is the secret; no site key is required. */
|
|
24
|
+
declare function getEntryByPreviewToken(token: string): Promise<Entry | null>;
|
|
28
25
|
|
|
29
|
-
export { type DispatchConfig, type
|
|
26
|
+
export { type DispatchConfig, type Entry, getConfig, getEntries, getEntry, getEntryByPreviewToken, initDispatch };
|
package/dist/index.js
CHANGED
|
@@ -21,9 +21,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
getConfig: () => getConfig,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
getEntries: () => getEntries,
|
|
25
|
+
getEntry: () => getEntry,
|
|
26
|
+
getEntryByPreviewToken: () => getEntryByPreviewToken,
|
|
27
27
|
initDispatch: () => initDispatch
|
|
28
28
|
});
|
|
29
29
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -42,10 +42,10 @@ function getConfig() {
|
|
|
42
42
|
}
|
|
43
43
|
return { siteKey };
|
|
44
44
|
}
|
|
45
|
-
async function
|
|
45
|
+
async function getEntries(collection, siteKey) {
|
|
46
46
|
const { siteKey: key } = getConfig();
|
|
47
47
|
const resolvedKey = siteKey ?? key;
|
|
48
|
-
const url = `${DISPATCH_API_BASE}/api/
|
|
48
|
+
const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;
|
|
49
49
|
const res = await fetch(url, {
|
|
50
50
|
headers: { "X-Site-Key": resolvedKey }
|
|
51
51
|
});
|
|
@@ -53,14 +53,14 @@ async function getPosts(siteKey) {
|
|
|
53
53
|
if (res.status === 401) {
|
|
54
54
|
throw new Error("Invalid or missing site key");
|
|
55
55
|
}
|
|
56
|
-
throw new Error(`Failed to fetch
|
|
56
|
+
throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);
|
|
57
57
|
}
|
|
58
58
|
return await res.json();
|
|
59
59
|
}
|
|
60
|
-
async function
|
|
60
|
+
async function getEntry(collection, slug, siteKey) {
|
|
61
61
|
const { siteKey: key } = getConfig();
|
|
62
62
|
const resolvedKey = siteKey ?? key;
|
|
63
|
-
const url = `${DISPATCH_API_BASE}/api/
|
|
63
|
+
const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;
|
|
64
64
|
const res = await fetch(url, {
|
|
65
65
|
headers: { "X-Site-Key": resolvedKey }
|
|
66
66
|
});
|
|
@@ -71,11 +71,11 @@ async function getPost(slug, siteKey) {
|
|
|
71
71
|
if (res.status === 401) {
|
|
72
72
|
throw new Error("Invalid or missing site key");
|
|
73
73
|
}
|
|
74
|
-
throw new Error(`Failed to fetch
|
|
74
|
+
throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);
|
|
75
75
|
}
|
|
76
76
|
return await res.json();
|
|
77
77
|
}
|
|
78
|
-
async function
|
|
78
|
+
async function getEntryByPreviewToken(token) {
|
|
79
79
|
if (!token?.trim()) return null;
|
|
80
80
|
const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;
|
|
81
81
|
const res = await fetch(url);
|
|
@@ -88,9 +88,9 @@ async function getPostByPreviewToken(token) {
|
|
|
88
88
|
// Annotate the CommonJS export names for ESM import in node:
|
|
89
89
|
0 && (module.exports = {
|
|
90
90
|
getConfig,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
getEntries,
|
|
92
|
+
getEntry,
|
|
93
|
+
getEntryByPreviewToken,
|
|
94
94
|
initDispatch
|
|
95
95
|
});
|
|
96
96
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type {
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Entry, DispatchConfig } from \"./client\";\nexport {\n initDispatch,\n getConfig,\n getEntries,\n getEntry,\n getEntryByPreviewToken,\n} from \"./client\";\n","export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAE7B,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,WAAW,YAAoB,SAAoC;AACvF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC;AAC9E,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,SAAS,YAAoB,MAAc,SAAyC;AACxG,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC,IAAI,mBAAmB,IAAI,CAAC;AAC1G,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAGA,eAAsB,uBAAuB,OAAsC;AACjF,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,QAAM,MAAM,GAAG,iBAAiB,sBAAsB,mBAAmB,MAAM,KAAK,CAAC,CAAC;AACtF,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -12,10 +12,10 @@ function getConfig() {
|
|
|
12
12
|
}
|
|
13
13
|
return { siteKey };
|
|
14
14
|
}
|
|
15
|
-
async function
|
|
15
|
+
async function getEntries(collection, siteKey) {
|
|
16
16
|
const { siteKey: key } = getConfig();
|
|
17
17
|
const resolvedKey = siteKey ?? key;
|
|
18
|
-
const url = `${DISPATCH_API_BASE}/api/
|
|
18
|
+
const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;
|
|
19
19
|
const res = await fetch(url, {
|
|
20
20
|
headers: { "X-Site-Key": resolvedKey }
|
|
21
21
|
});
|
|
@@ -23,14 +23,14 @@ async function getPosts(siteKey) {
|
|
|
23
23
|
if (res.status === 401) {
|
|
24
24
|
throw new Error("Invalid or missing site key");
|
|
25
25
|
}
|
|
26
|
-
throw new Error(`Failed to fetch
|
|
26
|
+
throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);
|
|
27
27
|
}
|
|
28
28
|
return await res.json();
|
|
29
29
|
}
|
|
30
|
-
async function
|
|
30
|
+
async function getEntry(collection, slug, siteKey) {
|
|
31
31
|
const { siteKey: key } = getConfig();
|
|
32
32
|
const resolvedKey = siteKey ?? key;
|
|
33
|
-
const url = `${DISPATCH_API_BASE}/api/
|
|
33
|
+
const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;
|
|
34
34
|
const res = await fetch(url, {
|
|
35
35
|
headers: { "X-Site-Key": resolvedKey }
|
|
36
36
|
});
|
|
@@ -41,11 +41,11 @@ async function getPost(slug, siteKey) {
|
|
|
41
41
|
if (res.status === 401) {
|
|
42
42
|
throw new Error("Invalid or missing site key");
|
|
43
43
|
}
|
|
44
|
-
throw new Error(`Failed to fetch
|
|
44
|
+
throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);
|
|
45
45
|
}
|
|
46
46
|
return await res.json();
|
|
47
47
|
}
|
|
48
|
-
async function
|
|
48
|
+
async function getEntryByPreviewToken(token) {
|
|
49
49
|
if (!token?.trim()) return null;
|
|
50
50
|
const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;
|
|
51
51
|
const res = await fetch(url);
|
|
@@ -57,9 +57,9 @@ async function getPostByPreviewToken(token) {
|
|
|
57
57
|
}
|
|
58
58
|
export {
|
|
59
59
|
getConfig,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
getEntries,
|
|
61
|
+
getEntry,
|
|
62
|
+
getEntryByPreviewToken,
|
|
63
63
|
initDispatch
|
|
64
64
|
};
|
|
65
65
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n"],"mappings":";AAcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAE7B,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,WAAW,YAAoB,SAAoC;AACvF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC;AAC9E,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,SAAS,YAAoB,MAAc,SAAyC;AACxG,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC,IAAI,mBAAmB,IAAI,CAAC;AAC1G,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAGA,eAAsB,uBAAuB,OAAsC;AACjF,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,QAAM,MAAM,GAAG,iBAAiB,sBAAsB,mBAAmB,MAAM,KAAK,CAAC,CAAC;AACtF,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dispatchcms/next",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Fetch published
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "Fetch published entries from a Dispatch CMS in Next.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -12,15 +12,11 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsup",
|
|
17
|
-
"test": "tsx test/client.cache.test.ts",
|
|
18
|
-
"test:manual": "tsx test/getPage.test.ts"
|
|
19
|
-
},
|
|
20
15
|
"devDependencies": {
|
|
21
|
-
"@types/node": "^22.
|
|
16
|
+
"@types/node": "^22.20.1",
|
|
17
|
+
"next": "^16.3.0",
|
|
22
18
|
"tsup": "^8.3.5",
|
|
23
|
-
"tsx": "
|
|
19
|
+
"tsx": "4.23.8",
|
|
24
20
|
"typescript": "^5.7.2"
|
|
25
21
|
},
|
|
26
22
|
"engines": {
|
|
@@ -35,5 +31,10 @@
|
|
|
35
31
|
"license": "MIT",
|
|
36
32
|
"peerDependencies": {
|
|
37
33
|
"next": ">=16.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"test": "tsx test/client.cache.test.ts",
|
|
38
|
+
"test:manual": "tsx test/getPage.test.ts"
|
|
38
39
|
}
|
|
39
40
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type Entry = {
|
|
2
2
|
id: string;
|
|
3
3
|
created_at: string;
|
|
4
|
-
published_at: string;
|
|
4
|
+
published_at: string | null;
|
|
5
5
|
updated_at: string;
|
|
6
6
|
site_id: string;
|
|
7
|
+
collection: string;
|
|
7
8
|
title: string;
|
|
8
|
-
slug: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
featured_image: string | null;
|
|
9
|
+
slug: string | null;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
sort_order: number;
|
|
12
12
|
published: boolean;
|
|
13
13
|
};
|
|
14
14
|
|
|
@@ -33,10 +33,10 @@ export function getConfig(): { siteKey: string } {
|
|
|
33
33
|
return { siteKey };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export async function
|
|
36
|
+
export async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {
|
|
37
37
|
const { siteKey: key } = getConfig();
|
|
38
38
|
const resolvedKey = siteKey ?? key;
|
|
39
|
-
const url = `${DISPATCH_API_BASE}/api/
|
|
39
|
+
const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;
|
|
40
40
|
const res = await fetch(url, {
|
|
41
41
|
headers: { "X-Site-Key": resolvedKey },
|
|
42
42
|
});
|
|
@@ -44,15 +44,15 @@ export async function getPosts(siteKey?: string): Promise<Post[]> {
|
|
|
44
44
|
if (res.status === 401) {
|
|
45
45
|
throw new Error("Invalid or missing site key");
|
|
46
46
|
}
|
|
47
|
-
throw new Error(`Failed to fetch
|
|
47
|
+
throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);
|
|
48
48
|
}
|
|
49
|
-
return (await res.json()) as
|
|
49
|
+
return (await res.json()) as Entry[];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export async function
|
|
52
|
+
export async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {
|
|
53
53
|
const { siteKey: key } = getConfig();
|
|
54
54
|
const resolvedKey = siteKey ?? key;
|
|
55
|
-
const url = `${DISPATCH_API_BASE}/api/
|
|
55
|
+
const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;
|
|
56
56
|
const res = await fetch(url, {
|
|
57
57
|
headers: { "X-Site-Key": resolvedKey },
|
|
58
58
|
});
|
|
@@ -63,16 +63,13 @@ export async function getPost(slug: string, siteKey?: string): Promise<Post | nu
|
|
|
63
63
|
if (res.status === 401) {
|
|
64
64
|
throw new Error("Invalid or missing site key");
|
|
65
65
|
}
|
|
66
|
-
throw new Error(`Failed to fetch
|
|
66
|
+
throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);
|
|
67
67
|
}
|
|
68
|
-
return (await res.json()) as
|
|
68
|
+
return (await res.json()) as Entry;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
* No site key required. Use this in your app's preview route (e.g. /preview?token=...).
|
|
74
|
-
*/
|
|
75
|
-
export async function getPostByPreviewToken(token: string): Promise<Post | null> {
|
|
71
|
+
/** Draft or published entry. The token is the secret; no site key is required. */
|
|
72
|
+
export async function getEntryByPreviewToken(token: string): Promise<Entry | null> {
|
|
76
73
|
if (!token?.trim()) return null;
|
|
77
74
|
const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;
|
|
78
75
|
const res = await fetch(url);
|
|
@@ -80,5 +77,5 @@ export async function getPostByPreviewToken(token: string): Promise<Post | null>
|
|
|
80
77
|
if (!res.ok) {
|
|
81
78
|
throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);
|
|
82
79
|
}
|
|
83
|
-
return (await res.json()) as
|
|
80
|
+
return (await res.json()) as Entry;
|
|
84
81
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { Entry, DispatchConfig } from "./client";
|
|
2
2
|
export {
|
|
3
3
|
initDispatch,
|
|
4
4
|
getConfig,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
getEntries,
|
|
6
|
+
getEntry,
|
|
7
|
+
getEntryByPreviewToken,
|
|
8
8
|
} from "./client";
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { initDispatch,
|
|
1
|
+
import { initDispatch, getEntries, getEntry } from "../src/index";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const mockEntry = (slug: string, title: string) => ({
|
|
4
4
|
id: `id-${slug}`,
|
|
5
5
|
created_at: "",
|
|
6
6
|
published_at: "",
|
|
7
7
|
updated_at: "",
|
|
8
8
|
site_id: "",
|
|
9
|
+
collection: "posts",
|
|
9
10
|
title,
|
|
10
11
|
slug,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
featured_image: null,
|
|
12
|
+
data: {},
|
|
13
|
+
sort_order: 0,
|
|
14
14
|
published: true,
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
type FetchCall = { url: string };
|
|
18
18
|
const fetchCalls: FetchCall[] = [];
|
|
19
|
-
let listResponse: ReturnType<typeof
|
|
20
|
-
let singleResponse: ReturnType<typeof
|
|
19
|
+
let listResponse: ReturnType<typeof mockEntry>[] = [];
|
|
20
|
+
let singleResponse: ReturnType<typeof mockEntry> | null = null;
|
|
21
21
|
let singleStatus = 200;
|
|
22
22
|
|
|
23
23
|
function mockFetch(url: string, _options?: RequestInit): Promise<Response> {
|
|
24
24
|
fetchCalls.push({ url });
|
|
25
25
|
const u = new URL(url);
|
|
26
|
-
if (u.pathname === "/api/posts") {
|
|
26
|
+
if (u.pathname === "/api/content/posts") {
|
|
27
27
|
return Promise.resolve({
|
|
28
28
|
ok: true,
|
|
29
29
|
status: 200,
|
|
30
30
|
json: () => Promise.resolve(listResponse),
|
|
31
31
|
} as Response);
|
|
32
32
|
}
|
|
33
|
-
const slugMatch = u.pathname.match(/^\/api\/posts\/(.+)$/);
|
|
33
|
+
const slugMatch = u.pathname.match(/^\/api\/content\/posts\/(.+)$/);
|
|
34
34
|
if (slugMatch) {
|
|
35
35
|
const slug = decodeURIComponent(slugMatch[1]);
|
|
36
36
|
if (singleStatus === 404) {
|
|
@@ -42,7 +42,7 @@ function mockFetch(url: string, _options?: RequestInit): Promise<Response> {
|
|
|
42
42
|
return Promise.resolve({
|
|
43
43
|
ok: true,
|
|
44
44
|
status: 200,
|
|
45
|
-
json: () => Promise.resolve(singleResponse ??
|
|
45
|
+
json: () => Promise.resolve(singleResponse ?? mockEntry(slug, "Single")),
|
|
46
46
|
} as Response);
|
|
47
47
|
}
|
|
48
48
|
return Promise.reject(new Error(`Unexpected URL: ${url}`));
|
|
@@ -61,40 +61,36 @@ async function run(): Promise<void> {
|
|
|
61
61
|
initDispatch({ siteKey: "pk_test" });
|
|
62
62
|
|
|
63
63
|
try {
|
|
64
|
-
// Test 1: getPosts() fetches and returns the list (no in-memory cache)
|
|
65
64
|
fetchCalls.length = 0;
|
|
66
|
-
listResponse = [
|
|
67
|
-
const list1 = await
|
|
68
|
-
assert(fetchCalls.length === 1, "
|
|
65
|
+
listResponse = [mockEntry("a", "Post A"), mockEntry("b", "Post B")];
|
|
66
|
+
const list1 = await getEntries("posts", "pk_1");
|
|
67
|
+
assert(fetchCalls.length === 1, "getEntries triggers one fetch");
|
|
69
68
|
assert(list1.length === 2 && list1[0].slug === "a", "returns correct list");
|
|
70
|
-
const list2 = await
|
|
71
|
-
assert(fetchCalls.length === 2, "second
|
|
69
|
+
const list2 = await getEntries("posts", "pk_1");
|
|
70
|
+
assert(fetchCalls.length === 2, "second getEntries triggers another fetch (no cache)");
|
|
72
71
|
assert(list2.length === 2, "second call returns same list");
|
|
73
72
|
|
|
74
|
-
// Test 2: getPost(slug) fetches and returns the post
|
|
75
73
|
fetchCalls.length = 0;
|
|
76
|
-
singleResponse =
|
|
77
|
-
const
|
|
78
|
-
assert(fetchCalls.length === 1, "
|
|
79
|
-
assert(
|
|
74
|
+
singleResponse = mockEntry("my-slug", "My Post");
|
|
75
|
+
const entry = await getEntry("posts", "my-slug", "pk_2");
|
|
76
|
+
assert(fetchCalls.length === 1, "getEntry triggers one fetch");
|
|
77
|
+
assert(entry !== null && entry.slug === "my-slug" && entry.title === "My Post", "returns correct entry");
|
|
80
78
|
|
|
81
|
-
// Test 3: getPost(slug) for missing slug returns null
|
|
82
79
|
fetchCalls.length = 0;
|
|
83
80
|
singleStatus = 404;
|
|
84
|
-
const missing = await
|
|
85
|
-
assert(fetchCalls.length === 1, "
|
|
81
|
+
const missing = await getEntry("posts", "nonexistent", "pk_3");
|
|
82
|
+
assert(fetchCalls.length === 1, "getEntry triggers one fetch");
|
|
86
83
|
assert(missing === null, "returns null for 404");
|
|
87
84
|
singleStatus = 200;
|
|
88
85
|
|
|
89
|
-
// Test 4: getPost(slug) then getPosts() each trigger their own fetch
|
|
90
86
|
fetchCalls.length = 0;
|
|
91
|
-
singleResponse =
|
|
92
|
-
listResponse = [
|
|
93
|
-
const singleFirst = await
|
|
94
|
-
assert(fetchCalls.length === 1 && singleFirst?.slug === "first", "
|
|
95
|
-
const listAfter = await
|
|
96
|
-
assert(fetchCalls.length === 2, "
|
|
97
|
-
assert(listAfter.length === 1 && listAfter[0].slug === "first", "
|
|
87
|
+
singleResponse = mockEntry("first", "First Post");
|
|
88
|
+
listResponse = [mockEntry("first", "First Post")];
|
|
89
|
+
const singleFirst = await getEntry("posts", "first", "pk_4");
|
|
90
|
+
assert(fetchCalls.length === 1 && singleFirst?.slug === "first", "getEntry fetches");
|
|
91
|
+
const listAfter = await getEntries("posts", "pk_4");
|
|
92
|
+
assert(fetchCalls.length === 2, "getEntries triggers second fetch");
|
|
93
|
+
assert(listAfter.length === 1 && listAfter[0].slug === "first", "getEntries returns correct list");
|
|
98
94
|
} finally {
|
|
99
95
|
globalThis.fetch = originalFetch;
|
|
100
96
|
}
|
package/test/getPage.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { initDispatch,
|
|
1
|
+
import { initDispatch, getEntries } from "../src/index";
|
|
2
2
|
|
|
3
3
|
async function main() {
|
|
4
4
|
initDispatch({
|
|
5
5
|
siteKey: process.env.NEXT_PUBLIC_DISPATCH_SITE_KEY ?? "pk_test",
|
|
6
6
|
});
|
|
7
|
-
const result = await
|
|
7
|
+
const result = await getEntries("posts");
|
|
8
8
|
console.log(result);
|
|
9
9
|
}
|
|
10
10
|
|