Cloud Platforms No. 186

Netlify Deploys skill for Claude Code

Builds, redirects and functions that behave per context. Claude moves Netlify configuration into netlify.toml with pinned Node versions, monorepo base directories and ignore commands, then scopes environment variables per deploy context. Redirects are ordered with shadowing and force understood, assets get immutable caching, functions and forms follow current limits, and every change is checked on a deploy preview with a one-step rollback ready.

  • namenetlify-deploys
  • size265 lines · ≈2,750 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Configures and debugs Netlify sites - netlify.toml build settings, base directories for monorepos, ignore commands, deploy contexts for production, deploy previews and branch deploys, environment variables by context and scope, redirects and rewrites with shadowing and force, proxying, custom headers and caching, Netlify Functions (v2 handler, background and scheduled), Edge Functions, Forms with spam protection, instant rollback, and the Netlify CLI. Use when editing netlify.toml, _redirects or _headers, when a Netlify build fails or a deploy shows 404s, when adding functions or forms, or when setting up previews for a monorepo.

Try asking

  • Our SPA on Netlify returns 404 on refresh for any route except the homepage
  • Set up Netlify for our monorepo so only the web app builds when it changes
  • Add a contact form and a serverless function that emails submissions on Netlify

Use it when

  • Editing netlify.toml, _redirects or _headers
  • A Netlify build fails or a deploy serves the wrong pages
  • Adding Netlify Functions, Edge Functions or Forms
  • Configuring deploy previews, branch deploys and environment variables

Skip it when

How to check the result

netlify build succeeds locally, the deploy preview passes curl -I checks for each redirect and header rule, a test form submission appears, and a previous deploy can be republished.

The common mistake it prevents

Defining runtime secrets in netlify.toml, which makes them available to the build but undefined inside functions at runtime.

~/.claude/skills/netlify-deploys/SKILL.md

--- name: netlify-deploys description: Configures and debugs Netlify sites - netlify.toml build settings, base directories for monorepos, ignore commands, deploy contexts for… license: MIT ---

A Netlify site is done when the build is defined in netlify.toml (not only the UI), each deploy context gets the right variables, redirects are ordered and tested, static assets are cached immutably, functions live in versioned code with known limits, and a bad production deploy can be rolled back in one click or one command.

Ground rules

  • Put build config, redirects and headers in netlify.toml (or _redirects/_headers in the publish dir) in Git. UI settings drift silently.
  • Secrets go in Netlify environment variables (UI/CLI/API) scoped to the contexts and scopes that need them; never commit them to netlify.toml.
  • Production deploys (netlify deploy --prod, publishing a deploy, rolling back) need explicit confirmation when run by hand.
  • Test redirects and functions locally with netlify dev and on a deploy preview before merging.

Workflow

- [ ] 1. Inspect: site link, build settings, env vars, recent deploy logs
- [ ] 2. Define build in netlify.toml (base, command, publish, Node version)
- [ ] 3. Contexts and environment variables
- [ ] 4. Redirects, rewrites, headers
- [ ] 5. Functions / Edge Functions / Forms as needed
- [ ] 6. Verify on a deploy preview; promote; know the rollback

1. Inspect

npm i -g netlify-cli          # or npx netlify-cli
netlify login && netlify link # associate the folder with the site
netlify status
netlify env:list --context production
netlify build                 # run the build locally the way Netlify does
netlify dev                   # local server with redirects, functions, edge functions

Read the failed deploy log from the top: the first error usually names a missing env var, a wrong base directory, or a Node version mismatch.

2. Build settings

# netlify.toml (at repo root or at the package base for monorepos)
[build]
  base = "apps/web"
  command = "pnpm run build"
  publish = "dist"                       # relative to base
  ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- . ../../packages/ui"

[build.environment]
  NODE_VERSION = "22"
  PNPM_FLAGS = "--frozen-lockfile"

[context.production.environment]
  PUBLIC_SITE_URL = "https://www.acme.com"

[context.deploy-preview]
  command = "pnpm run build -- --mode preview"

[context.branch-deploy.environment]
  PUBLIC_SITE_URL = "https://staging.acme.com"
  • ignore exit code 0 skips the build, 1 builds. Use it in monorepos so unrelated packages do not rebuild.
  • Framework sites (Next.js, Astro, SvelteKit, Remix/React Router, Nuxt) get adapters automatically or via the framework’s Netlify adapter; do not hand-write redirects that the adapter generates.
  • Pin the Node version (NODE_VERSION or .nvmrc) to match local and CI.

3. Environment variables

ContextWhen it applies
productionDeploys of the production branch
deploy-previewPull/merge request previews
branch-deployOther branches with branch deploys enabled
devnetlify dev locally
Branch-specific (context.staging)A named branch
netlify env:set STRIPE_SECRET_KEY sk_live_... --context production --scope functions --secret
netlify env:set STRIPE_SECRET_KEY sk_test_... --context deploy-preview branch-deploy --scope functions --secret
netlify env:get STRIPE_SECRET_KEY --context production
  • Variables in netlify.toml are available at build time only, not to functions at runtime. Runtime secrets must be set through the UI/CLI/API with the functions scope.
  • Anything prefixed for the client bundle (VITE_, NEXT_PUBLIC_, PUBLIC_) ships to browsers; never put secrets there.
  • Netlify scans build output for secret values and fails the build when it finds one; fix the leak rather than disabling the scan (omit only known-safe keys).

4. Redirects, rewrites, headers

[[redirects]]
  from = "/blog/*"
  to = "/articles/:splat"
  status = 301

