Multi-Domain Gatsby Sites

By Dave Allie
Published May 19, 2021 โ€ข 4 min read
My home page has been through multiple iterations (in terms of both style and tech) over the last few years, and I think I've finally hit a sweet spot for both. As of writing this, it's a simple static site built in React on top of Gatsby. A powerful front-end framework supported by a comprehensive site generator.
However, my blog has always been a bit of a sidecar, and never something I was quite happy with.
My blog was running on a self-hosted Ghost instance. Which, while super powerful, meant my blog content was stored and hosted off a cheap Vultr server. The recurring server bill was mildly annoying, but my main frustration was running something on a server which should be static. After seeing Josh Comeau's blog and learning that it previously existed in a similar form as a Gatsby site, I set out to merge both my blog and my homepage into a single codebase.
For continuityโ€™s sake, I wanted to keep my blog at blog.daveallie.com, my homepage at daveallie.com, and use my existing Gatsby repo to run both, but I couldn't find clear instructions on how to do it. So, this isn't an article on how awesome MDX is, or how to create a Gatsby blog, but instead how I set up, developed and deployed multiple Gatsby sites across several domains from a single repo.
error_outline
If you've never heard of Gatsby (static site builder), or React (front-end framework) before, then this blog post is probably going to be very confusing. There's an assumed level of knowledge about what Gatsby and React are. If you're still curious anyway, then read on at your own risk!

The Problem

