Your e-commerce site needs /catalog/fall to point to different collections each year. Marketing runs campaigns with vanity URLs that need to update destinations weekly. Product SKUs get retired, but inbound links from blog posts and emails still need to work.
Currently, you either update redirects in middleware code (which needs developer resources for every change and adds additional computation at request time) or query your CMS at runtime (which adds latency to requests).Vercel bulk redirects solves this with:
- Build-time execution - Fetch redirects from your CMS during deployment, not on every request
- CDN-level handling - Redirects happen at Vercel’s CDN level in 5-10ms, before your application code runs
In this guide, you will implement bulk redirects by configuring vercel.ts .
- Understanding bulk redirects with Vercel configuration
- Hands-on setup with the
cms-bulk-redirectsexample - Adapting the pattern to any CMS (Sanity, Strapi, etc.)
The deployment workflow with vercel.ts is as follows:
- The developer pushes code (or the CMS webhook triggers a deployment)
- The build process starts and runs
vercel.ts vercel.tsfetches all redirect rules from your CMS- The CMS returns redirect data
vercel.tsgenerates agenerated-redirects.jsonfile- Vercel publishes a deployment with redirects to CDN
Redirects now exist as static configuration in the CDN. When a user visits a path, Vercel’s CDN checks the redirect configuration and if there is a match, it redirects instantly
Build-time means you need to redeploy when redirects change. You can automate this by setting up a webhook in your CMS that triggers a Vercel deployment when redirect entries change. When your marketing team edits a redirect in your CMS and saves it, a new deployment starts automatically. We'll cover how to set this up later in the guide.
Here's how you tell Vercel to publish redirects at the CDN level:
import type { VercelConfig } from '@vercel/config/v1'
export const config: VercelConfig = { framework: 'nextjs', outputDirectory: '.next', bulkRedirectsPath: './generated-redirects.json', // This is the magic}In this example, we use Contentful as the CMS.
Setup (one time):
- The marketing team creates a "redirect" content type in Contentful
- The developer adds a webhook to trigger Vercel deployments on redirect changes
Every season:
- The marketing team logs into Contentful
- They create an entry:
/catalog/fall→/catalog/fall-2025, with status code 302 - They save the entry
- The webhook triggers a Vercel deployment
vercel.tsruns and fetches 100 redirects from Contentful- Vercel publishes redirects to the CDN across all regions
Customer experience:
- A customer clicks a link in an email to
/catalog/fall - The request hits the nearest Vercel CDN region
- The CDN checks bulk redirects and finds a match
- The CDN responds with 302 redirect in 5ms
- Your Next.js app never executes
- Your analytics show 0ms "server response time"
- Node.js 20+ and
pnpminstalled - A Contentful account (free tier is sufficient)
- Basic understanding of Next.js
pnpm create next-app --example https://github.com/vercel/examples/tree/main/cdn/cms-bulk-redirectscd cms-bulk-redirectspnpm installYou can also start from the template.
The demo includes fallback redirects, so you can explore it locally without CMS credentials: https://cms-bulk-redirects.vercel.app/
Start the development server:
pnpm devVisit http://localhost:3000 and try these paths:
/catalog/fall- redirects to/catalog/fall-2025/catalog/latest- redirects to/catalog/spring-2026/products/daybreak-pack- redirects to/catalog/limited-edition
Important: In development mode, Next.js handles these redirects through its dev server, not the CDN. This is just for local testing. In production on Vercel, these same rules run at the CDN level.
Open generated-redirects.json to see the fallback rules:
[ { "source": "/catalog/fall", "destination": "/catalog/fall-2025", "statusCode": 302 }, { "source": "/catalog/latest", "destination": "/catalog/spring-2026", "permanent": true }]These are static redirects for development. In production, they come from your CMS.
To see build-time execution in action, set up a real CMS. You can use the free tier of Contentful.
- Log into Contentful
- Go to Content model
- Add content type named "redirect"
- Add these fields:
| Field Name | Type | Required | Notes |
|---|---|---|---|
| source | Short text | Yes | e.g. /sale |
| destination | Short text | Yes | e.g. /catalog/archive |
| statusCode | Number | No | Default: 302 |
| permanent | Boolean | No | If true, uses 301 |
| caseSensitive | Boolean | No | Default: false |
| preserveQuery | Boolean | No | Pass query params |
- source:
/sale - destination:
/catalog/archive - statusCode:
302
Save and publish the entry.
Create a .env.local file:
CONTENTFUL_SPACE_ID=your_space_idCONTENTFUL_ACCESS_TOKEN=your_cda_tokenFind these values in Contentful:
- Space ID: Settings → General settings
- Access Token: Settings → API keys → Content Delivery API
Run a production build to see vercel.ts fetch redirects from your CMS:
pnpm buildWatch the console output:
> next buildfetching contentful redirects✓ Bulk redirects ready (34 rules) -> /generated-redirects.jsonThe CMS fetch happened at build time. Open generated-redirects.json to see your CMS data as a static file.
Now test the redirects:
pnpm devVisit http://localhost:3000/sale to test your redirect.
Deploy to see CDN-level redirects in action:
vercel --prodOr connect your GitHub repository in the Vercel dashboard and add the environment variables in project settings.
After deployment, test a redirect URL. Open your browser's network tab and visit your deployed URL with a redirect path like /sale.
This is build-time execution in action. The file runs once during deployment, not on every request.
The pattern works with any CMS. You only need to change the fetch function.
import { createClient } from '@sanity/client'
async function fetchSanityRedirects(): Promise<VercelRedirect[]> { const client = createClient({ projectId: process.env.SANITY_PROJECT_ID, dataset: process.env.SANITY_DATASET, apiVersion: '2024-01-01', useCdn: false, })
const query = '*[_type == "redirect"]{ source, destination, statusCode, permanent }' const redirects = await client.fetch(query)
return redirects.map((redirect) => ({ source: normalizePath(redirect.source), destination: normalizePath(redirect.destination), statusCode: redirect.statusCode, permanent: redirect.permanent, }))}async function fetchStrapiRedirects(): Promise<VercelRedirect[]> { const response = await fetch(`${process.env.STRAPI_URL}/api/redirects`, { headers: { Authorization: `Bearer ${process.env.STRAPI_TOKEN}`, }, })
const { data } = await response.json()
return data.map((item) => ({ source: normalizePath(item.attributes.source), destination: normalizePath(item.attributes.destination), statusCode: item.attributes.statusCode, permanent: item.attributes.permanent, }))}Set up CMS webhooks to trigger Vercel deployments when redirects change. In Contentful:
- Settings → Webhooks → Add webhook
- Trigger on: Entry publish/unpublish for "redirect" content type
- URL: Your Vercel deploy hook (Project Settings → Git → Deploy Hooks)
Add redirect validation in vercel.ts to catch errors at build time:
function validateRedirects(redirects: VercelRedirect[]): void { const sources = new Set<string>()
for (const redirect of redirects) { // Check for duplicates if (sources.has(redirect.source)) { throw new Error(`Duplicate source path: ${redirect.source}`) } sources.add(redirect.source)
// Check for circular redirects const destinations = redirects.map(r => r.source) if (destinations.includes(redirect.destination)) { console.warn(`⚠️ Potential circular redirect: ${redirect.source} → ${redirect.destination}`) } }}- Set up a working example that demonstrates redirects set up at build time and running at the CDN level
- Explored how
vercel.tsfetches from your CMS at build time and configures CDN redirects
- Deploy your own version with your preferred CMS
- Read the Vercel bulk redirects documentation for advanced configuration
- Explore the
@vercel/configAPI reference for other build-time options