[[redirects]]
  from = "/api/*"
  to = "https://api.acme.com/:splat"
  status = 200                            # proxy (rewrite), URL stays the same
  force = true
  headers = { X-From = "netlify" }

[[redirects]]
  from = "/app/*"
  to = "/app/index.html"
  status = 200                            # SPA fallback for a sub-app

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

[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"

Rules:

  • First matching rule wins; _redirects rules are evaluated before netlify.toml rules. Put specific rules before catch-alls.
  • Shadowing: a rule does not apply when a file exists at the from path, unless force = true. SPA fallbacks rely on this; forced catch-alls break static assets.
  • status = 200 is a rewrite/proxy; 301 permanent; 302 temporary; 404 custom not-found with a page.
  • Header rules apply to files served from the CDN, not to function responses or proxied responses; set those headers in the function or upstream.
  • Cache HTML briefly (Netlify’s default for HTML is effectively revalidate on every request); cache fingerprinted assets for a year.

5. Functions, Edge Functions, Forms

Functions (v2 API), netlify/functions/orders.mts:

import type { Config, Context } from "@netlify/functions";

export default async (req: Request, context: Context) => {
  if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
  const body = await req.json();
  const key = Netlify.env.get("STRIPE_SECRET_KEY");
  // ... do work
  return Response.json({ ok: true, id: body.id });
};

export const config: Config = { path: "/api/orders" };
TypeUseLimits (check current docs)
Synchronous functionAPIs, form handlers, webhooksExecution limit around 60 s (older docs say 10-30 s); request/response size limits apply
Background function (-background suffix)Slow work; caller gets 202 immediatelyUp to 15 minutes; retried on error
Scheduled function (config.schedule = "@hourly" or cron)Periodic jobsShort limit (about 30 s); production deploys only
Edge Function (netlify/edge-functions/, Deno)Geolocation, rewrites, A/B, auth gates at the edgeVery short CPU budget; no Node APIs unless node: compat

Edge Function example:

// netlify/edge-functions/geo-redirect.ts
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
  if (context.geo?.country?.code === "DE" && !new URL(req.url).pathname.startsWith("/de")) {
    return Response.redirect(new URL("/de" + new URL(req.url).pathname, req.url), 302);
  }
  return context.next();
};
export const config: Config = { path: "/*", excludedPath: ["/assets/*", "/api/*"] };

Forms:

<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field" action="/thanks">
  <input type="hidden" name="form-name" value="contact" />
  <p hidden><label>Skip <input name="bot-field" /></label></p>
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>
  • Netlify detects forms in deployed static HTML at build time. JS-rendered forms (React, SSR frameworks) need a static HTML copy of the form in the publish dir and the hidden form-name field in the submitted payload.
  • Enable form detection in site settings if submissions never appear.
  • AJAX submissions: POST application/x-www-form-urlencoded to / (or the static form path) including form-name.
  • Add the honeypot and, for public forms, reCAPTCHA (data-netlify-recaptcha="true").

6. Previews, promotion, rollback

  • Deploy previews build for every PR with the deploy-preview context; protect them with password or team auth if they hit real data.
  • Manual deploys from CI:
netlify deploy --dir dist --message "PR 123"              # draft deploy, prints a unique URL
netlify deploy --dir dist --prod --message "release 1.4"  # production (confirm first)
  • Rollback: publish a previous deploy from the Deploys list (instant, no rebuild), or lock auto-publishing during an incident so new merges do not deploy.
  • Branch deploys + branch subdomains (staging--site.netlify.app) give a stable staging URL.

Debugging

SymptomCauseFix
404 on deep links in SPANo fallback rewrite/* /index.html 200 (or framework adapter)
Static assets return index.htmlForced catch-allRemove force from SPA fallback
“Page not found” for whole siteWrong publish relative to baseCheck the build log’s publish path
Build works locally, fails on NetlifyNode/package manager version, missing env var in that context, case-sensitive pathsPin versions; set env per context; fix import casing
Function env var undefinedDefined only in netlify.toml or wrong scope/contextSet via CLI/UI with functions scope
Function 502 / timeoutExceeds execution limit, unhandled rejectionMove to background function or queue; check function logs
Form submissions missingForm not in static HTML, missing form-name, detection offAdd static form skeleton; enable detection
Proxy to API loses cookies/CORS issuesUpstream sets domain-specific cookiesProxy with 200 so same-origin; adjust upstream cookie domain
netlify logs:function orders     # stream function logs (CLI command names vary by version; see netlify help)
netlify open:admin

Gotchas

  • base changes the working directory for command, publish and functions; paths in netlify.toml under a base are relative to that base.
  • Deploy previews use deploy-preview variables; a missing preview value silently falls back to nothing, not to production.
  • _redirects must be in the publish directory after the build (copy it via the build), while netlify.toml sits at the base.
  • Large numbers of redirect rules slow matching; use splats and placeholders.
  • Scheduled functions do not run on deploy previews or branch deploys.
  • Newer accounts are on credit-based pricing where builds, bandwidth, requests and function compute draw from one pool; preview-heavy monorepos should use ignore.

Output

**Site:** acme-web (base apps/web, pnpm build, publish dist, Node 22)
**Contexts:** production / deploy-preview / branch-deploy (staging) with scoped env vars
**Routing:** /api/* proxied to api.acme.com; SPA fallback for /app/*; 301s for old blog URLs
**Headers:** immutable /assets/*; HSTS, nosniff, referrer policy
**Functions:** /api/orders (sync), report-background, cleanup (scheduled @daily)
**Verified:** deploy preview checked (redirects via curl -I, form submission received); rollback = publish deploy 65f...

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc