Configuring Redirects on Vercel for Static Sites
Redirects are where a static site meets its own history: every renamed page, merged section and migration leaves URLs that still receive links and search traffic. On Vercel, the configuration lives in vercel.json, and for frameworks Vercel knows well, some of it can come from the framework's config. For Hugo, Eleventy, Jekyll or a custom generator, vercel.json is the whole story, and it has a few behaviours worth knowing before writing hundreds of rules: pattern syntax, the 308/307 status codes, trailing-slash handling, host redirects, and a cap on the number of routes per deployment.
This guide configures redirects for an Eleventy documentation site moving to Vercel after a restructure that renamed 1,640 URLs. It covers the rule syntax, compressing a large map into patterns, trailing slashes, host redirects and a CI test of the whole map. It belongs to Netlify vs Vercel Deployment Strategies; the Netlify equivalent is Netlify Redirects and Rewrites for Static Sites.
Prerequisites
- A list of old URLs and their new destinations, generated from the restructure (here a CSV of 1,640 rows).
vercel.jsonat the project root and the Vercel CLI for local testing (vercel dev).
Step 1: The Rule Syntax
{
"trailingSlash": true,
"redirects": [
{ "source": "/guides/getting-started/", "destination": "/docs/start/", "permanent": true },
{ "source": "/blog/:year(\\d{4})/:month(\\d{2})/:slug/", "destination": "/blog/:slug/", "permanent": true },
{ "source": "/api/v1/:path*", "destination": "/reference/v1/:path*", "permanent": true },
{ "source": "/launch/", "destination": "/blog/announcing-v3/", "permanent": false }
]
}
:namematches one path segment;:name*matches zero or more; a regex in parentheses constrains a parameter.permanent: truereturns 308,falsereturns 307. Both preserve the request method, unlike 301/302. Search engines treat 308 like 301. AstatusCodefield sets 301 or 302 explicitly if needed.- Rules are evaluated in order, and the first match wins.
Step 2: Compress the Map Into Patterns
1,640 individual rules would come close to Vercel's route limit (just over 2,000 routes per deployment, shared with headers and rewrites) and make the file unreadable. Grouping the CSV by how the old and new paths relate found that most redirects followed a few patterns:
| Pattern | Old example | New example | Rows |
|---|---|---|---|
| Section rename | /guides/x/ | /docs/x/ | 820 |
| Date folders removed from blog | /blog/2021/04/x/ | /blog/x/ | 410 |
| API version path | /api/v1/x | /reference/v1/x | 260 |
| One-off moves | various | various | 150 |
A script verified that each pattern produced exactly the CSV's destination for every row it claimed, and wrote the remaining 150 as individual rules before the patterns, so specific rules win. The file went from a would-be 1,640 rules to 153.
Step 3: Trailing Slashes
Eleventy writes docs/start/index.html, so canonical URLs end with a slash. "trailingSlash": true makes Vercel redirect /docs/start to /docs/start/ with a 308. Every redirect source and destination in the file uses the slash form, and the source patterns match the slash form only — Vercel applies the trailing-slash redirect first, then the redirect rules, so /guides/x becomes /guides/x/ and then /docs/x/. That is two hops for slashless old links; to keep it to one for the 820 section-rename URLs, which had many inbound links without slashes, the pattern matches both forms:
{ "source": "/guides/:path((?:[^/]+/)*[^/]+)/?", "destination": "/docs/:path/", "permanent": true }
Step 4: Host and Protocol Redirects
The old site lived at docs.example.com; the new one at example.com/docs/. Host redirects use a has condition on the host header:
{
"source": "/:path*",
"has": [{ "type": "host", "value": "docs.example.com" }],
"destination": "https://example.com/docs/:path*",
"permanent": true
}
The old host is added to the Vercel project as a domain, so requests reach the deployment and the rule. HTTP-to-HTTPS is handled by Vercel automatically for every domain.
Step 5: Test the Whole Map in CI
Every row of the CSV is requested against the preview deployment, following redirects, and checked against its expected destination:
tail -n +2 redirects.csv | while IFS=, read old new; do
got=$(curl -s -o /dev/null -w '%{url_effective} %{http_code} %{num_redirects}' -L "$PREVIEW$old")
set -- $got
[ "$1" = "$PREVIEW$new" ] && [ "$2" = 200 ] && [ "$3" -le 2 ] || echo "FAIL $old -> $got (want $new)"
done | tee redirect-failures.txt
[ ! -s redirect-failures.txt ]
The first run found 11 failures — all one-off rules shadowed by a pattern placed above them. Fixing the order made it pass. The check runs on every pull request that touches vercel.json and takes about 90 seconds for 1,640 URLs with 8 parallel workers. Testing Redirects and Headers Before Deploy has a reusable version.
Beyond the Route Limit: Middleware for the Long Tail
Sites with tens of thousands of irregular redirects cannot express them as patterns. Vercel's options then are bulk redirects (a separate, larger list managed through the platform for plans that offer it) or a small edge middleware function that looks the path up in a map shipped with the deployment. A middleware lookup adds a function invocation to every request it runs on, so it should match only the old URL space:
// middleware.js — runs only on /archive/*
export const config = { matcher: '/archive/:path*' };
import map from './redirects-archive.json' assert { type: 'json' };
export default function middleware(req) {
const to = map[new URL(req.url).pathname];
if (to) return Response.redirect(new URL(to, req.url), 308);
}
The middleware's matcher keeps it off every other path, so the rest of the site stays purely static with no function invocations.
Keeping the Map Maintainable
The CSV stays in the repository as the source of truth, and vercel.json's redirect block is generated from it by the same script that verifies the patterns. Writers who rename a page add a row to the CSV in the same pull request; the CI check then proves the new row works on the preview. Hand-editing vercel.json redirects is discouraged by a comment at the top of the generated block and caught by a check that regenerates and diffs it.
Measured Impact
| Measure | Value | Tool |
|---|---|---|
Redirect rules in vercel.json | 153 (covering 1,640 URLs) | File |
| Old URLs resolving to the right page | 1,640 of 1,640 | CI check |
| Max hops from any old URL | 2 | CI check |
| 404s from old URLs, first 4 weeks | 37 (none in the map) | Edge logs |
| Organic clicks, 4 weeks after vs before | 99% | Search Console |
The 37 unexpected 404s were URLs that had never been in the old site's sitemap — mostly mistyped external links — and 12 with meaningful traffic got individual rules.
Pitfalls & Rollback
- One rule per URL for a large migration. Patterns keep you under the route limit and readable.
- Pattern rules above specific ones. First match wins; order specific rules first.
- Unset
trailingSlash. Both forms serve, creating duplicate URLs. - Testing with permanent redirects. Browsers cache them; use
permanent: falseuntil verified. - Rollback:
vercel.jsonchanges deploy with the site, and Vercel's instant rollback restores the previous deployment's rules along with its files.
Conclusion
Redirects on Vercel live in vercel.json and follow simple rules — first match wins, 308 for permanent, a cap on routes per deployment. For a 1,640-URL restructure, compressing the map into three patterns plus 150 specific rules, matching both slash forms for heavily linked URLs, adding a host rule for the old domain, and testing every row against the preview kept every old URL within two hops of its page and organic traffic flat through the move.
FAQ
Where do redirects go for a static site on Vercel?
In the redirects array of vercel.json at the project root, with source, destination and permanent or statusCode. Framework presets can also generate redirects, for example from Next.js config, but for generators without a Vercel integration such as Hugo, Eleventy or Jekyll, vercel.json is the place.
How many redirects can vercel.json hold?
Vercel limits the number of routes in a deployment, which includes redirects, rewrites and headers, to a little over 2,000. Sites migrating thousands of URLs should group redirects into patterns where possible, or handle the long tail with bulk redirects or a small middleware function that looks up a map.
Does Vercel handle trailing slashes automatically?
Only if configured. The trailingSlash option in vercel.json makes Vercel redirect to the form with or without a trailing slash. Leaving it unset serves both forms without redirecting, which can create duplicate URLs. Set it to match how the generator writes pages and how internal links are written.
Should redirects be permanent or temporary?
Permanent, with status 308 on Vercel when permanent is true, for URLs that have moved for good, such as after a migration. Temporary, 307, for redirects that may change, such as a campaign URL or a page under maintenance. Browsers cache permanent redirects, so use temporary ones while testing.
Related
- Parent: Netlify vs Vercel Deployment Strategies — choosing and configuring the platforms.
- Netlify Redirects and Rewrites for Static Sites — the same on Netlify.
- Keeping Redirects Working After an SSG Migration — building the redirect map.
- Configuring Vercel for Hugo and Eleventy — the rest of the Vercel setup.
- Fixing Redirect Loops on Cloudflare Pages — what goes wrong when rules disagree.