Skip to content

Commit 73947f6

Browse files
authored
[cdn] Add example showing how to update redirects real-time on CMS updates (#1365)
### Description <!-- ✍️ Write a short summary of your work. Screenshots and videos are welcome! --> ### Demo URL <!-- Provide a URL to a live deployment where we can test your PR. If a demo isn't possible feel free to omit this section. --> ### Type of Change - [ ] 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 - [ ] 📚 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 - [ ] 📱 Is it responsive? Are mobile and tablets considered?
1 parent b9a63f9 commit 73947f6

File tree

20 files changed

+2636
-12
lines changed

20 files changed

+2636
-12
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
VERCEL_TEAM_ID=team_your_id
2+
VERCEL_PROJECT_ID=prj_your_project_id
3+
VERCEL_BEARER_TOKEN=<vercel access token>
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
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: Contentful CMS bulk redirects (vercel.ts)
3+
slug: cms-webhook-update-redirects
4+
description: Updates project-level redirects from CMS using webhook + Vercel SDK
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-webhook-update-redirects&project-name=cms-webhook-update-redirects&repository-name=cms-webhook-update-redirects&env=VERCEL_TEAM_ID,VERCEL_PROJECT_ID,VERCEL_BEARER_TOKEN
9+
demoUrl: https://cms-webhook-update-redirects.vercel.app
10+
---
11+
12+
# Update Vercel redirects on CMS changes
13+
14+
This example shows how to respond to CMS webhook events to update redirects served on Vercel.
15+
16+
## Demo
17+
18+
https://cms-webhook-update-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-webhook-update-redirects&project-name=cms-webhook-update-redirects&repository-name=cms-webhook-update-redirects&env=VERCEL_TEAM_ID,VERCEL_PROJECT_ID,VERCEL_BEARER_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-webhook-update-redirects
36+
```
37+
38+
Next, run Next.js in development mode:
39+
40+
```bash
41+
pnpm dev
42+
```
43+
44+
## Environment variables
45+
46+
- `VERCEL_TEAM_ID` – Vercel team ID to update redirects for
47+
- `VERCEL_PROJECT_ID` – Vercel project ID to update redirects for
48+
- `VERCEL_BEARER_TOKEN` - Vercel access token for this team
49+
50+
## How it works
51+
52+
1. The Vercel project runs in production as normal. It can optionally use [Build-time redirects via a CMS](https://vercel.com/templates/cdn/bulk-redirects-via-a-cms) example to load some redirects at deployment time.
53+
2. When a redirect is updated on the CMS, a webhook handler receives a request with the updated redirect. That code uses the [Vercel SDK](https://vercel.com/docs/rest-api/reference/sdk) to modify the redirects served by Vercel. This can happen without a new deployment.
54+
3. After creating a new redirect, the webhook handler then promotes that new version to production with the changed value. Redirects are created in a staged version and must be explicitly promoted to production before being served to end users.
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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { NextResponse } from 'next/server'
2+
import { Vercel } from '@vercel/sdk'
3+
4+
const vercel = new Vercel({
5+
bearerToken: process.env.VERCEL_BEARER_TOKEN,
6+
})
7+
8+
/**
9+
* This route handler mimics a webhook from a CMS that takes the payload
10+
* and updates the redirects that are served for the Vercel project.
11+
*
12+
* When a webhook is received, the route handler can parse the data from
13+
* the request body and update the redirects. New redirects will initially
14+
* be staged, and the code can then decide to promote the new version
15+
* to production so it will serve end users.
16+
*/
17+
export async function POST() {
18+
const teamId = process.env.VERCEL_TEAM_ID as string
19+
const projectId = process.env.VERCEL_PROJECT_ID as string
20+
if (!teamId || !projectId) {
21+
return NextResponse.json(
22+
{ error: 'Missing environment variables' },
23+
{ status: 500 }
24+
)
25+
}
26+
27+
const randomSuffix = Math.floor(Math.random() * 10000)
28+
.toString()
29+
.padStart(4, '0')
30+
31+
const result = await vercel.bulkRedirects.stageRedirects({
32+
teamId,
33+
requestBody: {
34+
teamId,
35+
projectId,
36+
redirects: [
37+
{
38+
source: '/catalog/fall',
39+
destination: `/catalog/fall-${randomSuffix}`,
40+
statusCode: 307,
41+
},
42+
],
43+
},
44+
})
45+
46+
const newVersion = result.version
47+
if (!newVersion) {
48+
return NextResponse.json(
49+
{ error: 'Failed to create new version' },
50+
{ status: 500 }
51+
)
52+
}
53+
54+
const publishResult = await vercel.bulkRedirects.updateVersion({
55+
teamId,
56+
projectId,
57+
requestBody: {
58+
action: 'promote',
59+
id: newVersion.id,
60+
},
61+
})
62+
if (!publishResult || !publishResult.version) {
63+
return NextResponse.json(
64+
{ error: 'Failed to publish new version' },
65+
{ status: 500 }
66+
)
67+
}
68+
69+
return NextResponse.json({
70+
message: 'Success!',
71+
version: publishResult.version,
72+
})
73+
}

0 commit comments

Comments
 (0)