Skip to content

Commit cf7dc62

Browse files
[cdn] Create a new template for creating bulk redirects via a CMS (#1336)
### Description This is a template for using a CMS with bulk redirects to auto update the redirects at build time based on new entries and updates on the CMS. In this case the CMS is Contentful. This is meant to display our new typsecript based configuration file. Please view the readme for more info. ### Demo URL https://cms-bulk-redirects.vercel.app/ ### Type of Change - [x] New Example - [ ] Example updates (Bug fixes, new features, etc.) - [ ] Other (changes to the codebase, but not to examples) ### New Example Checklist - [ ] 🛫 `npm run new-example` was used to create the example - [x] 📚 The template wasn't used but I carefuly read the [Adding a new example](https://github.com/vercel/examples#adding-a-new-example) steps and implemented them in the example - [x] 📱 Is it responsive? Are mobile and tablets considered?
1 parent 415e97f commit cf7dc62

File tree

20 files changed

+1994
-0
lines changed

20 files changed

+1994
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CONTENTFUL_SPACE_ID=your_space_id
2+
CONTENTFUL_ACCESS_TOKEN=your_cda_token
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"root": true,
3+
"extends": "next/core-web-vitals"
4+
}

cdn/cms-bulk-redirects/.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# Dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# Testing
9+
/coverage
10+
11+
# Next.js
12+
/.next/
13+
/out/
14+
next-env.d.ts
15+
16+
# Production
17+
build
18+
dist
19+
20+
# Misc
21+
.DS_Store
22+
*.pem
23+
24+
# Debug
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*
28+
29+
# Local ENV files
30+
.env.local
31+
.env.development.local
32+
.env.test.local
33+
.env.production.local
34+
35+
# Vercel
36+
.vercel
37+
38+
# Turborepo
39+
.turbo
40+
41+
# typescript
42+
*.tsbuildinfo
43+
.env*.local

