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

A drop-in Node script that runs 15 regression checks against your built _site/ folder before every Netlify deploy. Blocks force=true redirect loops, missing sitemap.xml, template leakage, orphaned pages, stale tool references, broken in-page anchors. Exit 1 = deploy aborted.

Author: J.A. Watte
Published: April 26, 2026
Source: https://jwatte.com/blog/blog-tool-predeploy-check/

---

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](/tools/predeploy-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)**

6. `_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.
7. No redirect target is a page that doesn't exist in `_site/`. A 301 to a 404 is worse than a bare 404.
8. `_redirects` file has no duplicate `from` paths with conflicting targets.

**Cross-reference integrity (4 checks)**

9. 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.
10. 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.
11. The tool hub's registry (`_data/toolRegistry.json`) has no entries whose slugs don't have a matching folder under `_site/tools/`.
12. No tool page references a `/js/*.js` or `/css/*.css` file that doesn't exist in `_site/`.

**Content integrity (3 checks)**

13. No HTML file contains unrendered Nunjucks / Liquid / Mustache syntax (`{{ something }}`). Template leakage is the most embarrassing deploy bug because it's visible on the page.
14. No HTML file contains `lorem ipsum` or the literal strings used as placeholders in the template system. Catches TODOs that shipped.
15. 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](/tools/predeploy-check/) has the current script. The short version:

```bash
# 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](/tools/analyzer/), the static-HTML health check for a single URL, which the pre-deploy script complements at build time
- [Sitemap Audit](/tools/sitemap-audit/), validates sitemap.xml against live URL probes post-deploy
- [Internal Link Auditor](/tools/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.*


---

Canonical HTML: https://jwatte.com/blog/blog-tool-predeploy-check/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/blog-tool-predeploy-check.webp
