diff --git a/cdn/cms-bulk-redirects/.env.example b/cdn/cms-bulk-redirects/.env.example new file mode 100644 index 0000000000..b63a47363d --- /dev/null +++ b/cdn/cms-bulk-redirects/.env.example @@ -0,0 +1,2 @@ +CONTENTFUL_SPACE_ID=your_space_id +CONTENTFUL_ACCESS_TOKEN=your_cda_token diff --git a/cdn/cms-bulk-redirects/.eslintrc.json b/cdn/cms-bulk-redirects/.eslintrc.json new file mode 100644 index 0000000000..a2569c2c7c --- /dev/null +++ b/cdn/cms-bulk-redirects/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": "next/core-web-vitals" +} diff --git a/cdn/cms-bulk-redirects/.gitignore b/cdn/cms-bulk-redirects/.gitignore new file mode 100644 index 0000000000..2214749b41 --- /dev/null +++ b/cdn/cms-bulk-redirects/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +/node_modules +/.pnp +.pnp.js + +# Testing +/coverage + +# Next.js +/.next/ +/out/ +next-env.d.ts + +# Production +build +dist + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local ENV files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# Turborepo +.turbo + +# typescript +*.tsbuildinfo +.env*.local diff --git a/cdn/cms-bulk-redirects/README.md b/cdn/cms-bulk-redirects/README.md new file mode 100644 index 0000000000..084f5b3c9f --- /dev/null +++ b/cdn/cms-bulk-redirects/README.md @@ -0,0 +1,57 @@ +--- +name: Contentful CMS bulk redirects (vercel.ts) +slug: cms-bulk-redirects +description: Sync redirect entries from Contentful into Vercel bulk redirects using vercel.ts. +framework: Next.js +useCase: Redirects +css: Tailwind +deployUrl: https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/cdn/cms-bulk-redirects&project-name=cms-bulk-redirects&repository-name=cms-bulk-redirects&env=CONTENTFUL_SPACE_ID,CONTENTFUL_ACCESS_TOKEN +demoUrl: https://cms-bulk-redirects.vercel.app +--- + +# Contentful CMS bulk redirects (vercel.ts) example + +This example shows how to pull redirect entries from Contentful at build time, write them to a bulk redirects file, and publish them with the new `vercel.ts` config. The demo uses an e-commerce catalog so marketing can rotate seasonal URLs without shipping code. + +## Demo + +https://cms-bulk-redirects.vercel.app + +## How to Use + +You can choose from one of the following two methods to use this repository: + +### One-Click Deploy + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/cdn/cms-bulk-redirects&project-name=cms-bulk-redirects&repository-name=cms-bulk-redirects&env=CONTENTFUL_SPACE_ID,CONTENTFUL_ACCESS_TOKEN) + +### Clone and Deploy + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +pnpm create next-app --example https://github.com/vercel/examples/tree/main/cdn/cms-bulk-redirects +``` + +Next, run Next.js in development mode: + +```bash +pnpm dev +``` + +## Environment variables + +- `CONTENTFUL_SPACE_ID` – Contentful space ID +- `CONTENTFUL_ACCESS_TOKEN` – Content Delivery API token (CDA) + +The example ships a small `generated-redirects.json` for local runs. When the environment variables are present, `vercel.ts` fetches real entries from Contentful and rewrites the bulk redirects file before build. + +## How it works + +1. `vercel.ts` runs at build time. It pulls `redirect` entries from Contentful, transforms them into Vercel bulk redirect objects, and writes `generated-redirects.json`. +2. The `config` exported from `vercel.ts` sets `bulkRedirectsPath` to that file. Vercel publishes the redirects without touching Next.js routing, middleware, or edge functions. +3. The UI shows an e-commerce catalog with collections that map to redirect targets like `/catalog/fall-2025` or `/catalog/limited-edition`. Legacy vanity paths such as `/catalog/fall` or `/products/daybreak-pack` are captured by bulk redirects. + +You can extend this pattern to any CMS: swap the fetch logic, keep the same `bulkRedirectsPath`. diff --git a/cdn/cms-bulk-redirects/app/about/page.tsx b/cdn/cms-bulk-redirects/app/about/page.tsx new file mode 100644 index 0000000000..95eb970a91 --- /dev/null +++ b/cdn/cms-bulk-redirects/app/about/page.tsx @@ -0,0 +1,191 @@ +import Link from 'next/link' + +export default function About() { + return ( +
+ {/* Header */} +
+
+ + ← Back to store + +