cdn/cms-bulk-redirects/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
name: Contentful CMS bulk redirects (vercel.ts)
3+
slug: cms-bulk-redirects
4+
description: Sync redirect entries from Contentful into Vercel bulk redirects using vercel.ts.
5+
framework: Next.js
6+
useCase: Redirects
7+
css: Tailwind
8+
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
9+
demoUrl: https://cms-bulk-redirects.vercel.app
10+
---
11+
12+
# Contentful CMS bulk redirects (vercel.ts) example
13+
14+
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.
15+
16+
## Demo
17+
18+
https://cms-bulk-redirects.vercel.app
19+
20+
## How to Use
21+
22+
You can choose from one of the following two methods to use this repository:
23+
24+
### One-Click Deploy
25+
26+
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):
27+
28+
[![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)
29+
30+
### Clone and Deploy
31+
32+
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:
33+
34+
```bash
35+
pnpm create next-app --example https://github.com/vercel/examples/tree/main/cdn/cms-bulk-redirects
36+
```
37+
38+
Next, run Next.js in development mode:
39+
40+
```bash
41+
pnpm dev
42+
```
43+
44+
## Environment variables
45+
46+
- `CONTENTFUL_SPACE_ID` – Contentful space ID
47+
- `CONTENTFUL_ACCESS_TOKEN` – Content Delivery API token (CDA)
48+
49+
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.
50+
51+
## How it works
52+
53+
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`.
54+
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.
55+
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.
56+
57+
You can extend this pattern to any CMS: swap the fetch logic, keep the same `bulkRedirectsPath`.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import Link from 'next/link'
2+
3+
export default function About() {
4+
return (
5+
<main className="min-h-screen">
6+
{/* Header */}
7+
<section className="py-16 px-4 border-b border-gray-200 dark:border-gray-800">
8+
<div className="max-w-3xl mx-auto">
9+
<Link
10+
href="/"
11+
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"
12+
>
13+
← Back to store
14+
</Link>
15+
<h1 className="text-4xl font-bold text-black dark:text-white mb-4">
16+
How it works
17+
</h1>
18+
<p className="text-lg text-gray-600 dark:text-gray-400">
19+
This demo uses Vercel's bulk redirects feature to manage seasonal URLs without code changes.
20+
</p>
21+
</div>
22+
</section>
23+
24+
{/* Content */}
25+
<section className="py-16 px-4">
26+
<div className="max-w-3xl mx-auto space-y-16">
27+
{/* The Problem */}
28+
<div>
29+
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
30+
The problem
31+
</h2>
32+
<p className="text-gray-600 dark:text-gray-400 mb-4">
33+
E-commerce sites often need vanity URLs that stay consistent while the content behind them changes:
34+
</p>
35+
<ul className="space-y-2 text-gray-600 dark:text-gray-400">
36+
<li className="flex items-start gap-3">
37+
<span className="text-gray-400"></span>
38+
<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>
39+
</li>
40+
<li className="flex items-start gap-3">
41+
<span className="text-gray-400"></span>
42+
<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>
43+
</li>
44+
<li className="flex items-start gap-3">
45+
<span className="text-gray-400"></span>
46+
<span>Retired product SKUs should redirect to relevant collections</span>
47+
</li>
48+
</ul>
49+
</div>
50+
51+
{/* The Solution */}
52+
<div>
53+
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
54+
The solution
55+
</h2>
56+
<p className="text-gray-600 dark:text-gray-400 mb-6">
57+
Vercel's bulk redirects let you manage thousands of redirects at the edge—no middleware, no server-side logic.
58+
</p>
59+
<div className="space-y-4">
60+
<div className="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
61+
<div className="font-medium text-black dark:text-white mb-1">1. Define redirects in a JSON file</div>
62+
<p className="text-sm text-gray-600 dark:text-gray-400">
63+
Or fetch them from a CMS like Contentful, Sanity, or any API
64+
</p>
65+
</div>
66+
<div className="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
67+
<div className="font-medium text-black dark:text-white mb-1">2. Use vercel.ts to generate at build time</div>
68+
<p className="text-sm text-gray-600 dark:text-gray-400">
69+
The config file runs during build and outputs the redirect rules
70+
</p>
71+
</div>
72+
<div className="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
73+
<div className="font-medium text-black dark:text-white mb-1">3. Redirects execute at the edge</div>
74+
<p className="text-sm text-gray-600 dark:text-gray-400">
75+
Fast, globally distributed, no app code involved
76+
</p>
77+
</div>
78+
</div>
79+
</div>
80+
81+
{/* Code Example */}
82+
<div>
83+
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
84+
Example code
85+
</h2>
86+
<pre className="bg-gray-950 text-gray-100 p-6 rounded-lg overflow-x-auto text-sm font-mono">
87+
{`// vercel.ts
88+
import type { VercelConfig } from '@vercel/config/v1'
89+
import { writeFileSync } from 'fs'
90+
91+
const redirects = [
92+
{
93+
source: '/catalog/fall',
94+
destination: '/catalog/fall-2025',
95+
statusCode: 302
96+
},
97+
{
98+
source: '/catalog/latest',
99+
destination: '/catalog/spring-2026',
100+
permanent: true
101+
}
102+
]
103+
104+
writeFileSync(
105+
'generated-redirects.json',
106+
JSON.stringify(redirects, null, 2)
107+
)
108+
109+
export const config: VercelConfig = {
110+
bulkRedirectsPath: './generated-redirects.json',
111+
}`}
112+
</pre>
113+
</div>
114+
115+
{/* Try it */}
116+
<div>
117+
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
118+
Try it
119+
</h2>
120+
<p className="text-gray-600 dark:text-gray-400 mb-6">
121+
Click these links to see the redirects in action:
122+
</p>
123+
<div className="grid sm:grid-cols-2 gap-4">
124+
<Link
125+
href="/catalog/fall"
126+
className="block border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
127+
>
128+
<code className="text-sm font-mono text-black dark:text-white">/catalog/fall</code>
129+
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">→ fall-2025</p>
130+
</Link>
131+
<Link
132+
href="/catalog/winter"
133+
className="block border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
134+
>
135+
<code className="text-sm font-mono text-black dark:text-white">/catalog/winter</code>
136+
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">→ winter-2025</p>
137+
</Link>
138+
<Link
139+
href="/catalog/latest"
140+
className="block border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
141+
>
142+
<code className="text-sm font-mono text-black dark:text-white">/catalog/latest</code>
143+
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">→ spring-2026</p>
144+
</Link>
145+
<Link
146+
href="/catalog/outlet"
147+
className="block border border-gray-200 dark:border-gray-800 rounded-lg p-4 hover:border-black dark:hover:border-white transition-colors"
148+
>
149+
<code className="text-sm font-mono text-black dark:text-white">/catalog/outlet</code>
150+
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">→ archive</p>
151+
</Link>
152+
</div>
153+
</div>
154+
155+
{/* Resources */}
156+
<div>
157+
<h2 className="text-2xl font-bold text-black dark:text-white mb-4">
158+
Resources
159+
</h2>
160+
<div className="space-y-3">
161+
<a
162+
href="https://vercel.com/docs/edge-network/redirects"
163+
target="_blank"
164+
rel="noopener noreferrer"
165+
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"
166+
>
167+
<div>
168+
<div className="font-medium text-black dark:text-white">Vercel Redirects Docs</div>
169+
<div className="text-sm text-gray-500 dark:text-gray-500">Official documentation</div>
170+
</div>
171+
<span className="text-gray-400"></span>
172+
</a>
173+
<a
174+
href="https://github.com/vercel/examples/tree/main/cdn/cms-bulk-redirects"
175+
target="_blank"
176+
rel="noopener noreferrer"
177+
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"
178+
>
179+
<div>
180+
<div className="font-medium text-black dark:text-white">Source Code</div>
181+
<div className="text-sm text-gray-500 dark:text-gray-500">View on GitHub</div>
182+
</div>
183+
<span className="text-gray-400"></span>
184+
</a>
185+
</div>
186+
</div>
187+
</div>
188+
</section>
189+
</main>
190+
)
191+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NextResponse } from 'next/server'
2+
import { readFileSync } from 'fs'
3+
import { join } from 'path'
4+
5+
export async function GET() {
6+
try {
7+
const filePath = join(process.cwd(), 'generated-redirects.json')
8+
const contents = readFileSync(filePath, 'utf-8')
9+
return new NextResponse(contents, {
10+
status: 200,
11+
headers: { 'content-type': 'application/json' },
12+
})
13+
} catch (error) {
14+
return NextResponse.json({ error: 'Redirect file not found' }, { status: 500 })
15+
}
16+
}

0 commit comments

Comments
 (0)