Skip to main content
Technology & EngineeringDeployment Patterns399 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: 399 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

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 Patterns539L

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 Patterns479L

fly-io-deployment

Complete guide to deploying applications on Fly.io, covering flyctl CLI usage, Dockerfile-based deployments, fly.toml configuration, persistent volumes, horizontal and vertical scaling, multi-region deployments, managed Postgres and Redis, private networking, and auto-scaling strategies.

Deployment Patterns412L

github-actions-cd

Comprehensive guide to implementing continuous deployment with GitHub Actions, covering deploy workflows, environment protection rules, secrets management, matrix builds, dependency caching, artifact management, and deploying to multiple targets including Vercel, Fly.io, AWS, and container registries.

Deployment Patterns469L

monitoring-post-deploy

Comprehensive guide to post-deployment monitoring for web applications, covering uptime checks, error tracking with Sentry, application performance monitoring, log aggregation, alerting strategies, public status pages, and incident response procedures for production systems.

Deployment Patterns572L

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 Patterns434L