+ How it works +

+

+ This demo uses Vercel's bulk redirects feature to manage seasonal URLs without code changes. +

+
+
+ + {/* Content */} +
+
+ {/* The Problem */} +
+

+ The problem +

+

+ E-commerce sites often need vanity URLs that stay consistent while the content behind them changes: +

+
    +
  • + + /catalog/fall should always show the current fall collection +
  • +
  • + + /catalog/latest should point to the newest drop +
  • +
  • + + Retired product SKUs should redirect to relevant collections +
  • +
+
+ + {/* The Solution */} +
+

+ The solution +

+

+ Vercel's bulk redirects let you manage thousands of redirects at the edge—no middleware, no server-side logic. +

+
+
+
1. Define redirects in a JSON file
+

+ Or fetch them from a CMS like Contentful, Sanity, or any API +

+
+
+
2. Use vercel.ts to generate at build time
+

+ The config file runs during build and outputs the redirect rules +

+
+
+
3. Redirects execute at the edge
+

+ Fast, globally distributed, no app code involved +

+
+
+
+ + {/* Code Example */} +
+

+ Example code +

+
+{`// vercel.ts
+import type { VercelConfig } from '@vercel/config/v1'
+import { writeFileSync } from 'fs'
+
+const redirects = [
+  {
+    source: '/catalog/fall',
+    destination: '/catalog/fall-2025',
+    statusCode: 302
+  },
+  {
+    source: '/catalog/latest',
+    destination: '/catalog/spring-2026',
+    permanent: true
+  }
+]
+
+writeFileSync(
+  'generated-redirects.json',
+  JSON.stringify(redirects, null, 2)
+)
+
+export const config: VercelConfig = {
+  bulkRedirectsPath: './generated-redirects.json',
+}`}
+            
+
+ + {/* Try it */} +
+

+ Try it +

+

+ Click these links to see the redirects in action: +

+
+ + /catalog/fall +

→ fall-2025

+ + + /catalog/winter +

→ winter-2025

+ + + /catalog/latest +

→ spring-2026

+ + + /catalog/outlet +

→ archive

+ +
+
+ + {/* Resources */} +
+

+ Resources +

+
+ +
+
Vercel Redirects Docs
+
Official documentation
+
+ +
+ +
+
Source Code
+
View on GitHub
+
+ +
+
+
+
+
+
+ ) +} diff --git a/cdn/cms-bulk-redirects/app/api/redirects/route.ts b/cdn/cms-bulk-redirects/app/api/redirects/route.ts new file mode 100644 index 0000000000..c07d94f298 --- /dev/null +++ b/cdn/cms-bulk-redirects/app/api/redirects/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server' +import { readFileSync } from 'fs' +import { join } from 'path' + +export async function GET() { + try { + const filePath = join(process.cwd(), 'generated-redirects.json') + const contents = readFileSync(filePath, 'utf-8') + return new NextResponse(contents, { + status: 200, + headers: { 'content-type': 'application/json' }, + }) + } catch (error) { + return NextResponse.json({ error: 'Redirect file not found' }, { status: 500 }) + } +} diff --git a/cdn/cms-bulk-redirects/app/catalog/[collection]/page.tsx b/cdn/cms-bulk-redirects/app/catalog/[collection]/page.tsx new file mode 100644 index 0000000000..cc2976521e --- /dev/null +++ b/cdn/cms-bulk-redirects/app/catalog/[collection]/page.tsx @@ -0,0 +1,96 @@ +import { notFound } from 'next/navigation' +import Link from 'next/link' +import { getCollection, getCollectionParams } from '../../../lib/collections' + +export async function generateStaticParams() { + return getCollectionParams() +} + +export default async function CollectionPage({ params }: { params: Promise<{ collection: string }> }) { + const { collection: collectionSlug } = await params + const collection = getCollection(collectionSlug) + + if (!collection) return notFound() + + return ( +
+ {/* Breadcrumb */} +
+
+
+ + Home + + {' '}/{' '} + {collection.title} +
+
+
+ + {/* Hero */} +
+
+
+
+

