← Back to Blog

The Pre-Deploy Hook That Kills Regressions Before Netlify Ships Them

The Pre-Deploy Hook That Kills Regressions Before Netlify Ships Them

The mistake that taught me to ship this tool: I pushed a release where the sitemap had regenerated with the wrong hostname, the _redirects file had a force=true rule pointing at a deleted page, and three tool hub entries pointed at slugs I'd renamed. Three separate bugs, each of which would have taken me ten seconds to catch at build time. All three made it to production.

The fix was a single Node script that runs 15 checks against the _site/ folder after Eleventy finishes building and before the Netlify CLI uploads it. If anything fails, exit code 1 — which tells the CLI to abort the deploy.

The Pre-Deploy Check page has the full script, the install instructions, and the check-by-check rationale. This post summarizes what it does and why each check earned its spot.

The 15 checks

Build sanity (5 checks)

  1. _site/ exists and is non-empty. Prevents "the deploy ran against last build because the new build silently failed."
  2. _site/index.html is present and over a minimum byte threshold. Catches template engines that silently emit a zero-byte file when a variable blows up.
  3. _site/sitemap.xml is present, is valid XML, and has a <urlset> or <sitemapindex> root. If the sitemap breaks, search engines stop re-crawling.
  4. _site/robots.txt is present and does not contain Disallow: / (unless explicitly flagged in config). Single most costly accidental deploy.
  5. Every HTML file in _site/ has a <title> element longer than 10 characters. Bare builds sometimes emit <title></title> when a front-matter field is missing.

Routing & redirects (3 checks)

  1. _redirects file (if present) has no force=true rule pointing at a URL that would create a loop with another rule. Netlify cheerfully serves a 301 loop until the browser gives up.
  2. No redirect target is a page that doesn't exist in _site/. A 301 to a 404 is worse than a bare 404.
  3. _redirects file has no duplicate from paths with conflicting targets.

Cross-reference integrity (4 checks)

  1. Every internal link in any HTML file points to a page that exists in _site/ (or a known external origin). Catches stale tool-hub references after a slug rename.
  2. Every in-page anchor (href="#section-x") resolves to an element with a matching id. Broken TOC links and broken skip links both fail this.
  3. The tool hub's registry (_data/toolRegistry.json) has no entries whose slugs don't have a matching folder under _site/tools/.
  4. No tool page references a /js/*.js or /css/*.css file that doesn't exist in _site/.

Content integrity (3 checks)

  1. No HTML file contains unrendered Nunjucks / Liquid / Mustache syntax (``). Template leakage is the most embarrassing deploy bug because it's visible on the page.
  2. No HTML file contains lorem ipsum or the literal strings used as placeholders in the template system. Catches TODOs that shipped.
  3. No HTML file contains a common secret pattern (OpenAI / Anthropic / GitHub / Stripe key regexes). Last-line defense — don't let a paste into the wrong file become a live secret.

How to install it

The Pre-Deploy Check page has the current script. The short version:

# save scripts/predeploy-check.mjs into your repo

# wire it into your deploy script (deploy-site.mjs, package.json, or Netlify build)
# it MUST run AFTER `npx @11ty/eleventy` and BEFORE `netlify deploy`

node scripts/predeploy-check.mjs
if [ $? -ne 0 ]; then
  echo "Pre-deploy check failed; aborting deploy."
  exit 1
fi

If you use the deploy-site.mjs pattern from the jwatte.com repo, the check slots in between the build step and the deploy step.

Why it's a script, not a Netlify plugin

Netlify plugins are more powerful (they can veto deploys even when netlify deploy is run from CI), but they require a Netlify config update and run inside Netlify's build container. A Node script runs anywhere — your local machine, a CI runner, a manual deploy-site.mjs invocation. If you care about pre-deploy guarantees, you want the check to run even when the deploy is happening from a laptop.

What I add over time

Every production bug caught in the wild becomes a 16th / 17th / 18th check. Current additions in my own copy include:

  • Flag any blog post with a heroImage that references a file that doesn't exist in _site/images/
  • Flag any internal anchor that would be hidden by a sticky header of known height (WCAG 2.4.11 focus-not-obscured)
  • Flag any page whose <title> length would be truncated by Google's 60-char SERP limit

None of those are in the public version yet. The 15 core checks are enough to stop the bugs I see most often across dozens of client sites.

Related tools

  • Site Analyzer — the static-HTML health check for a single URL, which the pre-deploy script complements at build time
  • Sitemap Audit — validates sitemap.xml against live URL probes post-deploy
  • Internal Link Auditor — crawls a live site and catches what the pre-deploy check cannot see (URLs that only break after CDN caching)

This script is provided as a reference implementation. It will prevent many categories of deploy regression but does not replace human review of the deploy preview. Always eyeball the Netlify deploy-preview URL before promoting to production on anything customer-facing.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026