@crayonscodetech/cms-sdk 1.0.8 → 1.0.10
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 +167 -33
- package/dist/index.cjs +332 -207
- package/dist/index.d.cts +175 -42
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +175 -42
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +329 -182
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -19
package/README.md
CHANGED
|
@@ -777,15 +777,15 @@ Different page types follow different rendering strategies. Understanding these
|
|
|
777
777
|
| `[[...slug]]` CMS pages | `page` (+ inline `page.sections`) | `fetchPageByUrl(siteId, urlPath)` |
|
|
778
778
|
| `/about` | `page` + `about-us` | `fetchPageByUrl(siteId, "/about")` + `fetchAboutUs(siteId)` |
|
|
779
779
|
| `/services` | `page` + `services` | `fetchPageByUrl(siteId, "/services")` + `fetchServices(siteId)` |
|
|
780
|
-
| `/services/[slug]` | `services` | `
|
|
780
|
+
| `/services/[slug]` | `services` | `fetchServiceBySlug(siteId, slug)` |
|
|
781
781
|
| `/blog` or `/news` | `page` + `blog` | `fetchPageByUrl(siteId, "/blog")` (or your CMS-defined base url) + `fetchBlogs(siteId, params)` |
|
|
782
782
|
| `/blog/[slug]` | `blog` | `fetchBlogBySlug(siteId, slug)` |
|
|
783
783
|
| `/events` | `page` + `events` | `fetchPageByUrl(siteId, "/events")` + `fetchEvents(siteId, params)` |
|
|
784
|
-
| `/events/[slug]` | `events` | `
|
|
784
|
+
| `/events/[slug]` | `events` | `fetchEventBySlug(siteId, slug)` |
|
|
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
|
|
|
@@ -2598,13 +2663,13 @@ export interface FetchOptions extends RequestInit {
|
|
|
2598
2663
|
|
|
2599
2664
|
### Other Entities
|
|
2600
2665
|
|
|
2601
|
-
- `fetchServices(siteId, options?)`: Returns
|
|
2602
|
-
- `
|
|
2666
|
+
- `fetchServices(siteId, params?, options?)`: Returns paginated services. Params: `{ page, limit }`. Only published services are returned — the backend filters on `is_published`, and each item carries `is_published` plus a nullable `published_at`.
|
|
2667
|
+
- `fetchServiceBySlug(siteId, slug, options?)`: Returns a single published service by slug, including its `extra.sections`. Returns `null` if not found or unpublished.
|
|
2603
2668
|
- `fetchTeamMembers(siteId, params?, options?)`: Returns paginated team members. Params: `{ page, limit }`. Default limit: 20.
|
|
2604
2669
|
- `fetchTeamMembersByCategory(siteId, params, options?)`: Returns paginated team members filtered by category. Params: `{ categoryId, page?, limit? }`. Default limit: 20. **Note:** `categoryId` is now inside the params object.
|
|
2605
2670
|
- `fetchTestimonials(siteId, params?, options?)`: Returns paginated testimonials. Params: `{ type?, page?, limit? }`. Default limit: 20.
|
|
2606
2671
|
- `fetchEvents(siteId, params?, options?)`: Returns paginated events. Params: `{ page, limit, search }`.
|
|
2607
|
-
- `
|
|
2672
|
+
- `fetchEventBySlug(siteId, slug, options?)`: Returns a single published event by slug. Returns `null` if not found or unpublished.
|
|
2608
2673
|
- `fetchAlbums(siteId, params?, options?)`: Returns paginated albums. Params: `{ page, limit, search }`.
|
|
2609
2674
|
- `fetchAlbumItems(siteId, params, options?)`: Returns paginated items for an album. Params: `{ album?, album_id?, page?, limit? }`. Default limit: 20.
|
|
2610
2675
|
|
|
@@ -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, 3 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
|
|
|
@@ -2639,8 +2733,9 @@ export interface FetchOptions extends RequestInit {
|
|
|
2639
2733
|
|
|
2640
2734
|
> All store methods use the `/api/public/store/` API prefix, not `/api/public/cms/`.
|
|
2641
2735
|
|
|
2642
|
-
- `
|
|
2643
|
-
- `
|
|
2736
|
+
- `fetchStoreSettings(siteId, options?)`: Returns `{ currency, price_visibility, is_store_enabled }`. Fetch this first — when `price_visibility` is false the API strips `price`, `sale_price` and `cost_price` from every variant, so the storefront must render an inquiry flow rather than prices.
|
|
2737
|
+
- `fetchProductCategories(siteId, params?, options?)`: Returns paginated product categories. Params: `{ page, limit, search, ordering, parent_id }`.
|
|
2738
|
+
- `fetchProductBrands(siteId, params?, options?)`: Returns paginated product brands. Params: `{ page, limit, search, ordering }`.
|
|
2644
2739
|
- `fetchProducts(siteId, params?, options?)`: Returns paginated products. Params: `{ page, limit, search, category_id, tag_id, brand_id, is_featured }`.
|
|
2645
2740
|
- `category_id` is hierarchy-aware on the backend and includes child/grandchild categories.
|
|
2646
2741
|
- `fetchProductDetail(siteId, slug, options?)`: Returns a single product by slug, **including `variants`**.
|
|
@@ -2649,18 +2744,47 @@ export interface FetchOptions extends RequestInit {
|
|
|
2649
2744
|
- Params: `{ page, limit, category_id }`
|
|
2650
2745
|
- Works for both manual and smart collections
|
|
2651
2746
|
- `fetchCollectionDetailById(siteId, id, params?, options?)`: Same as `fetchCollectionDetail`, but keyed by collection ID for CMS-driven product sections.
|
|
2747
|
+
- `fetchProductsByTag(siteId, tag, params?, options?)`: Returns paginated products carrying a tag. Params: `{ page, limit }`.
|
|
2652
2748
|
- `placeOrder(siteId, payload, options?)`: Places an order. Call from a server API route — never client-side.
|
|
2749
|
+
- `metadata` on the payload must be a **flat `Record<string, string>`** — the API validates it on a strict schema, so nested objects, numbers and arrays are rejected with a 400.
|
|
2653
2750
|
|
|
2654
2751
|
---
|
|
2655
2752
|
|
|
2656
2753
|
## Sitemap
|
|
2657
2754
|
|
|
2658
|
-
The SDK exposes
|
|
2755
|
+
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
2756
|
|
|
2660
2757
|
> **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
2758
|
|
|
2662
2759
|
### Methods
|
|
2663
2760
|
|
|
2761
|
+
Use `fetchSitemap(siteId, resource, params?, options?)` for any resource. The
|
|
2762
|
+
return type narrows automatically from the resource key:
|
|
2763
|
+
|
|
2764
|
+
```typescript
|
|
2765
|
+
const pages = await cms.fetchSitemap(siteId, "pages");
|
|
2766
|
+
pages.data[0].url; // typed — pages are addressed by url
|
|
2767
|
+
const blogs = await cms.fetchSitemap(siteId, "blogs");
|
|
2768
|
+
blogs.data[0].slug; // typed — everything else is addressed by slug
|
|
2769
|
+
```
|
|
2770
|
+
|
|
2771
|
+
| `resource` | Namespace | Item type |
|
|
2772
|
+
| --------------------- | --------- | ----------------------------- |
|
|
2773
|
+
| `"blogs"` | cms | `SitemapBlogItem` |
|
|
2774
|
+
| `"pages"` | cms | `SitemapPageItem` |
|
|
2775
|
+
| `"services"` | cms | `SitemapServiceItem` |
|
|
2776
|
+
| `"events"` | cms | `SitemapEventItem` |
|
|
2777
|
+
| `"albums"` | cms | `SitemapAlbumItem` |
|
|
2778
|
+
| `"team-members"` | cms | `SitemapTeamMemberItem` |
|
|
2779
|
+
| `"team-categories"` | cms | `SitemapTeamCategoryItem` |
|
|
2780
|
+
| `"brand-groups"` | cms | `SitemapBrandGroupItem` |
|
|
2781
|
+
| `"products"` | store | `SitemapProductItem` |
|
|
2782
|
+
| `"collections"` | store | `SitemapCollectionItem` |
|
|
2783
|
+
| `"product-categories"`| store | `SitemapProductCategoryItem` |
|
|
2784
|
+
| `"product-brands"` | store | `SitemapProductBrandItem` |
|
|
2785
|
+
|
|
2786
|
+
These four remain available as named shortcuts:
|
|
2787
|
+
|
|
2664
2788
|
| Method | Returns |
|
|
2665
2789
|
| -------------------------------------------------------- | ---------------------------------------------- |
|
|
2666
2790
|
| `fetchSitemapBlogs(siteId, params?, options?)` | `PaginatedResponse<SitemapBlogItem>` |
|
|
@@ -2668,32 +2792,42 @@ The SDK exposes four lightweight sitemap endpoints that return only the fields n
|
|
|
2668
2792
|
| `fetchSitemapProducts(siteId, params?, options?)` | `PaginatedResponse<SitemapProductItem>` |
|
|
2669
2793
|
| `fetchSitemapCollections(siteId, params?, options?)` | `PaginatedResponse<SitemapCollectionItem>` |
|
|
2670
2794
|
|
|
2671
|
-
All
|
|
2795
|
+
All accept optional `{ page?: number; limit?: number }` params.
|
|
2796
|
+
|
|
2797
|
+
**Field naming:** most resources return `title`; `products` and `collections`
|
|
2798
|
+
return `name` instead (kept for backwards compatibility). `pages` is addressed
|
|
2799
|
+
by `url` rather than `slug`, and `pages`, `team-categories` and `brand-groups`
|
|
2800
|
+
have no `image` field.
|
|
2672
2801
|
|
|
2673
2802
|
### Types
|
|
2674
2803
|
|
|
2675
2804
|
```typescript
|
|
2805
|
+
// Every item type includes `updatedAt` — use it for `lastModified`.
|
|
2676
2806
|
interface SitemapBlogItem {
|
|
2677
2807
|
slug: string;
|
|
2678
2808
|
image: string | null;
|
|
2679
2809
|
title: string;
|
|
2810
|
+
updatedAt: string;
|
|
2680
2811
|
}
|
|
2681
2812
|
|
|
2682
2813
|
interface SitemapPageItem {
|
|
2683
2814
|
url: string; // e.g. "/about", "/services"
|
|
2684
2815
|
title: string;
|
|
2816
|
+
updatedAt: string;
|
|
2685
2817
|
}
|
|
2686
2818
|
|
|
2687
2819
|
interface SitemapProductItem {
|
|
2688
2820
|
slug: string;
|
|
2689
2821
|
image: string | null;
|
|
2690
2822
|
name: string;
|
|
2823
|
+
updatedAt: string;
|
|
2691
2824
|
}
|
|
2692
2825
|
|
|
2693
2826
|
interface SitemapCollectionItem {
|
|
2694
2827
|
slug: string;
|
|
2695
2828
|
image: string | null;
|
|
2696
2829
|
name: string;
|
|
2830
|
+
updatedAt: string;
|
|
2697
2831
|
}
|
|
2698
2832
|
```
|
|
2699
2833
|
|
|
@@ -2749,7 +2883,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
2749
2883
|
}
|
|
2750
2884
|
```
|
|
2751
2885
|
|
|
2752
|
-
> **Note:** If your site has more than
|
|
2886
|
+
> **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
2887
|
|
|
2754
2888
|
## Type System
|
|
2755
2889
|
|