Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save z0rs/586307568316eec30ccd39b439e6b526 to your computer and use it in GitHub Desktop.

Select an option

Save z0rs/586307568316eec30ccd39b439e6b526 to your computer and use it in GitHub Desktop.

Phase 1–6: Verification, Safe Patches & Maintainer Guide

Repository: z0rs.github.io
Stack: Gatsby 5 · React 18 · Tailwind CSS 3 · gatsby-plugin-mdx 3 · GitHub Actions
Reviewer: Senior SWE / DevOps / Security


AUDIT VALIDATION

Issue 1 — gatsby-plugin-mdx@^3 incompatible with gatsby@^5

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 — remove MDXRenderer, use children prop directly
  • src/templates/article.js — remove body from GraphQL query and destructuring
  • src/templates/ctf.js — same
  • src/templates/page.js — same
  • gatsby-config.js — remove wrapESMPlugin shim (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.


Issue 2 — .env tracked by git

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 ..


Issue 3 — Double build in CI

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.


Issue 4 — NODE_TLS_REJECT_UNAUTHORIZED: 0

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.


Issue 5 — dotenv missing from package.json

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.


Issue 6 — axios missing from package.json

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.


Issue 7 — ua-analytics.js uses deprecated Universal Analytics (UA) v3 API

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.


Issue 8 — No input validation in fauna-add-reaction.js

CONFIRMED.
JSON.parse(req.body) throws an unhandled exception on malformed input. No field sanitisation before the FaunaDB write.


Issue 9 — pathPrefix: "/z0rs.github.io" wrong with CNAME

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.


Issue 10 — CI cache key hashFiles('public')

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.


Issue 11 — Mixed CJS require() and ESM export default in API files

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.


Issue 12 — react/react-dom exact-pinned to 18.0.0

CONFIRMED.
Patch releases (18.3.x) and security fixes are excluded. Should use ^18.0.0.


Issue 13 — siteUrl: process.env.URL

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.


Issue 14 — fontWieght typo ×7 in tailwind.config.js

CONFIRMED.
All 7 occurrences on h1h6 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')).


Issue 15 — gatsby-plugin-gatsby-cloud in plugins

CONFIRMED.
This plugin is a deployment adapter for Gatsby Cloud (now defunct). It has no effect on GitHub Pages and adds dead install weight.


Issue 16 — gatsby-plugin-google-analytics in package.json but not in plugins array

CONFIRMED.
Installed but never registered. Dead package.


Issue 17 — @mdx-js/mdx@^1 and @mdx-js/react@^1 mismatch

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.


Issue 18 — .nvmrc pins v18.0.0, CI uses "18"

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.


Issue 19 — prism-react-renderer@^1.3.5

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.


BUILD IMPACT ANALYSIS

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:

  1. Run yarn install after package.json changes to update the lockfile.
  2. Migrate src/api/ua-analytics.js to the GA4 Data API (see Maintainer Notes).

FINAL PATCH

package.json

--- 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"
   },

.gitignore

--- a/.gitignore
+++ b/.gitignore
@@ -63,7 +63,7 @@
 # dotenv environment variables file
-#.env
+.env
 .env.test
 .env.development
 .env.production

After this change, copy .env to .env.example (with empty values) so the template remains visible to new contributors. The committed .env has empty values so no live secrets are exposed, but add a git rm --cached .env to stop tracking the file.


.nvmrc

--- a/.nvmrc
+++ b/.nvmrc
-v18.0.0
+v18

gatsby-config.js

--- 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' }]
+        ]
       }
     },

src/components/mdx-parser.js

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 embedded images: In gatsby-plugin-mdx@3, images were passed through MDXRenderer's props and accessed via props.embedded in MDX files. In v5, embedded images must be imported directly inside the MDX file itself using standard gatsby-plugin-image syntax, or passed via MDXProvider context. Remove embedded prop passing from the templates below.


src/templates/article.js

--- 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 &bull; {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 } }
   }
 `;

src/templates/ctf.js

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
     }
   }
 `;

src/templates/page.js

--- 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
     }
   }
 `;

tailwind.config.js

--- 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',

src/api/fauna-add-reaction.js

--- 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 });

.github/workflows/gatsby.yml

--- 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 build

CI/CD IMPROVEMENTS

Problem 1 — Detect-package-manager step is fragile

The 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.

Problem 2 — Cache saves public/ + .cache/ together

.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/.

Problem 3 — No secret injection step

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.

Improved workflow (complete replacement):

# .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@v4

Changes from original:

  • Removed Detect package manager step — hardcoded yarn
  • node-version-file: ".nvmrc" — consistent with local dev
  • --frozen-lockfile — fails fast if lockfile is out of sync
  • Removed public from cache path (only cache .cache/, not build output)
  • Cache key uses yarn.lock hash — stable and correct
  • Moved NODE_OPTIONS to the build step env (was leaking via NODE_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

SECURITY NOTES

1. .env file in git history

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.

2. NODE_TLS_REJECT_UNAUTHORIZED: 0 in CI

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.

3. FaunaDB key in environment

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.

4. Fauna reaction endpoint — no rate limiting

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.

5. src/api/ua-analytics.js — dead endpoint leaking error details

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 API

6. Dependency review

The 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)

MAINTAINER NOTES

Immediate commands to run

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 develop

Migration steps after applying patches

Step 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 the children prop instead of the body GraphQL field.
  • Custom components (Tweet, YouTube, Vimeo, GatsbyImage, etc.) continue to work via MDXProvider.
  • The embedded images pattern (passing embeddedImages through MDXRenderer) is removed. Any MDX file that currently uses props.embedded to 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 up

Step 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

Potential breaking changes from this patch set

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment