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