Upgrade/gatsby 5 - #152
Merged
Merged
Upgrade/gatsby 5#152
Conversation
C1 of the Gatsby 5 / Node 24 / React 18 / Decap 3 upgrade.
Captures the reference state before anything moves. This has to come first:
the Gatsby 2 tree cannot install or build on Node 24 (sharp 0.23.4 will not
compile), so once the upgrade starts there is no way to produce a "before"
to compare against. Baselines are taken from production instead.
The harness is deliberately self-contained — tests/ has its own package.json
and lockfile, installed and run independently of the root. The root is still
the Gatsby 2 tree and will not npm install on Node 24, so anything added
there would be unrunnable today.
Three suites:
smoke crawls built public/**/*.html off disk — title/description/canonical
/h1 on every page, no broken internal links, no images missing from
disk, sitemap + rss non-empty. No server needed. This is the suite
that would have caught the 2026-08-25 break, where the site stopped
building with no code change.
visual 6 pages x 3 viewports, diffed against production baselines.
e2e reader journeys, Algolia search, /admin/ boots.
The first visual capture was unusable: a deterministic 8% diff on every
re-run, because the navigator animates between its featured/aside states via
chained setTimeouts in src/utils/shared.js while SpringScrollbars runs spring
physics. The suite now zeroes animation/transition durations, force-scrolls
to trigger react-lazyload, waits on document.fonts.ready and network idle,
then settles. Three consecutive clean runs after that.
Selectors are structural throughout. Every styled element carries a generated
JSS class (jss5, jss122, ...) numbered by stylesheet registration order —
not stable across builds, and renumbered wholesale by the react-jss 8 -> 10
upgrade this harness exists to verify.
Baseline recorded in tests/BASELINE.md. Notable finding: there are no open
editorial-workflow entries — every cms/* PR is merged or closed. The Decap 3
label-prefix trap that nearly cost nyc-spa 86 in-flight drafts has nothing to
bite on here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C2 of the upgrade. Gatsby 2.0.35 (Oct 2018) -> 5.16.1, React 16.14 -> 18.3.1, @material-ui 3 -> @mui 9.4.0, react-jss 8 -> 10, yarn -> npm on Node 24. These move together by necessity: Gatsby 5 peers react ^18, MUI v3 cannot run on React 18, and MUI v5+ cannot run on React 16. There is no version-legal intermediate step, so this is one commit rather than a broken sequence. Verified: 149/149 pages build, 23 harness tests pass (4 skip -- they need Algolia credentials this build does not have), lint clean, and all 28 content pages render structurally identical to production. Styling: the app styles through react-jss, not MUI. v10 still exports withStyles as its default export, so all 31 `injectSheet(styles)(X)` call sites and every style object are untouched. What did have to change is the theme wiring: MUI v3 and react-jss v8 shared a theme broadcast channel (the `theming` + `brcast` packages), which is why `theme => ({...})` worked with only a MuiThemeProvider in the tree. MUI v5+ dropped that channel, so react-jss now gets its own ThemeProvider fed the same theme object. Bugs found and fixed while getting hydration clean -- each rendered the page blank or silently restyled it: * SVG imports. Stripping the `!svg-react-loader!` prefix looked like tidying up, but the `?name=X` query is load-bearing: svg-react-loader names the generated component after the file, so react.svg produced an identifier that collided with the loader's own `import React from "react"` and the module exported the React library. Rendering it threw "element type is invalid" and blanked every page. Also fixed the plugin options, which need `rule.include` -- a top-level `include` is silently ignored by v3. * JSS class names could never match between server and client: react-jss prefixes them with the component displayName, minified in the browser bundle ("t-") and not in the SSR bundle ("List-"). id={{minify:true}} drops the prefix. * Emotion injected MUI's styles into the body during SSR. Added proper extraction into <head> via @emotion/server. * ActionsBar rendered `screenfull.isEnabled` during hydration -- false on the server, true on the client. Gated behind a mounted flag, as is the localStorage font size read that used to run in componentWillMount. * CssBaseline removed. globals.js already carries a full normalize reset, and MUI 9's baseline additionally forces body font-size from theme.typography (1rem x 16/14 = 18.29px where this design inherits 16px) and breaks the html.wf-active webfont switch by setting body font-family explicitly. * theme.spacing.unit is gone in MUI v5+ (it is a function now). It evaluated to undefined, dropping the tag chip margins and shifting tagged pages 16px. * decap-cms-app pinned to 3.6.4, the version nyc-spa runs with gatsby-plugin-decap-cms 4.0.4. On 3.16.0 the admin panel failed to boot ("DecapCmsApp is not defined"); on 3.6.4 it reaches the Identity login with no console errors. * graphql pinned to 16.14.2 -- decap-cms-app pulls graphql 15 through apollo, and two copies made graphql-compose reject Gatsby's own scalar types. Content: gatsby-transformer-remark 6 parses CommonMark strictly, so lazy list continuations (an unindented line after `1. item`) now break the list. This silently reflowed 5 pages. Continuation lines are indented; a structural comparison of all 28 pages against production now reports zero differences. Also: react-popper 0.8 -> MUI's own Popper (drops react-popper and classnames), react-instantsearch 5 -> 7, react-share 5 (LinkedinShareCount no longer exists), redux 5 legacy_createStore, screenfull 6 isEnabled, StaticQuery -> useStaticQuery, react-helmet -> Gatsby Head API, GA (dead Universal Analytics) -> Tag Manager behind GTM_ID, and Gatsby 5 GraphQL sort/group syntax. Dropped 21 dependencies that were unused or replaced, including 9 that were never imported at all (react-loadable, react-headroom, react-facebook, gatsby-image, ...). Removed src/withRoot.js and src/getPageContext.js: MUI v3 plumbing that also threaded a prop named `pageContext`, colliding with the one Gatsby passes to page components. Sitemap plugin 6 emits sitemap-index.xml + sitemap-0.xml where v2 wrote sitemap.xml, so static/_redirects keeps the published URL alive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C3 of the upgrade. The plugin swap landed in C2 so the site could build; this
is the configuration that makes the panel behave.
* cms_label_prefix: 'netlify-cms/'. Decap 3 filters the Workflow board by
'decap-cms/*' labels and its built-in migration skips PRs it cannot find in
the legacy refs/meta/_netlify_cms store. There are no open workflow entries
right now (checked across every cms/* PR -- all merged or closed, recorded
in tests/BASELINE.md), so unlike nyc-spa nothing is at risk today. It is set
anyway: anything an editor starts between now and the deploy would
otherwise disappear from the Workflow tab with no error shown.
* repo: xsolve-pl/boldare-tech -> boldare/boldare-tech. git-gateway ignores
the field, but the stale org name misleads anyone reading the config.
* X-Robots-Tag: noindex, nofollow on /admin/*.
* A pages collection. content/pages/*.md already become real pages via
gatsby-node.js and are linked from the top menu, but had no collection, so
they could only be changed by editing git directly. create/delete are off:
adding a page also means touching the menu, which is a code change.
Verified against the built output: /admin/ reaches the Netlify Identity login
with no console errors and no config error, and the harness stays green
(23 passed, 4 skipped for absent Algolia credentials).
Still needs a real login on a deploy preview to exercise Identity auth, the
editorial workflow round-trip and publishing -- none of that can be checked
locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C4 of the upgrade, and the first CI this repo has ever had. Build on every push rather than only on the default branch. The failure that prompted this work was a build break with no code change -- GitHub disabled the git:// protocol on 2026-08-25 and the Netlify install started failing -- so the build is the test, and it has to run where it can still be seen early. Push is the trigger that matters here, since the branch is reviewed directly rather than through pull requests. The pull_request trigger is one line and means the checks are already wired if that changes. Sequence, matching what was run locally to validate it: npm ci, lint, build, smoke against public/ on disk, then gatsby serve with e2e and visual against it. Pinned to ubuntu-24.04 because Playwright snapshots are platform-suffixed (-linux.png) and would not be found elsewhere. Failure artifacts are uploaded for the visual diffs, which are unreadable from logs alone. Serving waits with a curl poll rather than npx wait-on, to avoid fetching an undeclared package mid-run. Also brings the tooling forward with the rest of the stack: eslint 4 -> 8.57 with @babel/eslint-parser (babel-eslint is deprecated and cannot parse modern syntax), prettier 1 -> 2.8.8, and eslint-config-google dropped. .prettierrc pins arrowParens "avoid" and trailingComma "none" -- prettier 2 changed both defaults, and taking the new ones would have reformatted every file and buried the actual upgrade diff in noise. Verified by running the whole sequence locally from a clean `npm ci` in both the root and tests/: lint 0 errors, 149/149 pages, 5 smoke, 3 e2e, 15 visual, 4 skipped for absent Algolia credentials. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repo moved from yarn to npm in the Gatsby 5 upgrade, so `yarn add-article` no longer works. The yarn commands inside the Docker post are about a different project and are left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for boldare-tech ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
The CI lint step failed on all 63 source files with "babel-preset-gatsby has been loaded, which consumes config generated by the Gatsby CLI". .eslintrc.json passed `babel-preset-gatsby` to @babel/eslint-parser. That preset reads config Gatsby writes into .cache/ during a build, so it only works after `gatsby build` has run. CI lints before building, on a fresh checkout with no .cache, so it could never work there. It passed locally purely because .cache/ was left over from earlier builds. That made the local check meaningless -- deleting .cache reproduces the CI failure exactly, 63 for 63. Lint has no business depending on build output, so the preset is gone rather than worked around with NODE_ENV=test. @babel/eslint-parser with requireConfigFile:false covers everything here except JSX, which needs @babel/plugin-syntax-jsx. That was resolving as an undeclared transitive dependency -- the same fragility in a different guise -- so it is now an explicit devDependency. Verified the way CI actually does it: rm -rf .cache public node_modules, npm ci, then lint (0 errors), build (149/149), smoke (5), e2e (3), visual (15). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two real visual regressions the deploy preview exposed, both invisible until
the rendered pages were compared against production pixel by pixel.
react-lazyload 3 wraps its children in a <div class="lazyload-wrapper"> that
2.x did not emit. The thumbnail rule is `.listItemPointer img { height: 100% }`,
and with the wrapper in between there is no longer a definite height to
resolve against, so every post thumbnail collapsed from the cropped 90x90 to
its natural aspect ratio (90x51). Giving the wrapper the pointer's dimensions
restores it.
MUI 9 sets the Chip label's line-height to 1.5 where v3 used 1.6. The chip box
is identical either way -- verified to the hundredth of a pixel against
production, 82.53 x 32 at the same x -- but the label text sits ~1.5px higher,
which showed up as text outlines across all ~120 chips on /tags/.
Both now match production exactly.
The visual threshold goes from 0.02 to 0.004. 0.02 was chosen for
anti-aliasing tolerance but was loose enough to pass the thumbnail regression
silently: the images are a small share of a full-page screenshot, so a bug
affecting every one of them still came in under 2%. The suite only earns its
keep if it fails on things like that.
The suite also now removes Netlify's deploy-preview drawer (an iframe from
app.netlify.com) before screenshotting. It was in the mask list, but masking
paints a block the production baseline does not have, turning a preview-only
widget into a guaranteed diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The deploy preview 404s on every root path: /, /admin/, /rss.xml, /robots.txt, and the /sitemap.xml redirect lands on a dead target. Only /tech-blog/* works. Cause: Gatsby 5's adapter prefixes every route with pathPrefix (gatsby/dist/utils/adapter/manager.js, "if (pathPrefix && !route.path .startsWith(pathPrefix)) route.path = join(pathPrefix, route.path)"), so with an adapter the deployed site genuinely lives at /tech-blog/ on the origin. Gatsby 2 did the opposite: --prefix-paths only rewrote URLs and left the publish directory flat, which is what the old "/tech-blog/* /:splat 200" rule existed to paper over. Confirmed with a hashed asset, which no redirect rule touches: /app-dd2066b375cb20455af9.js 404 /tech-blog/app-dd2066b375cb20455af9.js 200 This breaks the public site, not just previews. www.boldare.com proxies with the prefix STRIPPED -- "/tech-blog/* https://tech.boldare.com/:splat 200" in nyc-spa's static/_redirects -- so a reader on www.boldare.com/tech-blog/foo/ reaches this origin as /foo/, which now has nothing behind it. Every page of the blog would 404. So the old prefixed->unprefixed rewrite is replaced by its inverse. Netlify only applies a non-forced rule when no file matches, so /tech-blog/* is still served straight from disk and only unprefixed paths fall through. Note this is the narrower of two possible fixes. The other is to stop stripping the prefix in nyc-spa ("/tech-blog/* https://tech.boldare.com/tech-blog/:splat 200"), which is architecturally cleaner -- the origin would then be addressed the way it is actually laid out -- but it changes a different site, so it is a call to make deliberately rather than in passing. Not verifiable locally: gatsby serve ignores _redirects entirely, and the adapter only rewrites routes on a real Netlify build. This needs the redeploy to confirm, specifically that /, /admin/ and /rss.xml resolve and that Netlify Identity's /.netlify/* endpoints are untouched by the catch-all. Also checked while here: the per-post /<slug>/edit redirects and /a from gatsby-node.js createRedirect are not emitted on Netlify (the adapter disables gatsby-plugin-netlify). They already 404 on the production origin and nothing in the app links to them, so that is pre-existing dead weight, not a regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The admin check asserted only that document.body had some text and threw no
errors. A 404 page satisfies both, so it passed against a deploy preview where
/admin/ did not resolve at all -- the one test guarding the highest-risk part
of this migration was reporting green on a broken panel.
It now polls for text Decap itself renders ("Login with Netlify Identity" or
"Content Manager"). Verified it discriminates: passes against production and
against a local build, fails against the preview until the routing fix ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI failed on `post` at tablet with a deterministic 5305-pixel diff — the same count across all three retries, so not flakiness. The artifact shows the page matching everywhere except the monospace text: the inline `docker-compose.yml` and the code block, doubled and horizontally offset, with the surrounding line reflowed around the wider glyphs. `monospace` is whatever font the operating system supplies, and it resolves three different ways here: dev machine DejaVu Sans Mono GitHub runner (different again) mcr.microsoft.com/playwright WenQuanYi Zen Hei Mono Body text hid this completely, because Open Sans is a webfont and downloads identically everywhere. Only code blocks and inline <code> exposed it, which is why exactly two pages moved when the baselines were regenerated: `page` and `post`, the two with code in them. So a screenshot baseline is only meaningful alongside the environment that rasterised it. Capture and comparison now both run in the Playwright image, and the CI job runs the visual suite there too (build and serve stay on the runner; the container joins the host network to reach gatsby serve). Baselines are regenerated from production, which keeps the property that matters: they are still the pre-upgrade site, not a snapshot of our own output. `test:visual:update` now shells into the container itself, so the obvious command is the correct one rather than a trap that silently produces host-only baselines. tests/README.md explains why. Verified end to end locally: baselines regenerated from production in the container (16 passed), local build compared against them in the container (15 passed, 3 skipped for absent Algolia credentials), smoke 5 and e2e 3 on the host unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Netlify build failed in onPostBuild:
the objects must have internal.contentDigest. Current object: {...}
gatsby-plugin-algolia 1.x drives partial updates off internal.contentDigest and
calls panicOnBuild when an object lacks it. 0.2 never looked at the field, so
two things that were harmless before are now fatal: the query never selected
contentDigest, and the chunking transformer replaced `internal` wholesale with
`{ content: chunk }`, which would have dropped it anyway.
The query now selects it and the transformer spreads node.internal instead of
replacing it.
This never showed up locally or in CI because the plugin is only registered when
ALGOLIA_* credentials are present, and neither environment has them -- the whole
indexing path was invisible. So tests/smoke/algolia-config.spec.js now exercises
the query and transformer directly, with no network and no credentials. Verified
it discriminates by reintroducing the bug: the contentDigest assertion fails,
and passes again once reverted.
Verified the query itself compiles by building with dummy credentials: the run
reaches "Getting existing objects" and only then fails on the unreachable fake
app, so GraphQL and the transformer both executed.
Also drops `require("dotenv").config()` from gatsby-ssr.js. gatsby-config.js
already calls it in the same process, so the second call only pulled fs into
the SSR bundle -- the "Unsafe builtin usage fs.readFileSync" warning in the same
build log. Confirmed FB_APP_ID still reaches the rendered output afterwards.
Local: lint 0 errors, 149/149 pages, smoke 7, e2e 3, visual 15 in the container.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three differences reported against the deploy preview, all confirmed by measurement against production. Right-hand action bar icons sat at 43x43 instead of 48x48. Two MUI 9 changes compound: IconButton's padding went 12px -> 8px, and SvgIcon's fixed 24px became a pxToRem value, which this theme's typography.fontSize of 16 inflates by 16/14 to 27.43px. Pinned back in the theme, so every icon on the site is 48x48 with a 24px glyph again. Tag chips read lighter because v3 filled a solid #e0e0e0 while v9 uses an rgba(0,0,0,0.08) overlay. Pinned to the solid fill. Sidebar thumbnails rendered as broken-image placeholders, and that one is mine. The deployed layout is MIXED: Gatsby 5's adapter prefixes routes, so pages and bundles live under /tech-blog/, but files copied from static/ do not move -- /img/*, /icons/*, /avatar.jpg and the hashed /static/* stay at the root. The previous commit removed "/tech-blog/* /:splat 200" as obsolete; it was still the rule resolving the prefixed image URLs in the HTML onto those root-level files. Both directions are now present, with the shadowing that keeps them apart spelled out: Netlify only applies a non-forced rule when no file matches, so pages resolve one way and static assets the other. Neither the local build nor the deploy preview could have caught the icon sizing: the visual suite passed the whole time. Four small buttons are a rounding error on a full-page screenshot, so a 5px error on every icon on the site stayed under the pixel-ratio threshold. Lowering it further would only buy noise, so e2e/widget-metrics.spec.js asserts the numbers instead -- button box, padding, glyph size, chip fill, and that navigator thumbnails are square (the react-lazyload wrapper regression). Every expected value is what production renders; the suite passes against production and against the build. Local: lint 0 errors, 149/149 pages, smoke 7, e2e 6, visual 15 in the container. The static asset fix is in _redirects, which gatsby serve ignores, so it stays unverified until the next deploy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Production paints body #fafafa; the preview left it transparent, so the page rendered pure white. Another consequence of dropping CssBaseline. MUI v3's baseline set the body background from palette.background.default, and v3 defaulted that to #fafafa — so the shade the site has always shipped came from a framework default, not from this project's palette, which says #fff throughout. CssBaseline still had to go (it forced body font-size and font-family, breaking the html.wf-active webfont switch), so the background is now declared explicitly, as a named colour rather than a literal buried in a rule. The visual suite is structurally incapable of catching this. toHaveScreenshot has a per-pixel colour tolerance of its own — `threshold`, 0.2 by default — and #fafafa against #ffffff is 5/255 per channel, comfortably inside it. Not a single pixel counts as different, so the entire page can change shade with all 18 screenshots green. That is worth knowing about the suite generally: it is good at geometry and blind to subtle colour. So this is asserted numerically alongside the other widget metrics. Verified it discriminates: passes against production, fails against the current preview with rgba(0, 0, 0, 0). Local: lint 0 errors, 149/149 pages, smoke 7, e2e 7, visual 15 in the container. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Share icons sat flush against each other. The margin is applied by class name, and react-share renamed it between v2 (div.SocialMediaShareButton) and v5 (button.react-share__ShareButton), so the rule silently stopped matching. Button origins are back to production's 788/850/911/973. One more lazy list continuation, in the Slack/Spotify post. A line that continued a list item in production became a paragraph of its own under CommonMark-strict parsing. Same fix as the earlier batch: indent it. That one survived the previous sweep because the structural comparison counted ol/ul/li/pre/h2/h3/table/img but not p, so a paragraph appearing or vanishing was invisible to it. The comparison now counts p and li>p as well, and across all 28 pages only the Docker post still differs -- see below. Both are asserted numerically now, next to the other widget metrics, and both pass against production as well as the build. Remaining, deliberately not changed: the table of contents on Dev-and-prod-ready-Docker-setup-for-SPA-app renders tight (no <p> inside <li>) where production renders loose, making it ~96px shorter. The source has no blank lines between the items, which per CommonMark is a tight list -- so this is the new parser being correct and remark 9 having been lenient. Forcing the old rendering means putting blank lines through the TOC source purely to recreate spacing, and the CSS alternative would have to loosen every list on the site. Left as a call for a human. Local: lint 0 errors, 149/149 pages, smoke 7, e2e 8, visual 15 in the container. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.