Most Gatsby work that reaches us in 2026 does not start with "we want to build something." It starts with "the person who built this left, the build broke, and nobody here knows how it works." The repo exists. The deploy pipeline half-exists. Nobody can say with confidence what the site is made of, where the content lives, or what happens if someone runs npm install.
Before you can decide anything useful — upgrade, migrate, freeze, or rebuild — you need an inventory. This is the two-day audit we run on an inherited Gatsby codebase. It is deliberately read-only for the first day: you learn what you have before you change anything, because the fastest way to destroy the evidence is to let a package manager rewrite the lockfile on your first afternoon.
Day 1, morning: reproduce the build before you touch it
The single most valuable artifact is a build that works. Not a good build, not a modern build — any build. It becomes the baseline you diff everything against later.
Clone, and immediately make the current state recoverable:
git clone <repo> site-audit && cd site-audit
git log -1 --format='%H %ad %an %s' --date=short
git tag audit-baseline
cp package-lock.json package-lock.json.orig 2>/dev/null || true
Now find out which Node version this thing was born under. Check, in order: .nvmrc, engines in package.json, the CI config, and the lockfile's lockfileVersion.
cat .nvmrc 2>/dev/null
node -e "const p=require('./package.json');console.log(p.engines||'no engines field', p.dependencies.gatsby)"
grep -m1 lockfileVersion package-lock.json
ls .github/workflows netlify.toml vercel.json .circleci 2>/dev/null
A lockfileVersion: 1 means npm 6, which means Node 14 era, which means the build predates most of what is now on your machine. Map the Gatsby major to a plausible Node:
| Gatsby | Typical Node | Lockfile |
|---|---|---|
| v2 | 10–14 | v1 |
| v3 | 12–14 | v1/v2 |
| v4 | 14–16 | v2 |
| v5 | 18–22 | v2/v3 |
Then install without letting anything resolve fresh:
nvm install 16 && nvm use 16
npm ci --ignore-scripts
npm ci respects the lockfile exactly; npm install will happily "fix" it and you lose the historical record. --ignore-scripts is both a safety measure on an unaudited tree and a diagnostic: if the build later fails only without it, you have learned that something depends on a postinstall step, usually sharp fetching prebuilt binaries.
If npm ci fails outright, note the exact error and move on. A build that cannot be reproduced is itself a finding, and it changes the recommendation at the end of the audit. Do not spend the morning fighting it.
npx gatsby build 2>&1 | tee ../build-baseline.log
Whatever happens, you now have either public/ or a log that says why not.
Day 1, midday: inventory the dependency surface
Three questions: what is installed, what is abandoned, and what is pinned by nothing.
npm ls --depth=0 2>&1 | tee ../deps.txt
For every gatsby-* plugin in that list, check its last publish date and its peer range. This loop does it in one pass and is the single highest-signal command in the whole audit:
node -e "
const p=require('./package.json');
const deps={...p.dependencies,...p.devDependencies};
const names=Object.keys(deps).filter(n=>n.startsWith('gatsby'));
console.log(names.join('\n'));
" > ../plugins.txt
while read pkg; do
printf '%-45s' "$pkg"
npm view "$pkg" time.modified version --json 2>/dev/null | tr -d '\n' | cut -c1-80
echo
done < ../plugins.txt | tee ../plugin-age.txt
Anything whose last publish is more than ~two years old is a liability, and anything whose latest version still peer-depends on gatsby@^3 is a hard blocker for any upgrade path. Sort the list into four buckets and write them down, because this table is what the client is actually paying for:
- Core, maintained —
gatsby,gatsby-plugin-image,gatsby-source-filesystem. Fine. - Replaceable with first-party code — source plugins, sitemap/robots generators, small utility plugins. A hundred lines you own beats an abandoned dependency.
- Load-bearing and abandoned — a CMS source plugin, an i18n plugin, a theme. These set the project's difficulty.
- Dead weight — plugins listed but never referenced in
gatsby-config. Delete candidates.
Cross-check the last bucket:
for pkg in $(cat ../plugins.txt); do
grep -rq "$pkg" gatsby-config.* gatsby-node.* src/ 2>/dev/null || echo "UNUSED? $pkg"
done
Day 1, afternoon: map content and page creation
Where do pages come from? There are only four possibilities, and a real site usually has three of them.
ls src/pages # 1. file-system routes
grep -n "createPage\|createPages\|createRedirect" gatsby-node.* | head -40 # 2. programmatic
grep -rn "src/templates" gatsby-node.* | head # 3. templates behind programmatic routes
ls src/pages/*\[*\]* 2>/dev/null # 4. client-only / dynamic routes
Then find the content source. Read gatsby-config plugins for gatsby-source-* entries and chase each one:
gatsby-source-filesystem→ content is in the repo; you have everything.gatsby-source-contentful/-sanity/-datocms/-wordpress→ content is in someone else's system, and you need credentials before you can build at all. This is the question to ask the client on day one, because getting CMS access reliably takes longer than the rest of the audit.- A bespoke
sourceNodesingatsby-node.jshitting an internal API → check whether that API is still up.
Enumerate the environment variables the build actually reads, since there is rarely a .env.example:
grep -rhoE "process\.env\.[A-Z0-9_]+" src/ gatsby-*.js plugins/ 2>/dev/null \
| sort -u | sed 's/process\.env\.//'
That list is your credentials request. Send it as a single email; do not discover them one failed build at a time.
Day 1, end: the deployed site is also evidence
The repo tells you what the code intends. The live site tells you what is actually serving — and after a few years of "quick fixes in the Netlify UI," those differ more often than not.
BASE=https://www.example.com
wget --spider -r -l 3 -o ../crawl.log "$BASE"
grep -c '^--' ../crawl.log # rough URL count
curl -sI "$BASE/" | grep -iE 'server|x-powered|cache-control|x-nf|cf-ray'
curl -s "$BASE/sitemap-index.xml" | head
Compare the live URL count to the page count your local build emitted (find public -name index.html | wc -l). A large gap means either the build is missing content it cannot reach without credentials, or the live site contains pages that no longer exist in the repo. Both are findings worth money.
Check response headers for the host, for redirect rules that live in a dashboard rather than netlify.toml or _redirects, and for whether anything is being served by a function. Note the trailing-slash behaviour; it decides a lot later.
Day 2, morning: the controlled experiments
Now you may change things — on branches, one variable at a time, measuring each.
Experiment 1: does it build on a modern Node? This is the cheapest signal about the project's future.
git switch -c audit/node-22 && nvm use 22
npm ci --ignore-scripts && npx gatsby build 2>&1 | tail -40
Typical failures and what they mean:
digital envelope routines::unsupported— Node 17+ with old webpack. Workaround exists (NODE_OPTIONS=--openssl-legacy-provider); it signals a Gatsby 2/3 era build.Cannot find module 'sharp'or an ABI mismatch — native binary built for the wrong Node. Usually a rebuild away, occasionally an upgrade.ERR_REQUIRE_ESMfrom a remark or rehype plugin — the ESM wall, and a real piece of work.- Query errors on fields that existed yesterday — inferred schema drift, not a Node problem.
Experiment 2: what does a fresh resolve do? On a throwaway branch, delete the lockfile and resolve against today's registry. You are not keeping this; you are measuring how far the floor has moved.
git switch -c audit/fresh-resolve
rm package-lock.json && npm install --ignore-scripts 2>&1 | tail -30
npm ls --depth=0 2>&1 | grep -c 'invalid\|UNMET'
Experiment 3: is the output stable? Build twice from a clean cache and diff the HTML. Non-determinism — hashes in content, timestamps rendered into pages, randomly ordered lists — makes every future visual diff useless, and it is worth knowing before you promise anyone a safe migration.
rm -rf .cache public && npx gatsby build && cp -r public ../build-a
rm -rf .cache public && npx gatsby build && cp -r public ../build-b
diff -rq ../build-a ../build-b | grep index.html | head
Day 2, afternoon: write the findings down
The deliverable is not a cleaned-up repo. It is a one-page document that lets a non-technical owner make a decision. Ours has five sections:
1. What this site is. Gatsby version, Node version, hosting, content source, page count, who can deploy it today. Five lines.
2. Can it be built? Yes / yes-with-caveats / no, with the exact reproduction steps. If no, the cost of making it buildable, stated separately from everything else.
3. Risk register. Each item with a likelihood and a blast radius: abandoned load-bearing plugin, expiring API token, Node version that CI will drop, no deploy access, content locked in a CMS nobody pays for. Sorted by what breaks the site soonest, not by what is most interesting to fix.
4. The URL surface. Live URL count, how many the current build reproduces, and whether redirects live in code or in a dashboard. This number governs the risk of any future replatform.
5. Three options with rough numbers. Always three, always honest:
- Stabilise — pin everything, get CI building again, add a smoke test, leave the architecture alone. Right when the site is finished and just needs to keep working.
- Upgrade in place — step through the Gatsby majors, replace the dead plugins, modernise the toolchain. Right when the site is still being actively developed and the content model is sound.
- Replatform — move the content to a framework with a future. Right when the plugin bucket labelled "load-bearing and abandoned" has more than one or two entries, or when the build cannot be reproduced at all.
Resist the pull toward the option that is most fun to execute. A site with six pages, no CMS, and a broken build does not need a migration plan; it needs two days and a static export. A site with 4,000 pages, live Contentful content, and an abandoned source plugin is not going to be stabilised into safety, however appealing that quote looks.
The things that are not in the repo
Finally, ask the questions no amount of code reading answers, and write the answers in the same document:
- Who owns the domain and the DNS? Who owns the hosting account, and is anyone still paying for it?
- Who can merge to the default branch, and does a merge deploy automatically?
- Which third-party accounts does the site depend on — CMS, forms, analytics, search, image CDN — and is each one still funded and accessible?
- Is there an analytics property, and does anyone look at it? If traffic is zero, the honest recommendation may be to retire the site rather than invest in it.
- What is the content update cadence? A site edited weekly and a site untouched since 2021 deserve entirely different architectures.
An audit that answers those alongside the technical inventory is worth far more than one that produces a perfectly tidy dependency graph. The code is usually the easy part; the missing credentials, the unpaid CMS, and the nobody-left-who-knows are what actually block the work.
If you have inherited a Gatsby codebase and need an independent read on whether to stabilise, upgrade, or replatform it, get in touch — a fixed-scope audit like the one above is usually the cheapest first step a team can take.