Vercel Deployment Guide 2026 — Next.js, Edge Functions, and Production Optimization

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

Vercel is the platform Next.js was built for. It provides zero-config deployment, automatic HTTPS, global CDN, preview deployments on every PR, and Edge Functions that run in 100+ locations worldwide. For Next.js applications in 2026, Vercel is often the fastest path from code to production with the best developer experience available.

Getting Started with the Vercel CLI

# Install and deploy
npm install -g vercel
vercel          # Interactive setup on first run
vercel --prod   # Deploy to production
 
# Link an existing project
vercel link
vercel env pull  # Pull environment variables to .env.local

vercel.json Configuration

{
  "buildCommand": "npm run build",
  "installCommand": "npm ci",
  "framework": "nextjs",
  "regions": ["iad1", "sin1"],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-XSS-Protection", "value": "1; mode=block" }
      ]
    },
    {
      "source": "/_next/static/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    }
  ],
  "rewrites": [
    { "source": "/old-blog/:slug", "destination": "/blog/:slug" }
  ],
  "redirects": [
    { "source": "/about-us", "destination": "/about", "permanent": true }
  ],
  "crons": [
    { "path": "/api/cron/daily-digest", "schedule": "0 8 * * *" }
  ]
}

Environment Variables

# Add variables per environment
vercel env add DATABASE_URL production
vercel env add DATABASE_URL preview
vercel env add DATABASE_URL development
 
# Pull all to local
vercel env pull
 
# List all
vercel env ls

Environment variable scoping in Next.js:

// Server-only (never exposed to browser)
process.env.DATABASE_URL
process.env.JWT_SECRET
 
// Exposed to browser — must have NEXT_PUBLIC_ prefix
process.env.NEXT_PUBLIC_API_URL
process.env.NEXT_PUBLIC_POSTHOG_KEY

Edge Functions for Geo-Personalization

Edge Functions run in 100+ locations close to users — ideal for routing, personalization, and A/B testing:

// app/api/geo/route.ts
import { NextRequest, NextResponse } from 'next/server'
 
export const runtime = 'edge'
 
const COUNTRY_ROUTES: Record<string, string> = {
  IN: 'https://in.myapp.com',
  GB: 'https://uk.myapp.com',
  AU: 'https://au.myapp.com',
}
 
export async function GET(request: NextRequest) {
  const country = request.geo?.country || 'US'
  const city = request.geo?.city
 
  if (country in COUNTRY_ROUTES) {
    return NextResponse.redirect(COUNTRY_ROUTES[country])
  }
 
  return NextResponse.json({
    message: `Hello from ${city || 'your location'}!`,
    country,
    region: request.geo?.region,
    timezone: request.geo?.timezone,
  })
}
// middleware.ts — Edge middleware runs before every request
import { NextRequest, NextResponse } from 'next/server'
 
export function middleware(request: NextRequest) {
  const country = request.geo?.country
 
  // Add country header for downstream use
  const response = NextResponse.next()
  response.headers.set('x-country', country || 'unknown')
 
  // Block specific countries
  if (country === 'XX') {
    return new NextResponse('Service not available in your region', { status: 451 })
  }
 
  return response
}
 
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Vercel Analytics and Speed Insights

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  )
}

Track custom events:

import { track } from '@vercel/analytics'
 
// Track conversions and user actions
function SignupButton() {
  return (
    <button
      onClick={() => {
        track('signup_clicked', { plan: 'pro', source: 'hero' })
      }}
    >
      Get started
    </button>
  )
}

Incremental Static Regeneration (ISR)

// Static at build time, revalidated in the background
export const revalidate = 3600  // Re-generate every hour
 
export default async function BlogPage() {
  const posts = await getPosts()  // Cached and served statically
  return <PostList posts={posts} />
}

On-demand revalidation via webhook:

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
 
export async function POST(request: Request) {
  const { secret, slug } = await request.json()
 
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 })
  }
 
  revalidatePath(`/blog/${slug}`)
  revalidatePath('/blog')
 
  return Response.json({ revalidated: true, slug })
}

Preview Deployments and GitHub Integration

Every pull request automatically gets a unique preview URL. Notify the PR with the URL:

# .github/workflows/preview-comment.yml
name: Preview URL
 
on:
  deployment_status:
 
jobs:
  comment:
    runs-on: ubuntu-latest
    if: github.event.deployment_status.state == 'success'
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            const url = context.payload.deployment_status.target_url
            const prNumber = context.payload.deployment.payload.pr_number
            if (!prNumber) return
 
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: prNumber,
              body: `Preview deployed: ${url}`,
            })

Common Mistakes

  • Using process.env values at module level — they are not available during edge runtime build; access them inside request handlers
  • No revalidate on data-heavy pages — without ISR, every request hits your database even for identical content
  • Storing large files in the repo — Vercel has a 250MB uncompressed source limit; use S3 or Vercel Blob for media
  • Not scoping Edge Functions correctly — middleware matcher patterns that are too broad slow down static asset serving
  • Exceeding the 1MB response size limit on Edge — break large API responses into paginated chunks

Best Practices

  • Use revalidate on pages that fetch database content to avoid cold database hits on every request
  • Store secrets in Vercel Environment Variables, not in .env files committed to the repository
  • Use @vercel/postgres or Neon for serverless-compatible database connections that work in Edge runtime
  • Enable Vercel Web Analytics and Speed Insights on all projects — they are free and surface real user performance data
  • Use preview environments to review visual changes, test API integrations, and share work with stakeholders before merging

Key Takeaways

  • Vercel auto-deploys on every push to main and creates unique preview URLs for every pull request automatically
  • Edge Functions run in 100+ locations worldwide with near-zero cold starts — use them for auth checks, redirects, and personalization
  • ISR with revalidate serves static HTML instantly while regenerating content in the background — no server needed
  • NEXT_PUBLIC_ prefix is required for environment variables that need to be accessible in browser-side code
  • Vercel Analytics tracks page views and custom events; Speed Insights shows real user Core Web Vitals from production traffic
  • Cron jobs in vercel.json replace the need for external schedulers for simple recurring tasks
  • The Vercel free tier includes unlimited preview deployments, 100GB bandwidth, and analytics for personal projects
  • On-demand revalidation via revalidatePath() lets a CMS webhook trigger page regeneration instantly after content updates

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading