@crayonscodetech/cms-sdk 1.0.8 → 1.0.9
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 +157 -26
- package/dist/index.cjs +294 -192
- package/dist/index.d.cts +141 -40
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +141 -40
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +291 -168
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -19
package/README.md
CHANGED
|
@@ -785,7 +785,7 @@ Different page types follow different rendering strategies. Understanding these
|
|
|
785
785
|
| `/gallery` | `page` + `albums` | `fetchPageByUrl(siteId, "/gallery")` + `fetchAlbums(siteId, params)` |
|
|
786
786
|
| `/gallery/[slug]` | `albums` + `album-items` | `fetchAlbums(siteId, { limit })` + `fetchAlbumItems(siteId, { album: slug })` |
|
|
787
787
|
| `/team/[slug]` | `team-members` | `fetchTeamMembers(siteId)` (slug lookup) or custom `fetch` |
|
|
788
|
-
| `/contact` | `contact` (form submissions) | `submitContactForm(siteId, payload)`
|
|
788
|
+
| `/contact` | `contact` (form submissions) | `fetchContactConfig(siteId)` + `submitContactForm(siteId, payload, attachments?)` |
|
|
789
789
|
|
|
790
790
|
### Home Page — Section Rendering with Targeting
|
|
791
791
|
|
|
@@ -1264,22 +1264,42 @@ export default async function AlbumDetailPage({
|
|
|
1264
1264
|
|
|
1265
1265
|
The contact page does **not** use `RenderSections`. It is a dedicated form page that submits directly to the CMS via `submitContactForm`. Do not render CMS sections here — just build your form UI and wire it to the SDK.
|
|
1266
1266
|
|
|
1267
|
+
**If the site has Turnstile enabled, the form will not work without it.** Fetch the config
|
|
1268
|
+
on the server, render the widget with the site key, and forward the token through your own
|
|
1269
|
+
API route along with the rest of the payload.
|
|
1270
|
+
|
|
1267
1271
|
```tsx
|
|
1268
|
-
// components/pages/ContactPage.tsx
|
|
1272
|
+
// components/pages/ContactPage.tsx (server component)
|
|
1269
1273
|
import { ContactForm } from "@/components/contact-form";
|
|
1270
|
-
import
|
|
1274
|
+
import { cms, SITE_ID } from "@/lib/cms";
|
|
1275
|
+
import type { Page, SiteConfig } from "@crayonscodetech/cms-sdk";
|
|
1271
1276
|
|
|
1272
|
-
export default function ContactPage({
|
|
1277
|
+
export default async function ContactPage({
|
|
1273
1278
|
page,
|
|
1274
1279
|
site,
|
|
1275
1280
|
}: {
|
|
1276
1281
|
page: Page;
|
|
1277
1282
|
site?: SiteConfig | null;
|
|
1278
1283
|
}) {
|
|
1284
|
+
// Public site key only — safe to hand to the client.
|
|
1285
|
+
const contactConfig = await cms.fetchContactConfig(SITE_ID);
|
|
1286
|
+
const turnstile = contactConfig?.turnstile;
|
|
1287
|
+
|
|
1288
|
+
// enabled with no site key = misconfigured; the widget cannot render and every
|
|
1289
|
+
// submission would be rejected, so do not show a form that cannot succeed.
|
|
1290
|
+
if (turnstile?.enabled && !turnstile.site_key) {
|
|
1291
|
+
return (
|
|
1292
|
+
<main>
|
|
1293
|
+
<h1>Contact Us</h1>
|
|
1294
|
+
<p>The contact form is temporarily unavailable.</p>
|
|
1295
|
+
</main>
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1279
1299
|
return (
|
|
1280
1300
|
<main>
|
|
1281
1301
|
<h1>Contact Us</h1>
|
|
1282
|
-
<ContactForm />
|
|
1302
|
+
<ContactForm turnstileSiteKey={turnstile?.enabled ? turnstile.site_key : null} />
|
|
1283
1303
|
</main>
|
|
1284
1304
|
);
|
|
1285
1305
|
}
|
|
@@ -1289,39 +1309,63 @@ export default function ContactPage({
|
|
|
1289
1309
|
// components/contact-form.tsx (client component — handles submission)
|
|
1290
1310
|
"use client";
|
|
1291
1311
|
|
|
1292
|
-
import { useState } from "react";
|
|
1293
|
-
import type
|
|
1312
|
+
import { useRef, useState } from "react";
|
|
1313
|
+
import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile";
|
|
1294
1314
|
|
|
1295
|
-
export function ContactForm(
|
|
1315
|
+
export function ContactForm({
|
|
1316
|
+
turnstileSiteKey,
|
|
1317
|
+
}: {
|
|
1318
|
+
turnstileSiteKey: string | null;
|
|
1319
|
+
}) {
|
|
1296
1320
|
const [status, setStatus] = useState<
|
|
1297
1321
|
"idle" | "sending" | "success" | "error"
|
|
1298
1322
|
>("idle");
|
|
1323
|
+
const [error, setError] = useState("");
|
|
1324
|
+
const [token, setToken] = useState("");
|
|
1325
|
+
const widget = useRef<TurnstileInstance>(null);
|
|
1299
1326
|
|
|
1300
1327
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
1301
1328
|
e.preventDefault();
|
|
1329
|
+
if (turnstileSiteKey && !token) {
|
|
1330
|
+
setError("Please complete the verification.");
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1302
1333
|
setStatus("sending");
|
|
1334
|
+
setError("");
|
|
1303
1335
|
|
|
1304
1336
|
const form = e.currentTarget;
|
|
1305
|
-
const payload
|
|
1337
|
+
const payload = {
|
|
1306
1338
|
name: (form.elements.namedItem("name") as HTMLInputElement).value,
|
|
1307
1339
|
email: (form.elements.namedItem("email") as HTMLInputElement).value,
|
|
1308
1340
|
subject: (form.elements.namedItem("subject") as HTMLInputElement).value,
|
|
1309
|
-
message: (form.elements.namedItem("message") as HTMLTextAreaElement)
|
|
1310
|
-
.value,
|
|
1341
|
+
message: (form.elements.namedItem("message") as HTMLTextAreaElement).value,
|
|
1311
1342
|
type: "contact",
|
|
1343
|
+
turnstile_token: token,
|
|
1312
1344
|
};
|
|
1313
1345
|
|
|
1314
1346
|
try {
|
|
1315
|
-
//
|
|
1347
|
+
// Proxied through your own route so SITE_ID stays server-side.
|
|
1316
1348
|
const res = await fetch("/api/contact", {
|
|
1317
1349
|
method: "POST",
|
|
1318
1350
|
body: JSON.stringify(payload),
|
|
1319
1351
|
headers: { "Content-Type": "application/json" },
|
|
1320
1352
|
});
|
|
1321
1353
|
|
|
1322
|
-
|
|
1354
|
+
if (res.ok) {
|
|
1355
|
+
setStatus("success");
|
|
1356
|
+
} else {
|
|
1357
|
+
const body = await res.json().catch(() => ({}));
|
|
1358
|
+
setError(body.error ?? "Something went wrong. Please try again.");
|
|
1359
|
+
setStatus("error");
|
|
1360
|
+
}
|
|
1323
1361
|
} catch {
|
|
1362
|
+
setError("Something went wrong. Please try again.");
|
|
1324
1363
|
setStatus("error");
|
|
1364
|
+
} finally {
|
|
1365
|
+
// Turnstile tokens are single-use — always reset, success or failure, or
|
|
1366
|
+
// the next submit replays a spent token and is rejected.
|
|
1367
|
+
widget.current?.reset();
|
|
1368
|
+
setToken("");
|
|
1325
1369
|
}
|
|
1326
1370
|
}
|
|
1327
1371
|
|
|
@@ -1331,11 +1375,14 @@ export function ContactForm() {
|
|
|
1331
1375
|
<input name="email" type="email" placeholder="Email" />
|
|
1332
1376
|
<input name="subject" placeholder="Subject" />
|
|
1333
1377
|
<textarea name="message" placeholder="Message" required />
|
|
1378
|
+
{turnstileSiteKey && (
|
|
1379
|
+
<Turnstile ref={widget} siteKey={turnstileSiteKey} onSuccess={setToken} />
|
|
1380
|
+
)}
|
|
1334
1381
|
<button type="submit" disabled={status === "sending"}>
|
|
1335
1382
|
{status === "sending" ? "Sending…" : "Send"}
|
|
1336
1383
|
</button>
|
|
1337
1384
|
{status === "success" && <p>Message sent!</p>}
|
|
1338
|
-
{status === "error" && <p>
|
|
1385
|
+
{status === "error" && <p>{error}</p>}
|
|
1339
1386
|
</form>
|
|
1340
1387
|
);
|
|
1341
1388
|
}
|
|
@@ -1344,12 +1391,30 @@ export function ContactForm() {
|
|
|
1344
1391
|
```ts
|
|
1345
1392
|
// app/api/contact/route.ts (server — keeps SITE_ID out of the client bundle)
|
|
1346
1393
|
import { cms, SITE_ID } from "@/lib/cms";
|
|
1347
|
-
import type
|
|
1394
|
+
import { CmsError, type ContactPayload } from "@crayonscodetech/cms-sdk";
|
|
1395
|
+
|
|
1396
|
+
export const dynamic = "force-dynamic";
|
|
1348
1397
|
|
|
1349
1398
|
export async function POST(req: Request) {
|
|
1350
1399
|
const payload: ContactPayload = await req.json();
|
|
1351
|
-
|
|
1352
|
-
|
|
1400
|
+
|
|
1401
|
+
try {
|
|
1402
|
+
const contact = await cms.submitContactForm(SITE_ID, payload);
|
|
1403
|
+
return Response.json({ ok: true, contact });
|
|
1404
|
+
} catch (e) {
|
|
1405
|
+
// submitContactForm throws rather than returning null, so the visitor can be
|
|
1406
|
+
// told what actually went wrong.
|
|
1407
|
+
if (e instanceof CmsError) {
|
|
1408
|
+
const message =
|
|
1409
|
+
e.status === 403
|
|
1410
|
+
? "Verification failed. Please try again."
|
|
1411
|
+
: e.status === 429
|
|
1412
|
+
? "Too many attempts. Please wait a moment."
|
|
1413
|
+
: e.message;
|
|
1414
|
+
return Response.json({ ok: false, error: message }, { status: e.status ?? 502 });
|
|
1415
|
+
}
|
|
1416
|
+
return Response.json({ ok: false, error: "Submission failed" }, { status: 502 });
|
|
1417
|
+
}
|
|
1353
1418
|
}
|
|
1354
1419
|
```
|
|
1355
1420
|
|
|
@@ -2623,15 +2688,44 @@ export interface FetchOptions extends RequestInit {
|
|
|
2623
2688
|
|
|
2624
2689
|
### Forms & Submissions
|
|
2625
2690
|
|
|
2626
|
-
- `
|
|
2691
|
+
- `fetchContactConfig(siteId, options?)`: Public Turnstile settings for the contact form.
|
|
2692
|
+
Call it before rendering the form.
|
|
2693
|
+
- **Returns**: `{ turnstile: { enabled: boolean; site_key: string | null } }`
|
|
2694
|
+
- When `enabled` is true you MUST render the Turnstile widget with `site_key` and pass the
|
|
2695
|
+
resulting token as `turnstile_token`, or every submission is rejected with 403.
|
|
2696
|
+
- `enabled: true` with a null `site_key` means the site is misconfigured — hide the form
|
|
2697
|
+
rather than submitting into a guaranteed rejection.
|
|
2698
|
+
|
|
2699
|
+
- `submitContactForm(siteId, payload, attachments?, options?)`: Submits a contact form.
|
|
2627
2700
|
- **Payload Structure**:
|
|
2628
2701
|
```typescript
|
|
2629
2702
|
{
|
|
2630
|
-
name: string;
|
|
2631
|
-
message: string;
|
|
2632
|
-
email?: string;
|
|
2633
|
-
subject?: string;
|
|
2634
|
-
type?: string;
|
|
2703
|
+
name: string; // Required
|
|
2704
|
+
message: string; // Required
|
|
2705
|
+
email?: string; // Optional
|
|
2706
|
+
subject?: string; // Optional
|
|
2707
|
+
type?: string; // Default: "contact"
|
|
2708
|
+
turnstile_token?: string; // Required when Turnstile is enabled
|
|
2709
|
+
}
|
|
2710
|
+
```
|
|
2711
|
+
- **Attachments** (optional): `File[]`. Sending them switches the request to multipart.
|
|
2712
|
+
Caps, enforced server-side and pre-checked client-side: max 3 files, 4 MiB total, and a
|
|
2713
|
+
MIME allowlist (PDF, PNG, JPEG, WebP, GIF, plain text, DOC, DOCX).
|
|
2714
|
+
- **Throws `CmsError` instead of returning `null`.** Unlike the read methods, a form needs
|
|
2715
|
+
to distinguish rejection kinds, so failures throw with `error.status`:
|
|
2716
|
+
`403` captcha failed (reset the widget — tokens are single-use), `429` rate limited,
|
|
2717
|
+
`400` validation, `503` Turnstile misconfigured server-side.
|
|
2718
|
+
- Never retried: a resend would duplicate the submission.
|
|
2719
|
+
|
|
2720
|
+
```typescript
|
|
2721
|
+
import { CmsError } from "@crayonscodetech/cms-sdk";
|
|
2722
|
+
|
|
2723
|
+
try {
|
|
2724
|
+
const contact = await cms.submitContactForm(SITE_ID, payload, files);
|
|
2725
|
+
} catch (e) {
|
|
2726
|
+
if (e instanceof CmsError && e.status === 403) {
|
|
2727
|
+
// captcha rejected — reset the widget and let the visitor retry
|
|
2728
|
+
}
|
|
2635
2729
|
}
|
|
2636
2730
|
```
|
|
2637
2731
|
|
|
@@ -2655,12 +2749,39 @@ export interface FetchOptions extends RequestInit {
|
|
|
2655
2749
|
|
|
2656
2750
|
## Sitemap
|
|
2657
2751
|
|
|
2658
|
-
The SDK exposes
|
|
2752
|
+
The SDK exposes twelve lightweight sitemap endpoints that return only the fields needed to build an XML sitemap (slug/URL, image, title/name, plus `updatedAt` for `lastModified`). Each endpoint filters to published content only and defaults to up to **2,000 items per request** — enough for most sites without needing to paginate.
|
|
2659
2753
|
|
|
2660
2754
|
> **These endpoints must only be called once per day.** Place them inside Next.js's `app/sitemap.ts` file and export `revalidate = 86400`. Never call them at request time.
|
|
2661
2755
|
|
|
2662
2756
|
### Methods
|
|
2663
2757
|
|
|
2758
|
+
Use `fetchSitemap(siteId, resource, params?, options?)` for any resource. The
|
|
2759
|
+
return type narrows automatically from the resource key:
|
|
2760
|
+
|
|
2761
|
+
```typescript
|
|
2762
|
+
const pages = await cms.fetchSitemap(siteId, "pages");
|
|
2763
|
+
pages.data[0].url; // typed — pages are addressed by url
|
|
2764
|
+
const blogs = await cms.fetchSitemap(siteId, "blogs");
|
|
2765
|
+
blogs.data[0].slug; // typed — everything else is addressed by slug
|
|
2766
|
+
```
|
|
2767
|
+
|
|
2768
|
+
| `resource` | Namespace | Item type |
|
|
2769
|
+
| --------------------- | --------- | ----------------------------- |
|
|
2770
|
+
| `"blogs"` | cms | `SitemapBlogItem` |
|
|
2771
|
+
| `"pages"` | cms | `SitemapPageItem` |
|
|
2772
|
+
| `"services"` | cms | `SitemapServiceItem` |
|
|
2773
|
+
| `"events"` | cms | `SitemapEventItem` |
|
|
2774
|
+
| `"albums"` | cms | `SitemapAlbumItem` |
|
|
2775
|
+
| `"team-members"` | cms | `SitemapTeamMemberItem` |
|
|
2776
|
+
| `"team-categories"` | cms | `SitemapTeamCategoryItem` |
|
|
2777
|
+
| `"brand-groups"` | cms | `SitemapBrandGroupItem` |
|
|
2778
|
+
| `"products"` | store | `SitemapProductItem` |
|
|
2779
|
+
| `"collections"` | store | `SitemapCollectionItem` |
|
|
2780
|
+
| `"product-categories"`| store | `SitemapProductCategoryItem` |
|
|
2781
|
+
| `"product-brands"` | store | `SitemapProductBrandItem` |
|
|
2782
|
+
|
|
2783
|
+
These four remain available as named shortcuts:
|
|
2784
|
+
|
|
2664
2785
|
| Method | Returns |
|
|
2665
2786
|
| -------------------------------------------------------- | ---------------------------------------------- |
|
|
2666
2787
|
| `fetchSitemapBlogs(siteId, params?, options?)` | `PaginatedResponse<SitemapBlogItem>` |
|
|
@@ -2668,32 +2789,42 @@ The SDK exposes four lightweight sitemap endpoints that return only the fields n
|
|
|
2668
2789
|
| `fetchSitemapProducts(siteId, params?, options?)` | `PaginatedResponse<SitemapProductItem>` |
|
|
2669
2790
|
| `fetchSitemapCollections(siteId, params?, options?)` | `PaginatedResponse<SitemapCollectionItem>` |
|
|
2670
2791
|
|
|
2671
|
-
All
|
|
2792
|
+
All accept optional `{ page?: number; limit?: number }` params.
|
|
2793
|
+
|
|
2794
|
+
**Field naming:** most resources return `title`; `products` and `collections`
|
|
2795
|
+
return `name` instead (kept for backwards compatibility). `pages` is addressed
|
|
2796
|
+
by `url` rather than `slug`, and `pages`, `team-categories` and `brand-groups`
|
|
2797
|
+
have no `image` field.
|
|
2672
2798
|
|
|
2673
2799
|
### Types
|
|
2674
2800
|
|
|
2675
2801
|
```typescript
|
|
2802
|
+
// Every item type includes `updatedAt` — use it for `lastModified`.
|
|
2676
2803
|
interface SitemapBlogItem {
|
|
2677
2804
|
slug: string;
|
|
2678
2805
|
image: string | null;
|
|
2679
2806
|
title: string;
|
|
2807
|
+
updatedAt: string;
|
|
2680
2808
|
}
|
|
2681
2809
|
|
|
2682
2810
|
interface SitemapPageItem {
|
|
2683
2811
|
url: string; // e.g. "/about", "/services"
|
|
2684
2812
|
title: string;
|
|
2813
|
+
updatedAt: string;
|
|
2685
2814
|
}
|
|
2686
2815
|
|
|
2687
2816
|
interface SitemapProductItem {
|
|
2688
2817
|
slug: string;
|
|
2689
2818
|
image: string | null;
|
|
2690
2819
|
name: string;
|
|
2820
|
+
updatedAt: string;
|
|
2691
2821
|
}
|
|
2692
2822
|
|
|
2693
2823
|
interface SitemapCollectionItem {
|
|
2694
2824
|
slug: string;
|
|
2695
2825
|
image: string | null;
|
|
2696
2826
|
name: string;
|
|
2827
|
+
updatedAt: string;
|
|
2697
2828
|
}
|
|
2698
2829
|
```
|
|
2699
2830
|
|
|
@@ -2749,7 +2880,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
2749
2880
|
}
|
|
2750
2881
|
```
|
|
2751
2882
|
|
|
2752
|
-
> **Note:** If your site has more than
|
|
2883
|
+
> **Note:** If your site has more than 2,000 entries for any content type, use the `limit` param together with Next.js's [`generateSitemaps`](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generating-multiple-sitemaps) to split the output across multiple sitemap files.
|
|
2753
2884
|
|
|
2754
2885
|
## Type System
|
|
2755
2886
|
|