@sprintup-cms/sdk 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@sprintup-cms/sdk` will be documented here.
4
+
5
+ ## [1.0.0] — 2026-03-03
6
+
7
+ ### Added
8
+ - `@sprintup-cms/sdk` — zero-dependency typed API client (`cmsClient`, `createCMSClient`)
9
+ - `getPage`, `getPages`, `getBlogPosts`, `getEvents`, `getAnnouncements`
10
+ - `getPageType`, `getPageTypes`
11
+ - `getSiteStructure`
12
+ - `getPreviewPage`, `getPageWithPreview`
13
+ - `@sprintup-cms/sdk/next` — Next.js 15 App Router helpers
14
+ - `CMSCatchAllPage` — drop-in catch-all page with draft mode support
15
+ - `generateMetadata` — SEO metadata from CMS page data
16
+ - `POST` / `createRevalidateHandler` — on-demand ISR revalidation webhook
17
+ - `previewExitGET` / `createPreviewExitHandler` — draft mode exit
18
+ - `@sprintup-cms/sdk/react` — React block renderer
19
+ - `CMSBlocks` — renders all built-in block types + custom overrides
20
+ - `CMSPreviewBanner` — draft preview banner with exit link
21
+ - Full TypeScript types for all CMS entities
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # @sprintup-cms/sdk
2
+
3
+ Official SDK for **SprintUp Forge CMS** — typed API client, Next.js App Router helpers, and a React block renderer.
4
+
5
+ ---
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @sprintup-cms/sdk
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Quick start
16
+
17
+ ### 1. Environment variables
18
+
19
+ Add to your school website's `.env.local`:
20
+
21
+ ```env
22
+ NEXT_PUBLIC_CMS_URL=https://your-cms.vercel.app
23
+ CMS_API_KEY=cmsk_xxxxxxxxxxxxxxxxxxxx
24
+ CMS_APP_ID=school-website
25
+ CMS_WEBHOOK_SECRET=your-random-secret
26
+ ```
27
+
28
+ Generate a webhook secret with:
29
+
30
+ ```bash
31
+ openssl rand -hex 32
32
+ ```
33
+
34
+ ---
35
+
36
+ ### 2. Catch-all CMS page
37
+
38
+ Create `app/[...slug]/page.tsx`:
39
+
40
+ ```ts
41
+ export { CMSCatchAllPage as default, generateMetadata } from '@sprintup-cms/sdk/next'
42
+ ```
43
+
44
+ That's it. Every page published in the CMS will be automatically rendered.
45
+
46
+ ---
47
+
48
+ ### 3. On-demand revalidation webhook
49
+
50
+ Create `app/api/cms-revalidate/route.ts`:
51
+
52
+ ```ts
53
+ export { POST } from '@sprintup-cms/sdk/next'
54
+ ```
55
+
56
+ Then set the webhook URL in your CMS App Settings to:
57
+
58
+ ```
59
+ https://your-school-site.vercel.app/api/cms-revalidate
60
+ ```
61
+
62
+ ---
63
+
64
+ ### 4. Preview exit
65
+
66
+ Create `app/api/cms-preview/exit/route.ts`:
67
+
68
+ ```ts
69
+ export { previewExitGET as GET } from '@sprintup-cms/sdk/next'
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Manual usage
75
+
76
+ ### Core client
77
+
78
+ ```ts
79
+ import { cmsClient } from '@sprintup-cms/sdk'
80
+
81
+ // Get a single page by slug
82
+ const page = await cmsClient.getPage('about')
83
+
84
+ // Get all blog posts
85
+ const posts = await cmsClient.getBlogPosts()
86
+
87
+ // Get site structure (nav, footer)
88
+ const structure = await cmsClient.getSiteStructure()
89
+ ```
90
+
91
+ ### Custom client instance
92
+
93
+ ```ts
94
+ import { createCMSClient } from '@sprintup-cms/sdk'
95
+
96
+ const cms = createCMSClient({
97
+ baseUrl: 'https://your-cms.vercel.app',
98
+ apiKey: 'cmsk_xxxx',
99
+ appId: 'school-website',
100
+ })
101
+ ```
102
+
103
+ ### React block renderer
104
+
105
+ ```tsx
106
+ import { CMSBlocks, CMSPreviewBanner } from '@sprintup-cms/sdk/react'
107
+
108
+ export default function Page({ page, pageType, isPreview }) {
109
+ return (
110
+ <>
111
+ <CMSPreviewBanner isPreview={isPreview} status={page.status} slug={page.slug} />
112
+ <CMSBlocks
113
+ blocks={page.blocks}
114
+ pageType={pageType}
115
+ // Override any block type with your own component:
116
+ custom={{
117
+ 'my-hero': (block) => <MyHeroComponent {...block.data} />,
118
+ }}
119
+ />
120
+ </>
121
+ )
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Supported block types
128
+
129
+ | Type | Description |
130
+ |---|---|
131
+ | `heading` | H1–H4 heading |
132
+ | `text` | Plain paragraph |
133
+ | `richtext` | HTML rich text (rendered with `prose`) |
134
+ | `image` | Image with optional caption |
135
+ | `hero` / `hero-section` | Hero with title, subtitle, CTA buttons |
136
+ | `cta` | Call-to-action section |
137
+ | `faq` | Accordion FAQ list |
138
+ | `stats` | Stats grid |
139
+ | `testimonial` | Quote card with author |
140
+ | `quote` | Blockquote |
141
+ | `alert` | Info / success / warning / error banner |
142
+ | `divider` | Horizontal rule |
143
+ | `spacer` | Vertical spacing (sm/md/lg/xl) |
144
+ | `video` | YouTube / Vimeo embed |
145
+ | *(any page type section)* | Rendered as labelled fields from schema |
146
+
147
+ ---
148
+
149
+ ## Entry points
150
+
151
+ | Import | Contents |
152
+ |---|---|
153
+ | `@sprintup-cms/sdk` | Typed client, all interfaces |
154
+ | `@sprintup-cms/sdk/next` | `CMSCatchAllPage`, `POST` revalidate, `previewExitGET` |
155
+ | `@sprintup-cms/sdk/react` | `CMSBlocks`, `CMSPreviewBanner` |
156
+
157
+ ---
158
+
159
+ ## Requirements
160
+
161
+ - Node.js 18+
162
+ - Next.js 14+ (for `/next` entry)
163
+ - React 18+ (for `/react` entry)
164
+
165
+ ---
166
+
167
+ ## License
168
+
169
+ MIT — SprintUp IO
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ /* @sprintup-cms/sdk — https://forgecms.io */
4
+
5
+ // src/client.ts
6
+ function createCMSClient(options) {
7
+ function cfg() {
8
+ return {
9
+ baseUrl: (options?.baseUrl ?? process.env.NEXT_PUBLIC_CMS_URL ?? process.env.CMS_BASE_URL ?? "").replace(/\/$/, ""),
10
+ apiKey: options?.apiKey ?? process.env.CMS_API_KEY ?? "",
11
+ appId: options?.appId ?? process.env.CMS_APP_ID ?? ""
12
+ };
13
+ }
14
+ function headers() {
15
+ return { "X-CMS-API-Key": cfg().apiKey, "Content-Type": "application/json" };
16
+ }
17
+ async function getPages(params) {
18
+ const { baseUrl, apiKey, appId } = cfg();
19
+ if (!baseUrl || !apiKey || !appId) {
20
+ console.warn("[sprintup-cms] Missing CMS_BASE_URL / CMS_API_KEY / CMS_APP_ID \u2014 returning []");
21
+ return [];
22
+ }
23
+ try {
24
+ const qs = new URLSearchParams();
25
+ if (params?.type) qs.set("type", params.type);
26
+ if (params?.group) qs.set("group", params.group);
27
+ if (params?.page) qs.set("page", String(params.page));
28
+ if (params?.perPage) qs.set("perPage", String(params.perPage));
29
+ const url = `${baseUrl}/api/v1/${appId}/pages${qs.size ? `?${qs}` : ""}`;
30
+ const res = await fetch(url, {
31
+ headers: headers(),
32
+ next: { revalidate: 60, tags: [`cms-pages-${appId}`] }
33
+ });
34
+ if (!res.ok) {
35
+ console.error(`[sprintup-cms] getPages (${res.status})`);
36
+ return [];
37
+ }
38
+ const json = await res.json();
39
+ return json.data ?? [];
40
+ } catch (err) {
41
+ console.error("[sprintup-cms] getPages error:", err);
42
+ return [];
43
+ }
44
+ }
45
+ async function getPage(slug) {
46
+ const { baseUrl, apiKey, appId } = cfg();
47
+ if (!baseUrl || !apiKey || !appId) {
48
+ console.warn("[sprintup-cms] Missing config \u2014 returning null");
49
+ return null;
50
+ }
51
+ try {
52
+ const res = await fetch(`${baseUrl}/api/v1/${appId}/pages/${slug}`, {
53
+ headers: headers(),
54
+ next: { revalidate: 60, tags: [`cms-page-${slug}`, `cms-pages-${appId}`] }
55
+ });
56
+ if (res.status === 404) return null;
57
+ if (!res.ok) {
58
+ console.error(`[sprintup-cms] getPage "${slug}" (${res.status})`);
59
+ return null;
60
+ }
61
+ const json = await res.json();
62
+ return json.data ?? null;
63
+ } catch (err) {
64
+ console.error(`[sprintup-cms] getPage "${slug}" error:`, err);
65
+ return null;
66
+ }
67
+ }
68
+ async function getBlogPosts() {
69
+ return getPages({ type: "blog-post" });
70
+ }
71
+ async function getEvents() {
72
+ return getPages({ type: "event-page" });
73
+ }
74
+ async function getAnnouncements() {
75
+ return getPages({ type: "announcement-page" });
76
+ }
77
+ async function getPreviewPage(token) {
78
+ const { baseUrl, appId } = cfg();
79
+ if (!baseUrl || !appId) return null;
80
+ try {
81
+ const res = await fetch(`${baseUrl}/api/v1/${appId}/preview?token=${encodeURIComponent(token)}`, {
82
+ cache: "no-store"
83
+ });
84
+ if (!res.ok) return null;
85
+ const json = await res.json();
86
+ return json.data ?? null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ async function getPageWithPreview(slug, previewToken) {
92
+ if (previewToken) {
93
+ const preview = await getPreviewPage(previewToken);
94
+ if (preview?.slug === slug) return preview;
95
+ }
96
+ return getPage(slug);
97
+ }
98
+ async function getPageType(pageTypeId) {
99
+ const { baseUrl, apiKey, appId } = cfg();
100
+ if (!baseUrl || !apiKey || !appId || !pageTypeId) return null;
101
+ try {
102
+ const res = await fetch(`${baseUrl}/api/v1/${appId}/page-types/${pageTypeId}`, {
103
+ headers: headers(),
104
+ next: { revalidate: 3600, tags: [`cms-page-type-${pageTypeId}`] }
105
+ });
106
+ if (!res.ok) return null;
107
+ const json = await res.json();
108
+ return json.data ?? null;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+ async function getPageTypes() {
114
+ const { baseUrl, apiKey, appId } = cfg();
115
+ if (!baseUrl || !apiKey || !appId) return [];
116
+ try {
117
+ const res = await fetch(`${baseUrl}/api/v1/${appId}/page-types`, {
118
+ headers: headers(),
119
+ next: { revalidate: 3600, tags: [`cms-page-types-${appId}`] }
120
+ });
121
+ if (!res.ok) return [];
122
+ const json = await res.json();
123
+ return json.data ?? [];
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+ async function getSiteStructure() {
129
+ const { baseUrl, apiKey, appId } = cfg();
130
+ if (!baseUrl || !apiKey || !appId) return null;
131
+ try {
132
+ const res = await fetch(`${baseUrl}/api/v1/${appId}/site-structure`, {
133
+ headers: headers(),
134
+ next: {
135
+ revalidate: 300,
136
+ tags: [`site-structure-${appId}`]
137
+ }
138
+ });
139
+ if (!res.ok) return null;
140
+ const json = await res.json();
141
+ return json.data ?? null;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+ return {
147
+ getPages,
148
+ getPage,
149
+ getBlogPosts,
150
+ getEvents,
151
+ getAnnouncements,
152
+ getPreviewPage,
153
+ getPageWithPreview,
154
+ getPageType,
155
+ getPageTypes,
156
+ getSiteStructure
157
+ };
158
+ }
159
+ var cmsClient = createCMSClient();
160
+
161
+ exports.cmsClient = cmsClient;
162
+ exports.createCMSClient = createCMSClient;
163
+ //# sourceMappingURL=client.cjs.map
164
+ //# sourceMappingURL=client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"client.cjs","sourcesContent":["/**\n * @sprintup-cms/sdk — Core Client\n *\n * Zero-dependency, framework-agnostic typed API client for SprintUp Forge CMS.\n *\n * @example\n * import { cmsClient } from '@sprintup-cms/sdk'\n * const page = await cmsClient.getPage('about')\n *\n * @example Custom instance\n * import { createCMSClient } from '@sprintup-cms/sdk'\n * const cms = createCMSClient({ baseUrl: '...', apiKey: '...', appId: '...' })\n */\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface CMSBlock {\n id: string\n type: string\n label?: string\n locked?: boolean\n data?: Record<string, any>\n /** Legacy field — blocks created before v1.1 used `content` instead of `data` */\n content?: Record<string, any>\n order?: number\n}\n\nexport interface CMSPage {\n _id?: string\n slug: string\n title: string\n description?: string\n pageType?: string\n pageTypeId?: string\n variant?: string\n status: 'draft' | 'published' | 'archived'\n visibility?: 'public' | 'private' | 'password'\n blocks: CMSBlock[]\n publishedAt?: string\n updatedAt?: string\n seo?: {\n title?: string\n description?: string\n keywords?: string[]\n ogImage?: string\n noIndex?: boolean\n }\n}\n\nexport interface CMSPageTypeField {\n id: string\n name: string\n label: string\n fieldType:\n | 'text' | 'textarea' | 'richtext' | 'image' | 'url'\n | 'number' | 'boolean' | 'date' | 'select' | 'relation'\n | 'email' | 'phone' | 'slug' | 'color' | 'embed'\n | 'multi-select' | 'repeater' | 'file' | 'code'\n required?: boolean\n options?: string[]\n description?: string\n maxLength?: number\n multiple?: boolean\n targetPageTypeKey?: string\n}\n\nexport interface CMSPageTypeSection {\n id: string\n name: string\n label: string\n order: number\n locked?: boolean\n fields: CMSPageTypeField[]\n}\n\nexport interface CMSPageTypeVariant {\n key: string\n label: string\n description?: string\n visibleSections?: string[]\n}\n\nexport interface CMSPageType {\n _id: string\n name: string\n key: string\n description?: string\n icon?: string\n category: 'singleton' | 'collection' | 'global'\n contentCategory: 'singleton' | 'collection' | 'global'\n allowedRoles?: string[]\n variants: CMSPageTypeVariant[]\n sections: CMSPageTypeSection[]\n}\n\n// ── Site Structure ─────────────────────────────────────────────────────────────\n\nexport type CMSMenuItemType = 'page' | 'url' | 'dynamic'\n\nexport interface CMSMenuItem {\n id: string\n type: CMSMenuItemType\n label: string\n contentId?: string\n url?: string\n /** Resolved href — ready to pass to <a href> or <Link href>. Never null, falls back to \"#\". */\n href: string\n locked: boolean\n openInNewTab: boolean\n children: CMSMenuItem[]\n page?: {\n title?: string\n slug?: string\n seoTitle?: string\n seoDescription?: string\n }\n}\n\nexport interface CMSPageTreeNode {\n id: string\n contentId: string\n parentId: string | null\n order: number\n locked: boolean\n visible?: boolean\n page?: {\n title?: string\n slug?: string\n seoTitle?: string\n seoDescription?: string\n }\n}\n\nexport interface CMSFooterGroup {\n id: string\n heading: string\n headingUrl?: string\n locked: boolean\n links: CMSMenuItem[]\n}\n\nexport interface CMSSiteStructure {\n appId: string\n pageTree: CMSPageTreeNode[]\n menus: {\n header: CMSMenuItem[]\n footer: CMSFooterGroup[]\n footerBottom: CMSMenuItem[]\n sidebar: CMSMenuItem[]\n [slot: string]: CMSMenuItem[] | CMSFooterGroup[]\n }\n updatedAt?: string\n}\n\n// ── Pagination & Responses ─────────────────────────────────────────────────────\n\nexport interface CMSListMeta {\n total: number\n page: number\n perPage: number\n totalPages: number\n}\n\nexport interface CMSListResponse<T = CMSPage> {\n data: T[]\n meta: CMSListMeta\n appId: string\n}\n\nexport interface CMSSingleResponse<T = CMSPage> {\n data: T\n}\n\n// ── Client Options ─────────────────────────────────────────────────────────────\n\nexport interface CMSClientOptions {\n /** Base URL of your Forge CMS instance, e.g. https://cms.yourschool.io */\n baseUrl?: string\n /** API key generated in CMS Admin → API Keys. Server-side only. */\n apiKey?: string\n /** App ID from CMS Admin → Apps, e.g. \"school-website\" */\n appId?: string\n}\n\nexport interface CMSGetPagesOptions {\n type?: string\n group?: string\n page?: number\n perPage?: number\n status?: 'published' | 'draft' | 'archived'\n}\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\nexport function createCMSClient(options?: CMSClientOptions) {\n /**\n * Resolve config lazily at request time — NOT at module/build time.\n * This prevents static prerendering crashes when env vars are absent during build.\n */\n function cfg() {\n return {\n baseUrl: (options?.baseUrl ?? process.env.NEXT_PUBLIC_CMS_URL ?? process.env.CMS_BASE_URL ?? '').replace(/\\/$/, ''),\n apiKey: options?.apiKey ?? process.env.CMS_API_KEY ?? '',\n appId: options?.appId ?? process.env.CMS_APP_ID ?? '',\n }\n }\n\n function headers() {\n return { 'X-CMS-API-Key': cfg().apiKey, 'Content-Type': 'application/json' }\n }\n\n // ── Pages ──────────────────────────────────────────────────────────────────\n\n async function getPages(params?: CMSGetPagesOptions): Promise<CMSPage[]> {\n const { baseUrl, apiKey, appId } = cfg()\n if (!baseUrl || !apiKey || !appId) {\n console.warn('[sprintup-cms] Missing CMS_BASE_URL / CMS_API_KEY / CMS_APP_ID — returning []')\n return []\n }\n try {\n const qs = new URLSearchParams()\n if (params?.type) qs.set('type', params.type)\n if (params?.group) qs.set('group', params.group)\n if (params?.page) qs.set('page', String(params.page))\n if (params?.perPage) qs.set('perPage', String(params.perPage))\n const url = `${baseUrl}/api/v1/${appId}/pages${qs.size ? `?${qs}` : ''}`\n const res = await fetch(url, {\n headers: headers(),\n next: { revalidate: 60, tags: [`cms-pages-${appId}`] },\n } as RequestInit)\n if (!res.ok) { console.error(`[sprintup-cms] getPages (${res.status})`); return [] }\n const json: CMSListResponse = await res.json()\n return json.data ?? []\n } catch (err) {\n console.error('[sprintup-cms] getPages error:', err)\n return []\n }\n }\n\n async function getPage(slug: string): Promise<CMSPage | null> {\n const { baseUrl, apiKey, appId } = cfg()\n if (!baseUrl || !apiKey || !appId) {\n console.warn('[sprintup-cms] Missing config — returning null')\n return null\n }\n try {\n const res = await fetch(`${baseUrl}/api/v1/${appId}/pages/${slug}`, {\n headers: headers(),\n next: { revalidate: 60, tags: [`cms-page-${slug}`, `cms-pages-${appId}`] },\n } as RequestInit)\n if (res.status === 404) return null\n if (!res.ok) { console.error(`[sprintup-cms] getPage \"${slug}\" (${res.status})`); return null }\n const json: CMSSingleResponse = await res.json()\n return json.data ?? null\n } catch (err) {\n console.error(`[sprintup-cms] getPage \"${slug}\" error:`, err)\n return null\n }\n }\n\n async function getBlogPosts(): Promise<CMSPage[]> {\n return getPages({ type: 'blog-post' })\n }\n\n async function getEvents(): Promise<CMSPage[]> {\n return getPages({ type: 'event-page' })\n }\n\n async function getAnnouncements(): Promise<CMSPage[]> {\n return getPages({ type: 'announcement-page' })\n }\n\n // ── Preview ────────────────────────────────────────────────────────────────\n\n async function getPreviewPage(token: string): Promise<CMSPage | null> {\n const { baseUrl, appId } = cfg()\n if (!baseUrl || !appId) return null\n try {\n const res = await fetch(`${baseUrl}/api/v1/${appId}/preview?token=${encodeURIComponent(token)}`, {\n cache: 'no-store',\n })\n if (!res.ok) return null\n const json = await res.json()\n return json.data ?? null\n } catch {\n return null\n }\n }\n\n async function getPageWithPreview(slug: string, previewToken?: string | null): Promise<CMSPage | null> {\n if (previewToken) {\n const preview = await getPreviewPage(previewToken)\n if (preview?.slug === slug) return preview\n }\n return getPage(slug)\n }\n\n // ── Page Types ─────────────────────────────────────────────────────────────\n\n async function getPageType(pageTypeId: string): Promise<CMSPageType | null> {\n const { baseUrl, apiKey, appId } = cfg()\n if (!baseUrl || !apiKey || !appId || !pageTypeId) return null\n try {\n const res = await fetch(`${baseUrl}/api/v1/${appId}/page-types/${pageTypeId}`, {\n headers: headers(),\n next: { revalidate: 3600, tags: [`cms-page-type-${pageTypeId}`] },\n } as RequestInit)\n if (!res.ok) return null\n const json = await res.json()\n return json.data ?? null\n } catch {\n return null\n }\n }\n\n async function getPageTypes(): Promise<CMSPageType[]> {\n const { baseUrl, apiKey, appId } = cfg()\n if (!baseUrl || !apiKey || !appId) return []\n try {\n const res = await fetch(`${baseUrl}/api/v1/${appId}/page-types`, {\n headers: headers(),\n next: { revalidate: 3600, tags: [`cms-page-types-${appId}`] },\n } as RequestInit)\n if (!res.ok) return []\n const json = await res.json()\n return json.data ?? []\n } catch {\n return []\n }\n }\n\n // ── Site Structure ─────────────────────────────────────────────────────────\n\n async function getSiteStructure(): Promise<CMSSiteStructure | null> {\n const { baseUrl, apiKey, appId } = cfg()\n if (!baseUrl || !apiKey || !appId) return null\n try {\n const res = await fetch(`${baseUrl}/api/v1/${appId}/site-structure`, {\n headers: headers(),\n next: {\n revalidate: 300,\n tags: [`site-structure-${appId}`],\n },\n } as RequestInit)\n if (!res.ok) return null\n const json = await res.json()\n return json.data ?? null\n } catch {\n return null\n }\n }\n\n return {\n getPages,\n getPage,\n getBlogPosts,\n getEvents,\n getAnnouncements,\n getPreviewPage,\n getPageWithPreview,\n getPageType,\n getPageTypes,\n getSiteStructure,\n }\n}\n\n// ── Default singleton ─────────────────────────────────────────────────────────\n\n/** Pre-configured singleton. Reads env vars lazily at request time. */\nexport const cmsClient = createCMSClient()\n"]}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @sprintup-cms/sdk — Core Client
3
+ *
4
+ * Zero-dependency, framework-agnostic typed API client for SprintUp Forge CMS.
5
+ *
6
+ * @example
7
+ * import { cmsClient } from '@sprintup-cms/sdk'
8
+ * const page = await cmsClient.getPage('about')
9
+ *
10
+ * @example Custom instance
11
+ * import { createCMSClient } from '@sprintup-cms/sdk'
12
+ * const cms = createCMSClient({ baseUrl: '...', apiKey: '...', appId: '...' })
13
+ */
14
+ interface CMSBlock {
15
+ id: string;
16
+ type: string;
17
+ label?: string;
18
+ locked?: boolean;
19
+ data?: Record<string, any>;
20
+ /** Legacy field — blocks created before v1.1 used `content` instead of `data` */
21
+ content?: Record<string, any>;
22
+ order?: number;
23
+ }
24
+ interface CMSPage {
25
+ _id?: string;
26
+ slug: string;
27
+ title: string;
28
+ description?: string;
29
+ pageType?: string;
30
+ pageTypeId?: string;
31
+ variant?: string;
32
+ status: 'draft' | 'published' | 'archived';
33
+ visibility?: 'public' | 'private' | 'password';
34
+ blocks: CMSBlock[];
35
+ publishedAt?: string;
36
+ updatedAt?: string;
37
+ seo?: {
38
+ title?: string;
39
+ description?: string;
40
+ keywords?: string[];
41
+ ogImage?: string;
42
+ noIndex?: boolean;
43
+ };
44
+ }
45
+ interface CMSPageTypeField {
46
+ id: string;
47
+ name: string;
48
+ label: string;
49
+ fieldType: 'text' | 'textarea' | 'richtext' | 'image' | 'url' | 'number' | 'boolean' | 'date' | 'select' | 'relation' | 'email' | 'phone' | 'slug' | 'color' | 'embed' | 'multi-select' | 'repeater' | 'file' | 'code';
50
+ required?: boolean;
51
+ options?: string[];
52
+ description?: string;
53
+ maxLength?: number;
54
+ multiple?: boolean;
55
+ targetPageTypeKey?: string;
56
+ }
57
+ interface CMSPageTypeSection {
58
+ id: string;
59
+ name: string;
60
+ label: string;
61
+ order: number;
62
+ locked?: boolean;
63
+ fields: CMSPageTypeField[];
64
+ }
65
+ interface CMSPageTypeVariant {
66
+ key: string;
67
+ label: string;
68
+ description?: string;
69
+ visibleSections?: string[];
70
+ }
71
+ interface CMSPageType {
72
+ _id: string;
73
+ name: string;
74
+ key: string;
75
+ description?: string;
76
+ icon?: string;
77
+ category: 'singleton' | 'collection' | 'global';
78
+ contentCategory: 'singleton' | 'collection' | 'global';
79
+ allowedRoles?: string[];
80
+ variants: CMSPageTypeVariant[];
81
+ sections: CMSPageTypeSection[];
82
+ }
83
+ type CMSMenuItemType = 'page' | 'url' | 'dynamic';
84
+ interface CMSMenuItem {
85
+ id: string;
86
+ type: CMSMenuItemType;
87
+ label: string;
88
+ contentId?: string;
89
+ url?: string;
90
+ /** Resolved href — ready to pass to <a href> or <Link href>. Never null, falls back to "#". */
91
+ href: string;
92
+ locked: boolean;
93
+ openInNewTab: boolean;
94
+ children: CMSMenuItem[];
95
+ page?: {
96
+ title?: string;
97
+ slug?: string;
98
+ seoTitle?: string;
99
+ seoDescription?: string;
100
+ };
101
+ }
102
+ interface CMSPageTreeNode {
103
+ id: string;
104
+ contentId: string;
105
+ parentId: string | null;
106
+ order: number;
107
+ locked: boolean;
108
+ visible?: boolean;
109
+ page?: {
110
+ title?: string;
111
+ slug?: string;
112
+ seoTitle?: string;
113
+ seoDescription?: string;
114
+ };
115
+ }
116
+ interface CMSFooterGroup {
117
+ id: string;
118
+ heading: string;
119
+ headingUrl?: string;
120
+ locked: boolean;
121
+ links: CMSMenuItem[];
122
+ }
123
+ interface CMSSiteStructure {
124
+ appId: string;
125
+ pageTree: CMSPageTreeNode[];
126
+ menus: {
127
+ header: CMSMenuItem[];
128
+ footer: CMSFooterGroup[];
129
+ footerBottom: CMSMenuItem[];
130
+ sidebar: CMSMenuItem[];
131
+ [slot: string]: CMSMenuItem[] | CMSFooterGroup[];
132
+ };
133
+ updatedAt?: string;
134
+ }
135
+ interface CMSListMeta {
136
+ total: number;
137
+ page: number;
138
+ perPage: number;
139
+ totalPages: number;
140
+ }
141
+ interface CMSListResponse<T = CMSPage> {
142
+ data: T[];
143
+ meta: CMSListMeta;
144
+ appId: string;
145
+ }
146
+ interface CMSSingleResponse<T = CMSPage> {
147
+ data: T;
148
+ }
149
+ interface CMSClientOptions {
150
+ /** Base URL of your Forge CMS instance, e.g. https://cms.yourschool.io */
151
+ baseUrl?: string;
152
+ /** API key generated in CMS Admin → API Keys. Server-side only. */
153
+ apiKey?: string;
154
+ /** App ID from CMS Admin → Apps, e.g. "school-website" */
155
+ appId?: string;
156
+ }
157
+ interface CMSGetPagesOptions {
158
+ type?: string;
159
+ group?: string;
160
+ page?: number;
161
+ perPage?: number;
162
+ status?: 'published' | 'draft' | 'archived';
163
+ }
164
+ declare function createCMSClient(options?: CMSClientOptions): {
165
+ getPages: (params?: CMSGetPagesOptions) => Promise<CMSPage[]>;
166
+ getPage: (slug: string) => Promise<CMSPage | null>;
167
+ getBlogPosts: () => Promise<CMSPage[]>;
168
+ getEvents: () => Promise<CMSPage[]>;
169
+ getAnnouncements: () => Promise<CMSPage[]>;
170
+ getPreviewPage: (token: string) => Promise<CMSPage | null>;
171
+ getPageWithPreview: (slug: string, previewToken?: string | null) => Promise<CMSPage | null>;
172
+ getPageType: (pageTypeId: string) => Promise<CMSPageType | null>;
173
+ getPageTypes: () => Promise<CMSPageType[]>;
174
+ getSiteStructure: () => Promise<CMSSiteStructure | null>;
175
+ };
176
+ /** Pre-configured singleton. Reads env vars lazily at request time. */
177
+ declare const cmsClient: {
178
+ getPages: (params?: CMSGetPagesOptions) => Promise<CMSPage[]>;
179
+ getPage: (slug: string) => Promise<CMSPage | null>;
180
+ getBlogPosts: () => Promise<CMSPage[]>;
181
+ getEvents: () => Promise<CMSPage[]>;
182
+ getAnnouncements: () => Promise<CMSPage[]>;
183
+ getPreviewPage: (token: string) => Promise<CMSPage | null>;
184
+ getPageWithPreview: (slug: string, previewToken?: string | null) => Promise<CMSPage | null>;
185
+ getPageType: (pageTypeId: string) => Promise<CMSPageType | null>;
186
+ getPageTypes: () => Promise<CMSPageType[]>;
187
+ getSiteStructure: () => Promise<CMSSiteStructure | null>;
188
+ };
189
+
190
+ export { type CMSBlock, type CMSClientOptions, type CMSFooterGroup, type CMSGetPagesOptions, type CMSListMeta, type CMSListResponse, type CMSMenuItem, type CMSMenuItemType, type CMSPage, type CMSPageTreeNode, type CMSPageType, type CMSPageTypeField, type CMSPageTypeSection, type CMSPageTypeVariant, type CMSSingleResponse, type CMSSiteStructure, cmsClient, createCMSClient };