@dispatchcms/next 0.0.3 → 0.0.5
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/.turbo/turbo-build.log +8 -8
- package/README.md +15 -6
- package/dist/index.js +2 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +2 -35
- package/test/client.cache.test.ts +25 -28
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @dispatchcms/next@0.0.
|
|
3
|
+
> @dispatchcms/next@0.0.5 build /Users/jamescalmus/Documents/dispatch/packages/next
|
|
4
4
|
> tsup
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
[34mCLI[39m Cleaning output folder
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
13
|
[34mESM[39m Build start
|
|
14
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
15
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
16
|
-
[32mCJS[39m ⚡️ Build success in
|
|
17
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
18
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
19
|
-
[32mESM[39m ⚡️ Build success in
|
|
14
|
+
[32mCJS[39m [1mdist/index.js [22m[32m3.18 KB[39m
|
|
15
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m4.45 KB[39m
|
|
16
|
+
[32mCJS[39m ⚡️ Build success in 13ms
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m2.04 KB[39m
|
|
18
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m4.20 KB[39m
|
|
19
|
+
[32mESM[39m ⚡️ Build success in 13ms
|
|
20
20
|
DTS Build start
|
|
21
|
-
DTS ⚡️ Build success in
|
|
21
|
+
DTS ⚡️ Build success in 365ms
|
|
22
22
|
DTS dist/index.d.ts 938.00 B
|
|
23
23
|
DTS dist/index.d.mts 938.00 B
|
package/README.md
CHANGED
|
@@ -36,20 +36,20 @@ You can omit `initDispatch` entirely if `NEXT_PUBLIC_DISPATCH_SITE_KEY` is set;
|
|
|
36
36
|
|
|
37
37
|
## Caching
|
|
38
38
|
|
|
39
|
-
The
|
|
39
|
+
The package does **not** cache responses in memory. Each call to `getPosts()` or `getPost(slug)` fetches from the API, so your app always sees up-to-date data (e.g. when you unpublish a post in the CMS it disappears on the next load). For performance, rely on Next.js: use Server Components and the default `fetch` caching, or `revalidate` / ISR, so that responses are cached at the request level and stay fresh according to your revalidation settings.
|
|
40
40
|
|
|
41
41
|
## API
|
|
42
42
|
|
|
43
43
|
### `getPosts(siteKey?)`
|
|
44
44
|
|
|
45
|
-
Returns all published posts for the site.
|
|
45
|
+
Returns all published posts for the site. Always fetches from the API (no in-memory cache).
|
|
46
46
|
|
|
47
47
|
- **Returns:** `Promise<Post[]>`
|
|
48
48
|
- **Optional:** pass `siteKey` to override the configured site for this call.
|
|
49
49
|
|
|
50
50
|
### `getPost(slug, siteKey?)`
|
|
51
51
|
|
|
52
|
-
Returns a single published post by slug, or `null` if not found.
|
|
52
|
+
Returns a single published post by slug, or `null` if not found. Always fetches from the API (no in-memory cache).
|
|
53
53
|
|
|
54
54
|
- **Returns:** `Promise<Post | null>`
|
|
55
55
|
- **Optional:** pass `siteKey` as the second argument to override the configured site.
|
|
@@ -65,11 +65,20 @@ Returns a single post by its preview token (draft or published). Use this in you
|
|
|
65
65
|
|
|
66
66
|
Exportable types:
|
|
67
67
|
|
|
68
|
-
- **`Post`** – A post from the CMS. Main fields: `id`, `title`, `slug`, `content` (TipTap JSON), `excerpt`, `featured_image`, `published`, `published_at`, `created_at`, `updated_at`, `site_id`.
|
|
68
|
+
- **`Post`** – A post from the CMS. Main fields: `id`, `title`, `slug`, `content` (TipTap/ProseMirror JSON), `excerpt`, `featured_image`, `published`, `published_at`, `created_at`, `updated_at`, `site_id`. See **Rendering content** below for how to render `content`.
|
|
69
69
|
- **`DispatchConfig`** – Options for `initDispatch`: `{ siteKey: string }`.
|
|
70
70
|
|
|
71
71
|
Use your editor’s type hints or the generated `.d.ts` for the full shape.
|
|
72
72
|
|
|
73
|
+
## Rendering content
|
|
74
|
+
|
|
75
|
+
Post `content` is TipTap (ProseMirror) JSON. Render it with TipTap’s static renderer so formatting, links, and images match the CMS:
|
|
76
|
+
|
|
77
|
+
- **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
|
+
- **Client:** Use `renderToReactElement` from `@tiptap/static-renderer/pm/react` for a React node with no `dangerouslySetInnerHTML`.
|
|
79
|
+
|
|
80
|
+
Use the same extensions as the CMS editor for full parity. See [TipTap static renderer docs](https://tiptap.dev/docs/editor/api/utilities/static-renderer).
|
|
81
|
+
|
|
73
82
|
## Example (Next.js App Router)
|
|
74
83
|
|
|
75
84
|
**List posts (Server Component):**
|
|
@@ -106,7 +115,7 @@ export default async function PostPage({ params }: { params: { slug: string } })
|
|
|
106
115
|
<article>
|
|
107
116
|
<h1>{post.title}</h1>
|
|
108
117
|
{post.excerpt && <p>{post.excerpt}</p>}
|
|
109
|
-
{/* Render post.content
|
|
118
|
+
{/* Render post.content with TipTap: renderToHTMLString(extensions, post.content) then a div, or use renderToReactElement in a client component */}
|
|
110
119
|
</article>
|
|
111
120
|
);
|
|
112
121
|
}
|
|
@@ -137,7 +146,7 @@ export default async function PreviewPage({
|
|
|
137
146
|
<p className="text-sm text-muted-foreground">Preview</p>
|
|
138
147
|
<h1>{post.title}</h1>
|
|
139
148
|
{post.excerpt && <p>{post.excerpt}</p>}
|
|
140
|
-
{/* Render post.content
|
|
149
|
+
{/* Render post.content with TipTap (same as your live post page) */}
|
|
141
150
|
</article>
|
|
142
151
|
);
|
|
143
152
|
}
|
package/dist/index.js
CHANGED
|
@@ -31,9 +31,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
31
31
|
// src/client.ts
|
|
32
32
|
var DISPATCH_API_BASE = "https://dispatch-cms.vercel.app";
|
|
33
33
|
var config = null;
|
|
34
|
-
var allPostsBySiteKey = /* @__PURE__ */ new Map();
|
|
35
|
-
var postBySlugBySiteKey = /* @__PURE__ */ new Map();
|
|
36
|
-
var fullListFetchedForSiteKey = /* @__PURE__ */ new Set();
|
|
37
34
|
function initDispatch(options) {
|
|
38
35
|
config = { siteKey: options.siteKey };
|
|
39
36
|
}
|
|
@@ -48,10 +45,6 @@ function getConfig() {
|
|
|
48
45
|
async function getPosts(siteKey) {
|
|
49
46
|
const { siteKey: key } = getConfig();
|
|
50
47
|
const resolvedKey = siteKey ?? key;
|
|
51
|
-
const cached = allPostsBySiteKey.get(resolvedKey);
|
|
52
|
-
if (cached !== void 0) {
|
|
53
|
-
return cached;
|
|
54
|
-
}
|
|
55
48
|
const url = `${DISPATCH_API_BASE}/api/posts`;
|
|
56
49
|
const res = await fetch(url, {
|
|
57
50
|
headers: { "X-Site-Key": resolvedKey }
|
|
@@ -62,31 +55,11 @@ async function getPosts(siteKey) {
|
|
|
62
55
|
}
|
|
63
56
|
throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);
|
|
64
57
|
}
|
|
65
|
-
|
|
66
|
-
allPostsBySiteKey.set(resolvedKey, posts);
|
|
67
|
-
fullListFetchedForSiteKey.add(resolvedKey);
|
|
68
|
-
const bySlug = /* @__PURE__ */ new Map();
|
|
69
|
-
for (const post of posts) {
|
|
70
|
-
bySlug.set(post.slug, post);
|
|
71
|
-
}
|
|
72
|
-
postBySlugBySiteKey.set(resolvedKey, bySlug);
|
|
73
|
-
return posts;
|
|
58
|
+
return await res.json();
|
|
74
59
|
}
|
|
75
60
|
async function getPost(slug, siteKey) {
|
|
76
61
|
const { siteKey: key } = getConfig();
|
|
77
62
|
const resolvedKey = siteKey ?? key;
|
|
78
|
-
if (fullListFetchedForSiteKey.has(resolvedKey)) {
|
|
79
|
-
const list = allPostsBySiteKey.get(resolvedKey);
|
|
80
|
-
if (list) {
|
|
81
|
-
const post2 = list.find((p) => p.slug === slug);
|
|
82
|
-
return post2 ?? null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const bySlug = postBySlugBySiteKey.get(resolvedKey);
|
|
86
|
-
const cachedPost = bySlug?.get(slug);
|
|
87
|
-
if (cachedPost !== void 0) {
|
|
88
|
-
return cachedPost;
|
|
89
|
-
}
|
|
90
63
|
const url = `${DISPATCH_API_BASE}/api/posts/${encodeURIComponent(slug)}`;
|
|
91
64
|
const res = await fetch(url, {
|
|
92
65
|
headers: { "X-Site-Key": resolvedKey }
|
|
@@ -100,12 +73,7 @@ async function getPost(slug, siteKey) {
|
|
|
100
73
|
}
|
|
101
74
|
throw new Error(`Failed to fetch post: ${res.status} ${res.statusText}`);
|
|
102
75
|
}
|
|
103
|
-
|
|
104
|
-
if (!postBySlugBySiteKey.has(resolvedKey)) {
|
|
105
|
-
postBySlugBySiteKey.set(resolvedKey, /* @__PURE__ */ new Map());
|
|
106
|
-
}
|
|
107
|
-
postBySlugBySiteKey.get(resolvedKey).set(slug, post);
|
|
108
|
-
return post;
|
|
76
|
+
return await res.json();
|
|
109
77
|
}
|
|
110
78
|
async function getPostByPreviewToken(token) {
|
|
111
79
|
if (!token?.trim()) return null;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Post, DispatchConfig } from \"./client\";\nexport {\n initDispatch,\n getConfig,\n getPosts,\n getPost,\n getPostByPreviewToken,\n} from \"./client\";\n","export type Post = {\n id: string;\n created_at: string;\n published_at: string;\n updated_at: string;\n site_id: string;\n title: string;\n slug: string;\n content: unknown;\n excerpt: string | null;\n featured_image: string | null;\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\
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Post, DispatchConfig } from \"./client\";\nexport {\n initDispatch,\n getConfig,\n getPosts,\n getPost,\n getPostByPreviewToken,\n} from \"./client\";\n","export type Post = {\n id: string;\n created_at: string;\n published_at: string;\n updated_at: string;\n site_id: string;\n title: string;\n slug: string;\n content: unknown;\n excerpt: string | null;\n featured_image: string | null;\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 getPosts(siteKey?: string): Promise<Post[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/posts`;\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 posts: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Post[];\n}\n\nexport async function getPost(slug: string, siteKey?: string): Promise<Post | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/posts/${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 post: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Post;\n}\n\n/**\n * Fetch a single post by its preview token (for preview/draft pages).\n * No site key required. Use this in your app's preview route (e.g. /preview?token=...).\n */\nexport async function getPostByPreviewToken(token: string): Promise<Post | 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 Post;\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,SAAS,SAAmC;AAChE,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB;AAChC,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,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,QAAQ,MAAc,SAAwC;AAClF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,cAAc,mBAAmB,IAAI,CAAC;AACtE,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,yBAAyB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EACzE;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAMA,eAAsB,sBAAsB,OAAqC;AAC/E,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
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
var DISPATCH_API_BASE = "https://dispatch-cms.vercel.app";
|
|
3
3
|
var config = null;
|
|
4
|
-
var allPostsBySiteKey = /* @__PURE__ */ new Map();
|
|
5
|
-
var postBySlugBySiteKey = /* @__PURE__ */ new Map();
|
|
6
|
-
var fullListFetchedForSiteKey = /* @__PURE__ */ new Set();
|
|
7
4
|
function initDispatch(options) {
|
|
8
5
|
config = { siteKey: options.siteKey };
|
|
9
6
|
}
|
|
@@ -18,10 +15,6 @@ function getConfig() {
|
|
|
18
15
|
async function getPosts(siteKey) {
|
|
19
16
|
const { siteKey: key } = getConfig();
|
|
20
17
|
const resolvedKey = siteKey ?? key;
|
|
21
|
-
const cached = allPostsBySiteKey.get(resolvedKey);
|
|
22
|
-
if (cached !== void 0) {
|
|
23
|
-
return cached;
|
|
24
|
-
}
|
|
25
18
|
const url = `${DISPATCH_API_BASE}/api/posts`;
|
|
26
19
|
const res = await fetch(url, {
|
|
27
20
|
headers: { "X-Site-Key": resolvedKey }
|
|
@@ -32,31 +25,11 @@ async function getPosts(siteKey) {
|
|
|
32
25
|
}
|
|
33
26
|
throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);
|
|
34
27
|
}
|
|
35
|
-
|
|
36
|
-
allPostsBySiteKey.set(resolvedKey, posts);
|
|
37
|
-
fullListFetchedForSiteKey.add(resolvedKey);
|
|
38
|
-
const bySlug = /* @__PURE__ */ new Map();
|
|
39
|
-
for (const post of posts) {
|
|
40
|
-
bySlug.set(post.slug, post);
|
|
41
|
-
}
|
|
42
|
-
postBySlugBySiteKey.set(resolvedKey, bySlug);
|
|
43
|
-
return posts;
|
|
28
|
+
return await res.json();
|
|
44
29
|
}
|
|
45
30
|
async function getPost(slug, siteKey) {
|
|
46
31
|
const { siteKey: key } = getConfig();
|
|
47
32
|
const resolvedKey = siteKey ?? key;
|
|
48
|
-
if (fullListFetchedForSiteKey.has(resolvedKey)) {
|
|
49
|
-
const list = allPostsBySiteKey.get(resolvedKey);
|
|
50
|
-
if (list) {
|
|
51
|
-
const post2 = list.find((p) => p.slug === slug);
|
|
52
|
-
return post2 ?? null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const bySlug = postBySlugBySiteKey.get(resolvedKey);
|
|
56
|
-
const cachedPost = bySlug?.get(slug);
|
|
57
|
-
if (cachedPost !== void 0) {
|
|
58
|
-
return cachedPost;
|
|
59
|
-
}
|
|
60
33
|
const url = `${DISPATCH_API_BASE}/api/posts/${encodeURIComponent(slug)}`;
|
|
61
34
|
const res = await fetch(url, {
|
|
62
35
|
headers: { "X-Site-Key": resolvedKey }
|
|
@@ -70,12 +43,7 @@ async function getPost(slug, siteKey) {
|
|
|
70
43
|
}
|
|
71
44
|
throw new Error(`Failed to fetch post: ${res.status} ${res.statusText}`);
|
|
72
45
|
}
|
|
73
|
-
|
|
74
|
-
if (!postBySlugBySiteKey.has(resolvedKey)) {
|
|
75
|
-
postBySlugBySiteKey.set(resolvedKey, /* @__PURE__ */ new Map());
|
|
76
|
-
}
|
|
77
|
-
postBySlugBySiteKey.get(resolvedKey).set(slug, post);
|
|
78
|
-
return post;
|
|
46
|
+
return await res.json();
|
|
79
47
|
}
|
|
80
48
|
async function getPostByPreviewToken(token) {
|
|
81
49
|
if (!token?.trim()) return null;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Post = {\n id: string;\n created_at: string;\n published_at: string;\n updated_at: string;\n site_id: string;\n title: string;\n slug: string;\n content: unknown;\n excerpt: string | null;\n featured_image: string | null;\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\
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Post = {\n id: string;\n created_at: string;\n published_at: string;\n updated_at: string;\n site_id: string;\n title: string;\n slug: string;\n content: unknown;\n excerpt: string | null;\n featured_image: string | null;\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 getPosts(siteKey?: string): Promise<Post[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/posts`;\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 posts: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Post[];\n}\n\nexport async function getPost(slug: string, siteKey?: string): Promise<Post | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/posts/${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 post: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Post;\n}\n\n/**\n * Fetch a single post by its preview token (for preview/draft pages).\n * No site key required. Use this in your app's preview route (e.g. /preview?token=...).\n */\nexport async function getPostByPreviewToken(token: string): Promise<Post | 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 Post;\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,SAAS,SAAmC;AAChE,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB;AAChC,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,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,QAAQ,MAAc,SAAwC;AAClF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,cAAc,mBAAmB,IAAI,CAAC;AACtE,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,yBAAyB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EACzE;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAMA,eAAsB,sBAAsB,OAAqC;AAC/E,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
package/src/client.ts
CHANGED
|
@@ -20,10 +20,6 @@ export type DispatchConfig = {
|
|
|
20
20
|
|
|
21
21
|
let config: DispatchConfig | null = null;
|
|
22
22
|
|
|
23
|
-
const allPostsBySiteKey = new Map<string, Post[]>();
|
|
24
|
-
const postBySlugBySiteKey = new Map<string, Map<string, Post>>();
|
|
25
|
-
const fullListFetchedForSiteKey = new Set<string>();
|
|
26
|
-
|
|
27
23
|
export function initDispatch(options: DispatchConfig): void {
|
|
28
24
|
config = { siteKey: options.siteKey };
|
|
29
25
|
}
|
|
@@ -40,10 +36,6 @@ export function getConfig(): { siteKey: string } {
|
|
|
40
36
|
export async function getPosts(siteKey?: string): Promise<Post[]> {
|
|
41
37
|
const { siteKey: key } = getConfig();
|
|
42
38
|
const resolvedKey = siteKey ?? key;
|
|
43
|
-
const cached = allPostsBySiteKey.get(resolvedKey);
|
|
44
|
-
if (cached !== undefined) {
|
|
45
|
-
return cached;
|
|
46
|
-
}
|
|
47
39
|
const url = `${DISPATCH_API_BASE}/api/posts`;
|
|
48
40
|
const res = await fetch(url, {
|
|
49
41
|
headers: { "X-Site-Key": resolvedKey },
|
|
@@ -54,32 +46,12 @@ export async function getPosts(siteKey?: string): Promise<Post[]> {
|
|
|
54
46
|
}
|
|
55
47
|
throw new Error(`Failed to fetch posts: ${res.status} ${res.statusText}`);
|
|
56
48
|
}
|
|
57
|
-
|
|
58
|
-
allPostsBySiteKey.set(resolvedKey, posts);
|
|
59
|
-
fullListFetchedForSiteKey.add(resolvedKey);
|
|
60
|
-
const bySlug = new Map<string, Post>();
|
|
61
|
-
for (const post of posts) {
|
|
62
|
-
bySlug.set(post.slug, post);
|
|
63
|
-
}
|
|
64
|
-
postBySlugBySiteKey.set(resolvedKey, bySlug);
|
|
65
|
-
return posts;
|
|
49
|
+
return (await res.json()) as Post[];
|
|
66
50
|
}
|
|
67
51
|
|
|
68
52
|
export async function getPost(slug: string, siteKey?: string): Promise<Post | null> {
|
|
69
53
|
const { siteKey: key } = getConfig();
|
|
70
54
|
const resolvedKey = siteKey ?? key;
|
|
71
|
-
if (fullListFetchedForSiteKey.has(resolvedKey)) {
|
|
72
|
-
const list = allPostsBySiteKey.get(resolvedKey);
|
|
73
|
-
if (list) {
|
|
74
|
-
const post = list.find((p) => p.slug === slug);
|
|
75
|
-
return post ?? null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const bySlug = postBySlugBySiteKey.get(resolvedKey);
|
|
79
|
-
const cachedPost = bySlug?.get(slug);
|
|
80
|
-
if (cachedPost !== undefined) {
|
|
81
|
-
return cachedPost;
|
|
82
|
-
}
|
|
83
55
|
const url = `${DISPATCH_API_BASE}/api/posts/${encodeURIComponent(slug)}`;
|
|
84
56
|
const res = await fetch(url, {
|
|
85
57
|
headers: { "X-Site-Key": resolvedKey },
|
|
@@ -93,12 +65,7 @@ export async function getPost(slug: string, siteKey?: string): Promise<Post | nu
|
|
|
93
65
|
}
|
|
94
66
|
throw new Error(`Failed to fetch post: ${res.status} ${res.statusText}`);
|
|
95
67
|
}
|
|
96
|
-
|
|
97
|
-
if (!postBySlugBySiteKey.has(resolvedKey)) {
|
|
98
|
-
postBySlugBySiteKey.set(resolvedKey, new Map());
|
|
99
|
-
}
|
|
100
|
-
postBySlugBySiteKey.get(resolvedKey)!.set(slug, post);
|
|
101
|
-
return post;
|
|
68
|
+
return (await res.json()) as Post;
|
|
102
69
|
}
|
|
103
70
|
|
|
104
71
|
/**
|
|
@@ -20,7 +20,7 @@ let listResponse: ReturnType<typeof mockPost>[] = [];
|
|
|
20
20
|
let singleResponse: ReturnType<typeof mockPost> | null = null;
|
|
21
21
|
let singleStatus = 200;
|
|
22
22
|
|
|
23
|
-
function mockFetch(url: string,
|
|
23
|
+
function mockFetch(url: string, _options?: RequestInit): Promise<Response> {
|
|
24
24
|
fetchCalls.push({ url });
|
|
25
25
|
const u = new URL(url);
|
|
26
26
|
if (u.pathname === "/api/posts") {
|
|
@@ -61,43 +61,40 @@ async function run(): Promise<void> {
|
|
|
61
61
|
initDispatch({ siteKey: "pk_test" });
|
|
62
62
|
|
|
63
63
|
try {
|
|
64
|
-
// Test 1:
|
|
64
|
+
// Test 1: getPosts() fetches and returns the list (no in-memory cache)
|
|
65
65
|
fetchCalls.length = 0;
|
|
66
66
|
listResponse = [mockPost("a", "Post A"), mockPost("b", "Post B")];
|
|
67
|
-
const list1 = await getPosts("
|
|
68
|
-
|
|
69
|
-
assert(
|
|
70
|
-
|
|
71
|
-
assert(
|
|
67
|
+
const list1 = await getPosts("pk_1");
|
|
68
|
+
assert(fetchCalls.length === 1, "getPosts triggers one fetch");
|
|
69
|
+
assert(list1.length === 2 && list1[0].slug === "a", "returns correct list");
|
|
70
|
+
const list2 = await getPosts("pk_1");
|
|
71
|
+
assert(fetchCalls.length === 2, "second getPosts triggers another fetch (no cache)");
|
|
72
|
+
assert(list2.length === 2, "second call returns same list");
|
|
72
73
|
|
|
73
|
-
// Test 2:
|
|
74
|
+
// Test 2: getPost(slug) fetches and returns the post
|
|
74
75
|
fetchCalls.length = 0;
|
|
75
|
-
|
|
76
|
-
await
|
|
77
|
-
|
|
78
|
-
assert(
|
|
79
|
-
assert(post !== null && post.slug === "my-slug" && post.title === "My Post", "getPost returns correct post from list");
|
|
76
|
+
singleResponse = mockPost("my-slug", "My Post");
|
|
77
|
+
const post = await getPost("my-slug", "pk_2");
|
|
78
|
+
assert(fetchCalls.length === 1, "getPost triggers one fetch");
|
|
79
|
+
assert(post !== null && post.slug === "my-slug" && post.title === "My Post", "returns correct post");
|
|
80
80
|
|
|
81
|
-
// Test 3:
|
|
81
|
+
// Test 3: getPost(slug) for missing slug returns null
|
|
82
82
|
fetchCalls.length = 0;
|
|
83
|
-
|
|
84
|
-
await
|
|
85
|
-
|
|
86
|
-
assert(
|
|
87
|
-
|
|
83
|
+
singleStatus = 404;
|
|
84
|
+
const missing = await getPost("nonexistent", "pk_3");
|
|
85
|
+
assert(fetchCalls.length === 1, "getPost triggers one fetch");
|
|
86
|
+
assert(missing === null, "returns null for 404");
|
|
87
|
+
singleStatus = 200;
|
|
88
88
|
|
|
89
|
-
// Test 4: getPost(slug)
|
|
89
|
+
// Test 4: getPost(slug) then getPosts() each trigger their own fetch
|
|
90
90
|
fetchCalls.length = 0;
|
|
91
|
-
listResponse = [];
|
|
92
91
|
singleResponse = mockPost("first", "First Post");
|
|
93
|
-
const singleFirst = await getPost("first", "pk_cache4");
|
|
94
|
-
assert(fetchCalls.length === 1 && singleFirst?.slug === "first", "first getPost fetches");
|
|
95
92
|
listResponse = [mockPost("first", "First Post")];
|
|
96
|
-
await
|
|
93
|
+
const singleFirst = await getPost("first", "pk_4");
|
|
94
|
+
assert(fetchCalls.length === 1 && singleFirst?.slug === "first", "getPost fetches");
|
|
95
|
+
const listAfter = await getPosts("pk_4");
|
|
97
96
|
assert(fetchCalls.length === 2, "getPosts triggers second fetch");
|
|
98
|
-
|
|
99
|
-
assert(fetchCalls.length === 2, "getPost after getPosts does not fetch again");
|
|
100
|
-
assert(singleCached !== null && singleCached.slug === "first", "getPost returns post from list cache");
|
|
97
|
+
assert(listAfter.length === 1 && listAfter[0].slug === "first", "getPosts returns correct list");
|
|
101
98
|
} finally {
|
|
102
99
|
globalThis.fetch = originalFetch;
|
|
103
100
|
}
|
|
@@ -105,7 +102,7 @@ async function run(): Promise<void> {
|
|
|
105
102
|
|
|
106
103
|
run()
|
|
107
104
|
.then(() => {
|
|
108
|
-
console.log("All
|
|
105
|
+
console.log("All client tests passed.");
|
|
109
106
|
})
|
|
110
107
|
.catch((err) => {
|
|
111
108
|
console.error(err);
|