Skip to main content
Technology & EngineeringDeployment Patterns398 lines

netlify-deployment

Complete guide to deploying web applications on Netlify, covering build settings, deploy previews, serverless and edge functions, forms, identity, redirects and rewrites, split testing, and environment variable management for production workflows.

Quick Summary30 lines
The primary configuration file lives at the project root:

## Key Points

1. Deploy your variant to a separate Git branch.
2. In Netlify dashboard: Site settings > Split testing.
3. Assign traffic percentages to branches.
- **All** (default)
- **Production**
- **Deploy previews**
- **Branch deploys**
- **Local development**
1. **Not using `netlify.toml` for redirects**: Dashboard redirects are harder to version control and review.
2. **Forgetting `force = true` on proxied rewrites**: Without it, Netlify serves local files first.
3. **Using Netlify Identity for large user bases**: It's designed for small teams, not consumer auth. Use Auth0 or Clerk instead.
4. **Ignoring deploy context overrides**: Running production builds in preview wastes build minutes and exposes production secrets.

## Quick Example

```
https://deploy-preview-42--my-site.netlify.app
```

```
/old-blog/*    /blog/:splat    301
/api/*         https://api.example.com/:splat    200
/*             /index.html    200
```
skilldb get deployment-patterns-skills/netlify-deploymentFull skill: 398 lines
Paste into your CLAUDE.md or agent config

Deploying to Netlify

Build Settings and Configuration

netlify.toml

The primary configuration file lives at the project root:

[build]
  command = "npm run build"
  publish = "dist"
  functions = "netlify/functions"
  edge_functions = "netlify/edge-functions"

[build.environment]
  NODE_VERSION = "20"
  NPM_FLAGS = "--prefix=/dev/null"

# Production-specific overrides
[context.production]
  command = "npm run build:production"
  environment = { NODE_ENV = "production" }

# Deploy preview overrides
[context.deploy-preview]
  command = "npm run build:preview"

# Branch-specific overrides
[context.staging]
  command = "npm run build:staging"

Framework-Specific Settings

Netlify auto-detects frameworks, but be explicit:

# For Astro
[build]
  command = "astro build"
  publish = "dist"

# For SvelteKit
[build]
  command = "vite build"
  publish = "build"

# For Remix
[build]
  command = "remix build"
  publish = "public"

Anti-pattern: Relying solely on auto-detection. When Netlify misidentifies your framework, your build silently produces wrong output.

Deploy Previews

Every pull request gets an automatic deploy preview:

https://deploy-preview-42--my-site.netlify.app

Configuring Deploy Previews

[context.deploy-preview]
  command = "npm run build:preview"

[context.deploy-preview.environment]
  API_URL = "https://staging-api.example.com"
  ENABLE_DEBUG = "true"

Deploy Notifications

[[plugins]]
  package = "netlify-plugin-checklinks"

# Slack notification on deploy
[[notifications]]
  type = "slack"
  event = "deploy_succeeded"
  channel = "#deploys"

Use the Netlify GitHub integration for PR comments with preview links and deploy status.

Serverless Functions

Basic Function

// netlify/functions/hello.js
export default async (req, context) => {
  const name = new URL(req.url).searchParams.get('name') || 'World';
  return new Response(JSON.stringify({ message: `Hello, ${name}` }), {
    headers: { 'Content-Type': 'application/json' },
  });
};

export const config = {
  path: '/api/hello',
};

Scheduled Functions

// netlify/functions/daily-cleanup.js
export default async () => {
  await cleanupExpiredSessions();
  return new Response('Cleanup complete');
};

export const config = {
  schedule: '@daily',   // cron expressions also work: "0 0 * * *"
};

Background Functions

Long-running tasks (up to 15 minutes):

// netlify/functions/process-video-background.js
export default async (req) => {
  const { videoId } = await req.json();
  await processVideo(videoId);  // Can take up to 15 min
  return new Response('Processing complete');
};

export const config = {
  path: '/api/process-video',
  method: 'POST',
  preferStatic: true,
};

Anti-pattern: Using synchronous functions for tasks over 10 seconds. Use background functions for heavy processing.

Edge Functions

Run at the CDN edge with Deno runtime:

// netlify/edge-functions/geolocation.ts
import type { Context } from '@netlify/edge-functions';

export default async (request: Request, context: Context) => {
  const { country, city } = context.geo;

  // Modify the response
  const response = await context.next();
  response.headers.set('x-country', country?.code || 'unknown');

  return response;
};

export const config = {
  path: '/api/geo',
};

Edge Function for A/B Testing

// netlify/edge-functions/ab-test.ts
import type { Context } from '@netlify/edge-functions';

export default async (request: Request, context: Context) => {
  const cookie = request.headers.get('cookie') || '';
  let variant = cookie.includes('ab_variant=b') ? 'b' : null;

  if (!variant) {
    variant = Math.random() < 0.5 ? 'a' : 'b';
  }

  const url = new URL(request.url);
  if (variant === 'b') {
    url.pathname = `/variants/b${url.pathname}`;
  }

  const response = await context.rewrite(url);

  if (!cookie.includes('ab_variant=')) {
    response.headers.append('set-cookie', `ab_variant=${variant}; path=/; max-age=86400`);
  }

  return response;
};

Forms

Netlify detects HTML forms automatically:

<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
  <input type="hidden" name="form-name" value="contact" />
  <p class="hidden"><label>Ignore: <input name="bot-field" /></label></p>
  <input type="email" name="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