+ {collection.title} +

+

+ {collection.description} +

+
+ + {collection.status} + +
+
+
+ + {/* Products */} +
+
+

+ Products +

+
+ {collection.products.map((product) => ( +
+
+

+ {product.name} +

+ {product.badge && ( + + {product.badge} + + )} +
+

+ {product.tagline} +

+
+ {product.price} +
+
+ ))} +
+
+
+ + {/* Back */} +
+
+ + ← Back to all collections + +
+
+
+ ) +} diff --git a/cdn/cms-bulk-redirects/app/globals.css b/cdn/cms-bulk-redirects/app/globals.css new file mode 100644 index 0000000000..a2dc41ecee --- /dev/null +++ b/cdn/cms-bulk-redirects/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/cdn/cms-bulk-redirects/app/layout.tsx b/cdn/cms-bulk-redirects/app/layout.tsx new file mode 100644 index 0000000000..ca1f79a908 --- /dev/null +++ b/cdn/cms-bulk-redirects/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import type { ReactNode } from 'react' +import { GeistSans } from 'geist/font/sans' +import { GeistMono } from 'geist/font/mono' +import Navbar from '../components/Navbar' +import './globals.css' + +export const metadata: Metadata = { + title: 'Essential Carry - Bulk Redirects Demo', + description: 'A catalog site showcasing Vercel bulk redirects with vercel.ts', +} + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + + {children} + + + ) +} diff --git a/cdn/cms-bulk-redirects/app/not-found.tsx b/cdn/cms-bulk-redirects/app/not-found.tsx new file mode 100644 index 0000000000..d6be63018b --- /dev/null +++ b/cdn/cms-bulk-redirects/app/not-found.tsx @@ -0,0 +1,30 @@ +import Link from 'next/link' + +export default function NotFound() { + return ( +
+
+

+ 404 +

+

+ This page doesn't exist or has been moved. +

+
+ + Go home + + + Browse collections + +
+
+
+ ) +} diff --git a/cdn/cms-bulk-redirects/app/page.tsx b/cdn/cms-bulk-redirects/app/page.tsx new file mode 100644 index 0000000000..3ac58102e8 --- /dev/null +++ b/cdn/cms-bulk-redirects/app/page.tsx @@ -0,0 +1,152 @@ +import Link from 'next/link' + +export default function Home() { + return ( +
+ {/* Hero Section */} +
+
+

+ Essential Carry +

+

+ Thoughtfully designed everyday essentials for work, travel, and life. + Minimalist aesthetics meet maximum functionality. +

+
+ + Shop Fall Collection + + + View Latest + +
+
+
+ + {/* Collections Grid */} +
+
+
+

+ Shop by Season +

+

+ Discover our curated collections, each designed for specific moments and seasons. +

+
+ +
+ {/* Fall */} + +
+
+ 🍂 +

Fall

+

+ Cozy layers and warm essentials for crisp autumn days +

+
+ + Shop Collection → + +
+ + + {/* Winter */} + +
+
+ ❄️ +

Winter

+

+ Premium insulation and weather protection +

+
+ + Shop Collection → + +
+ + + {/* Spring */} + +
+
+ 🌱 +

Spring 2026

+

+ Fresh starts with lightweight, versatile pieces +

+
+ + Preview Collection → + +
+ + + {/* Limited Edition */} + +
+
+ +

Limited Edition

+

+ Exclusive collaborations and special releases +

+
+ + Explore → + +
+ + + {/* Archive */} + +
+
+ 📦 +

Outlet

+

+ Past seasons at special prices +

+
+ + Browse Deals → + +
+ +
+
+
+ + {/* Newsletter */} +
+
+

+ Stay in the loop +

+

+ Be the first to know about new collections and exclusive offers. +

+
+ + +
+
+
+
+ ) +} diff --git a/cdn/cms-bulk-redirects/components/Navbar.tsx b/cdn/cms-bulk-redirects/components/Navbar.tsx new file mode 100644 index 0000000000..411574870c --- /dev/null +++ b/cdn/cms-bulk-redirects/components/Navbar.tsx @@ -0,0 +1,41 @@ +import Link from 'next/link' + +export default function Navbar() { + return ( + + ) +} diff --git a/cdn/cms-bulk-redirects/generated-redirects.json b/cdn/cms-bulk-redirects/generated-redirects.json new file mode 100644 index 0000000000..4a1280dffe --- /dev/null +++ b/cdn/cms-bulk-redirects/generated-redirects.json @@ -0,0 +1,34 @@ +[ + { + "source": "/products/aurora-duffel", + "destination": "/catalog/limited-edition", + "statusCode": 302, + "query": true + }, + { + "source": "/catalog/outlet", + "destination": "/catalog/archive", + "statusCode": 308, + "caseSensitive": false + }, + { + "source": "/catalog/latest", + "destination": "/catalog/spring-2026", + "permanent": true + }, + { + "source": "/products/daybreak-pack", + "destination": "/catalog/limited-edition", + "statusCode": 302, + "query": true + }, + { + "source": "/catalog/winter", + "destination": "/catalog/winter-2025", + "permanent": true + }, + { + "source": "/catalog/fall", + "destination": "/catalog/fall-2025" + } +] \ No newline at end of file diff --git a/cdn/cms-bulk-redirects/lib/collections.ts b/cdn/cms-bulk-redirects/lib/collections.ts new file mode 100644 index 0000000000..02725917a3 --- /dev/null +++ b/cdn/cms-bulk-redirects/lib/collections.ts @@ -0,0 +1,123 @@ +export type Product = { + name: string + price: string + tagline: string + badge?: string +} + +export type Collection = { + slug: string + title: string + description: string + status: 'Live' | 'Pre-launch' | 'Limited' | 'Archived' + accent: string + incomingPaths: string[] + highlights: string[] + products: Product[] +} + +export const collections: Collection[] = [ + { + slug: 'fall-2025', + title: 'Fall 2025', + description: + 'Merino layers, structured carry, and weather-treated outerwear for a Northern-hemisphere launch.', + status: 'Live', + accent: 'from-amber-50 via-orange-50 to-amber-100', + incomingPaths: ['/catalog/fall', '/catalog/fall-2024'], + highlights: [ + 'Redirects keep the retired /catalog/fall path pointed at the new season', + 'Merchandising tweaks publish in Contentful without shipping code', + 'Great for evergreen articles that always point to “fall”', + ], + products: [ + { name: 'Transit Shell', price: '$198', tagline: 'Recycled, rain ready shell with heat mapping', badge: 'Featured' }, + { name: 'Layered Merino', price: '$128', tagline: 'Temperature-regulating knit built for travel days' }, + { name: 'Compression Pack', price: '$88', tagline: 'Carry-on sized with modular pouches' }, + ], + }, + { + slug: 'winter-2025', + title: 'Winter 2025', + description: + 'Thermal capsules and insulated accessories for cold-weather drops. Redirected from last year’s “winter” vanity URL.', + status: 'Live', + accent: 'from-slate-50 via-blue-50 to-indigo-100', + incomingPaths: ['/catalog/winter', '/catalog/winter-2024'], + highlights: [ + 'Marketing keeps /catalog/winter alive while inventory rotates', + 'Preserves inbound traffic from ads without re-cutting creatives', + 'Simple CSV export from Contentful drives the bulk redirect file', + ], + products: [ + { name: 'Glacier Parka', price: '$248', tagline: 'Storm-ready parka with recycled insulation', badge: 'New' }, + { name: 'Thermal Bottle', price: '$38', tagline: 'All-day heat retention with a low-profile lid' }, + { name: 'Cable Knit Beanie', price: '$34', tagline: 'Soft handfeel with traceable wool' }, + ], + }, + { + slug: 'spring-2026', + title: 'Spring 2026', + description: + 'Lightweight carry and breathable layers for the next launch wave. Great target for “/catalog/latest” redirects.', + status: 'Pre-launch', + accent: 'from-emerald-50 via-teal-50 to-emerald-100', + incomingPaths: ['/catalog/latest', '/campaign/spring-preview'], + highlights: [ + 'Preview route collects RSVPs even before the PDPs are published', + 'Alias /catalog/latest always points to the newest collection', + 'Pairs well with geotargeted email deep links', + ], + products: [ + { name: 'AirLight Shell', price: '$168', tagline: 'Packable windbreaker that folds into its own pocket' }, + { name: 'Commuter Tote', price: '$118', tagline: 'Laptop-friendly tote with wet pocket' }, + { name: 'Everyday Sandal', price: '$78', tagline: 'Minimal silhouette with soft webbing' }, + ], + }, + { + slug: 'limited-edition', + title: 'Limited Edition', + description: + 'Short-run collabs and “small batch” drops. Redirect product aliases to this landing page when a SKU sunsets.', + status: 'Limited', + accent: 'from-fuchsia-50 via-rose-50 to-purple-100', + incomingPaths: ['/products/daybreak-pack', '/products/aurora-duffel'], + highlights: [ + 'Great destination for influencer vanity URLs', + 'Surface “back in stock” signups before the PDP reactivates', + 'Redirect individual SKUs instead of the whole catalog', + ], + products: [ + { name: 'Daybreak Pack', price: '$168', tagline: 'Structured EDC pack with magnetic hardware', badge: 'Backorder' }, + { name: 'Aurora Duffel', price: '$198', tagline: 'Weekender with compression straps' }, + { name: 'Studio Sling', price: '$98', tagline: 'Hands-free sling with hidden laptop sleeve' }, + ], + }, + { + slug: 'archive', + title: 'Archive & Outlet', + description: + 'Keep revenue flowing from evergreen blog links by routing retired SKUs to the archive instead of 404s.', + status: 'Archived', + accent: 'from-amber-50 via-slate-50 to-slate-100', + incomingPaths: ['/catalog/outlet', '/catalog/archive'], + highlights: [ + 'Preserve SEO equity for long-lived merch articles', + 'Contentful entry controls whether we use 308 or 301 per path', + 'Pair with analytics to see how often legacy URLs are hit', + ], + products: [ + { name: 'Sample Sale Kits', price: '$58', tagline: 'Assorted pulls from last season' }, + { name: 'Seconds Bin', price: '$24', tagline: 'Minor cosmetic blemishes, full warranty' }, + { name: 'Gift with Purchase', price: '$0', tagline: 'Add-on items surfaced via redirect' }, + ], + }, +] + +export function getCollection(slug: string): Collection | undefined { + return collections.find((collection) => collection.slug === slug) +} + +export function getCollectionParams() { + return collections.map((collection) => ({ collection: collection.slug })) +} diff --git a/cdn/cms-bulk-redirects/package.json b/cdn/cms-bulk-redirects/package.json new file mode 100644 index 0000000000..9033e1bb73 --- /dev/null +++ b/cdn/cms-bulk-redirects/package.json @@ -0,0 +1,30 @@ +{ + "name": "cms-bulk-redirects", + "version": "1.0.0", + "private": true, + "repository": "https://github.com/vercel/examples.git", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@vercel/config": "^0.0.22", + "geist": "^1.5.1", + "next": "^16.0.7", + "react": "^19.2.1", + "react-dom": "^19.2.1" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + }, + "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" +} diff --git a/cdn/cms-bulk-redirects/pnpm-lock.yaml b/cdn/cms-bulk-redirects/pnpm-lock.yaml new file mode 100644 index 0000000000..af48c79670 --- /dev/null +++ b/cdn/cms-bulk-redirects/pnpm-lock.yaml @@ -0,0 +1,992 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@vercel/config': + specifier: ^0.0.22 + version: 0.0.22 + geist: + specifier: ^1.5.1 + version: 1.5.1(next@16.0.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) + next: + specifier: ^16.0.7 + version: 16.0.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: + specifier: ^19.2.1 + version: 19.2.1 + react-dom: + specifier: ^19.2.1 + version: 19.2.1(react@19.2.1) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.17 + '@types/node': + specifier: ^20 + version: 20.19.26 + '@types/react': + specifier: ^19 + version: 19.2.7 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.7) + tailwindcss: + specifier: ^4 + version: 4.1.17 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@next/env@16.0.8': + resolution: {integrity: sha512-xP4WrQZuj9MdmLJy3eWFHepo+R3vznsMSS8Dy3wdA7FKpjCiesQ6DxZvdGziQisj0tEtCgBKJzjcAc4yZOgLEQ==} + + '@next/swc-darwin-arm64@16.0.8': + resolution: {integrity: sha512-yjVMvTQN21ZHOclQnhSFbjBTEizle+1uo4NV6L4rtS9WO3nfjaeJYw+H91G+nEf3Ef43TaEZvY5mPWfB/De7tA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.0.8': + resolution: {integrity: sha512-+zu2N3QQ0ZOb6RyqQKfcu/pn0UPGmg+mUDqpAAEviAcEVEYgDckemOpiMRsBP3IsEKpcoKuNzekDcPczEeEIzA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.0.8': + resolution: {integrity: sha512-LConttk+BeD0e6RG0jGEP9GfvdaBVMYsLJ5aDDweKiJVVCu6sGvo+Ohz9nQhvj7EQDVVRJMCGhl19DmJwGr6bQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.0.8': + resolution: {integrity: sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.0.8': + resolution: {integrity: sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.0.8': + resolution: {integrity: sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.0.8': + resolution: {integrity: sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.0.8': + resolution: {integrity: sha512-jWpWjWcMQu2iZz4pEK2IktcfR+OA9+cCG8zenyLpcW8rN4rzjfOzH4yj/b1FiEAZHKS+5Vq8+bZyHi+2yqHbFA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.17': + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} + + '@tailwindcss/oxide-android-arm64@4.1.17': + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.17': + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.17': + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.17': + resolution: {integrity: sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==} + + '@types/node@20.19.26': + resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + + '@vercel/config@0.0.22': + resolution: {integrity: sha512-bh2x7Ex1mm97LG+GAna9wpKcgo3+p19GOrW0ZgNgC6k0ys4Y4+M/8gxhhszT3wo/KtV0hznqJW01lQrVFu1Rlw==} + hasBin: true + + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + geist@1.5.1: + resolution: {integrity: sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==} + peerDependencies: + next: '>=13.2.0' + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@16.0.8: + resolution: {integrity: sha512-LmcZzG04JuzNXi48s5P+TnJBsTGPJunViNKV/iE4uM6kstjTQsQhvsAv+xF6MJxU2Pr26tl15eVbp0jQnsv6/g==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-cache-header@1.0.0: + resolution: {integrity: sha512-xtXazslu25CdnGnUkByU1RoOjK55TqwatJkjjJLg5ZAdz2Lngko/mmaUgeET36P2GMlNwh3fdM7FWBO717pNcw==} + engines: {node: '>=12.13'} + + react-dom@19.2.1: + resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} + peerDependencies: + react: ^19.2.1 + + react@19.2.1: + resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} + engines: {node: '>=0.10.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + tailwindcss@4.1.17: + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + timestring@6.0.0: + resolution: {integrity: sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==} + engines: {node: '>=8'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@next/env@16.0.8': {} + + '@next/swc-darwin-arm64@16.0.8': + optional: true + + '@next/swc-darwin-x64@16.0.8': + optional: true + + '@next/swc-linux-arm64-gnu@16.0.8': + optional: true + + '@next/swc-linux-arm64-musl@16.0.8': + optional: true + + '@next/swc-linux-x64-gnu@16.0.8': + optional: true + + '@next/swc-linux-x64-musl@16.0.8': + optional: true + + '@next/swc-win32-arm64-msvc@16.0.8': + optional: true + + '@next/swc-win32-x64-msvc@16.0.8': + optional: true + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.17': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.17 + + '@tailwindcss/oxide-android-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide@4.1.17': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-x64': 4.1.17 + '@tailwindcss/oxide-freebsd-x64': 4.1.17 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 + + '@tailwindcss/postcss@4.1.17': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.17 + '@tailwindcss/oxide': 4.1.17 + postcss: 8.5.6 + tailwindcss: 4.1.17 + + '@types/node@20.19.26': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + + '@types/react@19.2.7': + dependencies: + csstype: 3.2.3 + + '@vercel/config@0.0.22': + dependencies: + pretty-cache-header: 1.0.0 + zod: 3.25.76 + + caniuse-lite@1.0.30001759: {} + + client-only@0.0.1: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + geist@1.5.1(next@16.0.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1)): + dependencies: + next: 16.0.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + + graceful-fs@4.2.11: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + nanoid@3.3.11: {} + + next@16.0.8(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + '@next/env': 16.0.8 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001759 + postcss: 8.4.31 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + styled-jsx: 5.1.6(react@19.2.1) + optionalDependencies: + '@next/swc-darwin-arm64': 16.0.8 + '@next/swc-darwin-x64': 16.0.8 + '@next/swc-linux-arm64-gnu': 16.0.8 + '@next/swc-linux-arm64-musl': 16.0.8 + '@next/swc-linux-x64-gnu': 16.0.8 + '@next/swc-linux-x64-musl': 16.0.8 + '@next/swc-win32-arm64-msvc': 16.0.8 + '@next/swc-win32-x64-msvc': 16.0.8 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + picocolors@1.1.1: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-cache-header@1.0.0: + dependencies: + timestring: 6.0.0 + + react-dom@19.2.1(react@19.2.1): + dependencies: + react: 19.2.1 + scheduler: 0.27.0 + + react@19.2.1: {} + + scheduler@0.27.0: {} + + semver@7.7.3: + optional: true + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + source-map-js@1.2.1: {} + + styled-jsx@5.1.6(react@19.2.1): + dependencies: + client-only: 0.0.1 + react: 19.2.1 + + tailwindcss@4.1.17: {} + + tapable@2.3.0: {} + + timestring@6.0.0: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + zod@3.25.76: {} diff --git a/cdn/cms-bulk-redirects/postcss.config.mjs b/cdn/cms-bulk-redirects/postcss.config.mjs new file mode 100644 index 0000000000..c7bcb4b1ee --- /dev/null +++ b/cdn/cms-bulk-redirects/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/cdn/cms-bulk-redirects/public/favicon.ico b/cdn/cms-bulk-redirects/public/favicon.ico new file mode 100644 index 0000000000..4ddd8fff70 Binary files /dev/null and b/cdn/cms-bulk-redirects/public/favicon.ico differ diff --git a/cdn/cms-bulk-redirects/tsconfig.json b/cdn/cms-bulk-redirects/tsconfig.json new file mode 100644 index 0000000000..a9f83b8ba3 --- /dev/null +++ b/cdn/cms-bulk-redirects/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "target": "es2020", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/cdn/cms-bulk-redirects/vercel.ts b/cdn/cms-bulk-redirects/vercel.ts new file mode 100644 index 0000000000..b9b7f5d5aa --- /dev/null +++ b/cdn/cms-bulk-redirects/vercel.ts @@ -0,0 +1,93 @@ +import type { VercelConfig } from '@vercel/config/v1' +import { writeFileSync } from 'fs' +import { join } from 'path' + +type VercelRedirect = { + source: string + destination: string + permanent?: boolean + statusCode?: number + caseSensitive?: boolean + query?: boolean +} + +type ContentfulEntry = { + fields: Record +} + +type ContentfulResponse = { + items?: ContentfulEntry[] +} + +const fallbackRedirects: VercelRedirect[] = [ + { source: '/catalog/fall', destination: '/catalog/fall-2025', statusCode: 302 }, + { source: '/catalog/winter', destination: '/catalog/winter-2025', permanent: true }, + { source: '/catalog/latest', destination: '/catalog/spring-2026', permanent: true }, + { source: '/products/daybreak-pack', destination: '/catalog/limited-edition', statusCode: 302, query: true }, + { source: '/catalog/outlet', destination: '/catalog/archive', statusCode: 308, caseSensitive: false }, +] + +const normalizePath = (path: string) => (path.startsWith('/') ? path : `/${path}`) + +async function fetchContentfulRedirects(): Promise { + console.log('fetching contentful redirects') + const spaceId = process.env.CONTENTFUL_SPACE_ID + const accessToken = process.env.CONTENTFUL_ACCESS_TOKEN + + if (!spaceId || !accessToken) { + console.warn('⚠️ Skipping Contentful sync: set CONTENTFUL_SPACE_ID and CONTENTFUL_ACCESS_TOKEN to pull CMS redirects') + return null + } + + const url = new URL(`https://cdn.contentful.com/spaces/${spaceId}/entries`) + url.searchParams.set('content_type', 'redirect') + url.searchParams.set('access_token', accessToken) + url.searchParams.set('limit', '1000') + + const response = await fetch(url.toString()) + + if (!response.ok) { + throw new Error(`Contentful API error: ${response.status} ${response.statusText}`) + } + + const data = (await response.json()) as ContentfulResponse + const entries = data.items ?? [] + + if (entries.length === 0) { + console.info('ℹ️ No redirects returned by Contentful') + return [] + } + + return entries.map((entry) => { + const fields = entry.fields + const redirect: VercelRedirect = { + source: normalizePath(fields.source), + destination: normalizePath(fields.destination), + } + + if (fields.statusCode !== undefined) redirect.statusCode = Number(fields.statusCode) + if (fields.permanent !== undefined) redirect.permanent = Boolean(fields.permanent) + if (fields.caseSensitive !== undefined) redirect.caseSensitive = Boolean(fields.caseSensitive) + if (fields.preserveQuery !== undefined) redirect.query = Boolean(fields.preserveQuery) + + return redirect + }) +} + +const redirectsFromContentful = await fetchContentfulRedirects() +const redirectsToWrite = + redirectsFromContentful === null + ? fallbackRedirects + : redirectsFromContentful.length > 0 + ? redirectsFromContentful + : fallbackRedirects + +const redirectsPath = join(process.cwd(), 'generated-redirects.json') +writeFileSync(redirectsPath, JSON.stringify(redirectsToWrite, null, 2)) +console.log(`✓ Bulk redirects ready (${redirectsToWrite.length} rules) -> ${redirectsPath}`) + +export const config: VercelConfig = { + framework: 'nextjs', + outputDirectory: '.next', + bulkRedirectsPath: './generated-redirects.json', +}