Due to how Gatsby (and all other static site generators) operate, there is no way to distinguish which site the user is looking at without client-side code (which won't be run by indexers or crawlers) or server-side rerouting (which requires the use of a server ๐Ÿคฎ).
General internet guidance suggested that multiple subsites either shouldn't be done with Gatsby, or should somehow use an environment variable. I wanted to reuse my components and configurations across subdomains and the idea of leveraging an environment variable to adjust build behaviour sounded promising, so I decided to go with the later suggestion.

The Solution: Subsites

To handle multiple different Gatsby deployments from the same repository, I created the concept of a "Subsite". The goal was to use SUBSITE as an environment variable to control the which domain / website Gatsby would build.
# Build Blog Subsite
SUBSITE=blog yarn build

# Build Home Subsite
SUBSITE=home yarn build
To support this, I created a small utility file to map the environment variable to the subsite:
config/util/subsite.js
const SUBSITES = {
  home: {
    url: 'https://daveallie.com',
  },
  blog: {
    url: 'https://blog.daveallie.com',
  },
};
const DEFAULT_SUBSITE = 'home';

const SUBSITE =
  Object.keys(SUBSITES).find((s) => s === process.env.SUBSITE) ||
  DEFAULT_SUBSITE;

module.exports = {
  SUBSITE,
  SUBSITE_URL: SUBSITES[SUBSITE].url,
};
From there, I could just import SUBSITE in gatsby-node.js and gatsby-config.js. The first thing I did with this new build-time information was adjust the site metadata and output which subsite I was building.
gatsby-node.js
const { SUBSITE } = require('./config/util/subsite');

exports.onPreInit = () => {
  console.log(`=====\nBuilding subsite: ${SUBSITE}\n=====`);
};
gatsby-config.js
const { SUBSITE_URL } = require('./config/util/subsite');

module.exports = {
  siteMetadata: {
    title: 'Dave Allie',
    siteUrl: SUBSITE_URL,
  },
  plugins: [
    // ...
  ],
};
Now, when running any Gatsby command, it would output which subsite I was building:
> SUBSITE=blog yarn develop

...
=====
Building subsite: blog
=====
...

Creating Subsite Specific Pages

Outputting the subsite in the console is fun, but doesn't really move us closer to the goal of having different site content on each subsite. At this point, each subsite looked identical, same landing page, same subpages, changing one meant changing all of them. The behaviour of page generation is controlled by Gatsby. By default, Gatsby will map jsx/tsx files under the src/pages directories to webpages with the same path, so a file at src/pages/my/special/page.tsx will be accessible at https://my.domain/my/special/page. To support subsite specific pages, I wanted to adjust this logic so subsite named subdirectories under pages only generate pages for that particular subsite.
As an example, given this file structure:
src/
โ””โ”€โ”€ pages/
    โ”œโ”€โ”€ blog/
    |   โ”œโ”€โ”€ index.tsx
    โ”‚   โ””โ”€โ”€ my-page.tsx
    โ”œโ”€โ”€ home/
    โ”‚   โ””โ”€โ”€ index.tsx
    โ”œโ”€โ”€ this/
    โ”‚   โ””โ”€โ”€ exists-everywhere.tsx
    โ”œโ”€โ”€ about.tsx
    โ””โ”€โ”€ 404.tsx
I wanted these pages:
  • daveallie.com (src/pages/home/index.tsx)
  • daveallie.com/this/exists-everywhere
  • daveallie.com/about
  • daveallie.com/404
  • blog.daveallie.com (src/pages/home/index.tsx)
  • blog.daveallie.com/my-page
  • blog.daveallie.com/this/exists-everywhere
  • blog.daveallie.com/about
  • blog.daveallie.com/404
Everything under the home directory would be available at the root of the home subsite, everything under the blog directory would be available at the root of the blog subsite, and everything else would be available across all subsites.
I created a new utility function in config/util/subsite.js to determine page's subsite (if any) based on the page's path:
config/util/subsite.js
const SUBSITES = {
  home: {
    url: 'https://daveallie.com',
  },
  blog: {
    url: 'https://blog.daveallie.com',
  },
};
const DEFAULT_SUBSITE = 'home';

const SUBSITE =
  Object.keys(SUBSITES).find((s) => s === process.env.SUBSITE) ||
  DEFAULT_SUBSITE;

const getPathSubsite = (path) =>
  Object.keys(SUBSITES).find((s) => path.startsWith(`/${s}/`));

module.exports = {
  SUBSITE,
  SUBSITE_URL: SUBSITES[SUBSITE].url,
  getPathSubsite,
};
Then in gatsby-node.js I exported onCreatePage to intercept the page creation event and include my custom logic to support my subsite pages.
gatsby-node.js
const { SUBSITE } = require('./config/util/subsite');
const { getPathSubsite } = require('./config/util/subsite');

exports.onPreInit = () => {
  console.log(`=====\nBuilding subsite: ${SUBSITE}\n=====`);
};

exports.onCreatePage = ({ page, actions }) => {
  const { createPage, deletePage } = actions;

  const pageSubsite = getPathSubsite(page.path);

  if (!pageSubsite) {
    // not a subsite page, ignore
    return;
  }

  if (pageSubsite !== SUBSITE) {
    // subsite doesn't match page, delete
    deletePage(page);
    return;
  }

  if (page.context.originalPath) {
    // already transformed, skip
    return;
  }
  const originalPath = page.path;

  // replace original page with new page
  deletePage(page);
  const pathReplaceRegex = new RegExp(`^\/${SUBSITE}`);
  const newPage = {
    ...page,
    path: page.path.replace(pathReplaceRegex, ''),
    matchPath: page.matchPath
      ? page.matchPath.replace(pathReplaceRegex, '')
      : page.matchPath,
    context: {
      ...page.context,
      originalPath,
    },
  };

  createPage(newPage);
};
At a high level, the above code is:
  • ignoring all non subsite pages
  • deleting all subsite pages which don't match the subsite currently being built
  • ignoring all pages which have already been moved
  • deteting subsite pages and recreating them without the subsite name in the page's path (/blog/my-page is being moved to /my-page)
Now when running SUBSITE=home yarn build, Gatsby would create just the home specific pages merged with the global pages for all sites. Perfect, this allowed for custom landing pages, and subsite specific logic. I won't go into it here but this also allowed me to only generate my blog's MDX pages (the source code for my posts) when SUBSITE was blog.

SUBSITE in React

Up until this point, I've only used SUBSITE in the site generation code, which has been the biggest value add. However, there's still some fun to be had by exposing SUBSITE in my React code and controlling functionality / output of a component based on the subsite. To use SUBSITE in my React code I needed to expose the SUBSITE environment variable using an additional plugin. By default, Gatsby will only expose GATSBY_* environment variables to React, and to support other environment variables, you can use the gatsby-plugin-env-variables plugin.
yarn add gatsby-plugin-env-variables
Once added, the plugin can be configured in gatsby-config.js to support SUBSITE:
gatsby-config.js
{
  // ...
  "plugins": [
    {
      "resolve": "gatsby-plugin-env-variables",
      "options": {
        "allowList": ["SUBSITE"]
      }
    }
    // ...
  ]
}
Under the hood, gatsby-plugin-env-variables includes and configures Webpack's DefinePlugin. DefinePlugin allows you to set compile time constants, meaning all occurrences of process.env.SUBSITE in my React code will be replaced with the string value of process.env.SUBSITE when the code is compiled.
Once added, the subsite constant can be accessed in React code. As an example, here's a small component which returns the subsite string wrapped in an InlineCode and span component with a simple click handler.
SubsiteEnv.tsx
import React, { useState } from 'react';
import InlineCode from '~/components/InlineCode';

export default function SubsiteEnv() {
  const [clickCount, setClickCount] = useState(0);

  return (
    <span onClick={() => setClickCount(clickCount + 1)}>
      <InlineCode>{process.env.SUBSITE}</InlineCode>{' '}
      (clicked {clickCount} times)
    </span>
  );
}
And here it is rendered out in this blog post:
Current site is: blog (clicked 0 times)
(rendered by Current site is: <SubsiteEnv />)
info_outline
Try clicking on the word 'blog' above!
Done! SUBSITE is now accessible within the client-side (and server-side) rendered React. I good use I had for subsite in React (beyond party tricks) was to set the OpenGraph page type metadata type dynamically.

Wrap Up

This whole post is just a long-winded way of saying: "Multi-Domain Gatsby Sites can be achieved by using environment variable controlled build logic".
If you're not looking at building similar sites with very similar configurations, I would instead suggest creating a shared component repository for all common components and having stand-alone repositories for each site. However, for a use-case such as mine, where the sites are similar, and I don't want to deal with the overhead of multiple, potentially diverging repositories, this approach is perfect. ๐Ÿ‘Œ

If you'd like to read more about Josh's blog and how he built it, he did a wonderful writeup which you can find on his blog.
You can find the entire source for my blog and homepage on Github
GitHub icon
daveallie
/daveallie.com
. Note: The actual blog post content is in a private sub-module as I commit unpublished posts which no one should torture themselves reading.