AJAX Form Submission

async function handleSubmit(event) {
  event.preventDefault();
  const form = event.target;
  const data = new FormData(form);

  const response = await fetch('/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams(data).toString(),
  });

  if (response.ok) {
    window.location.href = '/thank-you';
  }
}

Anti-pattern: Using Netlify Forms for high-volume submissions. The free tier limits to 100/month. Use a dedicated form service for heavy traffic.

Redirects and Rewrites

In netlify.toml

# Simple redirect
[[redirects]]
  from = "/old-path"
  to = "/new-path"
  status = 301

# SPA fallback
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

# Proxy to external API (hides CORS issues)
[[redirects]]
  from = "/api/*"
  to = "https://api.example.com/:splat"
  status = 200
  force = true

# Country-based redirect
[[redirects]]
  from = "/*"
  to = "/uk/:splat"
  status = 302
  conditions = { Country = ["GB"] }

# Role-based redirect (with Netlify Identity)
[[redirects]]
  from = "/admin/*"
  to = "/admin/:splat"
  status = 200
  conditions = { Role = ["admin"] }
  force = true

_redirects File

Place in your publish directory for simpler syntax:

/old-blog/*    /blog/:splat    301
/api/*         https://api.example.com/:splat    200
/*             /index.html    200

Anti-pattern: Placing _redirects in the project root instead of the publish directory. It gets ignored silently.

Headers

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'"

[[headers]]
  for = "/fonts/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/*.js"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

Split Testing

Split testing sends a percentage of traffic to different branches:

  1. Deploy your variant to a separate Git branch.
  2. In Netlify dashboard: Site settings > Split testing.
  3. Assign traffic percentages to branches.
main:     70%
redesign: 30%

Split tests use a cookie to keep users on their assigned branch. This is branch-level, not page-level.

Environment Variable Management

# Via Netlify CLI
netlify env:set DATABASE_URL "postgresql://..." --context production
netlify env:set DATABASE_URL "postgresql://..." --context deploy-preview
netlify env:list
netlify env:get DATABASE_URL
netlify env:unset DEPRECATED_VAR

# Pull to local
netlify env:pull .env

Scoping

Variables can be scoped to:

  • All (default)
  • Production
  • Deploy previews
  • Branch deploys
  • Local development

Build Plugins for Environment Setup

[[plugins]]
  package = "@netlify/plugin-nextjs"

[[plugins]]
  package = "netlify-plugin-inline-critical-css"

Netlify CLI Essentials

# Install and login
npm install -g netlify-cli
netlify login

# Link to existing site
netlify link

# Local dev with Netlify features (functions, redirects, etc.)
netlify dev

# Manual deploy (useful for CI or testing)
netlify deploy --dir=dist          # Draft deploy
netlify deploy --dir=dist --prod   # Production deploy

# Trigger a new build
netlify build

Common Anti-Patterns

  1. Not using netlify.toml for redirects: Dashboard redirects are harder to version control and review.
  2. Forgetting force = true on proxied rewrites: Without it, Netlify serves local files first.
  3. Using Netlify Identity for large user bases: It's designed for small teams, not consumer auth. Use Auth0 or Clerk instead.
  4. Ignoring deploy context overrides: Running production builds in preview wastes build minutes and exposes production secrets.
  5. Not pinning Node.js version: Netlify defaults can change. Always set NODE_VERSION in netlify.toml.

Deployment Checklist

  • netlify.toml committed with explicit build command and publish directory
  • Node.js version pinned in build environment
  • Redirects and headers configured and tested
  • Environment variables scoped to correct contexts
  • Deploy previews verified for PR workflow
  • Forms tested with honeypot spam protection
  • Functions tested locally with netlify dev
  • Custom domain configured with DNS verification

Install this skill directly: skilldb add deployment-patterns-skills

Get CLI access →

Related Skills

railway-deployment

Complete guide to deploying applications on Railway, covering project setup, environment variable management, services and databases (Postgres, Redis, MySQL), persistent volumes, monorepo support, private networking between services, and scheduled cron jobs.

Deployment Patterns433L

static-site-deployment

Comprehensive guide to deploying static sites and single-page applications, covering GitHub Pages, Cloudflare Pages, AWS S3 with CloudFront, cache busting strategies, prerendering for SEO, SPA routing configuration, and CDN setup for optimal performance.

Deployment Patterns489L

vercel-deployment

Comprehensive guide to deploying modern web applications on Vercel, covering framework-specific configuration for Next.js, SvelteKit, Astro, and Remix, along with environment variables, preview deployments, edge and serverless functions, ISR, custom domains, and monorepo support.

Deployment Patterns302L

zero-downtime-deployment

Comprehensive guide to zero-downtime deployment patterns including blue-green deployments, canary releases, rolling updates, database migrations during deployments, health check strategies, rollback mechanisms, and feature flag integration for safe progressive rollouts.

Deployment Patterns477L

database-deployment

Comprehensive guide to database deployment for web applications, covering managed database services (PlanetScale, Neon, Supabase, Turso), migration strategies, connection pooling, backup and restore procedures, data seeding, and schema management best practices for production environments.

Deployment Patterns538L

docker-deployment

Comprehensive guide to using Docker for production deployments, covering multi-stage builds, .dockerignore optimization, layer caching strategies, health checks, Docker Compose for local development, container registries, and security scanning best practices.

Deployment Patterns478L