Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cdn/cms-bulk-redirects/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_cda_token
4 changes: 4 additions & 0 deletions cdn/cms-bulk-redirects/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"root": true,
"extends": "next/core-web-vitals"
}
43 changes: 43 additions & 0 deletions cdn/cms-bulk-redirects/.gitignore
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions cdn/cms-bulk-redirects/README.md
Original file line number Diff line number Diff line change
@@ -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`.
191 changes: 191 additions & 0 deletions cdn/cms-bulk-redirects/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import Link from 'next/link'

export default function About() {
return (
<main className="min-h-screen">
{/* Header */}
<section className="py-16 px-4 border-b border-gray-200 dark:border-gray-800">
<div className="max-w-3xl mx-auto">
<Link
href="/"
className="inline-flex items-center text-sm text-gray-500 dark:text-gray-500 hover:text-black dark:hover:text-white transition-colors mb-8"
>
← Back to store
</Link>
<h1 className="text-4xl font-bold text-black dark:text-white mb-4">
How it works
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400">
This demo uses Vercel's bulk redirects feature to manage seasonal URLs without code changes.
</p>
</div>
</section>

{/* Content */}
<section className="py-16 px-4">
<div className="max-w-3xl mx-auto space-y-16">
{/* The Problem */}
<div>
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
The problem
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
E-commerce sites often need vanity URLs that stay consistent while the content behind them changes:
</p>
<ul className="space-y-2 text-gray-600 dark:text-gray-400">
<li className="flex items-start gap-3">
<span className="text-gray-400">•</span>
<span><code className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-900 rounded text-sm font-mono">/catalog/fall</code> should always show the current fall collection</span>
</li>
<li className="flex items-start gap-3">
<span className="text-gray-400">•</span>
<span><code className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-900 rounded text-sm font-mono">/catalog/latest</code> should point to the newest drop</span>
</li>
<li className="flex items-start gap-3">
<span className="text-gray-400">•</span>
<span>Retired product SKUs should redirect to relevant collections</span>
</li>
</ul>
</div>

{/* The Solution */}
<div>
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
The solution
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Vercel's bulk redirects let you manage thousands of redirects at the edge—no middleware, no server-side logic.
</p>
<div className="space-y-4">
<div className="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
<div className="font-medium text-black dark:text-white mb-1">1. Define redirects in a JSON file</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
Or fetch them from a CMS like Contentful, Sanity, or any API
</p>
</div>
<div className="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
<div className="font-medium text-black dark:text-white mb-1">2. Use vercel.ts to generate at build time</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
The config file runs during build and outputs the redirect rules
</p>
</div>
<div className="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
<div className="font-medium text-black dark:text-white mb-1">3. Redirects execute at the edge</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
Fast, globally distributed, no app code involved
</p>
</div>
</div>
</div>

{/* Code Example */}
<div>
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
Example code
</h2>
<pre className="bg-gray-950 text-gray-100 p-6 rounded-lg overflow-x-auto text-sm font-mono">
{`// 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',
}`}
</pre>
</div>

{/* Try it */}
<div>
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
Try it
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Click these links to see the redirects in action:
</p>
<div className="grid sm:grid-cols-2 gap-4">
<Link
href="/catalog/fall"
className="block border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
>
<code className="text-sm font-mono text-black dark:text-white">/catalog/fall</code>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">→ fall-2025</p>
</Link>
<Link
href="/catalog/winter"
className="block border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
>
<code className="text-sm font-mono text-black dark:text-white">/catalog/winter</code>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">→ winter-2025</p>
</Link>
<Link
href="/catalog/latest"
className="block border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
>
<code className="text-sm font-mono text-black dark:text-white">/catalog/latest</code>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">→ spring-2026</p>
</Link>
<Link
href="/catalog/outlet"
className="block border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
>
<code className="text-sm font-mono text-black dark:text-white">/catalog/outlet</code>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">→ archive</p>
</Link>
</div>
</div>

{/* Resources */}
<div>
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
Resources
</h2>
<div className="space-y-3">
<a
href="https://vercel.com/docs/edge-network/redirects"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
>
<div>
<div className="font-medium text-black dark:text-white">Vercel Redirects Docs</div>
<div className="text-sm text-gray-500 dark:text-gray-500">Official documentation</div>
</div>
<span className="text-gray-400">→</span>
</a>
<a
href="https://github.com/vercel/examples/tree/main/cdn/cms-bulk-redirects"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
>
<div>
<div className="font-medium text-black dark:text-white">Source Code</div>
<div className="text-sm text-gray-500 dark:text-gray-500">View on GitHub</div>
</div>
<span className="text-gray-400">→</span>
</a>
</div>
</div>
</div>
</section>
</main>
)
}
16 changes: 16 additions & 0 deletions cdn/cms-bulk-redirects/app/api/redirects/route.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
}
Loading