@sprintup-cms/sdk 1.0.0 → 1.2.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/CHANGELOG.md +33 -0
- package/README.md +362 -72
- package/dist/client.cjs +30 -15
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +36 -17
- package/dist/client.d.ts +36 -17
- package/dist/client.js +30 -15
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +179 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +176 -0
- package/dist/index.js.map +1 -0
- package/dist/next/index.cjs +46 -23
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +8 -2
- package/dist/next/index.d.ts +8 -2
- package/dist/next/index.js +47 -24
- package/dist/next/index.js.map +1 -1
- package/package.json +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@sprintup-cms/sdk` will be documented here.
|
|
4
4
|
|
|
5
|
+
## [1.2.0] — 2026-03-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `POST` revalidate handler no longer returns `400` for `structure.update` events (which
|
|
9
|
+
carry no `slug`). It now returns 400 only when both `tag` and `slug` are absent.
|
|
10
|
+
- `slug` is now derived from `data.slug` or the `tag` prefix (`content-home` → `home`)
|
|
11
|
+
so content events work even when the top-level `slug` field is not present.
|
|
12
|
+
- `structure.update` events now correctly call `revalidatePath('/', 'layout')` so
|
|
13
|
+
nav and footer refresh after a site structure change.
|
|
14
|
+
- `RevalidatePayload` type updated to include all CMS event names (`content.publish`,
|
|
15
|
+
`content.unpublish`, `structure.update`) in addition to the legacy names.
|
|
16
|
+
|
|
17
|
+
## [1.1.0] — 2026-03-05
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `getSitemap()` — all published page URLs with priority, changefreq, lastmod for `app/sitemap.ts`
|
|
21
|
+
- `getStatus()` — connectivity check returning page counts; always fresh (`cache: 'no-store'`)
|
|
22
|
+
- `CMSBlocksProps.custom` — override or extend any built-in block type with a custom React component
|
|
23
|
+
- `CMSSitemapResponse`, `CMSSitemapUrl`, `CMSStatusResponse` types exported
|
|
24
|
+
- `rendererKey` and `variant` fields on `CMSPageType`
|
|
25
|
+
- `createRevalidateHandler` `onRevalidate` callback for post-revalidation hooks
|
|
26
|
+
- Root `"."` export now resolves the `src/index.ts` barrel; new `"./client"` subpath added
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `CMSListResponse` corrected to flat `{ data, count }` shape (was `{ data, meta: { total } }`)
|
|
30
|
+
- `block.data` is now canonical; `block.content` kept as legacy fallback in all renderers
|
|
31
|
+
- Config resolved lazily at request time — no more build-time crashes when env vars are absent
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- Removed `require('./page-type-renderers')` CommonJS call that broke ES module consumers
|
|
35
|
+
- Removed non-existent `getPageTypes()` list method (no list endpoint exists in the v1 API)
|
|
36
|
+
- Internal link anchor regex now handles multi-segment slugs and all quote styles
|
|
37
|
+
|
|
5
38
|
## [1.0.0] — 2026-03-03
|
|
6
39
|
|
|
7
40
|
### Added
|
package/README.md
CHANGED
|
@@ -4,155 +4,445 @@ Official SDK for **SprintUp Forge CMS** — typed API client, Next.js App Router
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## Table of contents
|
|
8
|
+
|
|
9
|
+
1. [Install](#install)
|
|
10
|
+
2. [Environment variables](#environment-variables)
|
|
11
|
+
3. [Initial setup](#initial-setup)
|
|
12
|
+
- [Connect to the CMS](#connect-to-the-cms)
|
|
13
|
+
- [Catch-all page route](#catch-all-page-route)
|
|
14
|
+
- [On-demand revalidation webhook](#on-demand-revalidation-webhook)
|
|
15
|
+
- [Preview mode exit](#preview-mode-exit)
|
|
16
|
+
- [Sitemap](#sitemap)
|
|
17
|
+
4. [Verify your connection](#verify-your-connection)
|
|
18
|
+
5. [Adding new pages](#adding-new-pages)
|
|
19
|
+
6. [Custom page layouts](#custom-page-layouts)
|
|
20
|
+
7. [Navigation and footer](#navigation-and-footer)
|
|
21
|
+
8. [Extending with custom blocks](#extending-with-custom-blocks)
|
|
22
|
+
9. [ISR and caching strategy](#isr-and-caching-strategy)
|
|
23
|
+
10. [API reference](#api-reference)
|
|
24
|
+
11. [Block types reference](#block-types-reference)
|
|
25
|
+
12. [Troubleshooting](#troubleshooting)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
7
29
|
## Install
|
|
8
30
|
|
|
9
31
|
```bash
|
|
10
32
|
npm install @sprintup-cms/sdk
|
|
33
|
+
# or
|
|
34
|
+
pnpm add @sprintup-cms/sdk
|
|
35
|
+
# or
|
|
36
|
+
yarn add @sprintup-cms/sdk
|
|
11
37
|
```
|
|
12
38
|
|
|
13
39
|
---
|
|
14
40
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
### 1. Environment variables
|
|
41
|
+
## Environment variables
|
|
18
42
|
|
|
19
|
-
Add to your
|
|
43
|
+
Add the following to your website's `.env.local`. **Never commit these to git.**
|
|
20
44
|
|
|
21
45
|
```env
|
|
46
|
+
# ── Required ──────────────────────────────────────────────────────────────────
|
|
47
|
+
# Base URL of your SprintUp Forge CMS deployment
|
|
22
48
|
NEXT_PUBLIC_CMS_URL=https://your-cms.vercel.app
|
|
49
|
+
|
|
50
|
+
# API key from CMS Admin → Settings → API Keys → Create key
|
|
23
51
|
CMS_API_KEY=cmsk_xxxxxxxxxxxxxxxxxxxx
|
|
52
|
+
|
|
53
|
+
# App ID from CMS Admin → Settings → Apps
|
|
54
|
+
# (shown in the URL when you select a site: /admin/dashboard?app=school-website)
|
|
24
55
|
CMS_APP_ID=school-website
|
|
56
|
+
|
|
57
|
+
# ── Required for webhooks ──────────────────────────────────────────────────────
|
|
58
|
+
# Secret that the CMS will send with every webhook request — validate on your end
|
|
25
59
|
CMS_WEBHOOK_SECRET=your-random-secret
|
|
60
|
+
|
|
61
|
+
# ── Optional ──────────────────────────────────────────────────────────────────
|
|
62
|
+
# Public URL of this website — used by sitemap generation
|
|
63
|
+
NEXT_PUBLIC_SITE_URL=https://www.yourschool.edu
|
|
26
64
|
```
|
|
27
65
|
|
|
28
|
-
Generate a webhook secret
|
|
66
|
+
Generate a secure webhook secret:
|
|
29
67
|
|
|
30
68
|
```bash
|
|
31
69
|
openssl rand -hex 32
|
|
32
70
|
```
|
|
33
71
|
|
|
72
|
+
**Where to find your values in the CMS:**
|
|
73
|
+
|
|
74
|
+
| Variable | Location in CMS Admin |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `NEXT_PUBLIC_CMS_URL` | The domain your CMS is deployed to |
|
|
77
|
+
| `CMS_API_KEY` | Settings → API Keys → Create new key |
|
|
78
|
+
| `CMS_APP_ID` | Settings → Apps → click your site → copy App ID |
|
|
79
|
+
| `CMS_WEBHOOK_SECRET` | Settings → Webhooks → add secret when registering the URL |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Initial setup
|
|
84
|
+
|
|
85
|
+
### Connect to the CMS
|
|
86
|
+
|
|
87
|
+
The SDK reads your environment variables automatically. No extra configuration is required for the default singleton client.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// lib/cms.ts
|
|
91
|
+
import { cmsClient } from '@sprintup-cms/sdk'
|
|
92
|
+
export default cmsClient
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For multiple sites or custom configuration:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { createCMSClient } from '@sprintup-cms/sdk'
|
|
99
|
+
|
|
100
|
+
export const schoolCms = createCMSClient({
|
|
101
|
+
baseUrl: process.env.NEXT_PUBLIC_CMS_URL,
|
|
102
|
+
apiKey: process.env.CMS_API_KEY,
|
|
103
|
+
appId: process.env.CMS_APP_ID,
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
34
107
|
---
|
|
35
108
|
|
|
36
|
-
###
|
|
109
|
+
### Catch-all page route
|
|
37
110
|
|
|
38
|
-
|
|
111
|
+
This single file automatically renders **every page** published in your CMS — no manual routing needed.
|
|
39
112
|
|
|
40
113
|
```ts
|
|
114
|
+
// app/[...slug]/page.tsx
|
|
41
115
|
export { CMSCatchAllPage as default, generateMetadata } from '@sprintup-cms/sdk/next'
|
|
42
116
|
```
|
|
43
117
|
|
|
44
|
-
|
|
118
|
+
How it works:
|
|
119
|
+
- Fetches the page by `slug` from the CMS on each request (ISR, 60s revalidation).
|
|
120
|
+
- Falls through to `notFound()` when the slug does not exist in the CMS.
|
|
121
|
+
- Activates draft/preview mode automatically when `draftMode()` is enabled.
|
|
122
|
+
- Renders `<CMSBlocks>` for standard pages and structured content for page-type pages.
|
|
45
123
|
|
|
46
124
|
---
|
|
47
125
|
|
|
48
|
-
###
|
|
126
|
+
### On-demand revalidation webhook
|
|
49
127
|
|
|
50
|
-
|
|
128
|
+
When an editor publishes or updates a page in the CMS, this webhook fires to instantly clear the ISR cache for that page.
|
|
51
129
|
|
|
52
130
|
```ts
|
|
131
|
+
// app/api/cms-revalidate/route.ts
|
|
53
132
|
export { POST } from '@sprintup-cms/sdk/next'
|
|
54
133
|
```
|
|
55
134
|
|
|
56
|
-
Then
|
|
135
|
+
Then register the webhook in your **CMS Admin → Settings → Webhooks**:
|
|
57
136
|
|
|
58
137
|
```
|
|
59
|
-
https://
|
|
138
|
+
URL: https://www.yourschool.edu/api/cms-revalidate
|
|
139
|
+
Secret: (same value as CMS_WEBHOOK_SECRET)
|
|
140
|
+
Events: published, deleted, archived
|
|
60
141
|
```
|
|
61
142
|
|
|
62
|
-
|
|
143
|
+
Custom hook with extra logic:
|
|
63
144
|
|
|
64
|
-
|
|
145
|
+
```ts
|
|
146
|
+
// app/api/cms-revalidate/route.ts
|
|
147
|
+
import { createRevalidateHandler } from '@sprintup-cms/sdk/next'
|
|
148
|
+
|
|
149
|
+
export const POST = createRevalidateHandler({
|
|
150
|
+
secret: process.env.CMS_WEBHOOK_SECRET,
|
|
151
|
+
onRevalidate: async ({ slug, event }) => {
|
|
152
|
+
console.log(`CMS revalidated /${slug} — event: ${event}`)
|
|
153
|
+
// e.g. update a search index, notify Slack, etc.
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
```
|
|
65
157
|
|
|
66
|
-
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Preview mode exit
|
|
67
161
|
|
|
68
162
|
```ts
|
|
163
|
+
// app/api/cms-preview/exit/route.ts
|
|
69
164
|
export { previewExitGET as GET } from '@sprintup-cms/sdk/next'
|
|
70
165
|
```
|
|
71
166
|
|
|
72
|
-
|
|
167
|
+
The CMS editor links to `/api/cms-preview/exit?redirect=/your-slug` to leave draft mode. This handler disables `draftMode()` and redirects the editor back to the live page.
|
|
73
168
|
|
|
74
|
-
|
|
169
|
+
---
|
|
75
170
|
|
|
76
|
-
###
|
|
171
|
+
### Sitemap
|
|
77
172
|
|
|
78
173
|
```ts
|
|
174
|
+
// app/sitemap.ts
|
|
175
|
+
import type { MetadataRoute } from 'next'
|
|
79
176
|
import { cmsClient } from '@sprintup-cms/sdk'
|
|
80
177
|
|
|
81
|
-
|
|
82
|
-
const
|
|
178
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
179
|
+
const data = await cmsClient.getSitemap()
|
|
180
|
+
if (!data?.enabled) return []
|
|
83
181
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
182
|
+
return data.urls.map(url => ({
|
|
183
|
+
url: `${process.env.NEXT_PUBLIC_SITE_URL}${url.loc}`,
|
|
184
|
+
lastModified: url.lastmod,
|
|
185
|
+
changeFrequency: url.changefreq as MetadataRoute.Sitemap[0]['changeFrequency'],
|
|
186
|
+
priority: url.priority,
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
89
189
|
```
|
|
90
190
|
|
|
91
|
-
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Verify your connection
|
|
194
|
+
|
|
195
|
+
Add a temporary status check page to confirm your API key and App ID are working:
|
|
92
196
|
|
|
93
197
|
```ts
|
|
94
|
-
|
|
198
|
+
// app/cms-status/page.tsx (remove before going to production)
|
|
199
|
+
import { cmsClient } from '@sprintup-cms/sdk'
|
|
95
200
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
201
|
+
export default async function CmsStatusPage() {
|
|
202
|
+
const status = await cmsClient.getStatus()
|
|
203
|
+
if (!status) return <p>CMS connection failed — check your environment variables.</p>
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div style={{ padding: 32, fontFamily: 'monospace' }}>
|
|
207
|
+
<h1>CMS Status</h1>
|
|
208
|
+
<p>App ID: {status.appId}</p>
|
|
209
|
+
<p>Total pages: {status.totalPages}</p>
|
|
210
|
+
<p>Published: {status.publishedPages}</p>
|
|
211
|
+
<pre>{JSON.stringify(status.pages.slice(0, 5), null, 2)}</pre>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Adding new pages
|
|
220
|
+
|
|
221
|
+
You **never write code** to add a new page. All content management happens in the CMS:
|
|
222
|
+
|
|
223
|
+
1. Go to **CMS Admin → Content** (scoped to your site).
|
|
224
|
+
2. Click **New Page**.
|
|
225
|
+
3. Choose a page type (e.g. Standard Page, Blog Post, Event).
|
|
226
|
+
4. Fill in the title, slug, and content blocks.
|
|
227
|
+
5. Click **Publish**.
|
|
228
|
+
6. The webhook fires, your Next.js site revalidates within seconds, and the page is live at `https://yoursite.com/<slug>`.
|
|
229
|
+
|
|
230
|
+
To add a **custom page type** (e.g. Programme, Staff Profile):
|
|
231
|
+
|
|
232
|
+
1. Go to **CMS Admin → Page Types**.
|
|
233
|
+
2. Click **New Page Type**.
|
|
234
|
+
3. Define sections and fields.
|
|
235
|
+
4. Create content using that type.
|
|
236
|
+
5. In your website, customize the rendering — see [Custom page layouts](#custom-page-layouts).
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Custom page layouts
|
|
241
|
+
|
|
242
|
+
Override the default rendering for a specific page type by wrapping `CMSCatchAllPage`:
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
// app/[...slug]/page.tsx
|
|
246
|
+
import { notFound } from 'next/navigation'
|
|
247
|
+
import { cmsClient } from '@sprintup-cms/sdk'
|
|
248
|
+
import { CMSBlocks } from '@sprintup-cms/sdk/react'
|
|
249
|
+
import type { Metadata } from 'next'
|
|
250
|
+
|
|
251
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string[] }> }): Promise<Metadata> {
|
|
252
|
+
const { slug } = await params
|
|
253
|
+
const page = await cmsClient.getPage(slug.join('/'))
|
|
254
|
+
if (!page) return { title: 'Not Found' }
|
|
255
|
+
return {
|
|
256
|
+
title: page.seo?.title || page.title,
|
|
257
|
+
description: page.seo?.description || '',
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
|
|
262
|
+
const { slug } = await params
|
|
263
|
+
const page = await cmsClient.getPage(slug.join('/'))
|
|
264
|
+
if (!page) notFound()
|
|
265
|
+
|
|
266
|
+
// Render a blog post with a custom layout
|
|
267
|
+
if (page.pageType === 'blog-post') {
|
|
268
|
+
return (
|
|
269
|
+
<article className="max-w-2xl mx-auto py-16 px-6">
|
|
270
|
+
<h1 className="text-4xl font-bold">{page.title}</h1>
|
|
271
|
+
<time className="text-sm text-gray-500">{page.publishedAt}</time>
|
|
272
|
+
<CMSBlocks blocks={page.blocks} />
|
|
273
|
+
</article>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Fall back to default layout for all other page types
|
|
278
|
+
return (
|
|
279
|
+
<main className="max-w-5xl mx-auto py-12 px-6">
|
|
280
|
+
<h1 className="text-4xl font-bold mb-8">{page.title}</h1>
|
|
281
|
+
<CMSBlocks blocks={page.blocks} />
|
|
282
|
+
</main>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
101
285
|
```
|
|
102
286
|
|
|
103
|
-
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Navigation and footer
|
|
290
|
+
|
|
291
|
+
Pull live nav and footer data from the CMS site structure:
|
|
104
292
|
|
|
105
293
|
```tsx
|
|
106
|
-
|
|
294
|
+
// components/layout/header.tsx
|
|
295
|
+
import { cmsClient } from '@sprintup-cms/sdk'
|
|
296
|
+
import type { CMSMenuItem } from '@sprintup-cms/sdk'
|
|
297
|
+
import Link from 'next/link'
|
|
298
|
+
|
|
299
|
+
export async function Header() {
|
|
300
|
+
const structure = await cmsClient.getSiteStructure()
|
|
301
|
+
const navItems: CMSMenuItem[] = structure?.menus.header ?? []
|
|
107
302
|
|
|
108
|
-
export default function Page({ page, pageType, isPreview }) {
|
|
109
303
|
return (
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
304
|
+
<header className="border-b">
|
|
305
|
+
<nav className="max-w-5xl mx-auto px-6 py-4 flex gap-6">
|
|
306
|
+
{navItems.map(item => (
|
|
307
|
+
<Link key={item.id} href={item.href} target={item.openInNewTab ? '_blank' : undefined}>
|
|
308
|
+
{item.label}
|
|
309
|
+
</Link>
|
|
310
|
+
))}
|
|
311
|
+
</nav>
|
|
312
|
+
</header>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
// app/layout.tsx
|
|
319
|
+
import { Header } from '@/components/layout/header'
|
|
320
|
+
|
|
321
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
322
|
+
return (
|
|
323
|
+
<html lang="en">
|
|
324
|
+
<body>
|
|
325
|
+
<Header />
|
|
326
|
+
{children}
|
|
327
|
+
</body>
|
|
328
|
+
</html>
|
|
121
329
|
)
|
|
122
330
|
}
|
|
123
331
|
```
|
|
124
332
|
|
|
125
333
|
---
|
|
126
334
|
|
|
127
|
-
##
|
|
335
|
+
## Extending with custom blocks
|
|
128
336
|
|
|
129
|
-
|
|
337
|
+
Override any built-in block or add a completely new block type using the `custom` prop:
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
import { CMSBlocks } from '@sprintup-cms/sdk/react'
|
|
341
|
+
import { MyVideoPlayer } from '@/components/video-player'
|
|
342
|
+
import { ProgrammeCard } from '@/components/programme-card'
|
|
343
|
+
|
|
344
|
+
<CMSBlocks
|
|
345
|
+
blocks={page.blocks}
|
|
346
|
+
custom={{
|
|
347
|
+
// Override built-in video block with your own player
|
|
348
|
+
'video': (block) => <MyVideoPlayer src={block.data?.url} />,
|
|
349
|
+
|
|
350
|
+
// Add a completely custom block type created in the CMS
|
|
351
|
+
'programme-card': (block) => (
|
|
352
|
+
<ProgrammeCard
|
|
353
|
+
title={block.data?.title}
|
|
354
|
+
duration={block.data?.duration}
|
|
355
|
+
applyUrl={block.data?.applyUrl}
|
|
356
|
+
/>
|
|
357
|
+
),
|
|
358
|
+
}}
|
|
359
|
+
/>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The `custom` prop is a `Record<blockType, (block: CMSBlock) => React.ReactNode>`. It takes priority over all built-in renderers.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## ISR and caching strategy
|
|
367
|
+
|
|
368
|
+
| Data | Revalidation | Cache tag |
|
|
369
|
+
|---|---|---|
|
|
370
|
+
| Individual page | 60 seconds | `cms-page-{slug}` |
|
|
371
|
+
| All pages list | 60 seconds | `cms-pages-{appId}` |
|
|
372
|
+
| Page type schema | 3600 seconds | `cms-page-type-{id}` |
|
|
373
|
+
| Site structure (nav/footer) | 300 seconds | `site-structure-{appId}` |
|
|
374
|
+
| Sitemap | 3600 seconds | — |
|
|
375
|
+
| Status check | No cache | — |
|
|
376
|
+
|
|
377
|
+
The revalidation webhook calls `revalidateTag('cms-page-{slug}')` and `revalidatePath('/{slug}')` when the CMS publishes a page, giving you **instant updates** without a full rebuild.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## API reference
|
|
382
|
+
|
|
383
|
+
### `cmsClient`
|
|
384
|
+
|
|
385
|
+
| Method | Description |
|
|
130
386
|
|---|---|
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
## Entry points
|
|
150
|
-
|
|
151
|
-
| Import | Contents |
|
|
387
|
+
| `getPage(slug)` | Fetch a single published page by slug |
|
|
388
|
+
| `getPages(options?)` | Fetch multiple pages — filterable by `type`, `group`, `page`, `perPage` |
|
|
389
|
+
| `getBlogPosts()` | Shorthand for `getPages({ type: 'blog-post' })` |
|
|
390
|
+
| `getEvents()` | Shorthand for `getPages({ type: 'event-page' })` |
|
|
391
|
+
| `getAnnouncements()` | Shorthand for `getPages({ type: 'announcement-page' })` |
|
|
392
|
+
| `getPageType(id)` | Fetch a page type schema by ID |
|
|
393
|
+
| `getSiteStructure()` | Fetch nav, footer, and page tree |
|
|
394
|
+
| `getPreviewPage(token)` | Fetch a draft page for preview mode |
|
|
395
|
+
| `getPageWithPreview(slug, token?)` | Fetch page — preview if token present, live otherwise |
|
|
396
|
+
| `getSitemap()` | Fetch all published slugs with sitemap metadata |
|
|
397
|
+
| `getStatus()` | Connectivity check — returns counts and page list |
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Block types reference
|
|
402
|
+
|
|
403
|
+
| Block type | Key fields in `block.data` |
|
|
152
404
|
|---|---|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
405
|
+
| `heading` | `text`, `level` (1–4) |
|
|
406
|
+
| `text` | `text` |
|
|
407
|
+
| `richtext` | `content` (HTML string) |
|
|
408
|
+
| `image` | `src`, `alt`, `caption` |
|
|
409
|
+
| `hero` | `title`, `subtitle`, `primaryButton`, `primaryUrl`, `secondaryButton`, `secondaryUrl`, `badge` |
|
|
410
|
+
| `cta` | `title`, `subtitle`, `primaryButton`, `primaryUrl`, `secondaryButton`, `secondaryUrl` |
|
|
411
|
+
| `faq` | `title`, `items[]` — each `{ question, answer }` |
|
|
412
|
+
| `stats` | `items[]` — each `{ value, label, description }` |
|
|
413
|
+
| `testimonial` | `quote`, `author`, `role`, `avatar` |
|
|
414
|
+
| `quote` | `quote`, `author`, `source` |
|
|
415
|
+
| `alert` | `message`, `type` (info/success/warning/error) |
|
|
416
|
+
| `divider` | — |
|
|
417
|
+
| `spacer` | `size` (sm/md/lg/xl) |
|
|
418
|
+
| `video` | `url` (YouTube or Vimeo), `title`, `autoplay` |
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## Troubleshooting
|
|
423
|
+
|
|
424
|
+
**Pages return empty (`getPage` returns `null`)**
|
|
425
|
+
|
|
426
|
+
- Check `CMS_APP_ID` matches exactly what is shown in CMS Admin → Apps.
|
|
427
|
+
- Verify the page status is **Published** (not Draft).
|
|
428
|
+
- Confirm the API key has `read` permission for the app.
|
|
429
|
+
|
|
430
|
+
**`getStatus()` returns `null`**
|
|
431
|
+
|
|
432
|
+
- All three env vars (`NEXT_PUBLIC_CMS_URL`, `CMS_API_KEY`, `CMS_APP_ID`) must be set.
|
|
433
|
+
- The CMS URL must not have a trailing slash.
|
|
434
|
+
- Test the API key directly: `curl -H "X-CMS-API-Key: cmsk_xxx" https://your-cms.vercel.app/api/v1/school-website/status`
|
|
435
|
+
|
|
436
|
+
**Webhook not firing / pages stale**
|
|
437
|
+
|
|
438
|
+
- Confirm the webhook URL is registered in CMS Admin → Settings → Webhooks.
|
|
439
|
+
- `CMS_WEBHOOK_SECRET` on both sides must match exactly.
|
|
440
|
+
- Check your deployment logs for `[sprintup-cms] revalidate error:` messages.
|
|
441
|
+
|
|
442
|
+
**Preview mode not working**
|
|
443
|
+
|
|
444
|
+
- The `/api/cms-preview/exit` route must exist.
|
|
445
|
+
- `draftMode()` requires a Next.js App Router project (not Pages Router).
|
|
156
446
|
|
|
157
447
|
---
|
|
158
448
|
|
package/dist/client.cjs
CHANGED
|
@@ -110,31 +110,31 @@ function createCMSClient(options) {
|
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
async function
|
|
113
|
+
async function getSiteStructure() {
|
|
114
114
|
const { baseUrl, apiKey, appId } = cfg();
|
|
115
|
-
if (!baseUrl || !apiKey || !appId) return
|
|
115
|
+
if (!baseUrl || !apiKey || !appId) return null;
|
|
116
116
|
try {
|
|
117
|
-
const res = await fetch(`${baseUrl}/api/v1/${appId}/
|
|
117
|
+
const res = await fetch(`${baseUrl}/api/v1/${appId}/site-structure`, {
|
|
118
118
|
headers: headers(),
|
|
119
|
-
next: {
|
|
119
|
+
next: {
|
|
120
|
+
revalidate: 300,
|
|
121
|
+
tags: [`site-structure-${appId}`]
|
|
122
|
+
}
|
|
120
123
|
});
|
|
121
|
-
if (!res.ok) return
|
|
124
|
+
if (!res.ok) return null;
|
|
122
125
|
const json = await res.json();
|
|
123
|
-
return json.data ??
|
|
126
|
+
return json.data ?? null;
|
|
124
127
|
} catch {
|
|
125
|
-
return
|
|
128
|
+
return null;
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
|
-
async function
|
|
131
|
+
async function getSitemap() {
|
|
129
132
|
const { baseUrl, apiKey, appId } = cfg();
|
|
130
133
|
if (!baseUrl || !apiKey || !appId) return null;
|
|
131
134
|
try {
|
|
132
|
-
const res = await fetch(`${baseUrl}/api/v1/${appId}/
|
|
135
|
+
const res = await fetch(`${baseUrl}/api/v1/${appId}/sitemap`, {
|
|
133
136
|
headers: headers(),
|
|
134
|
-
next: {
|
|
135
|
-
revalidate: 300,
|
|
136
|
-
tags: [`site-structure-${appId}`]
|
|
137
|
-
}
|
|
137
|
+
next: { revalidate: 3600 }
|
|
138
138
|
});
|
|
139
139
|
if (!res.ok) return null;
|
|
140
140
|
const json = await res.json();
|
|
@@ -143,6 +143,20 @@ function createCMSClient(options) {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
async function getStatus() {
|
|
147
|
+
const { baseUrl, apiKey, appId } = cfg();
|
|
148
|
+
if (!baseUrl || !apiKey || !appId) return null;
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetch(`${baseUrl}/api/v1/${appId}/status`, {
|
|
151
|
+
headers: headers(),
|
|
152
|
+
cache: "no-store"
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok) return null;
|
|
155
|
+
return await res.json();
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
146
160
|
return {
|
|
147
161
|
getPages,
|
|
148
162
|
getPage,
|
|
@@ -152,8 +166,9 @@ function createCMSClient(options) {
|
|
|
152
166
|
getPreviewPage,
|
|
153
167
|
getPageWithPreview,
|
|
154
168
|
getPageType,
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
getSiteStructure,
|
|
170
|
+
getSitemap,
|
|
171
|
+
getStatus
|
|
157
172
|
};
|
|
158
173
|
}
|
|
159
174
|
var cmsClient = createCMSClient();
|