diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 40c762b..0000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -public/* -static/* -.cache/* \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 52e8c87..93a31de 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,51 +1,50 @@ { - "parser": "babel-eslint", + "parser": "@babel/eslint-parser", "extends": [ - "google", - "prettier", - "prettier/react", "eslint:recommended", - "plugin:react/recommended" + "plugin:react/recommended", + "prettier" ], "parserOptions": { - "ecmaVersion": 2016, + "ecmaVersion": 2022, "sourceType": "module", + "requireConfigFile": false, "ecmaFeatures": { "jsx": true + }, + "babelOptions": { + "plugins": [ + "@babel/plugin-syntax-jsx" + ] } }, - "plugins": ["prettier", "react", "jsx-a11y", "import", "graphql"], + "plugins": [ + "prettier", + "react", + "jsx-a11y", + "import" + ], "env": { "browser": true, - "es6": true, + "es2022": true, "node": true }, + "settings": { + "react": { + "version": "detect" + } + }, "rules": { - "arrow-body-style": "off", - "camelcase": "warn", - "func-names": "off", - "global-require": "warn", - "import/no-dynamic-require": "warn", - "import/no-extraneous-dependencies": "off", "no-console": "off", - "no-multi-assign": "off", - "no-param-reassign": "warn", - "no-plusplus": "off", - "no-shadow": "warn", - "no-underscore-dangle": "warn", - "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }], - "no-nested-ternary": "off", "no-unused-vars": "warn", - "prefer-destructuring": "off", - "prettier/prettier": ["error"], + "prettier/prettier": "error", + "react/prop-types": "off", "react/jsx-uses-vars": "error" }, - "overrides": [ - { - "files": ["*.spec.js", "*.integration.js"], - "rules": { - "no-unused-expressions": "off" - } - } + "ignorePatterns": [ + "public/", + "static/", + ".cache/", + "tests/" ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7f8edfa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: CI + +# Push is the trigger that matters: this repo is reviewed off a branch rather +# than through pull requests. The pull_request trigger costs one line and means +# the checks are already wired if that ever changes. +on: + push: + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Lint, build and verify + # Playwright snapshots are platform-suffixed (-linux.png). Must stay Linux. + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "npm" + + - name: Install + run: npm ci + + - name: Lint + run: npm run lint + + # The build is the test. This site broke on 2026-08-25 with no code + # change, when GitHub disabled git:// and the Netlify install started + # failing; nothing in the repo would have caught it. + - name: Build + run: npm run build + env: + GATSBY_TELEMETRY_DISABLED: "1" + NODE_OPTIONS: "--max_old_space_size=6144" + + - name: Install test harness + working-directory: tests + run: | + npm ci + npx playwright install --with-deps chromium + + # Reads public/ off disk, so it needs no server. + - name: Smoke (build output) + working-directory: tests + run: npm run test:smoke + + - name: Serve the build + run: | + npx gatsby serve --prefix-paths -p 9100 & + for i in $(seq 1 60); do + if curl -sf -o /dev/null http://localhost:9100/tech-blog/; then + echo "serving after ${i}s"; exit 0 + fi + sleep 1 + done + echo "gatsby serve did not come up within 60s"; exit 1 + + - name: End-to-end + working-directory: tests + run: npm run test:e2e + env: + BASE_URL: http://localhost:9100/tech-blog/ + + # Runs in the Playwright container, not on the runner. Screenshot + # comparison is only meaningful if the baselines were rasterised in the + # same environment: `monospace` is whatever font the OS supplies, and it + # resolves three different ways across a dev machine (DejaVu Sans Mono), + # this runner, and this image (WenQuanYi Zen Hei Mono). Body text hid the + # problem because Open Sans is a webfont; the code blocks did not. + # tests/README.md documents regenerating baselines the same way. + - name: Visual regression + run: | + docker run --rm --network host \ + -v "$PWD/tests":/tests -w /tests \ + --user "$(id -u):$(id -g)" -e HOME=/tmp \ + -e BASE_URL=http://localhost:9100/tech-blog/ \ + -e CI=true \ + mcr.microsoft.com/playwright:v1.62.1-noble \ + npx playwright test --project=desktop --project=tablet --project=mobile + + - name: Upload failure artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + tests/test-results/ + tests/playwright-report/ + retention-days: 7 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..42b7fbc --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +# gatsby-plugin-decap-cms declares its webpack toolchain as peer dependencies and +# several Gatsby 5 plugins still name React 16/17 in their peer ranges. Matching +# nyc-spa, which ships this same stack on Netlify. +legacy-peer-deps=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.prettierrc.json b/.prettierrc.json index 7733a7e..041f04f 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,6 @@ { "printWidth": 100, - "singleQuote": false + "singleQuote": false, + "arrowParens": "avoid", + "trailingComma": "none" } diff --git a/README.md b/README.md index c702684..09d347f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ boldare-tech/ 1. Clone this repository or add/edit a file [directly on GitHub](https://github.com/boldare/boldare-tech/new/master/content/posts) -2. Use `yarn add-article` command to create a new structure for the post πŸ“‚ +2. Use `npm run add-article` command to create a new structure for the post πŸ“‚ 3. Write an article in [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) syntax and save it in created file by script, English language is preferred ✍️ 4. Put an image (post avatar) in `static/img`, technology icon is preferred πŸ–Ό diff --git a/content/meta/config.js b/content/meta/config.js index f7aadd9..cd83ac2 100644 --- a/content/meta/config.js +++ b/content/meta/config.js @@ -43,6 +43,9 @@ Any helpful contributions are welcome. 3 lines, 3 paragraphs? Doesn’t matter.` appId: process.env.FB_APP_ID || "" }, google: { - analyticsId: process.env.GOOGLE_ANALYTICS_ID || "" + // Universal Analytics was shut down by Google, so the old GOOGLE_ANALYTICS_ID + // collected nothing. Tag Manager, matching the main Boldare site. The plugin + // is only registered when this is set, so builds work without it. + tagManagerId: process.env.GTM_ID || "" } }; diff --git a/content/pages/how-to-contribute.md b/content/pages/how-to-contribute.md index 0f89f56..5deb206 100644 --- a/content/pages/how-to-contribute.md +++ b/content/pages/how-to-contribute.md @@ -14,7 +14,7 @@ There are two ways to contribute: πŸ‘‰ Using CMS system 1. Go to [admin panel](../admin/) -or click `+` icon in sidebar (skip points 2. and 3. in that case). + or click `+` icon in sidebar (skip points 2. and 3. in that case). 2. Go to `Blog Articles` or click `Quick add` at the very top of the page. 3. Click `New Blog Article`. 4. Create article by providing all required data, also upload `cover`. @@ -40,10 +40,10 @@ boldare-tech/ ``` 1. Clone this repository or add/edit a file -[directly on GitHub](https://github.com/boldare/boldare-tech/new/master/content/posts) -2. Use `yarn add-article` command to create a new structure for the post πŸ“‚ + [directly on GitHub](https://github.com/boldare/boldare-tech/new/master/content/posts) +2. Use `npm run add-article` command to create a new structure for the post πŸ“‚ 3. Write an article in [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) -syntax and save it in created file by script, English language is preferred ✍️ + syntax and save it in created file by script, English language is preferred ✍️ 4. Put an image (post avatar) in `static/img`, technology icon is preferred πŸ–Ό 5. Update the following required tags at the very top of the article πŸ‘† @@ -59,4 +59,4 @@ postAuthor: Your Name 6. Create a pull request to `master` branch to collaborate πŸ™Œ 7. Before merging remember to bump the article date in filename -to the current date + to the current date diff --git a/content/posts/2018-04-03_how-does-slack-communicate-with-spotify.md b/content/posts/2018-04-03_how-does-slack-communicate-with-spotify.md index 9abe34b..2441a23 100644 --- a/content/posts/2018-04-03_how-does-slack-communicate-with-spotify.md +++ b/content/posts/2018-04-03_how-does-slack-communicate-with-spotify.md @@ -44,7 +44,7 @@ More precisely: - the app exposes HTTP port 4381, - request path is `/play.json`, - request parameters contain data about the song/album to be launched and some -additional security-related parameters (CSRF, oauth). + additional security-related parameters (CSRF, oauth). We found it! diff --git a/content/posts/2018-04-09_is-mysql-autoincrement-really-monotonic.md b/content/posts/2018-04-09_is-mysql-autoincrement-really-monotonic.md index 19aaf3c..9194386 100644 --- a/content/posts/2018-04-09_is-mysql-autoincrement-really-monotonic.md +++ b/content/posts/2018-04-09_is-mysql-autoincrement-really-monotonic.md @@ -134,7 +134,7 @@ we query the db during this short between-commit phase: 1. we retrieve a single row, `ID = 2`, 2. we process this batch of one row, 3. we store the `ID = 2` state for the next batch, telling it "you should -add `where ID > 2` to your query". + add `where ID > 2` to your query". We've just ommited the `alfa` row! @@ -143,7 +143,7 @@ We've just ommited the `alfa` row! Here's a list of proposals that might work (or might not): 1. Exploit the `created_at` column to improve the situation? I don't think it'll help, -but that's one of the modes that Kafka JDBC connector can operate in. + but that's one of the modes that Kafka JDBC connector can operate in. 2. Wait X seconds until all transactions with `ID < N` are commited (e.g. [ innodb_lock_wait_timeout](https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout) + 5 seconds). diff --git a/content/posts/2018-05-29_make-jaxb-great-again.md b/content/posts/2018-05-29_make-jaxb-great-again.md index d0fe8ac..198da1d 100644 --- a/content/posts/2018-05-29_make-jaxb-great-again.md +++ b/content/posts/2018-05-29_make-jaxb-great-again.md @@ -83,19 +83,19 @@ available. Two plugins seemed interesting for me: All right, let's plug them in! ;) 1. Add an appropriate _extension_ to your `pom.xml` file to grab the dependency -(kind of): + (kind of): ``` net.codesup.util:jaxb2-rich-contract-plugin:1.18.0 ``` -This goes into `build.plugins.configuration.extensions`. + This goes into `build.plugins.configuration.extensions`. 1. Add two _extensionArg_ elements in order to enable chosen plugins (consult - the docs in order to find the required flags for other plugins): + the docs in order to find the required flags for other plugins): ``` -Xfluent-builder -Ximmutable ``` -This, on the other hand, goes into `build.plugins.executions.configuration.xsdOptions.extensionArgs`. + This, on the other hand, goes into `build.plugins.executions.configuration.xsdOptions.extensionArgs`. So far so good! After a quick `mvn clean install` we can exploit a great, fluent builder API for object construction. A quick demo: @@ -158,7 +158,7 @@ your classes. How? ``` 1. Setup the bindings file in pom.xml and make sure that JAXB extensions are enabled. -In our scenarion, two lines have to be added to the `xsdOption` section: + In our scenarion, two lines have to be added to the `xsdOption` section: ```xml ${basedir}/src/main/resources/binding.xml diff --git a/content/posts/2018-10-24_jpa-and-uuid.md b/content/posts/2018-10-24_jpa-and-uuid.md index caad9db..3207fa5 100644 --- a/content/posts/2018-10-24_jpa-and-uuid.md +++ b/content/posts/2018-10-24_jpa-and-uuid.md @@ -159,8 +159,8 @@ the pretty `@Builder` generated by Lombok. ## Conclusions 1. `@PrePersist` is too late for programmatically generating an id, -2. Many thanks to the `Lombok` team for including the `Builder.Default` annotation, otherwise -we'd have to give up using `Builder` on this class, +2. Many thanks to the `Lombok` team for including the `Builder.Default` annotation, otherwise + we'd have to give up using `Builder` on this class, 3. All hail to `Java9` and its `ImmutableCollections` for their validation and nice error messages. diff --git a/content/posts/2019-05-09_how-to-quickly-remove-merged-remote-branches.md b/content/posts/2019-05-09_how-to-quickly-remove-merged-remote-branches.md index 0d3a79a..ee22050 100644 --- a/content/posts/2019-05-09_how-to-quickly-remove-merged-remote-branches.md +++ b/content/posts/2019-05-09_how-to-quickly-remove-merged-remote-branches.md @@ -18,20 +18,20 @@ let's find a better one. *Notice: pay attention to the name of your remote and change it if needed.* 1. Fetch all changes from the repository, cleaning up stale remote references -at the same time. + at the same time. ``` git fetch -p ``` 1. Checkout the branch you consider "if it's merged to this branch, it can be removed". -For us, it was `develop`. Update your local repository with most recent changes. + For us, it was `develop`. Update your local repository with most recent changes. ``` git checkout develop git merge --ff-only origin/develop ``` 1. Create a list of branches to be removed, filtering out all `release` branches, `master` -and `develop`. Dump it to a file. + and `develop`. Dump it to a file. ``` git branch -r --merged \ diff --git a/gatsby-browser.js b/gatsby-browser.js index 3e17a14..1d29282 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,32 +1,56 @@ import React from "react"; import { Provider } from "react-redux"; -import { JssProvider } from "react-jss"; -import CssBaseline from "@material-ui/core/CssBaseline"; +import { CacheProvider } from "@emotion/react"; +import { + JssProvider, + createGenerateId, + ThemeProvider as JssThemeProvider +} from "react-jss"; +import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; + import createStore from "./src/state/store"; -import { MuiThemeProvider } from "@material-ui/core/styles"; -import getPageContext from "./src/getPageContext"; +import createEmotionCache from "./src/styles/createEmotionCache"; +import theme from "./src/styles/theme"; + +const emotionCache = createEmotionCache(); export const onInitialClientRender = function() { const ssStyles = window.document.getElementById("server-side-jss"); ssStyles && ssStyles.parentNode.removeChild(ssStyles); }; +// This stack must mirror gatsby-ssr.js exactly -- same providers, same order, +// same class-name generator. Any asymmetry shows up as a hydration mismatch, +// which React 18 answers by throwing the server-rendered tree away and +// re-rendering everything on the client. +// +// Both theme providers get the same theme object on purpose. Under MUI v3 only +// MuiThemeProvider was here: MUI v3 and react-jss v8 shared a theme broadcast +// channel (the `theming` + `brcast` packages), so injectSheet's `theme => ({...})` +// style functions picked the theme up for free. MUI v5+ moved to Emotion and +// that channel is gone, so react-jss needs its own ThemeProvider or all 31 +// styled components see `theme === undefined`. export const wrapRootElement = ({ element }) => { const store = createStore(); - const pageContext = getPageContext(); - const ConnectRootElement = ( + + return ( - - - - {React.cloneElement(element, { pageContext })} - - + + + + + {/* No . src/styles/globals.js already carries a full + normalize reset (body margin, box-sizing, the html.wf-active + font switch), and MUI 9's CssBaseline additionally forces + `body { font-size }` from theme.typography -- computed as + 1rem x (typography.fontSize / 14) = 18.29px, where this design + inherits 16px from . MUI v3's CssBaseline did not do that, + so including it here silently restyles every page. */} + {element} + + + + ); - - return ConnectRootElement; }; diff --git a/gatsby-config.js b/gatsby-config.js index 531e338..c1f5027 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -15,22 +15,24 @@ module.exports = { algolia: { appId: config.algolia.appId, searchOnlyApiKey: config.algolia.searchOnlyApiKey, - indexName: config.algolia.indexName + indexName: config.algolia.indexName, }, facebook: { - appId: config.facebook.appId - } + appId: config.facebook.appId, + }, }, plugins: [ - { - resolve: `gatsby-plugin-algolia`, - options: { - appId: config.algolia.appId, - apiKey: config.algolia.adminApiKey, - indexName: config.algolia.indexName, - queries: [ + ...(config.algolia.appId && config.algolia.adminApiKey + ? [ { - query: `{ + resolve: `gatsby-plugin-algolia`, + options: { + appId: config.algolia.appId, + apiKey: config.algolia.adminApiKey, + indexName: config.algolia.indexName, + queries: [ + { + query: `{ allMarkdownRemark(filter: { fileAbsolutePath: { regex: "//posts|pages//" } }) { edges { node { @@ -40,6 +42,7 @@ module.exports = { } internal { content + contentDigest } frontmatter { title @@ -51,24 +54,32 @@ module.exports = { } } }`, - transformer: ({ data }) => - _.flatten( - data.allMarkdownRemark.edges.map(({ node }) => - chunk(node.internal.content, 1000).map(contentChunk => - Object.assign({}, node, { internal: { content: contentChunk } }) - ) - ) - ) - } + transformer: ({ data }) => + _.flatten( + data.allMarkdownRemark.edges.map(({ node }) => + chunk(node.internal.content, 1000).map((contentChunk) => + Object.assign({}, node, { + // Spread node.internal rather than replacing it. + // gatsby-plugin-algolia 1.x drives partial updates + // off internal.contentDigest and calls + // panicOnBuild without it; 0.2 never looked. + internal: { ...node.internal, content: contentChunk } + }) + ) + ) + ), + }, + ], + }, + }, ] - } - }, + : []), { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/content`, - name: "content" - } + name: "content", + }, }, { resolve: `gatsby-transformer-remark`, @@ -79,24 +90,23 @@ module.exports = { resolve: `gatsby-remark-images`, options: { maxWidth: 800, - backgroundColor: "transparent" - } + backgroundColor: "transparent", + }, }, { resolve: `gatsby-remark-responsive-iframe`, options: { - wrapperStyle: `margin-bottom: 2em` - } + wrapperStyle: `margin-bottom: 2em`, + }, }, `gatsby-remark-prismjs`, `gatsby-remark-copy-linked-files`, - `gatsby-remark-smartypants` - ] - } + `gatsby-remark-smartypants`, + ], + }, }, `gatsby-plugin-sharp`, `gatsby-transformer-sharp`, - `gatsby-plugin-react-helmet`, `gatsby-plugin-catch-links`, { resolve: `gatsby-plugin-manifest`, @@ -111,53 +121,45 @@ module.exports = { { src: "/icons/icon-48x48.png", sizes: "48x48", - type: "image/png" + type: "image/png", }, { src: "/icons/icon-96x96.png", sizes: "96x96", - type: "image/png" + type: "image/png", }, { src: "/icons/icon-144x144.png", sizes: "144x144", - type: "image/png" + type: "image/png", }, { src: "/icons/icon-192x192.png", sizes: "192x192", - type: "image/png" + type: "image/png", }, { src: "/icons/icon-256x256.png", sizes: "256x256", - type: "image/png" + type: "image/png", }, { src: "/icons/icon-384x384.png", sizes: "384x384", - type: "image/png" + type: "image/png", }, { src: "/icons/icon-512x512.png", sizes: "512x512", - type: "image/png" - } - ] - } - }, - { - resolve: `gatsby-plugin-offline`, - options: { - navigateFallbackBlacklist: [/\?(.+&)?no-cache=1/], - } - }, - { - resolve: `gatsby-plugin-google-analytics`, - options: { - trackingId: config.google.analyticsId - } + type: "image/png", + }, + ], + }, }, + // gatsby-plugin-offline 6 dropped the navigateFallback options entirely, so + // the old `navigateFallbackBlacklist: [/\?(.+&)?no-cache=1/]` has no + // equivalent and is gone. Default runtime caching only. + `gatsby-plugin-offline`, { resolve: `gatsby-plugin-feed`, options: { @@ -179,12 +181,12 @@ module.exports = { query: { site: { siteMetadata }, ...rest - } + }, }) => { return { ...siteMetadata, ...rest, - custom_namespaces: { media: "http://video.search.yahoo.com/mrss" } + custom_namespaces: { media: "http://video.search.yahoo.com/mrss" }, }; }, feeds: [ @@ -192,7 +194,7 @@ module.exports = { serialize: ({ query: { site, allMarkdownRemark } }) => { const siteUrl = site.siteMetadata.site_url + site.siteMetadata.pathPrefix; - return allMarkdownRemark.edges.map(edge => { + return allMarkdownRemark.edges.map((edge) => { return { title: edge.node.frontmatter.title, description: edge.node.frontmatter.subTitle, @@ -203,18 +205,18 @@ module.exports = { guid: siteUrl + edge.node.fields.slug, custom_elements: [ { - "content:encoded": edge.node.html + "content:encoded": edge.node.html, }, { "media:thumbnail": [ { _attr: { - url: siteUrl + edge.node.frontmatter.cover - } - } - ] - } - ] + url: siteUrl + edge.node.frontmatter.cover, + }, + }, + ], + }, + ], }; }); }, @@ -223,7 +225,7 @@ module.exports = { allMarkdownRemark( limit: 30, filter: { fileAbsolutePath: { regex: "//posts//" } } - sort: { fields: [fields___date], order: DESC } + sort: { fields: { date: DESC } } ) { edges { node { @@ -244,24 +246,41 @@ module.exports = { } } `, - output: "/rss.xml" - } - ] - } + output: "/rss.xml", + title: config.siteTitle, + }, + ], + }, }, { resolve: `gatsby-plugin-sitemap`, options: { - exclude: ["/contact"] - } + excludes: ["/contact"], + }, }, { resolve: "gatsby-plugin-react-svg", options: { - include: /svg-icons/ - } + // v3 reads options.rule.{include,exclude}; a top-level `include` is + // silently ignored and svg-react-loader ends up applied to every .svg. + rule: { + include: /svg-icons/, + }, + }, }, - `gatsby-plugin-netlify-cms`, - `gatsby-plugin-netlify` - ] + // Registered only when GTM_ID is set, so a build without it still succeeds. + ...(config.google.tagManagerId + ? [ + { + resolve: `gatsby-plugin-google-tagmanager`, + options: { + id: config.google.tagManagerId, + includeInDevelopment: false, + }, + }, + ] + : []), + `gatsby-plugin-decap-cms`, + `gatsby-plugin-netlify`, + ], }; diff --git a/gatsby-ssr.js b/gatsby-ssr.js index 15b28b6..b98298a 100644 --- a/gatsby-ssr.js +++ b/gatsby-ssr.js @@ -1,44 +1,77 @@ import React from "react"; import { renderToString } from "react-dom/server"; import { Provider } from "react-redux"; -import { JssProvider } from "react-jss"; -import { MuiThemeProvider } from "@material-ui/core/styles"; +import { CacheProvider } from "@emotion/react"; +import createEmotionServer from "@emotion/server/create-instance"; +import { + JssProvider, + SheetsRegistry, + createGenerateId, + ThemeProvider as JssThemeProvider +} from "react-jss"; +import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; -require("dotenv").config(); import createStore from "./src/state/store"; +import createEmotionCache from "./src/styles/createEmotionCache"; import theme from "./src/styles/theme"; -import getPageContext from "./src/getPageContext"; -import { CssBaseline } from "@material-ui/core"; import config from "./content/meta/config"; +// replaceRenderer is a current, supported Gatsby SSR API. It is used here rather +// than wrapRootElement because the JSS stylesheet can only be serialised after +// the tree has rendered -- the registry is empty until then. export const replaceRenderer = ({ bodyComponent, replaceBodyHTMLString, setHeadComponents }) => { const store = createStore(); - const pageContext = getPageContext(); - replaceBodyHTMLString( - renderToString( - - - - - {React.cloneElement(bodyComponent, { pageContext })} - + // Fresh per request: neither sheets nor Emotion caches may leak between + // connections. The provider stack below must mirror gatsby-browser.js exactly. + const sheets = new SheetsRegistry(); + const generateId = createGenerateId(); + const emotionCache = createEmotionCache(); + const { extractCriticalToChunks } = createEmotionServer(emotionCache); + + const html = renderToString( + + + + + + {/* No . src/styles/globals.js already carries a full + normalize reset (body margin, box-sizing, the html.wf-active + font switch), and MUI 9's CssBaseline additionally forces + `body { font-size }` from theme.typography -- computed as + 1rem x (typography.fontSize / 14) = 18.29px, where this design + inherits 16px from . MUI v3's CssBaseline did not do that, + so including it here silently restyles every page. */} + {bodyComponent} + + - - ) + + ); + replaceBodyHTMLString(html); + + // Emotion otherwise injects MUI's styles as a