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)
_site/exists and is non-empty. Prevents "the deploy ran against last build because the new build silently failed."_site/index.htmlis present and over a minimum byte threshold. Catches template engines that silently emit a zero-byte file when a variable blows up._site/sitemap.xmlis present, is valid XML, and has a<urlset>or<sitemapindex>root. If the sitemap breaks, search engines stop re-crawling._site/robots.txtis present and does not containDisallow: /(unless explicitly flagged in config). Single most costly accidental deploy.- 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)
_redirectsfile (if present) has noforce=truerule pointing at a URL that would create a loop with another rule. Netlify cheerfully serves a 301 loop until the browser gives up.- No redirect target is a page that doesn't exist in
_site/. A 301 to a 404 is worse than a bare 404. _redirectsfile has no duplicatefrompaths with conflicting targets.
Cross-reference integrity (4 checks)
- 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. - Every in-page anchor (
href="#section-x") resolves to an element with a matchingid. Broken TOC links and broken skip links both fail this. - The tool hub's registry (
_data/toolRegistry.json) has no entries whose slugs don't have a matching folder under_site/tools/. - No tool page references a
/js/*.jsor/css/*.cssfile that doesn't exist in_site/.
Content integrity (3 checks)
- No HTML file contains unrendered Nunjucks / Liquid / Mustache syntax (``). Template leakage is the most embarrassing deploy bug because it's visible on the page.
- No HTML file contains
lorem ipsumor the literal strings used as placeholders in the template system. Catches TODOs that shipped. - 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
heroImagethat 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.