diff --git a/.env.local.example b/.env.local.example index 7de28e6ee..da0cb5d08 100644 --- a/.env.local.example +++ b/.env.local.example @@ -20,6 +20,7 @@ DESIGN_SYSTEM_ACCESS_TOKEN="" NEXTAUTH_REDIRECT_PROXY_URL="http://localhost:3000/api/web/auth" NEXTAUTH_SECRET="" REVALIDATE_SECRET="" +SITEMAP_SYNC_SECRET="" SALESFORCE_PREFERENCE_BASE_URL="https://cloud.emails.scandichotels.com/preference-center" SEAMLESS_LOGIN_DA="http://www.example.dk/updatelogin" diff --git a/app/api/sitemap/route.ts b/app/api/sitemap/route.ts new file mode 100644 index 000000000..75a0bd539 --- /dev/null +++ b/app/api/sitemap/route.ts @@ -0,0 +1,99 @@ +import dayjs from "dayjs" +import { type NextRequest, NextResponse } from "next/server" + +import { env } from "@/env/server" + +import { + getEntries, + getSyncToken, + saveEntries, + saveLastUpdatedDate, + saveSitemapData, + saveSyncToken, +} from "@/utils/sitemap" + +import { contentstackSync } from "./sync" +import { + generateSitemapCounter, + generateSitemapFailCounter, + generateSitemapSuccessCounter, + saveEntriesCounter, + saveSitemapDataCounter, + saveSyncTokenCounter, +} from "./telemetry" +import { mapEntriesToSitemapData, mergeEntries } from "./utils" + +export const dynamic = "force-dynamic" + +export async function GET(request: NextRequest) { + try { + generateSitemapCounter.add(1) + console.info("sitemap.generate start") + const headersList = request.headers + const secret = headersList.get("x-sitemap-sync-secret") + + if (secret !== env.SITEMAP_SYNC_SECRET) { + throw Error( + `Can't sync and generate sitemap, invalid secret, received secret: ${secret}` + ) + } + + const syncToken = await getSyncToken() + const currentEntries = await getEntries() + + const responseData = await contentstackSync(syncToken) + const mergedEntries = mergeEntries(currentEntries, responseData.entries) + + saveEntriesCounter.add(1, { entriesCount: mergedEntries.length }) + console.info( + "sitemap.entries.save", + JSON.stringify({ entriesCount: mergedEntries.length }) + ) + await saveEntries(mergedEntries) + + const sitemapData = mapEntriesToSitemapData(mergedEntries) + const lastUpdated = dayjs.utc().format() + saveSitemapDataCounter.add(1, { + sitemapEntriesCount: sitemapData.length, + }) + console.info( + "sitemap.data.save", + JSON.stringify({ + sitemapEntriesCount: sitemapData.length, + lastUpdated, + }) + ) + await saveSitemapData(sitemapData) + await saveLastUpdatedDate(lastUpdated) + + if (syncToken !== responseData.syncToken) { + saveSyncTokenCounter.add(1, { + syncToken: responseData.syncToken, + }) + console.info( + "sitemap.synctoken.save", + JSON.stringify({ syncToken: responseData.syncToken }) + ) + await saveSyncToken(responseData.syncToken) + } + + generateSitemapSuccessCounter.add(1) + return NextResponse.json({ + message: "Sitemap data generated and stored successfully!", + now: dayjs.utc().format(), + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error) + generateSitemapFailCounter.add(1, { error: errorMessage }) + console.error("sitemap.generate.fail", errorMessage) + + return NextResponse.json( + { + error: "Failed to generate sitemap", + now: dayjs.utc().format(), + }, + { status: 500, statusText: "Internal Server Error" } + ) + } +} diff --git a/app/api/sitemap/sync.ts b/app/api/sitemap/sync.ts new file mode 100644 index 000000000..80ea1ebb2 --- /dev/null +++ b/app/api/sitemap/sync.ts @@ -0,0 +1,112 @@ +import { Region, Stack } from "contentstack" + +import { env } from "@/env/server" + +import { + syncEntriesCounter, + syncEntriesFailCounter, + syncEntriesPaginationCounter, + syncEntriesSuccessCounter, +} from "./telemetry" + +import type { SyncResponse } from "@/types/sitemap" + +const environment = env.CMS_ENVIRONMENT +const stack = Stack({ + api_key: env.CMS_API_KEY, + delivery_token: env.CMS_ACCESS_TOKEN, + branch: env.CMS_BRANCH, + environment, + region: Region.EU, +}) + +export async function contentstackSync(syncToken: string | null) { + const entries = [] + const syncOptions = syncToken ? { sync_token: syncToken } : { init: true } + + syncEntriesCounter.add(1, { + environment, + ...syncOptions, + }) + console.info( + "sitemap.entries.sync start", + JSON.stringify({ + environment, + ...syncOptions, + }) + ) + try { + let syncResponse: SyncResponse = await stack.sync(syncOptions) + + entries.push(...syncResponse.items) + + // Check if there is a pagination token, and fetch more data if needed + while (syncResponse.pagination_token && !syncResponse.sync_token) { + syncEntriesPaginationCounter.add(1, { + environment, + paginationToken: syncResponse.pagination_token, + }) + console.info( + "sitemap.entries.sync.pagination start", + JSON.stringify({ + environment, + paginationToken: syncResponse.pagination_token, + }) + ) + syncResponse = await stack.sync({ + pagination_token: syncResponse.pagination_token, + }) + + entries.push(...syncResponse.items) + + syncEntriesPaginationCounter.add(1, { + environment, + paginationToken: syncResponse.pagination_token, + entriesCount: syncResponse.items.length, + }) + console.info( + "sitemap.entries.sync.pagination success", + JSON.stringify({ + environment, + paginationToken: syncResponse.pagination_token, + entriesCount: syncResponse.items.length, + }) + ) + } + + if (syncResponse.sync_token) { + syncEntriesSuccessCounter.add(1, { + environment, + ...syncOptions, + newSyncToken: syncResponse.sync_token, + entriesCount: entries.length, + }) + console.info( + "sitemap.entries.sync success", + JSON.stringify({ + environment, + ...syncOptions, + newSyncToken: syncResponse.sync_token, + entriesCount: entries.length, + }) + ) + return { + syncToken: syncResponse.sync_token, + entries, + } + } else { + throw new Error("No sync token received, something went wrong") + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error) + syncEntriesFailCounter.add(1, { + environment, + ...syncOptions, + error: errorMessage, + }) + console.error("sitemap.entries.sync error", errorMessage) + + throw new Error("Failed to sync entries") + } +} diff --git a/app/api/sitemap/telemetry.ts b/app/api/sitemap/telemetry.ts new file mode 100644 index 000000000..23f61ee62 --- /dev/null +++ b/app/api/sitemap/telemetry.ts @@ -0,0 +1,43 @@ +import { metrics } from "@opentelemetry/api" + +const meter = metrics.getMeter("sitemap") + +// OpenTelemetry metrics +export const generateSitemapCounter = meter.createCounter("sitemap.generate") +export const generateSitemapSuccessCounter = meter.createCounter( + "sitemap.generate-success" +) +export const generateSitemapFailCounter = meter.createCounter( + "sitemap.generate-fail" +) + +export const syncEntriesCounter = meter.createCounter("sitemap.entries.sync") +export const syncEntriesSuccessCounter = meter.createCounter( + "sitemap.entries.sync-success" +) +export const syncEntriesFailCounter = meter.createCounter( + "sitemap.entries.sync-fail" +) +export const syncEntriesPaginationCounter = meter.createCounter( + "sitemap.entries.sync.pagination" +) +export const syncEntriesPaginationSuccessCounter = meter.createCounter( + "sitemap.entries.sync.pagination-success" +) + +export const mergeEntriesCounter = meter.createCounter("sitemap.entries.merge") +export const mergeEntriesSuccessCounter = meter.createCounter( + "sitemap.entries.merge-success" +) +export const saveEntriesCounter = meter.createCounter("sitemap.entries.save") + +export const transformEntriesCounter = meter.createCounter( + "sitemap.entries.transform" +) +export const transformEntriesSuccessCounter = meter.createCounter( + "sitemap.entries.transform-success" +) +export const saveSitemapDataCounter = meter.createCounter("sitemap.data.save") +export const saveSyncTokenCounter = meter.createCounter( + "sitemap.synctoken.save" +) diff --git a/app/api/sitemap/utils.ts b/app/api/sitemap/utils.ts new file mode 100644 index 000000000..05844ff70 --- /dev/null +++ b/app/api/sitemap/utils.ts @@ -0,0 +1,176 @@ +import { Lang } from "@/constants/languages" +import { env } from "@/env/server" + +import { + mergeEntriesCounter, + mergeEntriesSuccessCounter, + transformEntriesCounter, + transformEntriesSuccessCounter, +} from "./telemetry" + +import type { SitemapEntry, SyncItem } from "@/types/sitemap" + +export function mergeEntries( + currentEntries: SyncItem[], + newEntries: SyncItem[] +) { + mergeEntriesCounter.add(1, { + currentEntriesCount: currentEntries.length, + newEntriesCount: newEntries.length, + }) + console.info( + "sitemap.entries.merge start", + JSON.stringify({ + currentEntriesCount: currentEntries.length, + newEntriesCount: newEntries.length, + }) + ) + const entries = [...currentEntries] + newEntries.forEach((entry) => { + const index = entries.findIndex( + ({ data }) => + data.uid === entry.data.uid && data.locale === entry.data.locale + ) + if (index > -1) { + entries[index] = entry + } else { + entries.push(entry) + } + }) + + mergeEntriesSuccessCounter.add(1, { + entriesCount: entries.length, + }) + console.info( + "sitemap.entries.merge success", + JSON.stringify({ + entriesCount: entries.length, + }) + ) + + return entries +} + +export function mapEntriesToSitemapData(entries: SyncItem[]) { + transformEntriesCounter.add(1, { entriesCount: entries.length }) + console.info( + "sitemap.entries.transform start", + JSON.stringify({ + entriesCount: entries.length, + }) + ) + + const filteredEntries = filterEntriesToSitemapEntries(entries) + + const entriesByUid = groupEntriesByUid(filteredEntries) + const sitemapEntries = Object.entries(entriesByUid) + .map(([_, entries]) => mapEntriesToSitemapEntry(entries)) + .filter((entry): entry is SitemapEntry => !!entry) + + transformEntriesSuccessCounter.add(1, { + entriesCount: entries.length, + sitemapEntriesCount: sitemapEntries.length, + }) + console.info( + "sitemap.entries.transform success", + JSON.stringify({ + entriesCount: entries.length, + sitemapEntriesCount: sitemapEntries.length, + }) + ) + + return sitemapEntries +} + +function filterEntriesToSitemapEntries(entries: SyncItem[]) { + return entries.filter((entry: SyncItem) => { + const shouldIndex = !entry.data.web?.seo_metadata?.noindex + return !!(entry.type === "entry_published" && entry.data.url && shouldIndex) + }) +} + +// We group the entries by UID because Contentstack has the same `uid` for an +// entry regardless of locale. We want to display each locale as an alternate +// in the sitemap, therefore we group them here by `uid`. +function groupEntriesByUid(entries: SyncItem[]) { + return entries.reduce>((acc, entry) => { + const uid = entry.data.uid + if (!acc[uid]) { + acc[uid] = [] + } + acc[uid].push(entry) + return acc + }, {}) +} + +function mapEntriesToSitemapEntry(entries: SyncItem[]) { + // Main entry is always English. Without English, there can't be other pages in ContentStack. + const mainEntry = entries.find((entry) => entry.data.locale === Lang.en) + const alternates = getAlternates(entries) + const lastModified = getLastModified(entries) + const changeFrequency = getChangeFrequency(entries) + const priority = getPriority(entries) + + if (mainEntry) { + const { locale, url } = mainEntry.data + const sitemapEntry: SitemapEntry = { + url: `${env.PUBLIC_URL}/${locale}${url}`, + lastModified, + changeFrequency, + priority, + } + if (alternates) { + sitemapEntry.alternates = alternates + } + return sitemapEntry + } +} + +function getLastModified(entries: SyncItem[]) { + // Localized versions of the data can have a different last modified value. + // We make sure we take the latest. + return entries.reduce((latest, entry) => { + const entryDate = entry.data.updated_at + return entryDate > latest ? entryDate : latest + }, "") +} + +function getChangeFrequency(entries: SyncItem[]) { + // Localized versions of the data can have a different changeFrequency value. + // We make sure we take the highest. + const changeFrequencyPriority: SitemapEntry["changeFrequency"][] = [ + "never", + "yearly", + "monthly", + "weekly", + "daily", + "hourly", + "always", + ] + return entries.reduce((highest, entry) => { + const changeFrequency = + entry.data.web?.seo_metadata?.sitemap?.change_frequency ?? "daily" + return changeFrequencyPriority.indexOf(changeFrequency) > + changeFrequencyPriority.indexOf(highest) + ? changeFrequency + : highest + }, "never") +} + +function getPriority(entries: SyncItem[]) { + // Localized versions of the data can have a different priority. + // We make sure we take the highest. + return entries.reduce((highest, entry) => { + const priority = entry.data.web?.seo_metadata?.sitemap?.priority ?? 0.5 + return priority > highest ? priority : highest + }, 0.0) +} + +function getAlternates(entries: SyncItem[]) { + return entries + .filter((entry) => entry.data.locale !== Lang.en) + .reduce>>((acc, { data }) => { + acc[data.locale] = `${env.PUBLIC_URL}/${data.locale}${data.url}` + return acc + }, {}) +} diff --git a/env/server.ts b/env/server.ts index 4a5f900e9..83bd2fef7 100644 --- a/env/server.ts +++ b/env/server.ts @@ -57,6 +57,7 @@ export const env = createEnv({ .default("false"), PUBLIC_URL: z.string().default(""), REVALIDATE_SECRET: z.string(), + SITEMAP_SYNC_SECRET: z.string(), SALESFORCE_PREFERENCE_BASE_URL: z.string(), SEAMLESS_LOGIN_DA: z .string() @@ -220,6 +221,7 @@ export const env = createEnv({ PRINT_QUERY: process.env.PRINT_QUERY, PUBLIC_URL: process.env.PUBLIC_URL, REVALIDATE_SECRET: process.env.REVALIDATE_SECRET, + SITEMAP_SYNC_SECRET: process.env.SITEMAP_SYNC_SECRET, SALESFORCE_PREFERENCE_BASE_URL: process.env.SALESFORCE_PREFERENCE_BASE_URL, SEAMLESS_LOGIN_DA: process.env.SEAMLESS_LOGIN || process.env.SEAMLESS_LOGIN_DA, diff --git a/netlify.toml b/netlify.toml index 17324fbf2..b52d131a2 100644 --- a/netlify.toml +++ b/netlify.toml @@ -33,3 +33,6 @@ remote_images = [ "https://imagevault-stage.scandichotels.com.*", "https://imagevault.scandichotels.com.*", ] + +[functions."sitemap"] +schedule = "@daily" diff --git a/netlify/functions/sitemap/index.mts b/netlify/functions/sitemap/index.mts new file mode 100644 index 000000000..41cc4db4c --- /dev/null +++ b/netlify/functions/sitemap/index.mts @@ -0,0 +1,17 @@ +/* eslint-disable import/no-anonymous-default-export */ +import type { Context } from "@netlify/functions" + +export default async (request: Request, context: Context) => { + const { next_run } = await request.json() + const SITEMAP_SYNC_SECRET = Netlify.env.get("SITEMAP_SYNC_SECRET") + const PUBLIC_URL = Netlify.env.get("PUBLIC_URL") + console.info( + `Started sitemap sync at: ${new Date().toISOString()}! Next invocation at: ${next_run}` + ) + const headers = new Headers() + headers.set("x-sitemap-sync-secret", SITEMAP_SYNC_SECRET!) + + fetch(`${PUBLIC_URL}/api/sitemap`, { + headers, + }) +} diff --git a/package-lock.json b/package-lock.json index ec02f4c4a..1f4c22a9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@contentstack/live-preview-utils": "^3.0.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.3.4", + "@netlify/blobs": "^8.1.0", + "@netlify/functions": "^3.0.0", "@netlify/plugin-nextjs": "^5.9.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/sdk-metrics": "^1.25.1", @@ -36,6 +38,7 @@ "@vis.gl/react-google-maps": "^1.2.0", "class-variance-authority": "^0.7.0", "clean-deep": "^3.4.0", + "contentstack": "^3.23.0", "dayjs": "^1.11.10", "deepmerge": "^4.3.1", "downshift": "^9.0.8", @@ -138,15 +141,15 @@ } }, "node_modules/@auth/core": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.32.0.tgz", - "integrity": "sha512-3+ssTScBd+1fd0/fscAyQN1tSygXzuhysuVVzB942ggU4mdfiTbv36P0ccVnExKWYJKvu3E2r3/zxXCCAmTOrg==", + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", + "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", "dependencies": { - "@panva/hkdf": "^1.1.1", + "@panva/hkdf": "^1.2.1", "@types/cookie": "0.6.0", - "cookie": "0.6.0", - "jose": "^5.1.3", - "oauth4webapi": "^2.9.0", + "cookie": "0.7.1", + "jose": "^5.9.3", + "oauth4webapi": "^3.0.0", "preact": "10.11.3", "preact-render-to-string": "5.2.3" }, @@ -2518,6 +2521,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@contentstack/utils": { + "version": "1.3.17", + "resolved": "https://registry.npmjs.org/@contentstack/utils/-/utils-1.3.17.tgz", + "integrity": "sha512-RwuhAUa28wTuHoDkzMFFcookangKr3gJVzyk9nd48RO+8WHSboFGVy4NCI8Zx2BiqbhjcYGtPyMZrLiIuNtyVQ==" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3430,10 +3438,50 @@ "node": ">= 8.0.0" } }, + "node_modules/@netlify/blobs": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-8.1.1.tgz", + "integrity": "sha512-7Dg3PzArvQ0Owq4wpkLECC9iaDBOxuWUN2uwbQtUF6tZssyez2QN+eO0CjuIhhZUivbw+X9bwsyiEjWkdJnv/A==", + "engines": { + "node": "^14.16.0 || >=16.0.0" + } + }, + "node_modules/@netlify/functions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-3.0.0.tgz", + "integrity": "sha512-XXf9mNw4+fkxUzukDpJtzc32bl1+YlXZwEhc5ZgMcTbJPLpgRLDs5WWSPJ4eY/Mv1ZFvtxmMwmfgoQYVt68Qog==", + "dependencies": { + "@netlify/serverless-functions-api": "1.30.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@netlify/node-cookies": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@netlify/node-cookies/-/node-cookies-0.1.0.tgz", + "integrity": "sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g==", + "engines": { + "node": "^14.16.0 || >=16.0.0" + } + }, "node_modules/@netlify/plugin-nextjs": { "version": "5.9.4", "resolved": "https://registry.npmjs.org/@netlify/plugin-nextjs/-/plugin-nextjs-5.9.4.tgz", "integrity": "sha512-Q9qyhGUxFuM3kuWmoaj4yHPUfHhOsZmwdsQv4kIXkBWVu8FOe0RFHRP0A0EXciSVFGwq+XAo3H5jEtVSANAOWw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@netlify/serverless-functions-api": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.30.1.tgz", + "integrity": "sha512-JkbaWFeydQdeDHz1mAy4rw+E3bl9YtbCgkntfTxq+IlNX/aIMv2/b1kZnQZcil4/sPoZGL831Dq6E374qRpU1A==", + "dependencies": { + "@netlify/node-cookies": "^0.1.0", + "urlpattern-polyfill": "8.0.2" + }, "engines": { "node": ">=18.0.0" } @@ -8501,6 +8549,11 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" }, + "node_modules/@types/glob-to-regexp": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/glob-to-regexp/-/glob-to-regexp-0.4.4.tgz", + "integrity": "sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg==" + }, "node_modules/@types/google.maps": { "version": "3.58.0", "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.0.tgz", @@ -10438,6 +10491,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -11074,6 +11154,35 @@ "node": ">= 0.6" } }, + "node_modules/contentstack": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/contentstack/-/contentstack-3.24.1.tgz", + "integrity": "sha512-HG8YkyG2VxN70d30ygfd38wXHA94qnGsXnIc3K9F8dAeUTx5jIIqPXahvg4B0yotr0OyoOaxuR0zUhyO1hku9Q==", + "dependencies": { + "@contentstack/utils": "^1.3.15", + "es6-promise": "^4.2.8", + "fetch-mock": "^12.2.0", + "localStorage": "1.0.4", + "qs": "^6.14.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/contentstack/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -11081,9 +11190,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -11690,7 +11799,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -11935,6 +12043,19 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "license": "MIT" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -12172,13 +12293,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -12187,7 +12304,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -12227,7 +12343,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -12275,6 +12390,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -13109,6 +13229,21 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-mock": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.3.0.tgz", + "integrity": "sha512-+ZHzLuzrKpP3u5PZo8ghFP1Kr3UJUTZ5PT/uQZtLv7UagDCVRt1bSzVg6MoTFdjQ0GXsx/crq2t0tGabkbH2yA==", + "dependencies": { + "@types/glob-to-regexp": "^0.4.4", + "dequal": "^2.0.3", + "glob-to-regexp": "^0.4.1", + "is-subset-of": "^3.1.10", + "regexparam": "^3.0.0" + }, + "engines": { + "node": ">=18.11.0" + } + }, "node_modules/fetch-retry": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", @@ -13441,16 +13576,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -13476,6 +13615,18 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stdin": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", @@ -13596,8 +13747,7 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "peer": true + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", @@ -13721,12 +13871,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13860,10 +14009,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -14710,6 +14858,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-subset-of": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/is-subset-of/-/is-subset-of-3.1.10.tgz", + "integrity": "sha512-avvaYgVmYWyaZ1NDFiv4y9JGkrE2je3op1Po4VYKKJKR8H2qVPsg1GZuuXl5elCTxTlwAIsrAjWAs4BVrISFRw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "typedescriptor": "3.0.2" + } + }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -17239,6 +17396,14 @@ "node": ">=10" } }, + "node_modules/localStorage": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/localStorage/-/localStorage-1.0.4.tgz", + "integrity": "sha512-r35zrihcDiX+dqWlJSeIwS9nrF95OQTgqMFm3FB2D/+XgdmZtcutZOb7t0xXkhOEM8a9kpuu7cc28g1g36I5DQ==", + "engines": { + "node": ">= v0.2.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -17707,6 +17872,14 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -18111,12 +18284,12 @@ "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.19.tgz", "integrity": "sha512-YHu1igcAxZPh8ZB7GIM93dqgY6gcAzq66FOhQFheAdOx1raxNcApt05nNyNCSB6NegSiyJ4XOPsaNow4pfDmsg==", "dependencies": { - "@auth/core": "0.32.0" + "@auth/core": "0.37.2" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", - "next": "^14 || ^15.0.0-0", + "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, @@ -18246,9 +18419,9 @@ "dev": true }, "node_modules/oauth4webapi": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz", - "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.1.4.tgz", + "integrity": "sha512-eVfN3nZNbok2s/ROifO0UAc5G8nRoLSbrcKJ09OqmucgnhXEfdIQOR4gq1eJH1rN3gV7rNw62bDEgftsgFtBEg==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -18262,10 +18435,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -19960,6 +20135,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/regexpu-core": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", @@ -20601,15 +20784,65 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -21939,6 +22172,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedescriptor": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/typedescriptor/-/typedescriptor-3.0.2.tgz", + "integrity": "sha512-hyVbaCUd18UiXk656g/imaBLMogpdijIEpnhWYrSda9rhvO4gOU16n2nh7xG5lv/rjumnZzGOdz0CEGTmFe0fQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info." + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -22295,6 +22534,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", + "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==" + }, "node_modules/use-callback-ref": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", diff --git a/package.json b/package.json index adf67e4b6..6b31493f4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "@contentstack/live-preview-utils": "^3.0.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.3.4", + "@netlify/blobs": "^8.1.0", + "@netlify/functions": "^3.0.0", "@netlify/plugin-nextjs": "^5.9.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/sdk-metrics": "^1.25.1", @@ -51,6 +53,7 @@ "@vis.gl/react-google-maps": "^1.2.0", "class-variance-authority": "^0.7.0", "clean-deep": "^3.4.0", + "contentstack": "^3.23.0", "dayjs": "^1.11.10", "deepmerge": "^4.3.1", "downshift": "^9.0.8", diff --git a/tsconfig.json b/tsconfig.json index 3c96a445f..14bd213af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,12 @@ "@/*": ["./*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "netlify/functions/**/*.mts" + ], "exclude": ["node_modules"] } diff --git a/types/sitemap.ts b/types/sitemap.ts new file mode 100644 index 000000000..94068135f --- /dev/null +++ b/types/sitemap.ts @@ -0,0 +1,50 @@ +import type { SyncResult } from "contentstack" + +import type { Lang } from "@/constants/languages" + +export type ChangeFrequency = + | "always" + | "hourly" + | "daily" + | "weekly" + | "monthly" + | "yearly" + | "never" + +export interface SitemapEntry { + url: string + lastModified: string + changeFrequency: ChangeFrequency + priority: number + alternates?: Partial> +} + +export type SitemapData = SitemapEntry[] + +export interface SyncItemData { + uid: string + locale: Lang + url?: string + updated_at: string + web?: { + seo_metadata?: { + noindex?: boolean | null + sitemap?: { + change_frequency: SitemapEntry["changeFrequency"] + priority: SitemapEntry["priority"] + } | null + } + } +} +export interface SyncItem { + type: string + data: SyncItemData +} +export interface SyncResponse extends Omit { + items: SyncItem[] +} + +export type SyncItemsByUid = { + mainEntry: SyncItem + alternates: SyncItem[] +} diff --git a/utils/sitemap.ts b/utils/sitemap.ts new file mode 100644 index 000000000..79f011af1 --- /dev/null +++ b/utils/sitemap.ts @@ -0,0 +1,75 @@ +import { getStore } from "@netlify/blobs" + +import { env } from "@/env/server" + +import type { SitemapData, SyncItem } from "@/types/sitemap" + +const branch = env.CMS_BRANCH +const environment = env.CMS_ENVIRONMENT +const entriesKey = `${environment}/${branch}/entries` +const syncTokenKey = `${environment}/${branch}/syncToken` +const sitemapDataKey = `${environment}/${branch}/sitemapData` +const lastUpdatedKey = `${environment}/${branch}/lastUpdated` +const MAX_ENTRIES_PER_SITEMAP = 50000 + +// We need to wrap `getStore` because calling it in the root of the file causes +// it to be executed during build time. This is not supported by Netlify. +function store() { + return getStore("sitemap") +} + +export async function saveEntries(entries: SyncItem[]) { + await store().setJSON(entriesKey, entries) +} + +export async function saveSitemapData(sitemapData: SitemapData) { + await store().setJSON(sitemapDataKey, sitemapData) +} + +export async function saveLastUpdatedDate(lastUpdated: string) { + await store().set(lastUpdatedKey, lastUpdated) +} + +export async function saveSyncToken(syncToken: string) { + await store().set(syncTokenKey, syncToken) +} + +export async function getSyncToken() { + return await store().get(syncTokenKey) +} + +export async function getEntries() { + const entries: SyncItem[] = await store().get(entriesKey, { + type: "json", + }) + + return entries || [] +} + +export async function getLastUpdated() { + return await store().get(lastUpdatedKey) +} + +export async function getSitemapData() { + const sitemapData: SitemapData | null = await store().get(sitemapDataKey, { + type: "json", + }) + + return sitemapData || [] +} + +export async function getSitemapDataById(id: number) { + const sitemapData = await getSitemapData() + const index = id - 1 + const start = index * MAX_ENTRIES_PER_SITEMAP + const end = start + MAX_ENTRIES_PER_SITEMAP + return sitemapData.slice(start, end) +} + +export async function getSitemapIds() { + const sitemapData = await getSitemapData() + const numberOfSitemaps = Math.ceil( + sitemapData.length / MAX_ENTRIES_PER_SITEMAP + ) + return Array.from({ length: numberOfSitemaps }, (_, index) => index + 1) +}