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.json at 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 }
  ]
}
  • :name matches one path segment; :name* matches zero or more; a regex in parentheses constrains a parameter.
  • permanent: true returns 308, false returns 307. Both preserve the request method, unlike 301/302. Search engines treat 308 like 301. A statusCode field 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:

PatternOld exampleNew exampleRows
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/x260
One-off movesvariousvarious150
Number of redirects covered by each pattern rule Vertical bars. A section rename pattern covered 820 redirects. Removing date folders from blog URLs covered 410. An API version path pattern covered 260. 150 one-off moves needed individual rules. 1,640 redirects grouped by pattern 820 Section rename 410 Blog dates removed 260 API version path 150 One-off moves
Three pattern rules replaced 1,490 individual rules; vercel.json went from 1,640 rules to 153.

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.

Redirect sequence for an old documentation link without a trailing slash Three actors, the browser, Vercel's edge and the redirect rules. The browser requests docs.example.com guides deploy without a slash. Vercel matches the host rule and redirects to example.com docs guides deploy. The browser requests that URL. The section rename pattern, which accepts both slash forms, redirects to example.com docs deploy with a slash. The page is served. An old link through the rules Browser Vercel edge Rules GET docs.example.com/guides/deploy 308 host rule → example.com/docs/guides/deploy GET example.com/docs/guides/deploy 308 pattern → /docs/deploy/ 200 page
Two hops for the oldest links, one hop for links on the current host; never more.

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);
}
Decision tree for choosing a redirect mechanism on Vercel by number of rules The root asks how many redirects the site needs. Up to a few hundred specific redirects fit in vercel.json directly. Thousands that follow patterns fit in vercel.json as pattern rules. Thousands of irregular redirects use bulk redirects where the plan offers them. Tens of thousands of irregular redirects in one old URL space use a middleware lookup restricted to that path. Where redirects should live at each scale How many redirects, and how regular? Up to a few hundred vercel.json rules one per URL Thousands with patterns Pattern rules plus specific exceptions Thousands irregular Bulk redirects plan-dependent Tens of thousands Middleware lookup scoped matcher
This site needed only the first two; a sister archive site with 31,000 irregular URLs uses the last.

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

MeasureValueTool
Redirect rules in vercel.json153 (covering 1,640 URLs)File
Old URLs resolving to the right page1,640 of 1,640CI check
Max hops from any old URL2CI check
404s from old URLs, first 4 weeks37 (none in the map)Edge logs
Organic clicks, 4 weeks after vs before99%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: false until verified.
  • Rollback: vercel.json changes 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.

Filed underHow-tosVercel