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