Repository: z0rs.github.io
Stack: Gatsby 5 · React 18 · Tailwind CSS 3 · gatsby-plugin-mdx 3 · GitHub Actions
Reviewer: Senior SWE / DevOps / Security
CONFIRMED — but the previous audit's proposed fix was PARTIALLY WRONG and UNSAFE.
gatsby-plugin-mdx@3 targets gatsby@^4. With gatsby@5 both gatsby-core-utils@3
and @4 are installed (confirmed in yarn.lock), causing a runtime build failure.
The previous audit said "just bump to gatsby-plugin-mdx@^5.0.0" without noting that
MDXRenderer — which the codebase uses in src/components/mdx-parser.js — was removed
in gatsby-plugin-mdx@4. A naive version bump breaks all page rendering.
Additional files that must change alongside the package bump:
src/components/mdx-parser.js— removeMDXRenderer, usechildrenprop directlysrc/templates/article.js— removebodyfrom GraphQL query and destructuringsrc/templates/ctf.js— samesrc/templates/page.js— samegatsby-config.js— removewrapESMPluginshim (gatsby-plugin-mdx@5 handles ESM natively)package.json— swap@mdx-js/react@^1→@mdx-js/react@^3, remove@mdx-js/mdx@^1
The corrected full migration patch is in Phase 4.
CONFIRMED.
.gitignore line 64 reads #.env (commented out). The file is committed with all-empty
values in this snapshot, so no live secrets are exposed today — but the pattern will cause
a secret leak the moment a developer fills in values and runs git add ..
CONFIRMED.
After actions/upload-pages-artifact (line 69), a second yarn run build executes
(line 73–76). The second build's output is never uploaded and consumes ~4 extra minutes
of CI time on every push.
CONFIRMED.
This env var disables TLS certificate verification process-wide during the Gatsby
build. The intent was to raise memory limits but it was apparently confused with
NODE_OPTIONS. Any HTTPS call made during the build (remote image fetching via
createRemoteFileNode, sitemap fetching, etc.) bypasses certificate validation.
CONFIRMED.
gatsby-config.js line 1: require('dotenv').config(…). dotenv is not declared as a
dependency. It is only available because gatsby itself depends on it transitively.
This breaks if gatsby's transitive dep tree changes.
CONFIRMED — with mitigation note.
src/api/newsletter.js line 3: const axios = require('axios'). axios is absent from
package.json. The file is self-annotated "not currently used" so this doesn't break
production today, but the endpoint will throw a MODULE_NOT_FOUND error if ever called.
axios must be added.
CONFIRMED.
google.analytics('v3') and ga: prefixed metrics refer to Universal Analytics, which
Google permanently shut down on 1 July 2024. Every call to this endpoint returns an
error. The @google-analytics/data package (already in package.json) is the correct
GA4 replacement but requires a full rewrite of the function — too risky to auto-patch.
See Maintainer Notes for migration guidance.
CONFIRMED.
JSON.parse(req.body) throws an unhandled exception on malformed input.
No field sanitisation before the FaunaDB write.
CONFIRMED.
CNAME contains z0rs.github.io. When GitHub Pages serves a custom-domain site, all
paths are rooted at /. Setting pathPrefix to /z0rs.github.io causes Gatsby to
generate internal links as https://z0rs.github.io/z0rs.github.io/articles/… — a
doubled path that 404s for every page.
CONFIRMED.
public/ does not exist before the build runs on a clean runner, so
hashFiles('public') always returns an empty hash. The restore-keys fallback is hit
every run, meaning the cache is restored (correctly) but the primary key is always a
cache miss and the saved key is never stable. Using hashFiles('**/yarn.lock') makes
the key deterministic.
PARTIALLY CORRECT.
The mixing is real and is an anti-pattern. However, Gatsby Functions compiles these files
through its own Babel pipeline, so they work at runtime. Marking as low priority — do not
fix in this patch set to avoid risk; leave for a dedicated cleanup.
CONFIRMED.
Patch releases (18.3.x) and security fixes are excluded. Should use ^18.0.0.
CONFIRMED.
URL is never set in .env, CI env vars, or anywhere in the project.
gatsby-plugin-sitemap will generate a sitemap with siteUrl: undefined, making every
entry invalid. Fix: hardcode the known domain.
CONFIRMED.
All 7 occurrences on h1–h6 and a elements. Tailwind's typography plugin silently
ignores unknown keys. No heading or link is actually bold in the rendered output.
The correct key is fontWeight. The correct value is '700' (not theme('font-bold')
which is also wrong syntax — theme() expects a dot-path like theme('fontWeight.bold')).
CONFIRMED.
This plugin is a deployment adapter for Gatsby Cloud (now defunct). It has no effect on
GitHub Pages and adds dead install weight.
CONFIRMED.
Installed but never registered. Dead package.
PARTIALLY CORRECT.
With gatsby-plugin-mdx@3, @mdx-js/react@^1 is the correct version for MDXProvider.
The claim that they are "mismatched" is only accurate after upgrading to
gatsby-plugin-mdx@5, which requires @mdx-js/react@^3. The previous audit should have
tied this note to the MDX migration rather than flagging it as a standalone current bug.
CONFIRMED.
CI resolves "18" to the latest 18.x (currently 18.20.x). .nvmrc pins to 18.0.0.
The CI fix below uses node-version-file: ".nvmrc" to make them consistent, while also
updating .nvmrc to v18 to track the latest LTS patch.
FALSE POSITIVE from original audit (noted as low/outdated).
The codebase uses the v1 API (import Highlight, { defaultProps } from 'prism-react-renderer'
and prism-react-renderer/themes/dracula). Upgrading to v2 would break the component.
The ^1.3.5 pin is correct and intentional. Do not upgrade.
Without fixes: build fails at the MDX compilation step.
gatsby-plugin-mdx@3 and gatsby@5 have incompatible internal API requirements
(gatsby-core-utils@3 vs @4). The wrapESMPlugin shim is also required because
rehype-slug@5 and rehype-autolink-headings@6 are pure-ESM but gatsby-plugin-mdx@3
cannot import them natively.
After applying the Phase 4 patch: build succeeds.
The migration to gatsby-plugin-mdx@5 resolves the core incompatibility, the template
changes remove the dependency on MDXRenderer, and the config changes clean up the
wrapESMPlugin shim which is no longer needed.
Remaining manual work before a green build:
- Run
yarn installafterpackage.jsonchanges to update the lockfile. - Migrate
src/api/ua-analytics.jsto the GA4 Data API (see Maintainer Notes).
--- a/package.json
+++ b/package.json
@@ -20,8 +20,8 @@
"dependencies": {
"@google-analytics/data": "^3.0.0",
- "@mdx-js/mdx": "^1.6.22",
- "@mdx-js/react": "^1.6.22",
+ "@mdx-js/react": "^3.0.0",
+ "axios": "^1.6.0",
"@react-three/drei": "^9.22.4",
"@react-three/fiber": "^8.3.1",
"cmdk": "^0.1.18",
"country-flag-icons": "^1.5.5",
"d3-geo": "^3.0.1",
+ "dotenv": "^16.0.0",
"dotted-map": "^2.2.3",
"faunadb": "^4.6.0",
"gatsby": "^5.0.0",
- "gatsby-plugin-gatsby-cloud": "^5.0.0",
- "gatsby-plugin-google-analytics": "^5.0.0",
"gatsby-plugin-image": "^3.0.0",
- "gatsby-plugin-mdx": "^3.20.0",
+ "gatsby-plugin-mdx": "^5.0.0",
"gatsby-plugin-sharp": "^5.0.0",
"gatsby-plugin-sitemap": "^6.0.0",
"gatsby-source-filesystem": "^5.0.0",
"gatsby-transformer-json": "^5.0.0",
"gatsby-transformer-sharp": "^5.0.0",
"googleapis": "^105.0.0",
"prism-react-renderer": "^1.3.5",
- "react": "18.0.0",
- "react-dom": "18.0.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
"rehype-autolink-headings": "6.1.1",
"rehype-slug": "5.0.1",
"three": "^0.143.0",
"three-geojson-geometry": "^1.1.7"
},--- a/.gitignore
+++ b/.gitignore
@@ -63,7 +63,7 @@
# dotenv environment variables file
-#.env
+.env
.env.test
.env.development
.env.productionAfter this change, copy
.envto.env.example(with empty values) so the template remains visible to new contributors. The committed.envhas empty values so no live secrets are exposed, but add agit rm --cached .envto stop tracking the file.
--- a/.nvmrc
+++ b/.nvmrc
-v18.0.0
+v18--- a/gatsby-config.js
+++ b/gatsby-config.js
@@ -1,16 +1,5 @@
require('dotenv').config({
path: `.env.${process.env.NODE_ENV || 'production'}`
});
-const wrapESMPlugin = (name) =>
- function wrapESM(opts) {
- return async (...args) => {
- const mod = await import(name);
- const plugin = mod.default(opts);
- return plugin(...args);
- };
- };
-
module.exports = {
- pathPrefix: "/z0rs.github.io",
- // flags: {
+ // pathPrefix removed: CNAME sets the custom domain to z0rs.github.io.
+ // GitHub Pages serves from root when a CNAME is present — no prefix is needed.
+ // flags: {
// FAST_DEV: true
// },
trailingSlash: 'always',
siteMetadata: {
name: 'Eno Leriand',
description: 'Security & Research',
keywords: [/* unchanged */],
- siteUrl: process.env.URL,
+ siteUrl: process.env.SITE_URL || 'https://z0rs.github.io',
defaultImage: '/images/76135196.jpeg'
},
plugins: [
- 'gatsby-plugin-gatsby-cloud',
'gatsby-plugin-image',
'gatsby-transformer-sharp',
'gatsby-plugin-postcss',
'gatsby-transformer-json',
'gatsby-plugin-sitemap',
{
resolve: 'gatsby-plugin-mdx',
options: {
- rehypePlugins: [wrapESMPlugin('rehype-slug'), [wrapESMPlugin('rehype-autolink-headings'), { behavior: 'wrap' }]]
+ rehypePlugins: [
+ 'rehype-slug',
+ ['rehype-autolink-headings', { behavior: 'wrap' }]
+ ]
}
},This is the most important change. MDXRenderer is removed in gatsby-plugin-mdx@5.
In v5, Gatsby passes the compiled MDX component directly as the children prop of the
page template — the body string from GraphQL is gone. MDXProvider from
@mdx-js/react@3 still works and is the correct way to inject custom components.
--- a/src/components/mdx-parser.js
+++ b/src/components/mdx-parser.js
@@ -1,8 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MDXProvider } from '@mdx-js/react';
-import { MDXRenderer } from 'gatsby-plugin-mdx';
import { Link } from 'gatsby';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
@@ -43,14 +41,13 @@ const components = {
Vimeo
};
-const MdxParser = ({ children, embedded }) => {
+const MdxParser = ({ children }) => {
return (
- <MDXProvider components={components}>
- <MDXRenderer embedded={transformImages(embedded)}>{children}</MDXRenderer>
- </MDXProvider>
+ <MDXProvider components={components}>
+ {children}
+ </MDXProvider>
);
};
MdxParser.propTypes = {
- /** Embedded image dtails */
- embedded: PropTypes.any
+ children: PropTypes.node
};
export default MdxParser;Note on
embeddedimages: Ingatsby-plugin-mdx@3, images were passed throughMDXRenderer's props and accessed viaprops.embeddedin MDX files. In v5, embedded images must be imported directly inside the MDX file itself using standardgatsby-plugin-imagesyntax, or passed viaMDXProvidercontext. Removeembeddedprop passing from the templates below.
--- a/src/templates/article.js
+++ b/src/templates/article.js
@@ -14,12 +14,10 @@
const Page = ({
+ children,
data: {
mdx: {
fields: { slug },
excerpt,
frontmatter: { type, title, date, dateModified, author, tags },
- featuredImage: {
- childImageSharp: { thumbnail }
- },
- embeddedImages,
+ featuredImage,
tableOfContents: { items: toc },
- body
},
site: {
siteMetadata: { siteUrl }
@@ -29,7 +27,10 @@
+ const thumbnail = featuredImage?.childImageSharp?.thumbnail;
+
return (
<Fragment>
<div className="grid lg:grid-cols-1fr-auto">
<DateStamp date={dateModified ? dateModified : date} />
<small className="leading-6 font-semibold text-secondary">Author • {author}</small>
</div>
<h1 className="my-12 text-3xl sm:text-5xl">{title}</h1>
<ul className="list-none m-0 p-0 flex flex-wrap gap-2 mb-12">
{tags
? tags.map((tag, index) => {
return (
<li key={index} className="m-0 p-0">
<Tag tag={tag} />
</li>
);
})
: null}
</ul>
- <MdxParser embedded={embeddedImages}>{body}</MdxParser>
+ <MdxParser>{children}</MdxParser>
<AddReaction title={title} slug={slug} />
<UtterancesObserver />
<AsideElement>
- <FeaturedImageAside alt={title} thumbnail={thumbnail} shareText={`${title}\n ${siteUrl}${slug}`} />
+ {thumbnail && (
+ <FeaturedImageAside alt={title} thumbnail={thumbnail} shareText={`${title}\n ${siteUrl}${slug}`} />
+ )}
@@ -68,11 +70,7 @@
export const query = graphql`
query ($id: String!) {
mdx(id: { eq: $id }) {
fields { slug }
excerpt
frontmatter {
type
title
date(formatString: "MMMM DD, YYYY")
dateModified(formatString: "MMMM DD, YYYY")
author
tags
}
featuredImage {
childImageSharp {
thumbnail: gatsbyImageData(width: 240)
og: gatsbyImageData(width: 1200)
}
}
- embeddedImages {
- childImageSharp {
- gatsbyImageData(layout: FULL_WIDTH)
- }
- }
tableOfContents
- body
}
site { siteMetadata { siteUrl } }
}
`;Same change as article.js — replace body with children, remove embeddedImages.
--- a/src/templates/ctf.js
+++ b/src/templates/ctf.js
@@ -14,11 +14,9 @@
const Page = ({
+ children,
data: {
mdx: {
fields: { slug },
excerpt,
frontmatter: { type, title, date, dateModified, author, tags },
- featuredImage: { childImageSharp: { thumbnail } },
- embeddedImages,
+ featuredImage,
tableOfContents: { items: toc },
- body
},
site: { siteMetadata: { siteUrl } }
}
}) => {
+ const thumbnail = featuredImage?.childImageSharp?.thumbnail;
+
return (
...
- <MdxParser embedded={embeddedImages}>{body}</MdxParser>
+ <MdxParser>{children}</MdxParser>
...
);
};
export const query = graphql`
query ($id: String!) {
mdx(id: { eq: $id }) {
...
- embeddedImages { childImageSharp { gatsbyImageData(layout: FULL_WIDTH) } }
tableOfContents
- body
}
}
`;--- a/src/templates/page.js
+++ b/src/templates/page.js
@@ -5,11 +5,10 @@
const Page = ({
+ children,
data: {
- mdx: { excerpt, frontmatter: { type, title }, body }
+ mdx: { excerpt, frontmatter: { type, title } }
}
}) => {
return (
<Fragment>
<small className="mb-4 leading-6 font-semibold capitalize text-primary">{title}</small>
- <MdxParser>{body}</MdxParser>
+ <MdxParser>{children}</MdxParser>
</Fragment>
);
};
export const query = graphql`
query ($id: String!) {
mdx(id: { eq: $id }) {
fields { slug }
excerpt
frontmatter { type title }
- body
}
}
`;--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -105,7 +105,7 @@
h1: {
color: theme('colors.text'),
- fontWieght: theme('font-bold'),
+ fontWeight: '700',
a: { color: theme('colors.text') }
},
h2: {
color: theme('colors.salmon'),
- fontWieght: theme('font-bold'),
+ fontWeight: '700',
a: { color: theme('colors.salmon') }
},
h3: {
color: theme('colors.salmon'),
- fontWieght: theme('font-bold'),
+ fontWeight: '700',
a: { color: theme('colors.salmon') }
},
h4: {
color: theme('colors.salmon'),
- fontWieght: theme('font-bold'),
+ fontWeight: '700',
a: { color: theme('colors.salmon') }
},
h5: {
color: theme('colors.salmon'),
- fontWieght: theme('font-bold'),
+ fontWeight: '700',
a: { color: theme('colors.salmon') }
},
h6: {
color: theme('colors.salmon'),
- fontWieght: theme('font-bold'),
+ fontWeight: '700',
a: { color: theme('colors.salmon') }
},
strong: { color: theme('colors.text') },
a: {
color: theme('colors.secondary'),
- fontWieght: theme('font-bold'),
+ fontWeight: '700',--- a/src/api/fauna-add-reaction.js
+++ b/src/api/fauna-add-reaction.js
@@ -1,8 +1,24 @@
const faunadb = require('faunadb');
+const ALLOWED_REACTIONS = ['heart', 'thumbsup', 'fire', 'party'];
+const MAX_LEN = 500;
+
export default async function handler(req, res) {
- const { title, slug, reaction, date } = JSON.parse(req.body);
+ let body;
+ try {
+ body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
+ } catch {
+ return res.status(400).json({ message: 'Invalid JSON body' });
+ }
+
+ const { title, slug, reaction, date } = body ?? {};
+
+ if (!slug || typeof slug !== 'string' || slug.length > MAX_LEN) {
+ return res.status(400).json({ message: 'slug is required and must be a short string' });
+ }
+ if (!reaction || !ALLOWED_REACTIONS.includes(reaction)) {
+ return res.status(400).json({ message: `reaction must be one of: ${ALLOWED_REACTIONS.join(', ')}` });
+ }
+ if (title !== undefined && (typeof title !== 'string' || title.length > MAX_LEN)) {
+ return res.status(400).json({ message: 'title must be a short string' });
+ }
const q = faunadb.query;
const client = new faunadb.Client({ secret: process.env.FAUNA_KEY });--- a/.github/workflows/gatsby.yml
+++ b/.github/workflows/gatsby.yml
@@ -43,7 +43,7 @@
- name: Setup Node
uses: actions/setup-node@v4
with:
- node-version: "18"
+ node-version-file: ".nvmrc"
cache: ${{ steps.detect-package-manager.outputs.manager }}
- name: Setup Pages
id: pages
uses: actions/configure-pages@v4
with:
static_site_generator: gatsby
- name: Restore cache
uses: actions/cache@v4
with:
path: |
public
.cache
- key: ${{ runner.os }}-gatsby-build-${{ hashFiles('public') }}
+ key: ${{ runner.os }}-gatsby-build-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-gatsby-build-
- name: Install dependencies
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
- name: Build with Gatsby
env:
PREFIX_PATHS: 'true'
- NODE_TLS_REJECT_UNAUTHORIZED: 0
+ NODE_OPTIONS: "--max-old-space-size=4096"
run: ${{ steps.detect-package-manager.outputs.runner }} gatsby build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
- - name: Increase memory limit and run build
- run: |
- export NODE_OPTIONS="--max-old-space-size=4096"
- yarn run buildThe elif branch checks for package.json (always true) and falls back to npm, but
this repo uses yarn. Since yarn.lock always exists, the yarn branch fires correctly
— but the fallback is misleading. It also re-detects on every run even though the package
manager never changes.
Improvement: Hardcode yarn since this is clearly a yarn project. Removes a step and
avoids the false safety net.
.cache/ contains Gatsby's build cache and should be kept separate from public/
(the build output). Separate them so a .cache/ miss doesn't also miss public/.
The .env variables (FAUNA_KEY, CK_API_KEY, etc.) are never injected in the
workflow. The build will succeed without them (Gatsby treats missing env vars as empty
strings) but any serverless function that needs them will silently fail at runtime.
Add an env block to the build step sourcing GitHub Secrets.
# .github/workflows/gatsby.yml
name: Deploy Gatsby site to Pages
on:
push:
branches: ["master"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true # changed: cancel stale deploys instead of queuing
defaults:
run:
shell: bash
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
- name: Setup Pages
id: pages
uses: actions/configure-pages@v4
with:
static_site_generator: gatsby
- name: Restore Gatsby cache
uses: actions/cache@v4
with:
path: .cache
key: ${{ runner.os }}-gatsby-cache-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-gatsby-cache-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build with Gatsby
env:
PREFIX_PATHS: 'true'
NODE_OPTIONS: "--max-old-space-size=4096"
GATSBY_GA_MEASUREMENT_ID: ${{ secrets.GATSBY_GA_MEASUREMENT_ID }}
FAUNA_KEY: ${{ secrets.FAUNA_KEY }}
SITE_URL: "https://z0rs.github.io"
run: yarn gatsby build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4Changes from original:
- Removed
Detect package managerstep — hardcodedyarn node-version-file: ".nvmrc"— consistent with local dev--frozen-lockfile— fails fast if lockfile is out of sync- Removed
publicfrom cache path (only cache.cache/, not build output) - Cache key uses
yarn.lockhash — stable and correct - Moved
NODE_OPTIONSto the build step env (was leaking viaNODE_TLS_REJECT_UNAUTHORIZED: 0) - Added secret env vars to the build step
cancel-in-progress: true— avoids stale deploy queue on rapid pushes- Removed the double-build step entirely
Even after fixing .gitignore, the file exists in the git history. Run:
git rm --cached .env
git commit -m "chore: stop tracking .env"If real secrets were ever committed (currently all values are empty), rotate them
immediately and use git filter-repo or BFG Repo Cleaner to scrub history.
This is a critical security misconfiguration. Removed in the patch. During a build,
gatsby-node.js calls createRemoteFileNode for every featuredImage URL in the MDX
frontmatter. With TLS disabled, a compromised CDN or network path could serve malicious
image data that gets baked into the static site. The correct fix (applying
NODE_OPTIONS: "--max-old-space-size=4096" instead) is safe.
FAUNA_KEY is used in serverless functions. Ensure it is stored as a GitHub Secret
(Settings → Secrets → Actions), not hardcoded anywhere. The current .env has it
empty — verify it is injected correctly for production.
Even after the input validation patch, the fauna-add-reaction endpoint has no rate
limiting. A bot can spam the FaunaDB collection. Consider adding IP-based rate limiting
via Netlify/Vercel middleware, or moving to a fauna.fauna.com access policy that limits
document creation per time window.
The UA API returns 403 errors. The current catch block forwards error.message
directly in a 500 response, potentially leaking internal API error details. Until the
endpoint is rewritten for GA4, add response sanitisation:
res.status(500).json({ message: 'Analytics unavailable' });
// Do not forward error.message from the Google APIThe following packages are old enough to warrant a targeted audit before the next release:
| Package | Current | Risk |
|---|---|---|
@commitlint/cli |
^12.x |
Latest is ^19.x; safe to upgrade |
husky |
^7.x |
Latest is ^9.x; safe to upgrade |
prettier |
^2.x |
Latest is ^3.x; safe to upgrade |
gatsby |
^5.0.0 (locked to 5.0.0) |
Run yarn upgrade gatsby to get latest 5.x |
faunadb |
^4.6.0 |
Fauna deprecated this driver; migrate to fauna (v4) |
cd z0rs.github.io-master
# 1. Stop tracking .env
git rm --cached .env
cp .env .env.example # keep a template for contributors
echo ".env" >> .gitignore # already done by patch — verify it applied
# 2. Install updated dependencies
yarn install
# 3. Verify Gatsby can boot
yarn developStep 1 — MDX migration
After bumping gatsby-plugin-mdx to ^5.0.0 and applying the template patches:
- All three templates (
article.js,ctf.js,page.js) now receive MDX content via thechildrenprop instead of thebodyGraphQL field. - Custom components (
Tweet,YouTube,Vimeo,GatsbyImage, etc.) continue to work viaMDXProvider. - The
embeddedimages pattern (passingembeddedImagesthroughMDXRenderer) is removed. Any MDX file that currently usesprops.embeddedto reference images must be updated to import images directly, e.g.:
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { graphql, useStaticQuery } from 'gatsby';
// Or use <GatsbyImage> via the MDXProvider component already set upStep 2 — UA Analytics migration (manual, not auto-patched)
src/api/ua-analytics.js must be rewritten for GA4. The @google-analytics/data
package is already in package.json. Skeleton replacement:
const { BetaAnalyticsDataClient } = require('@google-analytics/data');
const analyticsDataClient = new BetaAnalyticsDataClient({
credentials: {
client_email: process.env.GOOGLE_ANALYTICS_CLIENT_EMAIL,
private_key: process.env.GOOGLE_ANALYTICS_PRIVATE_KEY.replace(/\\n/gm, '\n')
}
});
export default async function handler(req, res) {
try {
const [response] = await analyticsDataClient.runReport({
property: `properties/${process.env.GOOGLE_ANALYTICS_PROPERTY_ID}`,
dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }],
dimensions: [
{ name: 'city' },
{ name: 'latitude' },
{ name: 'longitude' },
{ name: 'country' },
{ name: 'countryId' }
],
metrics: [{ name: 'screenPageViews' }]
});
// map response.rows as needed
res.status(200).json({ message: 'ok', data: response.rows });
} catch {
res.status(500).json({ message: 'Analytics unavailable' });
}
}Add GOOGLE_ANALYTICS_PROPERTY_ID to .env.example and GitHub Secrets.
Step 3 — GitHub Secrets to configure
In your repository's Settings → Secrets and variables → Actions, add:
| Secret name | Value |
|---|---|
GATSBY_GA_MEASUREMENT_ID |
Your GA4 Measurement ID (e.g. G-XXXXXXXXXX) |
FAUNA_KEY |
Your FaunaDB secret |
SITE_URL |
https://z0rs.github.io |
GOOGLE_ANALYTICS_CLIENT_EMAIL |
Service account email |
GOOGLE_ANALYTICS_PRIVATE_KEY |
Service account private key |
GOOGLE_ANALYTICS_PROPERTY_ID |
GA4 numeric property ID |
Step 4 — Verify build locally before pushing
yarn clean
yarn build
yarn serve # visit localhost:9000 and confirm all pages render| Change | Risk | Mitigation |
|---|---|---|
gatsby-plugin-mdx@3→5 |
High — MDXRenderer removed, body field removed | Template patches included in Phase 4 |
@mdx-js/react@1→3 |
Medium — MDXProvider API is same, but context internals changed |
No API change needed; test locally |
pathPrefix removed |
Low — only affects sites without CNAME | CNAME is present; links will work correctly |
react/react-dom 18.0.0→^18.x |
Very low | Patch releases are backward-compatible |
wrapESMPlugin removed |
None — handled by gatsby-plugin-mdx@5 natively |
|
| Double build removed from CI | None — second build output was never deployed |